fix lots of stuff, migrate to new client api
This commit is contained in:
		
							parent
							
								
									4e9fd55093
								
							
						
					
					
						commit
						710460cc88
					
				
					 19 changed files with 384 additions and 224 deletions
				
			
		
							
								
								
									
										21
									
								
								default.nix
									
										
									
									
									
								
							
							
						
						
									
										21
									
								
								default.nix
									
										
									
									
									
								
							|  | @ -4,8 +4,6 @@ | ||||||
|   ...}: |   ...}: | ||||||
| let  | let  | ||||||
| 
 | 
 | ||||||
|   nixpkgs_unstable = import <nixpkgs_unstable> {}; |  | ||||||
| 
 |  | ||||||
|   urlobject = with python.pkgs; buildPythonPackage rec { |   urlobject = with python.pkgs; buildPythonPackage rec { | ||||||
|     pname = "URLObject"; |     pname = "URLObject"; | ||||||
|     version = "2.4.3"; |     version = "2.4.3"; | ||||||
|  | @ -16,9 +14,20 @@ let | ||||||
|     doCheck = true; |     doCheck = true; | ||||||
|     propagatedBuildInputs = [ |     propagatedBuildInputs = [ | ||||||
|     ]; |     ]; | ||||||
| 
 |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   python_attrs = with python.pkgs; buildPythonPackage rec { | ||||||
|  |     pname = "attrs"; | ||||||
|  |     version = "21.4.0"; | ||||||
|  |     src = fetchPypi { | ||||||
|  |       inherit pname version; | ||||||
|  |       sha256 = "626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"; | ||||||
|  |     }; | ||||||
|  |     #doCheck = true; | ||||||
|  |     doCheck = false; | ||||||
|  |     propagatedBuildInputs = [ | ||||||
|  |     ]; | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|   flask-dance = with python.pkgs; buildPythonPackage rec { |   flask-dance = with python.pkgs; buildPythonPackage rec { | ||||||
|     pname = "Flask-Dance"; |     pname = "Flask-Dance"; | ||||||
|  | @ -37,6 +46,7 @@ let | ||||||
|     ]; |     ]; | ||||||
|     checkInputs = [ |     checkInputs = [ | ||||||
|       pytest |       pytest | ||||||
|  |       nose | ||||||
|       pytest-mock |       pytest-mock | ||||||
|       responses |       responses | ||||||
|       freezegun |       freezegun | ||||||
|  | @ -89,7 +99,7 @@ let | ||||||
|     propagatedBuildInputs = [ |     propagatedBuildInputs = [ | ||||||
|       urllib3 |       urllib3 | ||||||
|       python-dateutil |       python-dateutil | ||||||
|       nixpkgs_unstable.python39Packages.attrs |       python_attrs | ||||||
|       httpx |       httpx | ||||||
|     ]; |     ]; | ||||||
|   }; |   }; | ||||||
|  | @ -110,6 +120,7 @@ in | ||||||
|       cryptography |       cryptography | ||||||
|       blinker |       blinker | ||||||
|       ory-hydra-client |       ory-hydra-client | ||||||
|  |       authlib | ||||||
| 
 | 
 | ||||||
|       gunicorn |       gunicorn | ||||||
| 
 | 
 | ||||||
|  | @ -127,6 +138,8 @@ in | ||||||
|     pytest-mypy |     pytest-mypy | ||||||
|     flask_testing |     flask_testing | ||||||
|     tox |     tox | ||||||
|  | 
 | ||||||
|  |     nose | ||||||
|     mypy |     mypy | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,18 +1,18 @@ | ||||||
| from flask.app import Flask | from flask.app import Flask | ||||||
| from flask import g, redirect, request | from flask import g, redirect, request | ||||||
| from flask.helpers import url_for | from flask.helpers import url_for | ||||||
| from flask_babel import Babel |  | ||||||
| from flask_login import LoginManager | from flask_login import LoginManager | ||||||
| import time | import time | ||||||
| import subprocess | import subprocess | ||||||
| import ory_hydra_client as hydra | from ory_hydra_client import Client | ||||||
| import ory_hydra_client.api.admin_api as hydra_admin_api |  | ||||||
| import os | import os | ||||||
| 
 | 
 | ||||||
| from ldap3 import Connection, Server, ALL | from ldap3 import Connection, Server, ALL | ||||||
| 
 | 
 | ||||||
| from . import model | from . import model | ||||||
| from .pki import Pki | from .pki import Pki | ||||||
|  | from .hydra import hydra_service | ||||||
|  | from .translations import init_babel | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_git_hash(): | def get_git_hash(): | ||||||
|  | @ -22,12 +22,7 @@ def get_git_hash(): | ||||||
|         return '' |         return '' | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def init_oauth2(app): | def create_app() -> Flask: | ||||||
|     pass |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def create_app(): |  | ||||||
|     name = "lenticular_cloud" |     name = "lenticular_cloud" | ||||||
|     app = Flask(name, template_folder='template') |     app = Flask(name, template_folder='template') | ||||||
|     app.config.from_pyfile('application.cfg') |     app.config.from_pyfile('application.cfg') | ||||||
|  | @ -47,35 +42,31 @@ def create_app(): | ||||||
|     with app.app_context(): |     with app.app_context(): | ||||||
|         db.create_all() |         db.create_all() | ||||||
| 
 | 
 | ||||||
|     app.babel = Babel(app) |     init_babel(app) | ||||||
|     init_oauth2(app) |  | ||||||
|     app.login_manager = LoginManager(app) |     app.login_manager = LoginManager(app) | ||||||
| 
 | 
 | ||||||
|     #init hydra admin api |     #init hydra admin api | ||||||
|     hydra_config = hydra.Configuration( | #    hydra_config = hydra.Configuration( | ||||||
|                         host=app.config['HYDRA_ADMIN_URL'], | #                        host=app.config['HYDRA_ADMIN_URL'], | ||||||
|                         username=app.config['HYDRA_ADMIN_USER'], | #                        username=app.config['HYDRA_ADMIN_USER'], | ||||||
|                         password=app.config['HYDRA_ADMIN_PASSWORD']) | #                        password=app.config['HYDRA_ADMIN_PASSWORD']) | ||||||
|     hydra_client = hydra.ApiClient(hydra_config) |     hydra_service.set_hydra_client(Client(base_url=app.config['HYDRA_ADMIN_URL'])) | ||||||
|     app.hydra_api = hydra_admin_api.AdminApi(hydra_client) |  | ||||||
| 
 | 
 | ||||||
|     from .views import auth_views, frontend_views, init_login_manager, api_views, pki_views, admin_views |     from .views import auth_views, frontend_views, init_login_manager, api_views, pki_views, admin_views, oauth2_views | ||||||
|     init_login_manager(app) |     init_login_manager(app) | ||||||
|  |     #oauth2.init_app(app) | ||||||
|     app.register_blueprint(auth_views) |     app.register_blueprint(auth_views) | ||||||
|     app.register_blueprint(frontend_views) |     app.register_blueprint(frontend_views) | ||||||
|     app.register_blueprint(api_views) |     app.register_blueprint(api_views) | ||||||
|     app.register_blueprint(pki_views) |     app.register_blueprint(pki_views) | ||||||
|     app.register_blueprint(admin_views) |     app.register_blueprint(admin_views) | ||||||
|  |     app.register_blueprint(oauth2_views) | ||||||
| 
 | 
 | ||||||
|     @app.before_request |     @app.before_request | ||||||
|     def befor_request(): |     def befor_request(): | ||||||
|         request_start_time = time.time() |         request_start_time = time.time() | ||||||
|         g.request_time = lambda: "%.5fs" % (time.time() - request_start_time) |         g.request_time = lambda: "%.5fs" % (time.time() - request_start_time) | ||||||
| 
 | 
 | ||||||
|     from .translations import init_babel |  | ||||||
| 
 |  | ||||||
|     init_babel(app) |  | ||||||
| 
 |  | ||||||
|     app.lenticular_services = {} |     app.lenticular_services = {} | ||||||
|     for service_name, service_config in app.config['LENTICULAR_CLOUD_SERVICES'].items(): |     for service_name, service_config in app.config['LENTICULAR_CLOUD_SERVICES'].items(): | ||||||
|         app.lenticular_services[service_name] = model.Service.from_config(service_name, service_config) |         app.lenticular_services[service_name] = model.Service.from_config(service_name, service_config) | ||||||
|  |  | ||||||
|  | @ -44,6 +44,7 @@ def entry_point(): | ||||||
|     app = create_app() |     app = create_app() | ||||||
|     if args.func == cli_run: |     if args.func == cli_run: | ||||||
|         cli_run(app,args) |         cli_run(app,args) | ||||||
|  |         return | ||||||
|     with app.app_context(): |     with app.app_context(): | ||||||
|         args.func(args) |         args.func(args) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,29 +6,53 @@ from wtforms import StringField, SubmitField, TextField, \ | ||||||
|         SelectField, Form as NoCsrfForm, SelectMultipleField |         SelectField, Form as NoCsrfForm, SelectMultipleField | ||||||
| 
 | 
 | ||||||
| from wtforms.fields.html5 import URLField | from wtforms.fields.html5 import URLField | ||||||
| from wtforms.fields import FormField | from wtforms.fields import FormField, SelectMultipleField | ||||||
| 
 | 
 | ||||||
| from .base import FieldList | from .base import FieldList | ||||||
| 
 | 
 | ||||||
|  | class SerilizedSelectField(SelectMultipleField): | ||||||
|  | 
 | ||||||
|  |     def process_data(self, value): | ||||||
|  |         try: | ||||||
|  |             self.data = ','.join(list(self.coerce(v) for v in value)) | ||||||
|  |         except (ValueError, TypeError): | ||||||
|  |             self.data = None | ||||||
|  | 
 | ||||||
|  |     def process_formdata(self, valuelist): | ||||||
|  |         try: | ||||||
|  |             self.data = ','.join(list(self.coerce(x) for x in valuelist)) | ||||||
|  |         except ValueError as exc: | ||||||
|  |             raise ValueError( | ||||||
|  |                 self.gettext( | ||||||
|  |                     "Invalid choice(s): one or more data inputs could not be coerced." | ||||||
|  |                 ) | ||||||
|  |             ) from exc | ||||||
|  | 
 | ||||||
|  |     def populate_obj(self, obj, name) -> None: | ||||||
|  |         setattr(obj, name, ','.join(self.data)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class OAuth2ClientForm(FlaskForm): | class OAuth2ClientForm(FlaskForm): | ||||||
|     client_id = StringField(gettext('client_id') ) |     client_id = StringField(gettext('client_id') ) | ||||||
|     client_name = StringField(gettext('client_name')) |     client_name = StringField(gettext('client_name')) | ||||||
|     client_uri = URLField(gettext('client_uri')) |     client_uri = URLField(gettext('client_uri')) | ||||||
|     client_secret = PasswordField(gettext('client_secret')) |     client_secret = PasswordField(gettext('client_secret')) | ||||||
|     logo_uri = URLField(gettext('logo_uri')) |     logo_uri = URLField(gettext('logo_uri')) | ||||||
|     redirect_uris = FieldList(FormField(URLField(gettext('logo_uri')))) |     redirect_uris = FieldList(URLField(gettext('redirect_uri')), min_entries=1) | ||||||
|     #contacts = List[str] |     contacts =   FieldList(StringField('contacts')) | ||||||
|     #grant_types = List[str] |     grant_types =  SelectMultipleField('grant_types',choices=[(x, x) for x in ['authorization_code', 'refresh_token', 'implicit']]) | ||||||
|     #response_types = List[str] |     response_types = SelectMultipleField('repsonse_type',choices=[(x, x) for x in ['code token', 'code', 'id_token']]) | ||||||
|     scope = StringField(gettext('scope')) |     scope = StringField(gettext('scope')) | ||||||
|     subject_type = StringField(gettext('subject_type')) |     subject_type = StringField(gettext('subject_type')) | ||||||
|     token_endpoint_auth_method = StringField(gettext('token_endpoint_auth_method')) |     token_endpoint_auth_method =  SelectField('token_endpoint_auth_method', choices=[(x, x) for x in ['client_secret_basic', 'client_secret_post']]) | ||||||
|     userinfo_signed_response_alg = StringField(gettext('userinfo_signed_response_alg')) |     userinfo_signed_response_alg = StringField(gettext('userinfo_signed_response_alg')) | ||||||
| 
 | 
 | ||||||
|     client_secret_expires_at = IntegerField('client_secret_expires_at') |     client_secret_expires_at = IntegerField('client_secret_expires_at') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     #allowed_cross_origins = Array |     #allowed_cross_origins = Array | ||||||
|  |     contacts =   FieldList(StringField('contacts')) | ||||||
|     #audience = List[str] |     #audience = List[str] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ from ..model import db | ||||||
| 
 | 
 | ||||||
| class FieldList(WTFFieldList): | class FieldList(WTFFieldList): | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|  |         self.modify = kwargs.pop("modify", True) | ||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
| 
 | 
 | ||||||
|     def get_template(self) -> Field: |     def get_template(self) -> Field: | ||||||
|  | @ -16,7 +17,6 @@ class FieldList(WTFFieldList): | ||||||
| class ModelFieldList(FieldList): | class ModelFieldList(FieldList): | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         self.model = kwargs.pop("model", None) |         self.model = kwargs.pop("model", None) | ||||||
|         self.modify = kwargs.pop("modify", True) |  | ||||||
|         super(ModelFieldList, self).__init__(*args, **kwargs) |         super(ModelFieldList, self).__init__(*args, **kwargs) | ||||||
|         if not self.model: |         if not self.model: | ||||||
|             raise ValueError("ModelFieldList requires model to be set") |             raise ValueError("ModelFieldList requires model to be set") | ||||||
|  |  | ||||||
							
								
								
									
										31
									
								
								lenticular_cloud/hydra.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								lenticular_cloud/hydra.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | ||||||
|  | from ory_hydra_client import Client | ||||||
|  | from typing import Optional | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class HydraService: | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         self._hydra_client = None # type: Optional[Client] | ||||||
|  |         self._oauth_client = None # type: Optional[Client] | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def hydra_client(self) -> Client: | ||||||
|  |         if self._hydra_client is None: | ||||||
|  |             raise RuntimeError('need to init client first') | ||||||
|  |         return self._hydra_client | ||||||
|  | 
 | ||||||
|  |     def set_hydra_client(self, client: Client) -> None: | ||||||
|  |         self._hydra_client = client | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def oauth_client(self) -> Client: | ||||||
|  |         if self._oauth_client is None: | ||||||
|  |             raise RuntimeError('need to init client first') | ||||||
|  |         return self._oauth_client | ||||||
|  | 
 | ||||||
|  |     def set_oauth_client(self, client: Client) -> None: | ||||||
|  |         self._hydra_client = client | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | hydra_service = HydraService() | ||||||
|  | @ -145,7 +145,7 @@ | ||||||
| 	{% if field.modify %} | 	{% if field.modify %} | ||||||
| 		<template id='{{field.name}}-template' data-fieldlist-name="{{ field.name }}" data-fieldlist-next-id="{{ field | length }}"> | 		<template id='{{field.name}}-template' data-fieldlist-name="{{ field.name }}" data-fieldlist-next-id="{{ field | length }}"> | ||||||
| 		<li class="list-group-item"> | 		<li class="list-group-item"> | ||||||
| 			{{ _render_form(field.get_template(), horizontal=true) }} | 			{{ _render_field(field.get_template(), horizontal=true) }} | ||||||
| 			<a class="btn btn-danger" onclick="fieldlist.remove(this)">{{ ('Remove') }}</a> | 			<a class="btn btn-danger" onclick="fieldlist.remove(this)">{{ ('Remove') }}</a> | ||||||
| 		</li> | 		</li> | ||||||
| 	</template> | 	</template> | ||||||
|  | @ -174,6 +174,8 @@ | ||||||
|         {{ render_password_field(f, **kwargs) }} |         {{ render_password_field(f, **kwargs) }} | ||||||
|     {% elif f.type == 'DecimalRangeField' %} |     {% elif f.type == 'DecimalRangeField' %} | ||||||
|         {{ render_range_field(f, **kwargs) }} |         {{ render_range_field(f, **kwargs) }} | ||||||
|  |     {% elif f.type == 'URLField' %} | ||||||
|  |         {{ render_field(f, **kwargs) }} | ||||||
|     {% elif f.type == 'SubmitField' %} |     {% elif f.type == 'SubmitField' %} | ||||||
|         {{ render_submit_field(f, **kwargs) }} |         {{ render_submit_field(f, **kwargs) }} | ||||||
|     {% elif f.type == 'FormField' %} |     {% elif f.type == 'FormField' %} | ||||||
|  |  | ||||||
|  | @ -34,6 +34,26 @@ | ||||||
| <script type="application/javascript" src="/static/main.js?v={{ GIT_HASH }}" ></script> | <script type="application/javascript" src="/static/main.js?v={{ GIT_HASH }}" ></script> | ||||||
| <script type="application/javascript" > | <script type="application/javascript" > | ||||||
| {% block script_js %}{% endblock %} | {% block script_js %}{% endblock %} | ||||||
|  | window.fieldlist = { | ||||||
|  | 	add: function(linkTag) { | ||||||
|  | 
 | ||||||
|  | 		var templateTag = linkTag.parentNode.querySelector('template'); | ||||||
|  | 		var name = templateTag.dataset['fieldlistName']; | ||||||
|  | 		var template = templateTag.content.cloneNode(true); | ||||||
|  | 		var id = templateTag.dataset['fieldlistNextId']++; | ||||||
|  | 		for(let tag of template.querySelectorAll('[name], [id]')){ | ||||||
|  | 			tag.name = tag.name.replace(/^custom-/, name+'-'+id+'-') | ||||||
|  | 			tag.id = tag.id.replace(/^custom-/, name+'-'+id+'-') | ||||||
|  | 		}; | ||||||
|  | 		templateTag.parentNode.querySelector('ul').appendChild(template); | ||||||
|  | 
 | ||||||
|  | 		return false; | ||||||
|  | 	}, | ||||||
|  | 	remove: function(base) { | ||||||
|  | 		base.parentElement.remove() | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | }; | ||||||
| </script> | </script> | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| from flask import g, request, Flask | from flask import g, request, Flask | ||||||
|  | from flask_babel import Babel | ||||||
| from flask_login import current_user | from flask_login import current_user | ||||||
| from typing import Optional | from typing import Optional | ||||||
| from lenticular_cloud.model import db, User | from lenticular_cloud.model import db, User | ||||||
|  | @ -8,38 +9,41 @@ LANGUAGES = { | ||||||
|     'de': 'Deutsch' |     'de': 'Deutsch' | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | babel = Babel() | ||||||
| 
 | 
 | ||||||
| def init_babel(app: Flask) -> None: |  | ||||||
|     babel = app.babel |  | ||||||
| 
 | 
 | ||||||
|     @babel.localeselector | @babel.localeselector | ||||||
|     def get_locale() -> str: | def get_locale() -> str: | ||||||
|         # if a user is logged in, use the locale from the user settings |     # if a user is logged in, use the locale from the user settings | ||||||
|         user = current_user # type: Optional[User] |     user = current_user # type: Optional[User] | ||||||
|         return 'de' |     return 'de' | ||||||
| 
 | 
 | ||||||
|         # prefer lang argument |     # prefer lang argument | ||||||
|         if 'lang' in request.args: |     if 'lang' in request.args: | ||||||
|             lang = request.args['lang'] # type: str |         lang = request.args['lang'] # type: str | ||||||
|             if lang in LANGUAGES: |         if lang in LANGUAGES: | ||||||
|                 if not isinstance(user, User): |             if not isinstance(user, User): | ||||||
|                         return lang |                     return lang | ||||||
|                 user.locale = lang |             user.locale = lang | ||||||
|                 db.session.commit() |             db.session.commit() | ||||||
| 
 | 
 | ||||||
|         if isinstance(user, User): |     if isinstance(user, User): | ||||||
|             return user.locale |         return user.locale | ||||||
|         # otherwise try to guess the language from the user accept |     # otherwise try to guess the language from the user accept | ||||||
|         # header the browser transmits.  We support de/fr/en in this |     # header the browser transmits.  We support de/fr/en in this | ||||||
|         # example.  The best match wins. |     # example.  The best match wins. | ||||||
|         return request.accept_languages.best_match(['de']) |     return request.accept_languages.best_match(['de']) | ||||||
| 
 | 
 | ||||||
|     @babel.timezoneselector | @babel.timezoneselector | ||||||
|     def get_timezone() -> Optional[str]: | def get_timezone() -> Optional[str]: | ||||||
| #       user = getattr(g, 'user', None) | #       user = getattr(g, 'user', None) | ||||||
| #       if user is not None: | #       if user is not None: | ||||||
| #           return user.timezone | #           return user.timezone | ||||||
|         return None |     return None | ||||||
|  | 
 | ||||||
|  | def init_babel(app: Flask) -> None: | ||||||
|  | 
 | ||||||
|  |     babel.init_app(app) | ||||||
| 
 | 
 | ||||||
|     @app.context_processor |     @app.context_processor | ||||||
|     def get_locale_jinja() -> dict: |     def get_locale_jinja() -> dict: | ||||||
|  | @ -47,3 +51,4 @@ def init_babel(app: Flask) -> None: | ||||||
|             return get_locale() |             return get_locale() | ||||||
| 
 | 
 | ||||||
|         return dict(get_locale=get_locale_) |         return dict(get_locale=get_locale_) | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| # pylint: disable=unused-import | # pylint: disable=unused-import | ||||||
| 
 | 
 | ||||||
| from .auth import auth_views  | from .auth import auth_views  | ||||||
| from .frontend import frontend_views, init_login_manager | from .oauth2 import init_login_manager, oauth2_views | ||||||
|  | from .frontend import frontend_views | ||||||
| from .admin import admin_views | from .admin import admin_views | ||||||
| from .api import api_views | from .api import api_views | ||||||
| from .pki import pki_views | from .pki import pki_views | ||||||
|  |  | ||||||
|  | @ -1,39 +1,43 @@ | ||||||
| import flask | from authlib.integrations.base_client.errors import MissingTokenError | ||||||
| from flask import Blueprint, redirect, request, url_for, render_template | from flask import Blueprint, redirect, request, url_for, render_template | ||||||
| from flask import current_app, session | from flask import current_app, session | ||||||
| from flask import jsonify | from flask import jsonify | ||||||
|  | from flask.typing import ResponseReturnValue | ||||||
| from flask_login import current_user, logout_user | from flask_login import current_user, logout_user | ||||||
| from oauthlib.oauth2.rfc6749.errors import TokenExpiredError | from oauthlib.oauth2.rfc6749.errors import TokenExpiredError | ||||||
| import ory_hydra_client | from ory_hydra_client.api.admin import list_o_auth_2_clients, get_o_auth_2_client, update_o_auth_2_client, create_o_auth_2_client  | ||||||
| from ory_hydra_client.model.o_auth2_client import OAuth2Client | from ory_hydra_client.models import OAuth2Client, GenericError | ||||||
|  | from typing import Optional | ||||||
| import logging | import logging | ||||||
| 
 | 
 | ||||||
| from ..model import db, User, UserSignUp | from ..model import db, User, UserSignUp | ||||||
| from .frontend import redirect_login | from .oauth2 import redirect_login, oauth2 | ||||||
| from ..form.admin import OAuth2ClientForm | from ..form.admin import OAuth2ClientForm | ||||||
|  | from ..hydra import hydra_service | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| admin_views = Blueprint('admin', __name__, url_prefix='/admin') | admin_views = Blueprint('admin', __name__, url_prefix='/admin') | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def before_request(): | def before_request() -> Optional[ResponseReturnValue]: | ||||||
|     try: |     try: | ||||||
|         resp = current_app.oauth.session.get('/userinfo') |         resp = oauth2.custom.get('/userinfo') | ||||||
|         data = resp.json() |         data = resp.json() | ||||||
|         if not current_user.is_authenticated or resp.status_code != 200: |         if not current_user.is_authenticated or resp.status_code != 200: | ||||||
|             return redirect_login() |             return redirect_login() | ||||||
|         if 'groups' not in data or 'admin' not in data['groups']: |         if 'groups' not in data or 'admin' not in data['groups']: | ||||||
|             return 'Not an admin', 403 |             return 'Not an admin', 403 | ||||||
|     except TokenExpiredError: |     except MissingTokenError: | ||||||
|         return redirect_login() |         return redirect_login() | ||||||
|  |     return None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| admin_views.before_request(before_request) | admin_views.before_request(before_request) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @admin_views.route('/', methods=['GET', 'POST']) | @admin_views.route('/', methods=['GET', 'POST']) | ||||||
| def index(): | def index() -> ResponseReturnValue: | ||||||
|     return render_template('admin/index.html.j2') |     return render_template('admin/index.html.j2') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -44,13 +48,13 @@ def users(): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @admin_views.route('/registrations', methods=['GET']) | @admin_views.route('/registrations', methods=['GET']) | ||||||
| def registrations(): | def registrations() -> ResponseReturnValue: | ||||||
|     users = UserSignUp.query.all() |     users = UserSignUp.query.all() | ||||||
|     return render_template('admin/registrations.html.j2', users=users) |     return render_template('admin/registrations.html.j2', users=users) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @admin_views.route('/registration/<registration_id>', methods=['DELETE']) | @admin_views.route('/registration/<registration_id>', methods=['DELETE']) | ||||||
| def registration_delete(registration_id): | def registration_delete(registration_id) -> ResponseReturnValue: | ||||||
|     user_data = UserSignUp.query.get(registration_id) |     user_data = UserSignUp.query.get(registration_id) | ||||||
|     if user_data is None: |     if user_data is None: | ||||||
|         return jsonify({}), 404 |         return jsonify({}), 404 | ||||||
|  | @ -60,7 +64,7 @@ def registration_delete(registration_id): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @admin_views.route('/registration/<registration_id>', methods=['PUT']) | @admin_views.route('/registration/<registration_id>', methods=['PUT']) | ||||||
| def registration_accept(registration_id): | def registration_accept(registration_id) -> ResponseReturnValue: | ||||||
|     user_data = UserSignUp.query.get(registration_id) |     user_data = UserSignUp.query.get(registration_id) | ||||||
|     #create user |     #create user | ||||||
|     user = User.new(user_data) |     user = User.new(user_data) | ||||||
|  | @ -72,16 +76,15 @@ def registration_accept(registration_id): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @admin_views.route('/clients') | @admin_views.route('/clients') | ||||||
| def clients(): | def clients() -> ResponseReturnValue: | ||||||
|     clients = current_app.hydra_api.list_o_auth2_clients() |     clients = list_o_auth_2_clients.sync(_client=hydra_service.hydra_client) | ||||||
|     return render_template('admin/clients.html.j2', clients=clients) |     return render_template('admin/clients.html.j2', clients=clients) | ||||||
| 
 | 
 | ||||||
| @admin_views.route('/client/<client_id>', methods=['GET', 'POST']) | @admin_views.route('/client/<client_id>', methods=['GET', 'POST']) | ||||||
| def client(client_id: str): | def client(client_id: str) -> ResponseReturnValue: | ||||||
|      |      | ||||||
|     try: |     client = get_o_auth_2_client.sync(client_id, _client=hydra_service.hydra_client) | ||||||
|         client = current_app.hydra_api.get_o_auth2_client(client_id) |     if client is None or isinstance( client, GenericError): | ||||||
|     except ory_hydra_client.ApiException as e: |  | ||||||
|         logger.error(f"oauth2 client not found with id: '{client_id}'") |         logger.error(f"oauth2 client not found with id: '{client_id}'") | ||||||
|         return 'client not found', 404 |         return 'client not found', 404 | ||||||
| 
 | 
 | ||||||
|  | @ -89,9 +92,8 @@ def client(client_id: str): | ||||||
|     if form.validate_on_submit(): |     if form.validate_on_submit(): | ||||||
|         form.populate_obj(client) |         form.populate_obj(client) | ||||||
|   |   | ||||||
|         try: |         client = update_o_auth_2_client.sync(id=client_id ,json_body=client, _client=hydra_service.hydra_client) | ||||||
|             client = current_app.hydra_api.update_o_auth2_client(client_id, client) |         if client is None or isinstance(client, GenericError): | ||||||
|         except ory_hydra_client.ApiException as e: |  | ||||||
|             logger.error(f"oauth2 client update failed: '{client_id}'") |             logger.error(f"oauth2 client update failed: '{client_id}'") | ||||||
|             return 'client update failed', 500 |             return 'client update failed', 500 | ||||||
| 
 | 
 | ||||||
|  | @ -101,7 +103,7 @@ def client(client_id: str): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @admin_views.route('/client_new', methods=['GET','POST']) | @admin_views.route('/client_new', methods=['GET','POST']) | ||||||
| def client_new(): | def client_new() -> ResponseReturnValue: | ||||||
|      |      | ||||||
|     client = OAuth2Client() |     client = OAuth2Client() | ||||||
| 
 | 
 | ||||||
|  | @ -109,9 +111,8 @@ def client_new(): | ||||||
|     if form.validate_on_submit(): |     if form.validate_on_submit(): | ||||||
|         form.populate_obj(client) |         form.populate_obj(client) | ||||||
|   |   | ||||||
|         try: |         resp_client = create_o_auth_2_client.sync(json_body=client, _client=hydra_service.hydra_client) | ||||||
|             client = current_app.hydra_api.create_o_auth2_client(client) |         if resp_client is None: | ||||||
|         except ory_hydra_client.ApiException as e: |  | ||||||
|             logger.error(f"oauth2 client update failed: '{client.client_id}'") |             logger.error(f"oauth2 client update failed: '{client.client_id}'") | ||||||
|             return 'internal error', 500 |             return 'internal error', 500 | ||||||
|         return redirect(url_for('.client', client_id=client.client_id)) |         return redirect(url_for('.client', client_id=client.client_id)) | ||||||
|  |  | ||||||
|  | @ -4,26 +4,32 @@ from flask import current_app, session | ||||||
| from flask import jsonify | from flask import jsonify | ||||||
| from flask.helpers import make_response | from flask.helpers import make_response | ||||||
| from flask.templating import render_template | from flask.templating import render_template | ||||||
|  | from flask.typing import ResponseReturnValue | ||||||
| 
 | 
 | ||||||
| from flask import Blueprint, render_template, request, url_for | from flask import Blueprint, render_template, request, url_for | ||||||
| import logging | import logging | ||||||
| import requests |  | ||||||
| 
 | 
 | ||||||
| from ..model import User | from ..model import User | ||||||
| from ..auth_providers import LdapAuthProvider | from ..auth_providers import LdapAuthProvider | ||||||
|  | from ..hydra import hydra_service | ||||||
|  | from ory_hydra_client.api.admin import introspect_o_auth_2_token | ||||||
|  | from ory_hydra_client.models import GenericError | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| api_views = Blueprint('api', __name__, url_prefix='/api') | api_views = Blueprint('api', __name__, url_prefix='/api') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @api_views.route('/users', methods=['GET']) | @api_views.route('/users', methods=['GET']) | ||||||
| def user_list(): | def user_list() -> ResponseReturnValue: | ||||||
|     if 'authorization' not in request.headers: |     if 'authorization' not in request.headers: | ||||||
|         return '', 403 |         return '', 403 | ||||||
|     token = request.headers['authorization'].replace('Bearer ', '') |     token = request.headers['authorization'].replace('Bearer ', '') | ||||||
|     token_info = current_app.hydra_api.introspect_o_auth2_token(token=token) |     token_info = introspect_o_auth_2_token.sync(_client=hydra_service.hydra_client) | ||||||
| 
 | 
 | ||||||
|     if 'lc_i_userlist' not in token_info.scope.split(' '): |     if token_info is None or isinstance(token_info, GenericError): | ||||||
|  |         return 'internal errror', 500 | ||||||
|  | 
 | ||||||
|  |     if not isinstance(token_info.scope, str) or 'lc_i_userlist' not in token_info.scope.split(' '): | ||||||
|         return '', 403 |         return '', 403 | ||||||
| 
 | 
 | ||||||
|     return jsonify([ |     return jsonify([ | ||||||
|  |  | ||||||
|  | @ -1,11 +1,12 @@ | ||||||
| 
 | 
 | ||||||
|  | from authlib.integrations.flask_client import OAuth | ||||||
| from urllib.parse import urlencode, parse_qs | from urllib.parse import urlencode, parse_qs | ||||||
| 
 | 
 | ||||||
| import flask | import flask | ||||||
| from flask import Blueprint, redirect | from flask import Blueprint, redirect, flash, current_app, session | ||||||
| from flask import current_app, session |  | ||||||
| from flask.templating import render_template | from flask.templating import render_template | ||||||
| from flask_babel import gettext | from flask_babel import gettext | ||||||
|  | from flask.typing import ResponseReturnValue | ||||||
| 
 | 
 | ||||||
| from flask import request, url_for, jsonify | from flask import request, url_for, jsonify | ||||||
| from flask_login import login_required, login_user, logout_user, current_user | from flask_login import login_required, login_user, logout_user, current_user | ||||||
|  | @ -14,14 +15,17 @@ from urllib.parse import urlparse | ||||||
| from base64 import b64decode, b64encode | from base64 import b64decode, b64encode | ||||||
| import http | import http | ||||||
| import crypt | import crypt | ||||||
| import ory_hydra_client |  | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| import logging | import logging | ||||||
| import json | import json | ||||||
|  | from ory_hydra_client.api.admin import get_consent_request, accept_consent_request, accept_login_request, get_login_request, accept_login_request, accept_logout_request, get_login_request | ||||||
|  | from ory_hydra_client.models import AcceptLoginRequest, AcceptConsentRequest, ConsentRequestSession, GenericError, ConsentRequestSessionAccessToken, ConsentRequestSessionIdToken | ||||||
|  | from typing import Optional | ||||||
| 
 | 
 | ||||||
| from ..model import db, User, SecurityUser, UserSignUp | from ..model import db, User, SecurityUser, UserSignUp | ||||||
| from ..form.auth import ConsentForm, LoginForm, RegistrationForm | from ..form.auth import ConsentForm, LoginForm, RegistrationForm | ||||||
| from ..auth_providers import AUTH_PROVIDER_LIST | from ..auth_providers import AUTH_PROVIDER_LIST | ||||||
|  | from ..hydra import hydra_service | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  | @ -29,26 +33,31 @@ logger = logging.getLogger(__name__) | ||||||
| auth_views = Blueprint('auth', __name__, url_prefix='/auth') | auth_views = Blueprint('auth', __name__, url_prefix='/auth') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @auth_views.route('/consent', methods=['GET', 'POST']) | @auth_views.route('/consent', methods=['GET', 'POST']) | ||||||
| def consent(): | def consent() -> ResponseReturnValue: | ||||||
|     """Always grant consent.""" |     """Always grant consent.""" | ||||||
|     # DUMMPY ONLY |     # DUMMPY ONLY | ||||||
| 
 | 
 | ||||||
|     form = ConsentForm() |     form = ConsentForm() | ||||||
|     remember_for = 60*60*24*30  # remember for 30 days |     remember_for = 60*60*24*30  # remember for 30 days | ||||||
| 
 | 
 | ||||||
|     try: |     #try: | ||||||
|         consent_request = current_app.hydra_api.get_consent_request( |     consent_request = get_consent_request.sync(consent_challenge=request.args['consent_challenge'],_client=hydra_service.hydra_client) | ||||||
|                                     request.args['consent_challenge']) |  | ||||||
|     except ory_hydra_client.exceptions.ApiValueError: |  | ||||||
|         logger.info('ory exception - could not fetch user data ApiValueError') |  | ||||||
|         return redirect(url_for('frontend.index')) |  | ||||||
|     except ory_hydra_client.exceptions.ApiException: |  | ||||||
|         logger.exception('ory exception - could not fetch user data') |  | ||||||
|         return redirect(url_for('frontend.index')) |  | ||||||
| 
 | 
 | ||||||
|     requested_scope = consent_request.requested_scope["value"] |     if consent_request is None or isinstance( consent_request, GenericError): | ||||||
|     requested_audiences = consent_request.requested_access_token_audience["value"] |        return redirect(url_for('frontend.index')) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #   except ory_hydra_client.exceptions.ApiValueError: | ||||||
|  | #       logger.info('ory exception - could not fetch user data ApiValueError') | ||||||
|  | #       return redirect(url_for('frontend.index')) | ||||||
|  | #   except ory_hydra_client.exceptions.ApiException: | ||||||
|  | #       logger.exception('ory exception - could not fetch user data') | ||||||
|  | #       return redirect(url_for('frontend.index')) | ||||||
|  | 
 | ||||||
|  |     requested_scope = consent_request.requested_scope | ||||||
|  |     requested_audiences = consent_request.requested_access_token_audience | ||||||
| 
 | 
 | ||||||
|     if form.validate_on_submit() or consent_request.skip: |     if form.validate_on_submit() or consent_request.skip: | ||||||
|         user = User.query.get(consent_request.subject) |         user = User.query.get(consent_request.subject) | ||||||
|  | @ -60,19 +69,25 @@ def consent(): | ||||||
|             'groups': [group.name for group in user.groups] |             'groups': [group.name for group in user.groups] | ||||||
|         } |         } | ||||||
|         id_token_data = {} |         id_token_data = {} | ||||||
|         if 'openid' in requested_scope: |         if isinstance(requested_scope, list) and 'openid' in requested_scope: | ||||||
|             id_token_data = token_data |             id_token_data = token_data | ||||||
|         resp = current_app.hydra_api.accept_consent_request( |         access_token=ConsentRequestSessionAccessToken.from_dict(token_data) | ||||||
|             consent_request.challenge, body={ |         id_token=ConsentRequestSessionIdToken.from_dict(id_token_data) | ||||||
|                 'grant_scope': requested_scope, |         body = AcceptConsentRequest( | ||||||
|                 'grant_access_token_audience': requested_audiences, |                 grant_scope= requested_scope, | ||||||
|                 'remember': form.data['remember'], |                 grant_access_token_audience= requested_audiences, | ||||||
|                 'remember_for': remember_for, |                 remember= form.data['remember'], | ||||||
|                 'session': { |                 remember_for= remember_for, | ||||||
|                     'access_token': token_data, |                 session= ConsentRequestSession( | ||||||
|                     'id_token': id_token_data |                     access_token= access_token, | ||||||
|                 } |                     id_token= id_token | ||||||
|             }) |                 ) | ||||||
|  |         ) | ||||||
|  |         resp = accept_consent_request.sync(_client=hydra_service.hydra_client, | ||||||
|  |             json_body=body, | ||||||
|  |             consent_challenge=consent_request.challenge) | ||||||
|  |         if resp is None or isinstance( resp, GenericError): | ||||||
|  |             return 'internal error, could not forward request', 503 | ||||||
|         return redirect(resp.redirect_to) |         return redirect(resp.redirect_to) | ||||||
|     return render_template( |     return render_template( | ||||||
|             'auth/consent.html.j2', |             'auth/consent.html.j2', | ||||||
|  | @ -83,18 +98,22 @@ def consent(): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @auth_views.route('/login', methods=['GET', 'POST']) | @auth_views.route('/login', methods=['GET', 'POST']) | ||||||
| def login(): | def login() -> ResponseReturnValue: | ||||||
|     login_challenge = request.args.get('login_challenge') |     login_challenge = request.args.get('login_challenge') | ||||||
|     try: |     if login_challenge is None: | ||||||
|         login_request = current_app.hydra_api.get_login_request(login_challenge) |         return 'login_challenge missing', 400 | ||||||
|     except ory_hydra_client.exceptions.ApiException as e: |     login_request = get_login_request.sync(_client=hydra_service.hydra_client, login_challenge=login_challenge) | ||||||
|  |     if login_request is None or isinstance( login_request, GenericError): | ||||||
|         logger.exception("could not fetch login request") |         logger.exception("could not fetch login request") | ||||||
|         return redirect(url_for('frontend.index')) |         return redirect(url_for('frontend.index')) | ||||||
| 
 | 
 | ||||||
|     if login_request.skip: |     if login_request.skip: | ||||||
|         resp = current_app.hydra_api.accept_login_request( |         resp = accept_login_request.sync(_client=hydra_service.hydra_client, | ||||||
|             login_challenge, |             login_challenge=login_challenge, | ||||||
|             body={'subject': login_request.subject}) |             json_body=AcceptLoginRequest(subject=login_request.subject)) | ||||||
|  |         if resp is None or isinstance( resp, GenericError): | ||||||
|  |             return 'internal error, could not forward request', 503 | ||||||
|  | 
 | ||||||
|         return redirect(resp.redirect_to) |         return redirect(resp.redirect_to) | ||||||
|     form = LoginForm() |     form = LoginForm() | ||||||
|     if form.validate_on_submit(): |     if form.validate_on_submit(): | ||||||
|  | @ -110,11 +129,12 @@ def login(): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @auth_views.route('/login/auth', methods=['GET', 'POST']) | @auth_views.route('/login/auth', methods=['GET', 'POST']) | ||||||
| def login_auth(): | def login_auth() -> ResponseReturnValue: | ||||||
|     login_challenge = request.args.get('login_challenge') |     login_challenge = request.args.get('login_challenge') | ||||||
|     try: |     if login_challenge is None: | ||||||
|         login_request = current_app.hydra_api.get_login_request(login_challenge) |         return 'missing login_challenge, bad request', 400 | ||||||
|     except ory_hydra_client.exceptions.ApiValueError: |     login_request = get_login_request.sync(_client=hydra_service.hydra_client, login_challenge=login_challenge) | ||||||
|  |     if login_request is None: | ||||||
|         return redirect(url_for('frontend.index')) |         return redirect(url_for('frontend.index')) | ||||||
| 
 | 
 | ||||||
|     if 'username' not in session: |     if 'username' not in session: | ||||||
|  | @ -140,25 +160,31 @@ def login_auth(): | ||||||
|         subject = user.id |         subject = user.id | ||||||
|         user.last_login = datetime.now() |         user.last_login = datetime.now() | ||||||
|         db.session.commit() |         db.session.commit() | ||||||
|         resp = current_app.hydra_api.accept_login_request( |         resp = accept_login_request.sync(_client=hydra_service.hydra_client, | ||||||
|             login_challenge, body={ |             login_challenge=login_challenge, json_body=AcceptLoginRequest( | ||||||
|                 'subject': subject, |                 subject=subject, | ||||||
|                 'remember': remember_me, |                 remember=remember_me, | ||||||
|             }) |             )) | ||||||
|  |         if resp is None or isinstance( resp, GenericError): | ||||||
|  |             return 'internal error, could not forward request', 503 | ||||||
|         return redirect(resp.redirect_to) |         return redirect(resp.redirect_to) | ||||||
|     return render_template('auth/login_auth.html.j2', forms=auth_forms) |     return render_template('auth/login_auth.html.j2', forms=auth_forms) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @auth_views.route("/logout") | @auth_views.route("/logout") | ||||||
| def logout(): | def logout() -> ResponseReturnValue: | ||||||
|     logout_challenge = request.args.get('logout_challenge') |     logout_challenge = request.args.get('logout_challenge') | ||||||
|  |     if logout_challenge is None: | ||||||
|  |         return 'invalid request, logout_challenge not set', 400 | ||||||
|     # TODO confirm |     # TODO confirm | ||||||
|     resp = current_app.hydra_api.accept_logout_request(logout_challenge) |     resp = accept_logout_request.sync(_client=hydra_service.hydra_client, logout_challenge=logout_challenge) | ||||||
|  |     if resp is None or isinstance( resp, GenericError): | ||||||
|  |         return 'internal error, could not forward request', 503 | ||||||
|     return redirect(resp.redirect_to) |     return redirect(resp.redirect_to) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @auth_views.route("/error", methods=["GET"]) | @auth_views.route("/error", methods=["GET"]) | ||||||
| def auth_error(): | def auth_error() -> ResponseReturnValue: | ||||||
|     error = request.args.get('error') |     error = request.args.get('error') | ||||||
|     error_description = request.args.get('error_description') |     error_description = request.args.get('error_description') | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,109 +1,58 @@ | ||||||
| 
 | 
 | ||||||
|  | from authlib.integrations.base_client.errors import MissingTokenError, InvalidTokenError | ||||||
| from urllib.parse import urlencode, parse_qs | from urllib.parse import urlencode, parse_qs | ||||||
| 
 |  | ||||||
| from flask import Blueprint, redirect, request | from flask import Blueprint, redirect, request | ||||||
| from flask import current_app | from flask import current_app | ||||||
| from flask import jsonify, session | from flask import jsonify, session | ||||||
| from flask import render_template, url_for, flash | from flask import render_template, url_for | ||||||
| from flask_login import login_user, logout_user, current_user | from flask_login import login_user, logout_user, current_user | ||||||
| from werkzeug.utils import redirect | from werkzeug.utils import redirect | ||||||
| import logging | import logging | ||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
| from base64 import b64decode | from base64 import b64decode | ||||||
| from flask_dance.consumer import oauth_authorized | from flask.typing import ResponseReturnValue  | ||||||
| from flask_dance.consumer.base import oauth_before_login |  | ||||||
| from flask_dance.consumer import OAuth2ConsumerBlueprint |  | ||||||
| from oauthlib.oauth2.rfc6749.errors import TokenExpiredError | from oauthlib.oauth2.rfc6749.errors import TokenExpiredError | ||||||
|  | from ory_hydra_client.api.admin import list_subject_consent_sessions, revoke_consent_sessions | ||||||
|  | from ory_hydra_client.models import GenericError | ||||||
|  | from typing import Optional | ||||||
| 
 | 
 | ||||||
| from ..model import db, User, SecurityUser, Totp | from ..model import db, User, SecurityUser, Totp | ||||||
| from ..form.frontend import ClientCertForm, TOTPForm, \ | from ..form.frontend import ClientCertForm, TOTPForm, \ | ||||||
|     TOTPDeleteForm, PasswordChangeForm |     TOTPDeleteForm, PasswordChangeForm | ||||||
| from ..auth_providers import LdapAuthProvider | from ..auth_providers import LdapAuthProvider | ||||||
|  | from .oauth2 import redirect_login, oauth2 | ||||||
|  | from ..hydra import hydra_service | ||||||
| 
 | 
 | ||||||
| frontend_views = Blueprint('frontend', __name__, url_prefix='') | frontend_views = Blueprint('frontend', __name__, url_prefix='') | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| 
 | 
 | ||||||
| def redirect_login(): |  | ||||||
|     logout_user() |  | ||||||
|     session['next_url'] = request.path |  | ||||||
|     return redirect(url_for('oauth.login', next_url=request.path)) |  | ||||||
| 
 | 
 | ||||||
| def before_request(): | def before_request() -> Optional[ResponseReturnValue]: | ||||||
|     try: |     try: | ||||||
|         resp = current_app.oauth.session.get('/userinfo') |         resp = oauth2.custom.get('/userinfo') | ||||||
|         if not current_user.is_authenticated or resp.status_code != 200: |         if not current_user.is_authenticated or resp.status_code != 200: | ||||||
|             logger.info('user not logged in redirect') |             logger.info('user not logged in redirect') | ||||||
|             return redirect_login() |             return redirect_login() | ||||||
|     except TokenExpiredError: |     except MissingTokenError: | ||||||
|         return redirect_login() |         return redirect_login() | ||||||
|  |     except InvalidTokenError: | ||||||
|  |         return redirect_login() | ||||||
|  | 
 | ||||||
|  |     return None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| frontend_views.before_request(before_request) | frontend_views.before_request(before_request) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def init_login_manager(app): |  | ||||||
|     @app.login_manager.user_loader |  | ||||||
|     def user_loader(username): |  | ||||||
|         return User.query_().by_username(username) |  | ||||||
| 
 |  | ||||||
|     @app.login_manager.request_loader |  | ||||||
|     def request_loader(_request): |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
|     @app.login_manager.unauthorized_handler |  | ||||||
|     def unauthorized(): |  | ||||||
|         redirect_login() |  | ||||||
| 
 |  | ||||||
|     base_url = app.config['HYDRA_PUBLIC_URL'] |  | ||||||
|     example_blueprint = OAuth2ConsumerBlueprint( |  | ||||||
|         "oauth", __name__, |  | ||||||
|         client_id=app.config['OAUTH_ID'], |  | ||||||
|         client_secret=app.config['OAUTH_SECRET'], |  | ||||||
|         base_url=base_url, |  | ||||||
|         token_url=f"{base_url}/oauth2/token", |  | ||||||
|         authorization_url=f"{base_url}/oauth2/auth", |  | ||||||
|         scope=['openid', 'profile', 'manage'] |  | ||||||
|     ) |  | ||||||
|     app.register_blueprint(example_blueprint, url_prefix="/") |  | ||||||
|     app.oauth = example_blueprint |  | ||||||
| 
 |  | ||||||
|     @oauth_authorized.connect_via(app.oauth) |  | ||||||
|     def oauth2_logged_in(blueprint, token): |  | ||||||
|         if not token: |  | ||||||
|             flash("Failed to log in.", category="error") |  | ||||||
|             return False |  | ||||||
|         #print(f'debug ---------------{token}') |  | ||||||
| 
 |  | ||||||
|         resp = blueprint.session.get("/userinfo") |  | ||||||
|         if not resp.ok: |  | ||||||
|             msg = "Failed to fetch user info from hydra." |  | ||||||
|             flash(msg, category="error") |  | ||||||
|             return False |  | ||||||
| 
 |  | ||||||
|         oauth_info = resp.json() |  | ||||||
| 
 |  | ||||||
|         db_user = User.query.get(str(oauth_info["sub"])) |  | ||||||
| 
 |  | ||||||
|         login_user(SecurityUser(db_user.username)) |  | ||||||
|         #flash("Successfully signed in with GitHub.") |  | ||||||
| 
 |  | ||||||
|         # Since we're manually creating the OAuth model in the database, |  | ||||||
|         # we should return False so that Flask-Dance knows that |  | ||||||
|         # it doesn't have to do it. If we don't return False, the OAuth token |  | ||||||
|         # could be saved twice, or Flask-Dance could throw an error when |  | ||||||
|         # trying to incorrectly save it for us. |  | ||||||
|         return True |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @frontend_views.route('/logout') | @frontend_views.route('/logout') | ||||||
| def logout(): | def logout() -> ResponseReturnValue: | ||||||
|     logout_user() |     logout_user() | ||||||
|     return redirect( |     return redirect( | ||||||
|         f'{current_app.config["HYDRA_PUBLIC_URL"]}/oauth2/sessions/logout') |         f'{current_app.config["HYDRA_PUBLIC_URL"]}/oauth2/sessions/logout') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frontend_views.route('/', methods=['GET']) | @frontend_views.route('/', methods=['GET']) | ||||||
| def index(): | def index() -> ResponseReturnValue: | ||||||
|     if 'next_url' in session: |     if 'next_url' in session: | ||||||
|         next_url = session['next_url'] |         next_url = session['next_url'] | ||||||
|         del session['next_url'] |         del session['next_url'] | ||||||
|  | @ -112,7 +61,7 @@ def index(): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frontend_views.route('/client_cert') | @frontend_views.route('/client_cert') | ||||||
| def client_cert(): | def client_cert() -> ResponseReturnValue: | ||||||
|     client_certs = {} |     client_certs = {} | ||||||
|     for service in current_app.lenticular_services.values(): |     for service in current_app.lenticular_services.values(): | ||||||
|         client_certs[str(service.name)] = \ |         client_certs[str(service.name)] = \ | ||||||
|  | @ -125,7 +74,7 @@ def client_cert(): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frontend_views.route('/client_cert/<service_name>/<serial_number>') | @frontend_views.route('/client_cert/<service_name>/<serial_number>') | ||||||
| def get_client_cert(service_name, serial_number): | def get_client_cert(service_name, serial_number) -> ResponseReturnValue: | ||||||
|     service = current_app.lenticular_services[service_name] |     service = current_app.lenticular_services[service_name] | ||||||
|     cert = current_app.pki.get_client_cert( |     cert = current_app.pki.get_client_cert( | ||||||
|             current_user, service, serial_number) |             current_user, service, serial_number) | ||||||
|  | @ -137,7 +86,7 @@ def get_client_cert(service_name, serial_number): | ||||||
| 
 | 
 | ||||||
| @frontend_views.route( | @frontend_views.route( | ||||||
|         '/client_cert/<service_name>/<serial_number>', methods=['DELETE']) |         '/client_cert/<service_name>/<serial_number>', methods=['DELETE']) | ||||||
| def revoke_client_cert(service_name, serial_number): | def revoke_client_cert(service_name, serial_number) -> ResponseReturnValue: | ||||||
|     service = current_app.lenticular_services[service_name] |     service = current_app.lenticular_services[service_name] | ||||||
|     cert = current_app.pki.get_client_cert( |     cert = current_app.pki.get_client_cert( | ||||||
|             current_user, service, serial_number) |             current_user, service, serial_number) | ||||||
|  | @ -148,7 +97,7 @@ def revoke_client_cert(service_name, serial_number): | ||||||
| @frontend_views.route( | @frontend_views.route( | ||||||
|         '/client_cert/<service_name>/new', |         '/client_cert/<service_name>/new', | ||||||
|         methods=['GET', 'POST']) |         methods=['GET', 'POST']) | ||||||
| def client_cert_new(service_name): | def client_cert_new(service_name) -> ResponseReturnValue: | ||||||
|     service = current_app.lenticular_services[service_name] |     service = current_app.lenticular_services[service_name] | ||||||
|     form = ClientCertForm() |     form = ClientCertForm() | ||||||
|     if form.validate_on_submit(): |     if form.validate_on_submit(): | ||||||
|  | @ -177,13 +126,13 @@ def client_cert_new(service_name): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frontend_views.route('/totp') | @frontend_views.route('/totp') | ||||||
| def totp(): | def totp() -> ResponseReturnValue: | ||||||
|     delete_form = TOTPDeleteForm() |     delete_form = TOTPDeleteForm() | ||||||
|     return render_template('frontend/totp.html.j2', delete_form=delete_form) |     return render_template('frontend/totp.html.j2', delete_form=delete_form) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frontend_views.route('/totp/new', methods=['GET', 'POST']) | @frontend_views.route('/totp/new', methods=['GET', 'POST']) | ||||||
| def totp_new(): | def totp_new() -> ResponseReturnValue: | ||||||
|     form = TOTPForm() |     form = TOTPForm() | ||||||
| 
 | 
 | ||||||
|     if form.validate_on_submit(): |     if form.validate_on_submit(): | ||||||
|  | @ -203,7 +152,7 @@ def totp_new(): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frontend_views.route('/totp/<totp_name>/delete', methods=['GET', 'POST']) | @frontend_views.route('/totp/<totp_name>/delete', methods=['GET', 'POST']) | ||||||
| def totp_delete(totp_name): | def totp_delete(totp_name) -> ResponseReturnValue: | ||||||
|     totp = Totp.query.filter(Totp.name == totp_name).first() |     totp = Totp.query.filter(Totp.name == totp_name).first() | ||||||
|     db.session.delete(totp) |     db.session.delete(totp) | ||||||
|     db.session.commit() |     db.session.commit() | ||||||
|  | @ -213,13 +162,13 @@ def totp_delete(totp_name): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frontend_views.route('/password_change') | @frontend_views.route('/password_change') | ||||||
| def password_change(): | def password_change() -> ResponseReturnValue: | ||||||
|     form = PasswordChangeForm() |     form = PasswordChangeForm() | ||||||
|     return render_template('frontend/password_change.html.j2', form=form) |     return render_template('frontend/password_change.html.j2', form=form) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frontend_views.route('/password_change', methods=['POST']) | @frontend_views.route('/password_change', methods=['POST']) | ||||||
| def password_change_post(): | def password_change_post() -> ResponseReturnValue: | ||||||
|     form = PasswordChangeForm() |     form = PasswordChangeForm() | ||||||
|     if form.validate(): |     if form.validate(): | ||||||
|         password_old = str(form.data['password_old']) |         password_old = str(form.data['password_old']) | ||||||
|  | @ -230,7 +179,6 @@ def password_change_post(): | ||||||
|                     {'errors': {'password_old': 'Old Password is invalid'}}) |                     {'errors': {'password_old': 'Old Password is invalid'}}) | ||||||
|         resp = current_user.change_password(password_new) |         resp = current_user.change_password(password_new) | ||||||
|         if resp: |         if resp: | ||||||
|             print(current_user) |  | ||||||
|             return jsonify({}) |             return jsonify({}) | ||||||
|         else: |         else: | ||||||
|             return jsonify({'errors': {'internal': 'internal server errror'}}) |             return jsonify({'errors': {'internal': 'internal server errror'}}) | ||||||
|  | @ -238,23 +186,22 @@ def password_change_post(): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frontend_views.route('/oauth2_token') | @frontend_views.route('/oauth2_token') | ||||||
| def oauth2_tokens(): | def oauth2_tokens() -> ResponseReturnValue: | ||||||
| 
 | 
 | ||||||
|     subject = current_app.oauth.session.get('/userinfo').json()['sub'] |     subject = oauth2.custom.get('/userinfo').json()['sub'] | ||||||
|     consent_sessions = current_app.hydra_api.list_subject_consent_sessions( |     consent_sessions = list_subject_consent_sessions.sync(subject=subject, _client=hydra_service.hydra_client) | ||||||
|                                 subject) |     if consent_sessions is None or isinstance( consent_sessions, GenericError): | ||||||
| 
 |        return 'internal error, could not fetch sessions', 500 | ||||||
|     print(consent_sessions) |  | ||||||
|     return render_template( |     return render_template( | ||||||
|             'frontend/oauth2_tokens.html.j2', |             'frontend/oauth2_tokens.html.j2', | ||||||
|             consent_sessions=consent_sessions) |             consent_sessions=consent_sessions) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE']) | @frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE']) | ||||||
| def oauth2_token_revoke(client_id: str): | def oauth2_token_revoke(client_id: str) -> ResponseReturnValue: | ||||||
|     subject = current_app.oauth.session.get('/userinfo').json()['sub'] |     subject = oauth2.session.get('/userinfo').json()['sub'] | ||||||
|     current_app.hydra_api.revoke_consent_sessions( |     revoke_consent_sessions.sync( _client=hydra_service.hydra_client, | ||||||
|                                 subject, |                                 subject=subject, | ||||||
|                                 client=client_id) |                                 client=client_id) | ||||||
| 
 | 
 | ||||||
|     return jsonify({}) |     return jsonify({}) | ||||||
|  |  | ||||||
							
								
								
									
										87
									
								
								lenticular_cloud/views/oauth2.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								lenticular_cloud/views/oauth2.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,87 @@ | ||||||
|  | from authlib.integrations.flask_client import OAuth | ||||||
|  | from authlib.integrations.base_client.errors import MismatchingStateError | ||||||
|  | from flask import Flask, Blueprint, session, request, redirect, url_for | ||||||
|  | from flask_login import login_user, logout_user, current_user | ||||||
|  | from flask.typing import ResponseReturnValue | ||||||
|  | from flask_login import LoginManager | ||||||
|  | from typing import Optional | ||||||
|  | 
 | ||||||
|  | from ..model import User, SecurityUser | ||||||
|  | 
 | ||||||
|  | def fetch_token(name: str) -> Optional[dict]: | ||||||
|  |     token = session['token'] | ||||||
|  |     if isinstance(token, dict): | ||||||
|  |         return token | ||||||
|  |     return None | ||||||
|  | 
 | ||||||
|  | oauth2 = OAuth(fetch_token=fetch_token) | ||||||
|  | 
 | ||||||
|  | oauth2_views = Blueprint('oauth2', __name__, url_prefix='/oauth') | ||||||
|  | 
 | ||||||
|  | login_manager = LoginManager() | ||||||
|  | 
 | ||||||
|  | def redirect_login() -> ResponseReturnValue: | ||||||
|  |     logout_user() | ||||||
|  |     session['next_url'] = request.path | ||||||
|  |     redirect_uri = url_for('oauth2.authorized', _external=True) | ||||||
|  |     return oauth2.custom.authorize_redirect(redirect_uri) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @oauth2_views.route('/authorized') | ||||||
|  | def authorized() -> ResponseReturnValue: | ||||||
|  |     try: | ||||||
|  |         token = oauth2.custom.authorize_access_token() | ||||||
|  |     except MismatchingStateError: | ||||||
|  |         return redirect(url_for('oauth2.login')) | ||||||
|  |     if token is None: | ||||||
|  |         return 'bad request', 400 | ||||||
|  |     session['token'] = token | ||||||
|  |     userinfo = oauth2.custom.get('/userinfo').json() | ||||||
|  |     db_user = User.query.get(str(userinfo["sub"])) | ||||||
|  |     login_user(SecurityUser(db_user.username)) | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  |     next_url = request.args.get('next_url') | ||||||
|  |     if next_url is None: | ||||||
|  |         next_url = '/' | ||||||
|  |     return redirect(next_url) | ||||||
|  | 
 | ||||||
|  | @oauth2_views.route('login') | ||||||
|  | def login() -> ResponseReturnValue: | ||||||
|  |     redirect_uri = url_for('.authorized', _external=True) | ||||||
|  |     return oauth2.custom.authorize_redirect(redirect_uri) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @login_manager.user_loader | ||||||
|  | def user_loader(username) -> Optional[User]: | ||||||
|  |     user =  User.query_().by_username(username) | ||||||
|  |     if isinstance(user, User): | ||||||
|  |         return user | ||||||
|  |     else: | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  | @login_manager.request_loader | ||||||
|  | def request_loader(_request): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | @login_manager.unauthorized_handler | ||||||
|  | def unauthorized(): | ||||||
|  |     redirect_login() | ||||||
|  | 
 | ||||||
|  | def init_login_manager(app: Flask): | ||||||
|  | 
 | ||||||
|  |     base_url = app.config['HYDRA_PUBLIC_URL'] | ||||||
|  |     oauth2.register( | ||||||
|  |         name="custom", | ||||||
|  |         client_id=app.config['OAUTH_ID'], | ||||||
|  |         client_secret=app.config['OAUTH_SECRET'], | ||||||
|  |         access_token_url=f"{base_url}/oauth2/token", | ||||||
|  |         authorize_url=f"{base_url}/oauth2/auth", | ||||||
|  |         api_base_url=base_url, | ||||||
|  | 
 | ||||||
|  |         client_kwargs={'scope': ' '.join(['openid', 'profile', 'manage'])} | ||||||
|  |     ) | ||||||
|  |     oauth2.init_app(app) | ||||||
|  |     login_manager.init_app(app) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @ -1,4 +1,6 @@ | ||||||
| 
 | 
 | ||||||
| ## regenerate lib | ## regenerate lib | ||||||
| openapi-python-client generate --path ../specs/hydra.yaml --meta setup --custom-template-path ../specs/api-template | ```sh | ||||||
|  | openapi-python-client generate --path ../specs/hydra.yaml --meta setup --custom-template-path ../specs/api_template/ | ||||||
|  | ``` | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								mypy.ini
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								mypy.ini
									
										
									
									
									
								
							|  | @ -4,3 +4,5 @@ | ||||||
| warn_return_any = True | warn_return_any = True | ||||||
| warn_unused_configs = True | warn_unused_configs = True | ||||||
| ignore_missing_imports = True | ignore_missing_imports = True | ||||||
|  | follow_imports_for_stubs= True | ||||||
|  | files=lenticular_cloud/**/*.py | ||||||
|  |  | ||||||
|  | @ -1 +1,2 @@ | ||||||
| flask-debug | flask-debug | ||||||
|  | types-python-dateutil | ||||||
|  |  | ||||||
|  | @ -15,6 +15,6 @@ cryptography | ||||||
| requests | requests | ||||||
| requests_oauthlib | requests_oauthlib | ||||||
| blinker | blinker | ||||||
| ory-hydra-client |  | ||||||
| 
 | 
 | ||||||
|  | ./libs/ory-hydra-client | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue