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  | ||||
| 
 | ||||
|   nixpkgs_unstable = import <nixpkgs_unstable> {}; | ||||
| 
 | ||||
|   urlobject = with python.pkgs; buildPythonPackage rec { | ||||
|     pname = "URLObject"; | ||||
|     version = "2.4.3"; | ||||
|  | @ -16,9 +14,20 @@ let | |||
|     doCheck = true; | ||||
|     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 { | ||||
|     pname = "Flask-Dance"; | ||||
|  | @ -37,6 +46,7 @@ let | |||
|     ]; | ||||
|     checkInputs = [ | ||||
|       pytest | ||||
|       nose | ||||
|       pytest-mock | ||||
|       responses | ||||
|       freezegun | ||||
|  | @ -89,7 +99,7 @@ let | |||
|     propagatedBuildInputs = [ | ||||
|       urllib3 | ||||
|       python-dateutil | ||||
|       nixpkgs_unstable.python39Packages.attrs | ||||
|       python_attrs | ||||
|       httpx | ||||
|     ]; | ||||
|   }; | ||||
|  | @ -110,6 +120,7 @@ in | |||
|       cryptography | ||||
|       blinker | ||||
|       ory-hydra-client | ||||
|       authlib | ||||
| 
 | ||||
|       gunicorn | ||||
| 
 | ||||
|  | @ -127,6 +138,8 @@ in | |||
|     pytest-mypy | ||||
|     flask_testing | ||||
|     tox | ||||
| 
 | ||||
|     nose | ||||
|     mypy | ||||
|   ]; | ||||
| } | ||||
|  |  | |||
|  | @ -1,18 +1,18 @@ | |||
| from flask.app import Flask | ||||
| from flask import g, redirect, request | ||||
| from flask.helpers import url_for | ||||
| from flask_babel import Babel | ||||
| from flask_login import LoginManager | ||||
| import time | ||||
| import subprocess | ||||
| import ory_hydra_client as hydra | ||||
| import ory_hydra_client.api.admin_api as hydra_admin_api | ||||
| from ory_hydra_client import Client | ||||
| import os | ||||
| 
 | ||||
| from ldap3 import Connection, Server, ALL | ||||
| 
 | ||||
| from . import model | ||||
| from .pki import Pki | ||||
| from .hydra import hydra_service | ||||
| from .translations import init_babel | ||||
| 
 | ||||
| 
 | ||||
| def get_git_hash(): | ||||
|  | @ -22,12 +22,7 @@ def get_git_hash(): | |||
|         return '' | ||||
| 
 | ||||
| 
 | ||||
| def init_oauth2(app): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| def create_app(): | ||||
| def create_app() -> Flask: | ||||
|     name = "lenticular_cloud" | ||||
|     app = Flask(name, template_folder='template') | ||||
|     app.config.from_pyfile('application.cfg') | ||||
|  | @ -47,35 +42,31 @@ def create_app(): | |||
|     with app.app_context(): | ||||
|         db.create_all() | ||||
| 
 | ||||
|     app.babel = Babel(app) | ||||
|     init_oauth2(app) | ||||
|     init_babel(app) | ||||
|     app.login_manager = LoginManager(app) | ||||
| 
 | ||||
|     #init hydra admin api | ||||
|     hydra_config = hydra.Configuration( | ||||
|                         host=app.config['HYDRA_ADMIN_URL'], | ||||
|                         username=app.config['HYDRA_ADMIN_USER'], | ||||
|                         password=app.config['HYDRA_ADMIN_PASSWORD']) | ||||
|     hydra_client = hydra.ApiClient(hydra_config) | ||||
|     app.hydra_api = hydra_admin_api.AdminApi(hydra_client) | ||||
| #    hydra_config = hydra.Configuration( | ||||
| #                        host=app.config['HYDRA_ADMIN_URL'], | ||||
| #                        username=app.config['HYDRA_ADMIN_USER'], | ||||
| #                        password=app.config['HYDRA_ADMIN_PASSWORD']) | ||||
|     hydra_service.set_hydra_client(Client(base_url=app.config['HYDRA_ADMIN_URL'])) | ||||
| 
 | ||||
|     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) | ||||
|     #oauth2.init_app(app) | ||||
|     app.register_blueprint(auth_views) | ||||
|     app.register_blueprint(frontend_views) | ||||
|     app.register_blueprint(api_views) | ||||
|     app.register_blueprint(pki_views) | ||||
|     app.register_blueprint(admin_views) | ||||
|     app.register_blueprint(oauth2_views) | ||||
| 
 | ||||
|     @app.before_request | ||||
|     def befor_request(): | ||||
|         request_start_time = time.time() | ||||
|         g.request_time = lambda: "%.5fs" % (time.time() - request_start_time) | ||||
| 
 | ||||
|     from .translations import init_babel | ||||
| 
 | ||||
|     init_babel(app) | ||||
| 
 | ||||
|     app.lenticular_services = {} | ||||
|     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) | ||||
|  |  | |||
|  | @ -44,6 +44,7 @@ def entry_point(): | |||
|     app = create_app() | ||||
|     if args.func == cli_run: | ||||
|         cli_run(app,args) | ||||
|         return | ||||
|     with app.app_context(): | ||||
|         args.func(args) | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,29 +6,53 @@ from wtforms import StringField, SubmitField, TextField, \ | |||
|         SelectField, Form as NoCsrfForm, SelectMultipleField | ||||
| 
 | ||||
| from wtforms.fields.html5 import URLField | ||||
| from wtforms.fields import FormField | ||||
| from wtforms.fields import FormField, SelectMultipleField | ||||
| 
 | ||||
| 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): | ||||
|     client_id = StringField(gettext('client_id') ) | ||||
|     client_name = StringField(gettext('client_name')) | ||||
|     client_uri = URLField(gettext('client_uri')) | ||||
|     client_secret = PasswordField(gettext('client_secret')) | ||||
|     logo_uri = URLField(gettext('logo_uri')) | ||||
|     redirect_uris = FieldList(FormField(URLField(gettext('logo_uri')))) | ||||
|     #contacts = List[str] | ||||
|     #grant_types = List[str] | ||||
|     #response_types = List[str] | ||||
|     redirect_uris = FieldList(URLField(gettext('redirect_uri')), min_entries=1) | ||||
|     contacts =   FieldList(StringField('contacts')) | ||||
|     grant_types =  SelectMultipleField('grant_types',choices=[(x, x) for x in ['authorization_code', 'refresh_token', 'implicit']]) | ||||
|     response_types = SelectMultipleField('repsonse_type',choices=[(x, x) for x in ['code token', 'code', 'id_token']]) | ||||
|     scope = StringField(gettext('scope')) | ||||
|     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')) | ||||
| 
 | ||||
|     client_secret_expires_at = IntegerField('client_secret_expires_at') | ||||
| 
 | ||||
| 
 | ||||
|     #allowed_cross_origins = Array | ||||
|     contacts =   FieldList(StringField('contacts')) | ||||
|     #audience = List[str] | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ from ..model import db | |||
| 
 | ||||
| class FieldList(WTFFieldList): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         self.modify = kwargs.pop("modify", True) | ||||
|         super().__init__(*args, **kwargs) | ||||
| 
 | ||||
|     def get_template(self) -> Field: | ||||
|  | @ -16,7 +17,6 @@ class FieldList(WTFFieldList): | |||
| class ModelFieldList(FieldList): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         self.model = kwargs.pop("model", None) | ||||
|         self.modify = kwargs.pop("modify", True) | ||||
|         super(ModelFieldList, self).__init__(*args, **kwargs) | ||||
|         if not self.model: | ||||
|             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 %} | ||||
| 		<template id='{{field.name}}-template' data-fieldlist-name="{{ field.name }}" data-fieldlist-next-id="{{ field | length }}"> | ||||
| 		<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> | ||||
| 		</li> | ||||
| 	</template> | ||||
|  | @ -174,6 +174,8 @@ | |||
|         {{ render_password_field(f, **kwargs) }} | ||||
|     {% elif f.type == 'DecimalRangeField' %} | ||||
|         {{ render_range_field(f, **kwargs) }} | ||||
|     {% elif f.type == 'URLField' %} | ||||
|         {{ render_field(f, **kwargs) }} | ||||
|     {% elif f.type == 'SubmitField' %} | ||||
|         {{ render_submit_field(f, **kwargs) }} | ||||
|     {% elif f.type == 'FormField' %} | ||||
|  |  | |||
|  | @ -34,6 +34,26 @@ | |||
| <script type="application/javascript" src="/static/main.js?v={{ GIT_HASH }}" ></script> | ||||
| <script type="application/javascript" > | ||||
| {% 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> | ||||
| </body> | ||||
| </html> | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| from flask import g, request, Flask | ||||
| from flask_babel import Babel | ||||
| from flask_login import current_user | ||||
| from typing import Optional | ||||
| from lenticular_cloud.model import db, User | ||||
|  | @ -8,38 +9,41 @@ LANGUAGES = { | |||
|     'de': 'Deutsch' | ||||
| } | ||||
| 
 | ||||
| babel = Babel() | ||||
| 
 | ||||
| def init_babel(app: Flask) -> None: | ||||
|     babel = app.babel | ||||
| 
 | ||||
|     @babel.localeselector | ||||
|     def get_locale() -> str: | ||||
|         # if a user is logged in, use the locale from the user settings | ||||
|         user = current_user # type: Optional[User] | ||||
|         return 'de' | ||||
| @babel.localeselector | ||||
| def get_locale() -> str: | ||||
|     # if a user is logged in, use the locale from the user settings | ||||
|     user = current_user # type: Optional[User] | ||||
|     return 'de' | ||||
| 
 | ||||
|         # prefer lang argument | ||||
|         if 'lang' in request.args: | ||||
|             lang = request.args['lang'] # type: str | ||||
|             if lang in LANGUAGES: | ||||
|                 if not isinstance(user, User): | ||||
|                         return lang | ||||
|                 user.locale = lang | ||||
|                 db.session.commit() | ||||
|     # prefer lang argument | ||||
|     if 'lang' in request.args: | ||||
|         lang = request.args['lang'] # type: str | ||||
|         if lang in LANGUAGES: | ||||
|             if not isinstance(user, User): | ||||
|                     return lang | ||||
|             user.locale = lang | ||||
|             db.session.commit() | ||||
| 
 | ||||
|         if isinstance(user, User): | ||||
|             return user.locale | ||||
|         # otherwise try to guess the language from the user accept | ||||
|         # header the browser transmits.  We support de/fr/en in this | ||||
|         # example.  The best match wins. | ||||
|         return request.accept_languages.best_match(['de']) | ||||
|     if isinstance(user, User): | ||||
|         return user.locale | ||||
|     # otherwise try to guess the language from the user accept | ||||
|     # header the browser transmits.  We support de/fr/en in this | ||||
|     # example.  The best match wins. | ||||
|     return request.accept_languages.best_match(['de']) | ||||
| 
 | ||||
|     @babel.timezoneselector | ||||
|     def get_timezone() -> Optional[str]: | ||||
| @babel.timezoneselector | ||||
| def get_timezone() -> Optional[str]: | ||||
| #       user = getattr(g, 'user', None) | ||||
| #       if user is not None: | ||||
| #           return user.timezone | ||||
|         return None | ||||
|     return None | ||||
| 
 | ||||
| def init_babel(app: Flask) -> None: | ||||
| 
 | ||||
|     babel.init_app(app) | ||||
| 
 | ||||
|     @app.context_processor | ||||
|     def get_locale_jinja() -> dict: | ||||
|  | @ -47,3 +51,4 @@ def init_babel(app: Flask) -> None: | |||
|             return get_locale() | ||||
| 
 | ||||
|         return dict(get_locale=get_locale_) | ||||
|     return None | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| # pylint: disable=unused-import | ||||
| 
 | ||||
| from .auth import auth_views | ||||
| from .frontend import frontend_views, init_login_manager | ||||
| from .auth import auth_views  | ||||
| from .oauth2 import init_login_manager, oauth2_views | ||||
| from .frontend import frontend_views | ||||
| from .admin import admin_views | ||||
| from .api import api_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 current_app, session | ||||
| from flask import jsonify | ||||
| from flask.typing import ResponseReturnValue | ||||
| from flask_login import current_user, logout_user | ||||
| from oauthlib.oauth2.rfc6749.errors import TokenExpiredError | ||||
| import ory_hydra_client | ||||
| from ory_hydra_client.model.o_auth2_client import OAuth2Client | ||||
| 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.models import OAuth2Client, GenericError | ||||
| from typing import Optional | ||||
| import logging | ||||
| 
 | ||||
| from ..model import db, User, UserSignUp | ||||
| from .frontend import redirect_login | ||||
| from .oauth2 import redirect_login, oauth2 | ||||
| from ..form.admin import OAuth2ClientForm | ||||
| from ..hydra import hydra_service | ||||
| 
 | ||||
| 
 | ||||
| admin_views = Blueprint('admin', __name__, url_prefix='/admin') | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def before_request(): | ||||
| def before_request() -> Optional[ResponseReturnValue]: | ||||
|     try: | ||||
|         resp = current_app.oauth.session.get('/userinfo') | ||||
|         resp = oauth2.custom.get('/userinfo') | ||||
|         data = resp.json() | ||||
|         if not current_user.is_authenticated or resp.status_code != 200: | ||||
|             return redirect_login() | ||||
|         if 'groups' not in data or 'admin' not in data['groups']: | ||||
|             return 'Not an admin', 403 | ||||
|     except TokenExpiredError: | ||||
|     except MissingTokenError: | ||||
|         return redirect_login() | ||||
|     return None | ||||
| 
 | ||||
| 
 | ||||
| admin_views.before_request(before_request) | ||||
| 
 | ||||
| 
 | ||||
| @admin_views.route('/', methods=['GET', 'POST']) | ||||
| def index(): | ||||
| def index() -> ResponseReturnValue: | ||||
|     return render_template('admin/index.html.j2') | ||||
| 
 | ||||
| 
 | ||||
|  | @ -44,13 +48,13 @@ def users(): | |||
| 
 | ||||
| 
 | ||||
| @admin_views.route('/registrations', methods=['GET']) | ||||
| def registrations(): | ||||
| def registrations() -> ResponseReturnValue: | ||||
|     users = UserSignUp.query.all() | ||||
|     return render_template('admin/registrations.html.j2', users=users) | ||||
| 
 | ||||
| 
 | ||||
| @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) | ||||
|     if user_data is None: | ||||
|         return jsonify({}), 404 | ||||
|  | @ -60,7 +64,7 @@ def registration_delete(registration_id): | |||
| 
 | ||||
| 
 | ||||
| @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) | ||||
|     #create user | ||||
|     user = User.new(user_data) | ||||
|  | @ -72,16 +76,15 @@ def registration_accept(registration_id): | |||
| 
 | ||||
| 
 | ||||
| @admin_views.route('/clients') | ||||
| def clients(): | ||||
|     clients = current_app.hydra_api.list_o_auth2_clients() | ||||
| def clients() -> ResponseReturnValue: | ||||
|     clients = list_o_auth_2_clients.sync(_client=hydra_service.hydra_client) | ||||
|     return render_template('admin/clients.html.j2', clients=clients) | ||||
| 
 | ||||
| @admin_views.route('/client/<client_id>', methods=['GET', 'POST']) | ||||
| def client(client_id: str): | ||||
| def client(client_id: str) -> ResponseReturnValue: | ||||
|      | ||||
|     try: | ||||
|         client = current_app.hydra_api.get_o_auth2_client(client_id) | ||||
|     except ory_hydra_client.ApiException as e: | ||||
|     client = get_o_auth_2_client.sync(client_id, _client=hydra_service.hydra_client) | ||||
|     if client is None or isinstance( client, GenericError): | ||||
|         logger.error(f"oauth2 client not found with id: '{client_id}'") | ||||
|         return 'client not found', 404 | ||||
| 
 | ||||
|  | @ -89,9 +92,8 @@ def client(client_id: str): | |||
|     if form.validate_on_submit(): | ||||
|         form.populate_obj(client) | ||||
|   | ||||
|         try: | ||||
|             client = current_app.hydra_api.update_o_auth2_client(client_id, client) | ||||
|         except ory_hydra_client.ApiException as e: | ||||
|         client = update_o_auth_2_client.sync(id=client_id ,json_body=client, _client=hydra_service.hydra_client) | ||||
|         if client is None or isinstance(client, GenericError): | ||||
|             logger.error(f"oauth2 client update failed: '{client_id}'") | ||||
|             return 'client update failed', 500 | ||||
| 
 | ||||
|  | @ -101,7 +103,7 @@ def client(client_id: str): | |||
| 
 | ||||
| 
 | ||||
| @admin_views.route('/client_new', methods=['GET','POST']) | ||||
| def client_new(): | ||||
| def client_new() -> ResponseReturnValue: | ||||
|      | ||||
|     client = OAuth2Client() | ||||
| 
 | ||||
|  | @ -109,9 +111,8 @@ def client_new(): | |||
|     if form.validate_on_submit(): | ||||
|         form.populate_obj(client) | ||||
|   | ||||
|         try: | ||||
|             client = current_app.hydra_api.create_o_auth2_client(client) | ||||
|         except ory_hydra_client.ApiException as e: | ||||
|         resp_client = create_o_auth_2_client.sync(json_body=client, _client=hydra_service.hydra_client) | ||||
|         if resp_client is None: | ||||
|             logger.error(f"oauth2 client update failed: '{client.client_id}'") | ||||
|             return 'internal error', 500 | ||||
|         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.helpers import make_response | ||||
| from flask.templating import render_template | ||||
| from flask.typing import ResponseReturnValue | ||||
| 
 | ||||
| from flask import Blueprint, render_template, request, url_for | ||||
| import logging | ||||
| import requests | ||||
| 
 | ||||
| from ..model import User | ||||
| 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.route('/users', methods=['GET']) | ||||
| def user_list(): | ||||
| def user_list() -> ResponseReturnValue: | ||||
|     if 'authorization' not in request.headers: | ||||
|         return '', 403 | ||||
|     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 jsonify([ | ||||
|  |  | |||
|  | @ -1,11 +1,12 @@ | |||
| 
 | ||||
| from authlib.integrations.flask_client import OAuth | ||||
| from urllib.parse import urlencode, parse_qs | ||||
| 
 | ||||
| import flask | ||||
| from flask import Blueprint, redirect | ||||
| from flask import current_app, session | ||||
| from flask import Blueprint, redirect, flash, current_app, session | ||||
| from flask.templating import render_template | ||||
| from flask_babel import gettext | ||||
| from flask.typing import ResponseReturnValue | ||||
| 
 | ||||
| from flask import request, url_for, jsonify | ||||
| 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 | ||||
| import http | ||||
| import crypt | ||||
| import ory_hydra_client | ||||
| from datetime import datetime | ||||
| import logging | ||||
| 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 ..form.auth import ConsentForm, LoginForm, RegistrationForm | ||||
| from ..auth_providers import AUTH_PROVIDER_LIST | ||||
| from ..hydra import hydra_service | ||||
| 
 | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
|  | @ -29,26 +33,31 @@ logger = logging.getLogger(__name__) | |||
| auth_views = Blueprint('auth', __name__, url_prefix='/auth') | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @auth_views.route('/consent', methods=['GET', 'POST']) | ||||
| def consent(): | ||||
| def consent() -> ResponseReturnValue: | ||||
|     """Always grant consent.""" | ||||
|     # DUMMPY ONLY | ||||
| 
 | ||||
|     form = ConsentForm() | ||||
|     remember_for = 60*60*24*30  # remember for 30 days | ||||
| 
 | ||||
|     try: | ||||
|         consent_request = current_app.hydra_api.get_consent_request( | ||||
|                                     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')) | ||||
|     #try: | ||||
|     consent_request = get_consent_request.sync(consent_challenge=request.args['consent_challenge'],_client=hydra_service.hydra_client) | ||||
| 
 | ||||
|     requested_scope = consent_request.requested_scope["value"] | ||||
|     requested_audiences = consent_request.requested_access_token_audience["value"] | ||||
|     if consent_request is None or isinstance( consent_request, GenericError): | ||||
|        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: | ||||
|         user = User.query.get(consent_request.subject) | ||||
|  | @ -60,19 +69,25 @@ def consent(): | |||
|             'groups': [group.name for group in user.groups] | ||||
|         } | ||||
|         id_token_data = {} | ||||
|         if 'openid' in requested_scope: | ||||
|         if isinstance(requested_scope, list) and 'openid' in requested_scope: | ||||
|             id_token_data = token_data | ||||
|         resp = current_app.hydra_api.accept_consent_request( | ||||
|             consent_request.challenge, body={ | ||||
|                 'grant_scope': requested_scope, | ||||
|                 'grant_access_token_audience': requested_audiences, | ||||
|                 'remember': form.data['remember'], | ||||
|                 'remember_for': remember_for, | ||||
|                 'session': { | ||||
|                     'access_token': token_data, | ||||
|                     'id_token': id_token_data | ||||
|                 } | ||||
|             }) | ||||
|         access_token=ConsentRequestSessionAccessToken.from_dict(token_data) | ||||
|         id_token=ConsentRequestSessionIdToken.from_dict(id_token_data) | ||||
|         body = AcceptConsentRequest( | ||||
|                 grant_scope= requested_scope, | ||||
|                 grant_access_token_audience= requested_audiences, | ||||
|                 remember= form.data['remember'], | ||||
|                 remember_for= remember_for, | ||||
|                 session= ConsentRequestSession( | ||||
|                     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 render_template( | ||||
|             'auth/consent.html.j2', | ||||
|  | @ -83,18 +98,22 @@ def consent(): | |||
| 
 | ||||
| 
 | ||||
| @auth_views.route('/login', methods=['GET', 'POST']) | ||||
| def login(): | ||||
| def login() -> ResponseReturnValue: | ||||
|     login_challenge = request.args.get('login_challenge') | ||||
|     try: | ||||
|         login_request = current_app.hydra_api.get_login_request(login_challenge) | ||||
|     except ory_hydra_client.exceptions.ApiException as e: | ||||
|     if login_challenge is None: | ||||
|         return 'login_challenge missing', 400 | ||||
|     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") | ||||
|         return redirect(url_for('frontend.index')) | ||||
| 
 | ||||
|     if login_request.skip: | ||||
|         resp = current_app.hydra_api.accept_login_request( | ||||
|             login_challenge, | ||||
|             body={'subject': login_request.subject}) | ||||
|         resp = accept_login_request.sync(_client=hydra_service.hydra_client, | ||||
|             login_challenge=login_challenge, | ||||
|             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) | ||||
|     form = LoginForm() | ||||
|     if form.validate_on_submit(): | ||||
|  | @ -110,11 +129,12 @@ def login(): | |||
| 
 | ||||
| 
 | ||||
| @auth_views.route('/login/auth', methods=['GET', 'POST']) | ||||
| def login_auth(): | ||||
| def login_auth() -> ResponseReturnValue: | ||||
|     login_challenge = request.args.get('login_challenge') | ||||
|     try: | ||||
|         login_request = current_app.hydra_api.get_login_request(login_challenge) | ||||
|     except ory_hydra_client.exceptions.ApiValueError: | ||||
|     if login_challenge is None: | ||||
|         return 'missing login_challenge, bad request', 400 | ||||
|     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')) | ||||
| 
 | ||||
|     if 'username' not in session: | ||||
|  | @ -140,25 +160,31 @@ def login_auth(): | |||
|         subject = user.id | ||||
|         user.last_login = datetime.now() | ||||
|         db.session.commit() | ||||
|         resp = current_app.hydra_api.accept_login_request( | ||||
|             login_challenge, body={ | ||||
|                 'subject': subject, | ||||
|                 'remember': remember_me, | ||||
|             }) | ||||
|         resp = accept_login_request.sync(_client=hydra_service.hydra_client, | ||||
|             login_challenge=login_challenge, json_body=AcceptLoginRequest( | ||||
|                 subject=subject, | ||||
|                 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 render_template('auth/login_auth.html.j2', forms=auth_forms) | ||||
| 
 | ||||
| 
 | ||||
| @auth_views.route("/logout") | ||||
| def logout(): | ||||
| def logout() -> ResponseReturnValue: | ||||
|     logout_challenge = request.args.get('logout_challenge') | ||||
|     if logout_challenge is None: | ||||
|         return 'invalid request, logout_challenge not set', 400 | ||||
|     # 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) | ||||
| 
 | ||||
| 
 | ||||
| @auth_views.route("/error", methods=["GET"]) | ||||
| def auth_error(): | ||||
| def auth_error() -> ResponseReturnValue: | ||||
|     error = request.args.get('error') | ||||
|     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 flask import Blueprint, redirect, request | ||||
| from flask import current_app | ||||
| 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 werkzeug.utils import redirect | ||||
| import logging | ||||
| from datetime import timedelta | ||||
| from base64 import b64decode | ||||
| from flask_dance.consumer import oauth_authorized | ||||
| from flask_dance.consumer.base import oauth_before_login | ||||
| from flask_dance.consumer import OAuth2ConsumerBlueprint | ||||
| from flask.typing import ResponseReturnValue  | ||||
| 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 ..form.frontend import ClientCertForm, TOTPForm, \ | ||||
|     TOTPDeleteForm, PasswordChangeForm | ||||
| from ..auth_providers import LdapAuthProvider | ||||
| from .oauth2 import redirect_login, oauth2 | ||||
| from ..hydra import hydra_service | ||||
| 
 | ||||
| frontend_views = Blueprint('frontend', __name__, url_prefix='') | ||||
| 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: | ||||
|         resp = current_app.oauth.session.get('/userinfo') | ||||
|         resp = oauth2.custom.get('/userinfo') | ||||
|         if not current_user.is_authenticated or resp.status_code != 200: | ||||
|             logger.info('user not logged in redirect') | ||||
|             return redirect_login() | ||||
|     except TokenExpiredError: | ||||
|     except MissingTokenError: | ||||
|         return redirect_login() | ||||
|     except InvalidTokenError: | ||||
|         return redirect_login() | ||||
| 
 | ||||
|     return None | ||||
| 
 | ||||
| 
 | ||||
| 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') | ||||
| def logout(): | ||||
| def logout() -> ResponseReturnValue: | ||||
|     logout_user() | ||||
|     return redirect( | ||||
|         f'{current_app.config["HYDRA_PUBLIC_URL"]}/oauth2/sessions/logout') | ||||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/', methods=['GET']) | ||||
| def index(): | ||||
| def index() -> ResponseReturnValue: | ||||
|     if 'next_url' in session: | ||||
|         next_url = session['next_url'] | ||||
|         del session['next_url'] | ||||
|  | @ -112,7 +61,7 @@ def index(): | |||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/client_cert') | ||||
| def client_cert(): | ||||
| def client_cert() -> ResponseReturnValue: | ||||
|     client_certs = {} | ||||
|     for service in current_app.lenticular_services.values(): | ||||
|         client_certs[str(service.name)] = \ | ||||
|  | @ -125,7 +74,7 @@ def client_cert(): | |||
| 
 | ||||
| 
 | ||||
| @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] | ||||
|     cert = current_app.pki.get_client_cert( | ||||
|             current_user, service, serial_number) | ||||
|  | @ -137,7 +86,7 @@ def get_client_cert(service_name, serial_number): | |||
| 
 | ||||
| @frontend_views.route( | ||||
|         '/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] | ||||
|     cert = current_app.pki.get_client_cert( | ||||
|             current_user, service, serial_number) | ||||
|  | @ -148,7 +97,7 @@ def revoke_client_cert(service_name, serial_number): | |||
| @frontend_views.route( | ||||
|         '/client_cert/<service_name>/new', | ||||
|         methods=['GET', 'POST']) | ||||
| def client_cert_new(service_name): | ||||
| def client_cert_new(service_name) -> ResponseReturnValue: | ||||
|     service = current_app.lenticular_services[service_name] | ||||
|     form = ClientCertForm() | ||||
|     if form.validate_on_submit(): | ||||
|  | @ -177,13 +126,13 @@ def client_cert_new(service_name): | |||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/totp') | ||||
| def totp(): | ||||
| def totp() -> ResponseReturnValue: | ||||
|     delete_form = TOTPDeleteForm() | ||||
|     return render_template('frontend/totp.html.j2', delete_form=delete_form) | ||||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/totp/new', methods=['GET', 'POST']) | ||||
| def totp_new(): | ||||
| def totp_new() -> ResponseReturnValue: | ||||
|     form = TOTPForm() | ||||
| 
 | ||||
|     if form.validate_on_submit(): | ||||
|  | @ -203,7 +152,7 @@ def totp_new(): | |||
| 
 | ||||
| 
 | ||||
| @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() | ||||
|     db.session.delete(totp) | ||||
|     db.session.commit() | ||||
|  | @ -213,13 +162,13 @@ def totp_delete(totp_name): | |||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/password_change') | ||||
| def password_change(): | ||||
| def password_change() -> ResponseReturnValue: | ||||
|     form = PasswordChangeForm() | ||||
|     return render_template('frontend/password_change.html.j2', form=form) | ||||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/password_change', methods=['POST']) | ||||
| def password_change_post(): | ||||
| def password_change_post() -> ResponseReturnValue: | ||||
|     form = PasswordChangeForm() | ||||
|     if form.validate(): | ||||
|         password_old = str(form.data['password_old']) | ||||
|  | @ -230,7 +179,6 @@ def password_change_post(): | |||
|                     {'errors': {'password_old': 'Old Password is invalid'}}) | ||||
|         resp = current_user.change_password(password_new) | ||||
|         if resp: | ||||
|             print(current_user) | ||||
|             return jsonify({}) | ||||
|         else: | ||||
|             return jsonify({'errors': {'internal': 'internal server errror'}}) | ||||
|  | @ -238,23 +186,22 @@ def password_change_post(): | |||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/oauth2_token') | ||||
| def oauth2_tokens(): | ||||
| def oauth2_tokens() -> ResponseReturnValue: | ||||
| 
 | ||||
|     subject = current_app.oauth.session.get('/userinfo').json()['sub'] | ||||
|     consent_sessions = current_app.hydra_api.list_subject_consent_sessions( | ||||
|                                 subject) | ||||
| 
 | ||||
|     print(consent_sessions) | ||||
|     subject = oauth2.custom.get('/userinfo').json()['sub'] | ||||
|     consent_sessions = list_subject_consent_sessions.sync(subject=subject, _client=hydra_service.hydra_client) | ||||
|     if consent_sessions is None or isinstance( consent_sessions, GenericError): | ||||
|        return 'internal error, could not fetch sessions', 500 | ||||
|     return render_template( | ||||
|             'frontend/oauth2_tokens.html.j2', | ||||
|             consent_sessions=consent_sessions) | ||||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE']) | ||||
| def oauth2_token_revoke(client_id: str): | ||||
|     subject = current_app.oauth.session.get('/userinfo').json()['sub'] | ||||
|     current_app.hydra_api.revoke_consent_sessions( | ||||
|                                 subject, | ||||
| def oauth2_token_revoke(client_id: str) -> ResponseReturnValue: | ||||
|     subject = oauth2.session.get('/userinfo').json()['sub'] | ||||
|     revoke_consent_sessions.sync( _client=hydra_service.hydra_client, | ||||
|                                 subject=subject, | ||||
|                                 client=client_id) | ||||
| 
 | ||||
|     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 | ||||
| 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_unused_configs = True | ||||
| ignore_missing_imports = True | ||||
| follow_imports_for_stubs= True | ||||
| files=lenticular_cloud/**/*.py | ||||
|  |  | |||
|  | @ -1 +1,2 @@ | |||
| flask-debug | ||||
| types-python-dateutil | ||||
|  |  | |||
|  | @ -15,6 +15,6 @@ cryptography | |||
| requests | ||||
| requests_oauthlib | ||||
| blinker | ||||
| ory-hydra-client | ||||
| 
 | ||||
| ./libs/ory-hydra-client | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue