add dpp and oidc modules
This commit is contained in:
parent
8f333e04ae
commit
a6684999a8
|
@ -127,7 +127,7 @@ yarn.lock
|
||||||
# ESLint Report
|
# ESLint Report
|
||||||
eslint_report.json
|
eslint_report.json
|
||||||
|
|
||||||
modules/
|
# modules/
|
||||||
tmp/
|
tmp/
|
||||||
.env*
|
.env*
|
||||||
bin/
|
bin/
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
script_location = migrations
|
||||||
|
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# timezone to use when rendering the date
|
||||||
|
# within the migration file as well as the filename.
|
||||||
|
# string value is passed to dateutil.tz.gettz()
|
||||||
|
# leave blank for localtime
|
||||||
|
# timezone =
|
||||||
|
|
||||||
|
# max length of characters to apply to the
|
||||||
|
# "slug" field
|
||||||
|
#truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version location specification; this defaults
|
||||||
|
# to alembic/versions. When using multiple version
|
||||||
|
# directories, initial revisions must be specified with --version-path
|
||||||
|
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
|
@ -0,0 +1,34 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterUserDlt:
|
||||||
|
# "Operator", "Verifier" or "Witness"
|
||||||
|
|
||||||
|
def __init__(self, app) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.app = app
|
||||||
|
help = "Register user in Dlt with params: email password rols"
|
||||||
|
self.app.cli.command('dlt_register_user', short_help=help)(self.run)
|
||||||
|
|
||||||
|
@click.argument('email')
|
||||||
|
@click.argument('password')
|
||||||
|
@click.argument('rols')
|
||||||
|
def run(self, email, password, rols):
|
||||||
|
if not rols:
|
||||||
|
rols = "Operator"
|
||||||
|
user = User.query.filter_by(email=email).one()
|
||||||
|
|
||||||
|
token_dlt = user.set_new_dlt_keys(password)
|
||||||
|
result = user.allow_permitions(api_token=token_dlt, rols=rols)
|
||||||
|
rols = user.get_rols(token_dlt=token_dlt)
|
||||||
|
rols = [k for k, v in rols]
|
||||||
|
user.rols_dlt = json.dumps(rols)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return result, rols
|
|
@ -0,0 +1 @@
|
||||||
|
Generic single-database configuration.
|
|
@ -0,0 +1,33 @@
|
||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlalchemy_utils
|
||||||
|
import citext
|
||||||
|
import teal
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def get_inv():
|
||||||
|
INV = context.get_x_argument(as_dictionary=True).get('inventory')
|
||||||
|
if not INV:
|
||||||
|
raise ValueError("Inventory value is not specified")
|
||||||
|
return INV
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def register_user(email, password, rols="Operator"):
|
||||||
|
# rols = 'Issuer, Operator, Witness, Verifier'
|
||||||
|
user = User.query.filter_by(email=email).one()
|
||||||
|
|
||||||
|
token_dlt = user.set_new_dlt_keys(password)
|
||||||
|
result = user.allow_permitions(api_token=token_dlt, rols=rols)
|
||||||
|
rols = user.get_rols(token_dlt=token_dlt)
|
||||||
|
rols = [k for k, v in rols]
|
||||||
|
user.rols_dlt = json.dumps(rols)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return result, rols
|
|
@ -0,0 +1,63 @@
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from decouple import config
|
||||||
|
from ereuseapi.methods import API, register_user
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
|
from ereuse_devicehub.modules.dpp.utils import encrypt
|
||||||
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
email = sys.argv[1]
|
||||||
|
password = sys.argv[2]
|
||||||
|
schema = config('DB_SCHEMA')
|
||||||
|
app = Devicehub(inventory=schema)
|
||||||
|
app.app_context().push()
|
||||||
|
api_dlt = app.config.get('API_DLT')
|
||||||
|
keyUser1 = app.config.get('API_DLT_TOKEN')
|
||||||
|
|
||||||
|
user = User.query.filter_by(email=email).one()
|
||||||
|
|
||||||
|
data = register_user(api_dlt)
|
||||||
|
api_token = data.get('data', {}).get('api_token')
|
||||||
|
data = json.dumps(data)
|
||||||
|
user.api_keys_dlt = encrypt(password, data)
|
||||||
|
result = allow_permitions(keyUser1, api_dlt, api_token)
|
||||||
|
rols = get_rols(api_dlt, api_token)
|
||||||
|
user.rols_dlt = json.dumps(rols)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return result, rols
|
||||||
|
|
||||||
|
|
||||||
|
def get_rols(api_dlt, token_dlt):
|
||||||
|
api = API(api_dlt, token_dlt, "ethereum")
|
||||||
|
|
||||||
|
result = api.check_user_roles()
|
||||||
|
if result.get('Status') != 200:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if 'Success' not in result.get('Data', {}).get('status'):
|
||||||
|
return []
|
||||||
|
|
||||||
|
rols = result.get('Data', {}).get('data', {})
|
||||||
|
return [k for k, v in rols.items() if v]
|
||||||
|
|
||||||
|
|
||||||
|
def allow_permitions(keyUser1, api_dlt, token_dlt):
|
||||||
|
apiUser1 = API(api_dlt, keyUser1, "ethereum")
|
||||||
|
rols = "isOperator"
|
||||||
|
if len(sys.argv) > 3:
|
||||||
|
rols = sys.argv[3]
|
||||||
|
|
||||||
|
result = apiUser1.issue_credential(rols, token_dlt)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# ['isIssuer', 'isOperator', 'isWitness', 'isVerifier']
|
||||||
|
main()
|
|
@ -0,0 +1,9 @@
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def set_dlt_user(email, password):
|
||||||
|
u = User.query.filter_by(email=email).one()
|
||||||
|
api_token = u.set_new_dlt_keys(password)
|
||||||
|
u.allow_permitions(api_token)
|
||||||
|
db.session.commit()
|
|
@ -0,0 +1,17 @@
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(key, msg):
|
||||||
|
key = (key * 32)[:32]
|
||||||
|
key = base64.urlsafe_b64encode(key.encode())
|
||||||
|
f = Fernet(key)
|
||||||
|
return f.encrypt(msg.encode()).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(key, msg):
|
||||||
|
key = (key * 32)[:32]
|
||||||
|
key = base64.urlsafe_b64encode(key.encode())
|
||||||
|
f = Fernet(key)
|
||||||
|
return f.decrypt(msg.encode()).decode()
|
|
@ -0,0 +1,3 @@
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
dpp = Blueprint('dpp', __name__, template_folder='templates')
|
|
@ -0,0 +1,74 @@
|
||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
script_location = migrations
|
||||||
|
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# timezone to use when rendering the date
|
||||||
|
# within the migration file as well as the filename.
|
||||||
|
# string value is passed to dateutil.tz.gettz()
|
||||||
|
# leave blank for localtime
|
||||||
|
# timezone =
|
||||||
|
|
||||||
|
# max length of characters to apply to the
|
||||||
|
# "slug" field
|
||||||
|
#truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version location specification; this defaults
|
||||||
|
# to alembic/versions. When using multiple version
|
||||||
|
# directories, initial revisions must be specified with --version-path
|
||||||
|
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
|
@ -0,0 +1,24 @@
|
||||||
|
import click
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.modules.oidc.models import MemberFederated
|
||||||
|
|
||||||
|
|
||||||
|
class AddMember:
|
||||||
|
def __init__(self, app) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.app = app
|
||||||
|
help = "Add member to the federated net"
|
||||||
|
self.app.cli.command('dlt_add_member', short_help=help)(self.run)
|
||||||
|
|
||||||
|
@click.argument('dlt_id_provider')
|
||||||
|
@click.argument('domain')
|
||||||
|
def run(self, dlt_id_provider, domain):
|
||||||
|
member = MemberFederated.query.filter_by(domain=domain).first()
|
||||||
|
if member:
|
||||||
|
return
|
||||||
|
|
||||||
|
member = MemberFederated(domain=domain, dlt_id_provider=dlt_id_provider)
|
||||||
|
|
||||||
|
db.session.add(member)
|
||||||
|
db.session.commit()
|
|
@ -0,0 +1,25 @@
|
||||||
|
import click
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.modules.oidc.models import MemberFederated
|
||||||
|
|
||||||
|
|
||||||
|
class AddClientOidc:
|
||||||
|
def __init__(self, app) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.app = app
|
||||||
|
help = "Add client oidc"
|
||||||
|
self.app.cli.command('add_client_oidc', short_help=help)(self.run)
|
||||||
|
|
||||||
|
@click.argument('domain')
|
||||||
|
@click.argument('client_id')
|
||||||
|
@click.argument('client_secret')
|
||||||
|
def run(self, domain, client_id, client_secret):
|
||||||
|
member = MemberFederated.query.filter_by(domain=domain).first()
|
||||||
|
if not member:
|
||||||
|
return
|
||||||
|
|
||||||
|
member.client_id = client_id
|
||||||
|
member.client_secret = client_secret
|
||||||
|
|
||||||
|
db.session.commit()
|
|
@ -0,0 +1,28 @@
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
|
|
||||||
|
class InsertMember:
|
||||||
|
def __init__(self, app) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.app = app
|
||||||
|
help = 'Add a new members to api dlt.'
|
||||||
|
self.app.cli.command('dlt_insert_members', short_help=help)(self.run)
|
||||||
|
|
||||||
|
@click.argument('domain')
|
||||||
|
def run(self, domain):
|
||||||
|
api = config("API_RESOLVER", None)
|
||||||
|
if "http" not in domain:
|
||||||
|
print("Error: you need put https:// in domain")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not api:
|
||||||
|
print("Error: you need a entry var API_RESOLVER in .env")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = {"url": domain}
|
||||||
|
url = api + '/registerURL'
|
||||||
|
res = requests.post(url, json=data)
|
||||||
|
print(res.json())
|
||||||
|
return
|
|
@ -0,0 +1,45 @@
|
||||||
|
import requests
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.modules.oidc.models import MemberFederated
|
||||||
|
|
||||||
|
|
||||||
|
class GetMembers:
|
||||||
|
def __init__(self, app) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.app = app
|
||||||
|
self.app.cli.command(
|
||||||
|
'dlt_rsync_members', short_help='Synchronize members of dlt.'
|
||||||
|
)(self.run)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
api = config("API_RESOLVER", None)
|
||||||
|
if not api:
|
||||||
|
print("Error: you need a entry var API_RESOLVER in .env")
|
||||||
|
return
|
||||||
|
|
||||||
|
url = api + '/getAll'
|
||||||
|
res = requests.get(url)
|
||||||
|
if res.status_code != 200:
|
||||||
|
return "Error, {}".format(res.text)
|
||||||
|
response = res.json()
|
||||||
|
members = response['url']
|
||||||
|
counter = members.pop('counter')
|
||||||
|
if counter <= MemberFederated.query.count():
|
||||||
|
return "All ok"
|
||||||
|
|
||||||
|
for k, v in members.items():
|
||||||
|
id = self.clean_id(k)
|
||||||
|
member = MemberFederated.query.filter_by(dlt_id_provider=id).first()
|
||||||
|
if member:
|
||||||
|
if member.domain != v:
|
||||||
|
member.domain = v
|
||||||
|
continue
|
||||||
|
member = MemberFederated(dlt_id_provider=id, domain=v)
|
||||||
|
db.session.add(member)
|
||||||
|
db.session.commit()
|
||||||
|
return res.text
|
||||||
|
|
||||||
|
def clean_id(self, id):
|
||||||
|
return int(id.split('DH')[-1])
|
|
@ -0,0 +1,159 @@
|
||||||
|
import time
|
||||||
|
|
||||||
|
from flask import g, request, session
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from werkzeug.security import gen_salt
|
||||||
|
from wtforms import (
|
||||||
|
BooleanField,
|
||||||
|
SelectField,
|
||||||
|
StringField,
|
||||||
|
TextAreaField,
|
||||||
|
URLField,
|
||||||
|
validators,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.modules.oidc.models import MemberFederated, OAuth2Client
|
||||||
|
|
||||||
|
AUTH_METHODS = [
|
||||||
|
('client_secret_basic', 'Client Secret Basic'),
|
||||||
|
('client_secret_post', 'Client Secret Post'),
|
||||||
|
('none', ''),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def split_by_crlf(s):
|
||||||
|
return [v for v in s.splitlines() if v]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateClientForm(FlaskForm):
|
||||||
|
client_name = StringField(
|
||||||
|
'Client Name', description="", render_kw={'class': "form-control"}
|
||||||
|
)
|
||||||
|
client_uri = URLField(
|
||||||
|
'Client url', description="", render_kw={'class': "form-control"}
|
||||||
|
)
|
||||||
|
scope = StringField(
|
||||||
|
'Allowed Scope', description="", render_kw={'class': "form-control"}
|
||||||
|
)
|
||||||
|
redirect_uris = TextAreaField(
|
||||||
|
'Redirect URIs', description="", render_kw={'class': "form-control"}
|
||||||
|
)
|
||||||
|
grant_types = TextAreaField(
|
||||||
|
'Allowed Grant Types', description="", render_kw={'class': "form-control"}
|
||||||
|
)
|
||||||
|
response_types = TextAreaField(
|
||||||
|
'Allowed Response Types', description="", render_kw={'class': "form-control"}
|
||||||
|
)
|
||||||
|
token_endpoint_auth_method = SelectField(
|
||||||
|
'Token Endpoint Auth Method',
|
||||||
|
choices=AUTH_METHODS,
|
||||||
|
description="",
|
||||||
|
render_kw={'class': "form-control, form-select"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
user = g.user
|
||||||
|
self.client = OAuth2Client.query.filter_by(user_id=user.id).first()
|
||||||
|
if request.method == 'GET':
|
||||||
|
if hasattr(self.client, 'client_metadata'):
|
||||||
|
kwargs.update(self.client.client_metadata)
|
||||||
|
grant_types = '\n'.join(kwargs.get('grant_types', ["authorization_code"]))
|
||||||
|
redirect_uris = '\n'.join(kwargs.get('redirect_uris', []))
|
||||||
|
response_types = '\n'.join(kwargs.get('response_types', ["code"]))
|
||||||
|
kwargs['grant_types'] = grant_types
|
||||||
|
kwargs['redirect_uris'] = redirect_uris
|
||||||
|
kwargs['response_types'] = response_types
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def validate(self, extra_validators=None):
|
||||||
|
is_valid = super().validate(extra_validators)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
domain = self.client_uri.data
|
||||||
|
self.member = MemberFederated.query.filter_by(domain=domain).first()
|
||||||
|
if not self.member:
|
||||||
|
txt = ["This domain is not federated."]
|
||||||
|
self.client_uri.errors = txt
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.member.user and self.member.user != g.user:
|
||||||
|
txt = ["This domain is register from other user."]
|
||||||
|
self.client_uri.errors = txt
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
if not self.client:
|
||||||
|
client_id = gen_salt(24)
|
||||||
|
self.client = OAuth2Client(client_id=client_id, user_id=g.user.id)
|
||||||
|
self.client.client_id_issued_at = int(time.time())
|
||||||
|
|
||||||
|
if self.token_endpoint_auth_method.data == 'none':
|
||||||
|
self.client.client_secret = ''
|
||||||
|
elif not self.client.client_secret:
|
||||||
|
self.client.client_secret = gen_salt(48)
|
||||||
|
|
||||||
|
self.member.client_id = self.client.client_id
|
||||||
|
self.member.client_secret = self.client.client_secret
|
||||||
|
if not self.member.user:
|
||||||
|
self.member.user = g.user
|
||||||
|
|
||||||
|
client_metadata = {
|
||||||
|
"client_name": self.client_name.data,
|
||||||
|
"client_uri": self.client_uri.data,
|
||||||
|
"grant_types": split_by_crlf(self.grant_types.data),
|
||||||
|
"redirect_uris": split_by_crlf(self.redirect_uris.data),
|
||||||
|
"response_types": split_by_crlf(self.response_types.data),
|
||||||
|
"scope": self.scope.data,
|
||||||
|
"token_endpoint_auth_method": self.token_endpoint_auth_method.data,
|
||||||
|
}
|
||||||
|
self.client.set_client_metadata(client_metadata)
|
||||||
|
self.client.member_id = self.member.dlt_id_provider
|
||||||
|
|
||||||
|
if not self.client.id:
|
||||||
|
db.session.add(self.client)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return self.client
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizeForm(FlaskForm):
|
||||||
|
consent = BooleanField(
|
||||||
|
'Consent?', [validators.Optional()], default=False, description=""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ListInventoryForm(FlaskForm):
|
||||||
|
inventory = SelectField(
|
||||||
|
'Select your inventory',
|
||||||
|
choices=[],
|
||||||
|
description="",
|
||||||
|
render_kw={'class': "form-control, form-select"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.inventories = MemberFederated.query.filter(
|
||||||
|
MemberFederated.client_id.isnot(None),
|
||||||
|
MemberFederated.client_secret.isnot(None),
|
||||||
|
)
|
||||||
|
for i in self.inventories:
|
||||||
|
self.inventory.choices.append((i.dlt_id_provider, i.domain))
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
next = request.args.get('next', '')
|
||||||
|
iv = self.inventories.filter_by(dlt_id_provider=self.inventory.data).first()
|
||||||
|
|
||||||
|
if not iv:
|
||||||
|
return next
|
||||||
|
|
||||||
|
session['next_url'] = next
|
||||||
|
session['oidc'] = iv.dlt_id_provider
|
||||||
|
client_id = iv.client_id
|
||||||
|
dh = iv.domain + f'/oauth/authorize?client_id={client_id}'
|
||||||
|
dh += '&scope=openid+profile+rols&response_type=code&nonce=abc'
|
||||||
|
return dh
|
|
@ -0,0 +1 @@
|
||||||
|
Generic single-database configuration.
|
|
@ -0,0 +1,33 @@
|
||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlalchemy_utils
|
||||||
|
import citext
|
||||||
|
import teal
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def get_inv():
|
||||||
|
INV = context.get_x_argument(as_dictionary=True).get('inventory')
|
||||||
|
if not INV:
|
||||||
|
raise ValueError("Inventory value is not specified")
|
||||||
|
return INV
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,175 @@
|
||||||
|
"""Open Connect OIDC
|
||||||
|
|
||||||
|
Revision ID: abba37ff5c80
|
||||||
|
Revises:
|
||||||
|
Create Date: 2022-09-30 10:01:19.761864
|
||||||
|
|
||||||
|
"""
|
||||||
|
import citext
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import context, op
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'abba37ff5c80'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_inv():
|
||||||
|
INV = context.get_x_argument(as_dictionary=True).get('inventory')
|
||||||
|
if not INV:
|
||||||
|
raise ValueError("Inventory value is not specified")
|
||||||
|
return INV
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table(
|
||||||
|
'member_federated',
|
||||||
|
sa.Column('dlt_id_provider', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
'updated',
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
'created',
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column('domain', citext.CIText(), nullable=False),
|
||||||
|
sa.Column('client_id', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('client_secret', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('dlt_id_provider'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ondelete='CASCADE'),
|
||||||
|
schema=f'{get_inv()}',
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'oauth2_client',
|
||||||
|
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
'updated',
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
'created',
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column('client_id_issued_at', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column('client_secret_expires_at', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column('client_id', citext.CIText(), nullable=False),
|
||||||
|
sa.Column('client_secret', citext.CIText(), nullable=False),
|
||||||
|
sa.Column('client_metadata', citext.CIText(), nullable=False),
|
||||||
|
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('member_id', sa.BigInteger(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
['member_id'],
|
||||||
|
[f'{get_inv()}.member_federated.dlt_id_provider'],
|
||||||
|
ondelete='CASCADE',
|
||||||
|
),
|
||||||
|
schema=f'{get_inv()}',
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'oauth2_code',
|
||||||
|
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
'updated',
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
'created',
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column('client_id', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('code', citext.CIText(), nullable=False),
|
||||||
|
sa.Column('redirect_uri', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('response_type', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('scope', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('nonce', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('code_challenge', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('code_challenge_method', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('auth_time', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('member_id', sa.BigInteger(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
['member_id'],
|
||||||
|
[f'{get_inv()}.member_federated.dlt_id_provider'],
|
||||||
|
ondelete='CASCADE',
|
||||||
|
),
|
||||||
|
sa.UniqueConstraint('code'),
|
||||||
|
schema=f'{get_inv()}',
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'oauth2_token',
|
||||||
|
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
'updated',
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
'created',
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column('client_id', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('token_type', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('access_token', citext.CIText(), nullable=False),
|
||||||
|
sa.Column('refresh_token', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('scope', citext.CIText(), nullable=True),
|
||||||
|
sa.Column('issued_at', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column('access_token_revoked_at', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column('refresh_token_revoked_at', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column('expires_in', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('member_id', sa.BigInteger(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
['member_id'],
|
||||||
|
[f'{get_inv()}.member_federated.dlt_id_provider'],
|
||||||
|
ondelete='CASCADE',
|
||||||
|
),
|
||||||
|
sa.UniqueConstraint('access_token'),
|
||||||
|
schema=f'{get_inv()}',
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(f"CREATE SEQUENCE {get_inv()}.oauth2_client_seq;")
|
||||||
|
op.execute(f"CREATE SEQUENCE {get_inv()}.member_federated_seq;")
|
||||||
|
op.execute(f"CREATE SEQUENCE {get_inv()}.oauth2_code_seq;")
|
||||||
|
op.execute(f"CREATE SEQUENCE {get_inv()}.oauth2_token_seq;")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table('oauth2_client', schema=f'{get_inv()}')
|
||||||
|
op.execute(f"DROP SEQUENCE {get_inv()}.oauth2_client_seq;")
|
||||||
|
|
||||||
|
op.drop_table('oauth2_code', schema=f'{get_inv()}')
|
||||||
|
op.execute(f"DROP SEQUENCE {get_inv()}.oauth2_code_seq;")
|
||||||
|
|
||||||
|
op.drop_table('oauth2_token', schema=f'{get_inv()}')
|
||||||
|
op.execute(f"DROP SEQUENCE {get_inv()}.oauth2_token_seq;")
|
||||||
|
|
||||||
|
op.drop_table('member_federated', schema=f'{get_inv()}')
|
||||||
|
op.execute(f"DROP SEQUENCE {get_inv()}.member_federated_seq;")
|
|
@ -0,0 +1,76 @@
|
||||||
|
from authlib.integrations.sqla_oauth2 import (
|
||||||
|
OAuth2AuthorizationCodeMixin,
|
||||||
|
OAuth2ClientMixin,
|
||||||
|
OAuth2TokenMixin,
|
||||||
|
)
|
||||||
|
from flask import g
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.resources.models import Thing
|
||||||
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class MemberFederated(Thing):
|
||||||
|
__tablename__ = 'member_federated'
|
||||||
|
|
||||||
|
dlt_id_provider = db.Column(db.Integer, primary_key=True)
|
||||||
|
domain = db.Column(db.String(40), unique=False)
|
||||||
|
# This client_id and client_secret is used for connected to this domain as
|
||||||
|
# a client and this domain then is the server of auth
|
||||||
|
client_id = db.Column(db.String(40), unique=False, nullable=True)
|
||||||
|
client_secret = db.Column(db.String(60), unique=False, nullable=True)
|
||||||
|
user_id = db.Column(
|
||||||
|
db.UUID(as_uuid=True), db.ForeignKey(User.id, ondelete='CASCADE'), nullable=True
|
||||||
|
)
|
||||||
|
user = db.relationship(User)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.domain
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2Client(Thing, OAuth2ClientMixin):
|
||||||
|
__tablename__ = 'oauth2_client'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(
|
||||||
|
db.UUID(as_uuid=True),
|
||||||
|
db.ForeignKey(User.id, ondelete='CASCADE'),
|
||||||
|
nullable=False,
|
||||||
|
default=lambda: g.user.id,
|
||||||
|
)
|
||||||
|
user = db.relationship(User)
|
||||||
|
member_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey('member_federated.dlt_id_provider', ondelete='CASCADE'),
|
||||||
|
)
|
||||||
|
member = db.relationship(MemberFederated)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2AuthorizationCode(Thing, OAuth2AuthorizationCodeMixin):
|
||||||
|
__tablename__ = 'oauth2_code'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(
|
||||||
|
db.UUID(as_uuid=True), db.ForeignKey(User.id, ondelete='CASCADE')
|
||||||
|
)
|
||||||
|
user = db.relationship(User)
|
||||||
|
member_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey('member_federated.dlt_id_provider', ondelete='CASCADE'),
|
||||||
|
)
|
||||||
|
member = db.relationship('MemberFederated')
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2Token(Thing, OAuth2TokenMixin):
|
||||||
|
__tablename__ = 'oauth2_token'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(
|
||||||
|
db.UUID(as_uuid=True), db.ForeignKey(User.id, ondelete='CASCADE')
|
||||||
|
)
|
||||||
|
user = db.relationship(User)
|
||||||
|
member_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey('member_federated.dlt_id_provider', ondelete='CASCADE'),
|
||||||
|
)
|
||||||
|
member = db.relationship('MemberFederated')
|
|
@ -0,0 +1,171 @@
|
||||||
|
from authlib.integrations.flask_oauth2 import (
|
||||||
|
AuthorizationServer as _AuthorizationServer,
|
||||||
|
)
|
||||||
|
from authlib.integrations.flask_oauth2 import ResourceProtector
|
||||||
|
from authlib.integrations.sqla_oauth2 import (
|
||||||
|
create_bearer_token_validator,
|
||||||
|
create_query_client_func,
|
||||||
|
create_save_token_func,
|
||||||
|
)
|
||||||
|
from authlib.oauth2.rfc6749.grants import (
|
||||||
|
AuthorizationCodeGrant as _AuthorizationCodeGrant,
|
||||||
|
)
|
||||||
|
from authlib.oidc.core import UserInfo
|
||||||
|
from authlib.oidc.core.grants import OpenIDCode as _OpenIDCode
|
||||||
|
from authlib.oidc.core.grants import OpenIDHybridGrant as _OpenIDHybridGrant
|
||||||
|
from authlib.oidc.core.grants import OpenIDImplicitGrant as _OpenIDImplicitGrant
|
||||||
|
from decouple import config
|
||||||
|
from werkzeug.security import gen_salt
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
from .models import OAuth2AuthorizationCode, OAuth2Client, OAuth2Token
|
||||||
|
|
||||||
|
DUMMY_JWT_CONFIG = {
|
||||||
|
'key': config('SECRET_KEY'),
|
||||||
|
'alg': 'HS256',
|
||||||
|
'iss': config("HOST", 'https://authlib.org'),
|
||||||
|
'exp': 3600,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def exists_nonce(nonce, req):
|
||||||
|
exists = OAuth2AuthorizationCode.query.filter_by(
|
||||||
|
client_id=req.client_id, nonce=nonce
|
||||||
|
).first()
|
||||||
|
return bool(exists)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_user_info(user, scope):
|
||||||
|
if 'rols' in scope:
|
||||||
|
rols = user.get_rols_dlt()
|
||||||
|
return UserInfo(rols=rols, sub=str(user.id), name=user.email)
|
||||||
|
return UserInfo(sub=str(user.id), name=user.email)
|
||||||
|
|
||||||
|
|
||||||
|
def create_authorization_code(client, grant_user, request):
|
||||||
|
code = gen_salt(48)
|
||||||
|
nonce = request.data.get('nonce')
|
||||||
|
item = OAuth2AuthorizationCode(
|
||||||
|
code=code,
|
||||||
|
client_id=client.client_id,
|
||||||
|
redirect_uri=request.redirect_uri,
|
||||||
|
scope=request.scope,
|
||||||
|
user_id=grant_user.id,
|
||||||
|
nonce=nonce,
|
||||||
|
member_id=client.member_id,
|
||||||
|
)
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationCodeGrant(_AuthorizationCodeGrant):
|
||||||
|
def create_authorization_code(self, client, grant_user, request):
|
||||||
|
return create_authorization_code(client, grant_user, request)
|
||||||
|
|
||||||
|
def parse_authorization_code(self, code, client):
|
||||||
|
item = OAuth2AuthorizationCode.query.filter_by(
|
||||||
|
code=code, client_id=client.client_id
|
||||||
|
).first()
|
||||||
|
if item and not item.is_expired():
|
||||||
|
return item
|
||||||
|
|
||||||
|
def delete_authorization_code(self, authorization_code):
|
||||||
|
db.session.delete(authorization_code)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def authenticate_user(self, authorization_code):
|
||||||
|
return User.query.get(authorization_code.user_id)
|
||||||
|
|
||||||
|
def save_authorization_code(self, code, request):
|
||||||
|
if not request.data.get('consent'):
|
||||||
|
return code
|
||||||
|
|
||||||
|
item = OAuth2AuthorizationCode(
|
||||||
|
code=code,
|
||||||
|
client_id=request.client.client_id,
|
||||||
|
redirect_uri=request.redirect_uri,
|
||||||
|
scope=request.scope,
|
||||||
|
user_id=request.user.id,
|
||||||
|
nonce=request.data.get('nonce'),
|
||||||
|
member_id=request.client.member_id,
|
||||||
|
)
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
return code
|
||||||
|
|
||||||
|
def query_authorization_code(self, code, client):
|
||||||
|
return OAuth2AuthorizationCode.query.filter_by(
|
||||||
|
code=code, client_id=client.client_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
|
||||||
|
class OpenIDCode(_OpenIDCode):
|
||||||
|
def exists_nonce(self, nonce, request):
|
||||||
|
return exists_nonce(nonce, request)
|
||||||
|
|
||||||
|
def get_jwt_config(self, grant):
|
||||||
|
return DUMMY_JWT_CONFIG
|
||||||
|
|
||||||
|
def generate_user_info(self, user, scope):
|
||||||
|
return generate_user_info(user, scope)
|
||||||
|
|
||||||
|
|
||||||
|
class ImplicitGrant(_OpenIDImplicitGrant):
|
||||||
|
def exists_nonce(self, nonce, request):
|
||||||
|
return exists_nonce(nonce, request)
|
||||||
|
|
||||||
|
def get_jwt_config(self, grant):
|
||||||
|
return DUMMY_JWT_CONFIG
|
||||||
|
|
||||||
|
def generate_user_info(self, user, scope):
|
||||||
|
return generate_user_info(user, scope)
|
||||||
|
|
||||||
|
|
||||||
|
class HybridGrant(_OpenIDHybridGrant):
|
||||||
|
def create_authorization_code(self, client, grant_user, request):
|
||||||
|
return create_authorization_code(client, grant_user, request)
|
||||||
|
|
||||||
|
def exists_nonce(self, nonce, request):
|
||||||
|
return exists_nonce(nonce, request)
|
||||||
|
|
||||||
|
def get_jwt_config(self):
|
||||||
|
return DUMMY_JWT_CONFIG
|
||||||
|
|
||||||
|
def generate_user_info(self, user, scope):
|
||||||
|
return generate_user_info(user, scope)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationServer(_AuthorizationServer):
|
||||||
|
def validate_consent_request(self, request=None, end_user=None):
|
||||||
|
return self.get_consent_grant(request=request, end_user=end_user)
|
||||||
|
|
||||||
|
def save_token(self, token, request):
|
||||||
|
token['member_id'] = request.client.member_id
|
||||||
|
return super().save_token(token, request)
|
||||||
|
|
||||||
|
|
||||||
|
authorization = AuthorizationServer()
|
||||||
|
require_oauth = ResourceProtector()
|
||||||
|
|
||||||
|
|
||||||
|
def config_oauth(app):
|
||||||
|
query_client = create_query_client_func(db.session, OAuth2Client)
|
||||||
|
save_token = create_save_token_func(db.session, OAuth2Token)
|
||||||
|
authorization.init_app(app, query_client=query_client, save_token=save_token)
|
||||||
|
|
||||||
|
# support all openid grants
|
||||||
|
authorization.register_grant(
|
||||||
|
AuthorizationCodeGrant,
|
||||||
|
[
|
||||||
|
OpenIDCode(require_nonce=True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
authorization.register_grant(ImplicitGrant)
|
||||||
|
authorization.register_grant(HybridGrant)
|
||||||
|
|
||||||
|
# protect resource
|
||||||
|
bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
|
||||||
|
require_oauth.register_token_validator(bearer_cls())
|
|
@ -0,0 +1,22 @@
|
||||||
|
discovery = {
|
||||||
|
"issuer": "{ host }",
|
||||||
|
"authorization_endpoint": "{ host }/oauth/authorize",
|
||||||
|
"token_endpoint": "{ host }/oauth/token",
|
||||||
|
"token_endpoint_auth_methods_supported": ["client_secret_basic", "private_key_jwt"],
|
||||||
|
"token_endpoint_auth_signing_alg_values_supported": ["RS256", "ES256"],
|
||||||
|
"userinfo_endpoint": "{ host }/oauth/userinfo",
|
||||||
|
"scopes_supported": ["openid", "profile", "rols"],
|
||||||
|
"response_types_supported": ["code", "code id_token", "id_token", "token id_token"],
|
||||||
|
"userinfo_signing_alg_values_supported": ["RS256", "ES256", "HS256"],
|
||||||
|
"userinfo_encryption_alg_values_supported": ["RSA1_5", "A128KW"],
|
||||||
|
"userinfo_encryption_enc_values_supported": ["A128CBC-HS256", "A128GCM"],
|
||||||
|
"id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"],
|
||||||
|
"id_token_encryption_alg_values_supported": ["RSA1_5", "A128KW"],
|
||||||
|
"id_token_encryption_enc_values_supported": ["A128CBC-HS256", "A128GCM"],
|
||||||
|
"request_object_signing_alg_values_supported": ["none", "RS256", "ES256"],
|
||||||
|
"display_values_supported": ["page", "popup"],
|
||||||
|
"claim_types_supported": ["normal", "distributed"],
|
||||||
|
"claims_supported": [],
|
||||||
|
"claims_parameter_supported": True,
|
||||||
|
"ui_locales_supported": ["en-US"],
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
|
from ereuse_devicehub.modules.oidc.models import MemberFederated
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
schema = config('DB_SCHEMA')
|
||||||
|
app = Devicehub(inventory=schema)
|
||||||
|
app.app_context().push()
|
||||||
|
dlt_id_provider = sys.argv[1]
|
||||||
|
domain = sys.argv[2]
|
||||||
|
member = MemberFederated.query.filter_by(domain=domain).first()
|
||||||
|
if member:
|
||||||
|
return
|
||||||
|
|
||||||
|
member = MemberFederated(domain=domain, dlt_id_provider=dlt_id_provider)
|
||||||
|
|
||||||
|
db.session.add(member)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,32 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
|
from ereuse_devicehub.modules.oidc.models import MemberFederated
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
We need add client_id and client_secret for every server
|
||||||
|
than we want connect.
|
||||||
|
"""
|
||||||
|
schema = config('DB_SCHEMA')
|
||||||
|
app = Devicehub(inventory=schema)
|
||||||
|
app.app_context().push()
|
||||||
|
domain = sys.argv[1]
|
||||||
|
client_id = sys.argv[2]
|
||||||
|
client_secret = sys.argv[3]
|
||||||
|
member = MemberFederated.query.filter_by(domain=domain).first()
|
||||||
|
if not member:
|
||||||
|
return
|
||||||
|
|
||||||
|
member.client_id = client_id
|
||||||
|
member.client_secret = client_secret
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,39 @@
|
||||||
|
{% extends "ereuse_devicehub/base_site.html" %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="pagetitle">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="section profile">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="pt-4 pb-2">
|
||||||
|
<h5 class="card-title text-center pb-0 fs-4">{{ title }}</h5>
|
||||||
|
<p>{{grant.client.client_name}} is requesting:
|
||||||
|
<strong>{{ grant.request.scope }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form action="" method="post" class="row g-3 needs-validation" novalidate>
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{% for field in form %}
|
||||||
|
{% if field != form.csrf_token %}
|
||||||
|
<div class="col-12">
|
||||||
|
{{ field.label(class_="form-label") }}
|
||||||
|
{{ field }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('core.user-profile') }}" class="btn btn-danger">Cancel</a>
|
||||||
|
<button class="btn btn-primary" type="submit">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,48 @@
|
||||||
|
{% extends "ereuse_devicehub/base_site.html" %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="pagetitle">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="section profile">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-6">
|
||||||
|
<div class="card">
|
||||||
|
{% if form.client %}
|
||||||
|
<div class="card-body">
|
||||||
|
<label class="form-label"><strong>Client_id:</strong></label>
|
||||||
|
<span class="form-control border-0">{{ form.client.client_id }}</span><br />
|
||||||
|
<label class="form-label"><strong>Client_secret:</strong></label>
|
||||||
|
<span class="form-control border-0">{{ form.client.client_secret }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="" method="post" class="row g-3 needs-validation" novalidate>
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{% for field in form %}
|
||||||
|
{% if field != form.csrf_token %}
|
||||||
|
<div class="col-12">
|
||||||
|
{{ field.label(class_="form-label") }}
|
||||||
|
{{ field }}
|
||||||
|
{% if field.errors %}
|
||||||
|
<p class="text-danger">
|
||||||
|
{% for error in field.errors %}
|
||||||
|
{{ error }}<br/>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="{{ referrer }}" class="btn btn-danger">Cancel</a>
|
||||||
|
<button class="btn btn-primary" type="submit">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,72 @@
|
||||||
|
{% extends "ereuse_devicehub/base.html" %}
|
||||||
|
{% block page_title %}{{ title }} - {{ page_title }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<main id="main" class="main">
|
||||||
|
{% block messages %}
|
||||||
|
{% for level, message in get_flashed_messages(with_categories=true) %}
|
||||||
|
<div class="alert alert-{{ level}} alert-dismissible fade show" role="alert">
|
||||||
|
{% if '_message_icon' in session %}
|
||||||
|
<i class="bi bi-{{ session['_message_icon'][level]}} me-1"></i>
|
||||||
|
{% else %}
|
||||||
|
<!-- fallback if 3rd party libraries (e.g. flask_login.login_required) -->
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
<section class="section profile">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-9">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="pagetitle">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
<form action="" method="post" class="row g-3 needs-validation" novalidate>
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{% for field in form %}
|
||||||
|
{% if field != form.csrf_token %}
|
||||||
|
<div class="col-12">
|
||||||
|
{{ field.label(class_="form-label") }}
|
||||||
|
{{ field }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="{{ next }}" class="btn btn-danger">Cancel</a>
|
||||||
|
<button class="btn btn-primary" type="submit">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- ======= Footer ======= -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="copyright">
|
||||||
|
© Copyright <strong><span>Usody</span></strong>. All Rights Reserved
|
||||||
|
</div>
|
||||||
|
<div class="credits">
|
||||||
|
<a href="https://help.usody.com/en/" target="_blank">Help</a> |
|
||||||
|
<a href="https://www.usody.com/legal/privacy-policy" target="_blank">Privacy</a> |
|
||||||
|
<a href="https://www.usody.com/legal/terms" target="_blank">Terms</a>
|
||||||
|
</div>
|
||||||
|
<div class="credits">
|
||||||
|
DeviceHub
|
||||||
|
</div>
|
||||||
|
</footer><!-- End Footer -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock body %}
|
|
@ -0,0 +1,232 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from authlib.integrations.flask_oauth2 import current_token
|
||||||
|
from authlib.oauth2 import OAuth2Error
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
g,
|
||||||
|
jsonify,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
session,
|
||||||
|
url_for,
|
||||||
|
)
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
from ereuse_devicehub import __version__, messages
|
||||||
|
from ereuse_devicehub.modules.oidc.forms import (
|
||||||
|
AuthorizeForm,
|
||||||
|
CreateClientForm,
|
||||||
|
ListInventoryForm,
|
||||||
|
)
|
||||||
|
from ereuse_devicehub.modules.oidc.models import MemberFederated, OAuth2Client
|
||||||
|
from ereuse_devicehub.modules.oidc.oauth2 import (
|
||||||
|
authorization,
|
||||||
|
generate_user_info,
|
||||||
|
require_oauth,
|
||||||
|
)
|
||||||
|
from ereuse_devicehub.views import GenericMixin
|
||||||
|
|
||||||
|
oidc = Blueprint('oidc', __name__, url_prefix='/', template_folder='templates')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Server #
|
||||||
|
##########
|
||||||
|
class CreateClientView(GenericMixin):
|
||||||
|
methods = ['GET', 'POST']
|
||||||
|
decorators = [login_required]
|
||||||
|
template_name = 'create_client.html'
|
||||||
|
title = "Edit Open Id Connect Client"
|
||||||
|
|
||||||
|
def dispatch_request(self):
|
||||||
|
form = CreateClientForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
form.save()
|
||||||
|
next_url = url_for('core.user-profile')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
self.get_context()
|
||||||
|
self.context.update(
|
||||||
|
{
|
||||||
|
'form': form,
|
||||||
|
'title': self.title,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return render_template(self.template_name, **self.context)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizeView(GenericMixin):
|
||||||
|
methods = ['GET', 'POST']
|
||||||
|
decorators = [login_required]
|
||||||
|
template_name = 'authorize.html'
|
||||||
|
title = "Authorize"
|
||||||
|
|
||||||
|
def dispatch_request(self):
|
||||||
|
form = AuthorizeForm()
|
||||||
|
client = OAuth2Client.query.filter_by(
|
||||||
|
client_id=request.args.get('client_id')
|
||||||
|
).first()
|
||||||
|
if not client:
|
||||||
|
messages.error('Not exist client')
|
||||||
|
return redirect(url_for('core.user-profile'))
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
if not form.consent.data:
|
||||||
|
return redirect(url_for('core.user-profile'))
|
||||||
|
|
||||||
|
# import pdb; pdb.set_trace()
|
||||||
|
return authorization.create_authorization_response(grant_user=g.user)
|
||||||
|
|
||||||
|
try:
|
||||||
|
grant = authorization.validate_consent_request(end_user=g.user)
|
||||||
|
except OAuth2Error as error:
|
||||||
|
messages.error(error.error)
|
||||||
|
return redirect(url_for('core.user-profile'))
|
||||||
|
|
||||||
|
self.get_context()
|
||||||
|
self.context.update(
|
||||||
|
{'form': form, 'title': self.title, 'user': g.user, 'grant': grant}
|
||||||
|
)
|
||||||
|
return render_template(self.template_name, **self.context)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueTokenView(GenericMixin):
|
||||||
|
methods = ['POST']
|
||||||
|
decorators = []
|
||||||
|
|
||||||
|
def dispatch_request(self):
|
||||||
|
return authorization.create_token_response()
|
||||||
|
|
||||||
|
|
||||||
|
class OauthProfileView(GenericMixin):
|
||||||
|
methods = ['GET']
|
||||||
|
decorators = []
|
||||||
|
template_name = 'authorize.html'
|
||||||
|
title = "Authorize"
|
||||||
|
|
||||||
|
@require_oauth('profile')
|
||||||
|
def dispatch_request(self):
|
||||||
|
return jsonify(generate_user_info(current_token.user, current_token.scope))
|
||||||
|
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Client #
|
||||||
|
##########
|
||||||
|
class SelectInventoryView(GenericMixin):
|
||||||
|
methods = ['GET', 'POST']
|
||||||
|
decorators = []
|
||||||
|
template_name = 'select_inventory.html'
|
||||||
|
title = "Select an Inventory"
|
||||||
|
|
||||||
|
def dispatch_request(self):
|
||||||
|
form = ListInventoryForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
return redirect(form.save(), code=302)
|
||||||
|
|
||||||
|
next = request.args.get('next', '#')
|
||||||
|
context = {
|
||||||
|
'next': next,
|
||||||
|
'form': form,
|
||||||
|
'title': self.title,
|
||||||
|
'user': g.user,
|
||||||
|
'grant': '',
|
||||||
|
'version': __version__,
|
||||||
|
}
|
||||||
|
return render_template(self.template_name, **context)
|
||||||
|
|
||||||
|
|
||||||
|
class AllowCodeView(GenericMixin):
|
||||||
|
methods = ['GET', 'POST']
|
||||||
|
decorators = []
|
||||||
|
userinfo = None
|
||||||
|
token = None
|
||||||
|
discovery = {}
|
||||||
|
|
||||||
|
def dispatch_request(self):
|
||||||
|
# import pdb.set_trace()
|
||||||
|
self.code = request.args.get('code')
|
||||||
|
self.oidc = session.get('oidc')
|
||||||
|
if not self.code or not self.oidc:
|
||||||
|
return self.redirect()
|
||||||
|
|
||||||
|
self.member = MemberFederated.query.filter(
|
||||||
|
MemberFederated.dlt_id_provider == self.oidc,
|
||||||
|
MemberFederated.client_id.isnot(None),
|
||||||
|
MemberFederated.client_secret.isnot(None),
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not self.member:
|
||||||
|
return self.redirect()
|
||||||
|
|
||||||
|
self.get_token()
|
||||||
|
if 'error' in self.token:
|
||||||
|
messages.error(self.token.get('error', ''))
|
||||||
|
return self.redirect()
|
||||||
|
|
||||||
|
self.get_user_info()
|
||||||
|
return self.redirect()
|
||||||
|
|
||||||
|
def get_discovery(self):
|
||||||
|
if self.discovery:
|
||||||
|
return self.discovery
|
||||||
|
|
||||||
|
try:
|
||||||
|
url_well_known = self.member.domain + '.well-known/openid-configuration'
|
||||||
|
self.discovery = requests.get(url_well_known).json()
|
||||||
|
except Exception:
|
||||||
|
self.discovery = {'code': 404}
|
||||||
|
|
||||||
|
return self.discovery
|
||||||
|
|
||||||
|
def get_token(self):
|
||||||
|
|
||||||
|
data = {'grant_type': 'authorization_code', 'code': self.code}
|
||||||
|
url = self.member.domain + '/oauth/token'
|
||||||
|
url = self.get_discovery().get('token_endpoint', url)
|
||||||
|
|
||||||
|
auth = (self.member.client_id, self.member.client_secret)
|
||||||
|
msg = requests.post(url, data=data, auth=auth)
|
||||||
|
self.token = json.loads(msg.text)
|
||||||
|
|
||||||
|
def redirect(self):
|
||||||
|
url = session.get('next_url') or '/login'
|
||||||
|
return redirect(url)
|
||||||
|
|
||||||
|
def get_user_info(self):
|
||||||
|
if self.userinfo:
|
||||||
|
return self.userinfo
|
||||||
|
if 'access_token' not in self.token:
|
||||||
|
return
|
||||||
|
|
||||||
|
url = self.member.domain + '/oauth/userinfo'
|
||||||
|
url = self.get_discovery().get('userinfo_endpoint', url)
|
||||||
|
access_token = self.token['access_token']
|
||||||
|
token_type = self.token.get('token_type', 'Bearer')
|
||||||
|
headers = {"Authorization": f"{token_type} {access_token}"}
|
||||||
|
|
||||||
|
msg = requests.get(url, headers=headers)
|
||||||
|
self.userinfo = json.loads(msg.text)
|
||||||
|
rols = self.userinfo.get('rols', self.userinfo)
|
||||||
|
session['rols'] = [(k, k) for k in rols]
|
||||||
|
return self.userinfo
|
||||||
|
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Routes #
|
||||||
|
##########
|
||||||
|
oidc.add_url_rule('/create_client', view_func=CreateClientView.as_view('create_client'))
|
||||||
|
oidc.add_url_rule('/oauth/authorize', view_func=AuthorizeView.as_view('autorize_oidc'))
|
||||||
|
oidc.add_url_rule('/allow_code', view_func=AllowCodeView.as_view('allow_code'))
|
||||||
|
oidc.add_url_rule('/oauth/token', view_func=IssueTokenView.as_view('oauth_issue_token'))
|
||||||
|
oidc.add_url_rule(
|
||||||
|
'/oauth/userinfo', view_func=OauthProfileView.as_view('oauth_user_info')
|
||||||
|
)
|
||||||
|
oidc.add_url_rule(
|
||||||
|
'/oidc/client/select',
|
||||||
|
view_func=SelectInventoryView.as_view('login_other_inventory'),
|
||||||
|
)
|
Reference in New Issue