add dpp and oidc modules
This commit is contained in:
parent
8f333e04ae
commit
a6684999a8
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -127,7 +127,7 @@ yarn.lock
|
|||
# ESLint Report
|
||||
eslint_report.json
|
||||
|
||||
modules/
|
||||
# modules/
|
||||
tmp/
|
||||
.env*
|
||||
bin/
|
||||
|
|
0
ereuse_devicehub/modules/dpp/__init__.py
Normal file
0
ereuse_devicehub/modules/dpp/__init__.py
Normal file
74
ereuse_devicehub/modules/dpp/alembic.ini
Normal file
74
ereuse_devicehub/modules/dpp/alembic.ini
Normal file
|
@ -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
|
34
ereuse_devicehub/modules/dpp/commands/register_user_dlt.py
Normal file
34
ereuse_devicehub/modules/dpp/commands/register_user_dlt.py
Normal file
|
@ -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
|
1
ereuse_devicehub/modules/dpp/migrations/README
Normal file
1
ereuse_devicehub/modules/dpp/migrations/README
Normal file
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
33
ereuse_devicehub/modules/dpp/migrations/script.py.mako
Normal file
33
ereuse_devicehub/modules/dpp/migrations/script.py.mako
Normal file
|
@ -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
ereuse_devicehub/modules/dpp/scripts/__init__.py
Normal file
0
ereuse_devicehub/modules/dpp/scripts/__init__.py
Normal file
19
ereuse_devicehub/modules/dpp/scripts/register_user_dlt.py
Normal file
19
ereuse_devicehub/modules/dpp/scripts/register_user_dlt.py
Normal file
|
@ -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
|
63
ereuse_devicehub/modules/dpp/scripts/register_user_dlt2.py
Normal file
63
ereuse_devicehub/modules/dpp/scripts/register_user_dlt2.py
Normal file
|
@ -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()
|
9
ereuse_devicehub/modules/dpp/users.py
Normal file
9
ereuse_devicehub/modules/dpp/users.py
Normal file
|
@ -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()
|
17
ereuse_devicehub/modules/dpp/utils.py
Normal file
17
ereuse_devicehub/modules/dpp/utils.py
Normal file
|
@ -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()
|
3
ereuse_devicehub/modules/dpp/views.py
Normal file
3
ereuse_devicehub/modules/dpp/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from flask import Blueprint
|
||||
|
||||
dpp = Blueprint('dpp', __name__, template_folder='templates')
|
0
ereuse_devicehub/modules/oidc/__init__.py
Normal file
0
ereuse_devicehub/modules/oidc/__init__.py
Normal file
74
ereuse_devicehub/modules/oidc/alembic.ini
Normal file
74
ereuse_devicehub/modules/oidc/alembic.ini
Normal file
|
@ -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
|
24
ereuse_devicehub/modules/oidc/commands/add_member.py
Normal file
24
ereuse_devicehub/modules/oidc/commands/add_member.py
Normal file
|
@ -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()
|
25
ereuse_devicehub/modules/oidc/commands/client_member.py
Normal file
25
ereuse_devicehub/modules/oidc/commands/client_member.py
Normal file
|
@ -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
|
45
ereuse_devicehub/modules/oidc/commands/sync_dlt.py
Normal file
45
ereuse_devicehub/modules/oidc/commands/sync_dlt.py
Normal file
|
@ -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])
|
159
ereuse_devicehub/modules/oidc/forms.py
Normal file
159
ereuse_devicehub/modules/oidc/forms.py
Normal file
|
@ -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
|
1
ereuse_devicehub/modules/oidc/migrations/README
Normal file
1
ereuse_devicehub/modules/oidc/migrations/README
Normal file
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
33
ereuse_devicehub/modules/oidc/migrations/script.py.mako
Normal file
33
ereuse_devicehub/modules/oidc/migrations/script.py.mako
Normal file
|
@ -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;")
|
76
ereuse_devicehub/modules/oidc/models.py
Normal file
76
ereuse_devicehub/modules/oidc/models.py
Normal file
|
@ -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')
|
171
ereuse_devicehub/modules/oidc/oauth2.py
Normal file
171
ereuse_devicehub/modules/oidc/oauth2.py
Normal file
|
@ -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())
|
22
ereuse_devicehub/modules/oidc/provider_discovery.py
Normal file
22
ereuse_devicehub/modules/oidc/provider_discovery.py
Normal file
|
@ -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"],
|
||||
}
|
27
ereuse_devicehub/modules/oidc/scripts/add_member.py
Normal file
27
ereuse_devicehub/modules/oidc/scripts/add_member.py
Normal file
|
@ -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()
|
32
ereuse_devicehub/modules/oidc/scripts/client_member.py
Normal file
32
ereuse_devicehub/modules/oidc/scripts/client_member.py
Normal file
|
@ -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()
|
39
ereuse_devicehub/modules/oidc/templates/authorize.html
Normal file
39
ereuse_devicehub/modules/oidc/templates/authorize.html
Normal file
|
@ -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 %}
|
48
ereuse_devicehub/modules/oidc/templates/create_client.html
Normal file
48
ereuse_devicehub/modules/oidc/templates/create_client.html
Normal file
|
@ -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 %}
|
232
ereuse_devicehub/modules/oidc/views.py
Normal file
232
ereuse_devicehub/modules/oidc/views.py
Normal file
|
@ -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 a new issue