fix lots of stuff, migrate to new client api
This commit is contained in:
parent
4e9fd55093
commit
710460cc88
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,9 +9,8 @@ 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:
|
||||||
|
@ -41,9 +41,14 @@ def init_babel(app: Flask) -> 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:
|
||||||
def get_locale_() -> str:
|
def get_locale_() -> str:
|
||||||
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:
|
if consent_request is None or isinstance( consent_request, GenericError):
|
||||||
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'))
|
return redirect(url_for('frontend.index'))
|
||||||
|
|
||||||
requested_scope = consent_request.requested_scope["value"]
|
|
||||||
requested_audiences = consent_request.requested_access_token_audience["value"]
|
# 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…
Reference in a new issue