update fixes, ...
This commit is contained in:
parent
c6042973fe
commit
1947a6f24a
|
@ -82,7 +82,7 @@ window.auth = {
|
|||
submit: function(form) {
|
||||
SimpleFormSubmit.submitForm(form.action, form)
|
||||
.then(response =>{
|
||||
response.json().then(function(data) {
|
||||
response.json().then(data => {
|
||||
if (data.errors) {
|
||||
var msg ='<ul>';
|
||||
for( var field in data.errors) {
|
||||
|
@ -156,6 +156,8 @@ window.password_change= {
|
|||
new Dialog('Password changed', 'Password changed successfully!').show();
|
||||
}
|
||||
});
|
||||
}).error(error =>{
|
||||
new Dialog('Password change Error', `Error Happend: ${msg}`).show()
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
@ -201,10 +203,10 @@ window.client_cert = {
|
|||
|
||||
SimpleFormSubmit.submitForm(form_sign_key.action, form_sign_key)
|
||||
.then(response => {
|
||||
response.json().then( response => {
|
||||
if (data.errors) {
|
||||
response.json().then( json_data => {
|
||||
if (json_data.errors) {
|
||||
var msg ='<ul>';
|
||||
for( var field in data.errors) {
|
||||
for( var field in json_data.repsonse) {
|
||||
msg += `<li>${field}: ${data.errors[field]}</li>`;
|
||||
}
|
||||
msg += '</ul>';
|
||||
|
|
194
index.html
Normal file
194
index.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -36,12 +36,11 @@ LENTICULAR_CLOUD_SERVICES = {
|
|||
'client_cert': True,
|
||||
'pki_config':{
|
||||
'email': '{username}@jabber.{domain}'
|
||||
}
|
||||
},
|
||||
'calendar': {
|
||||
'client_cert': True
|
||||
'app_token': True
|
||||
},
|
||||
'mail': {
|
||||
'client_cert': True
|
||||
'mail-cardav': {
|
||||
'client_cert': False,
|
||||
'app_token': True
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ def cli_run(app: Flask, args) -> None:
|
|||
print("running in debug mode")
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
#app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
|
||||
app.run(debug=False, host='127.0.0.1', port=5000)
|
||||
app.run(debug=True, host='127.0.0.1', port=5000)
|
||||
|
||||
|
||||
def cli_db_upgrade(args) -> None:
|
||||
|
|
|
@ -33,6 +33,12 @@ class TOTPForm(FlaskForm):
|
|||
class TOTPDeleteForm(FlaskForm):
|
||||
submit = SubmitField(gettext('Delete'))
|
||||
|
||||
class AppTokenForm(FlaskForm):
|
||||
name = StringField(gettext('name'), validators=[DataRequired(),Length(min=1, max=255) ])
|
||||
submit = SubmitField(gettext('Activate'))
|
||||
|
||||
class AppTokenDeleteForm(FlaskForm):
|
||||
submit = SubmitField(gettext('Delete'))
|
||||
|
||||
class WebauthnRegisterForm(FlaskForm):
|
||||
"""webauthn register token form"""
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
"""fix app token
|
||||
|
||||
Revision ID: 0f217e90cd07
|
||||
Revises: 0518a8625b50
|
||||
Create Date: 2022-06-18 23:24:12.687324
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0f217e90cd07'
|
||||
down_revision = '0518a8625b50'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('app_token', sa.Column('user_id', sa.String(length=36), nullable=False))
|
||||
op.add_column('app_token', sa.Column('last_used', sa.DateTime(), nullable=True))
|
||||
op.create_foreign_key(None, 'app_token', 'user', ['user_id'], ['id'])
|
||||
op.add_column('totp', sa.Column('last_used', sa.DateTime(), nullable=True))
|
||||
tmp_table = sa.Table('_alembic_tmp_user', sa.MetaData())
|
||||
op.execute(sa.schema.DropTable(tmp_table, if_exists=True))
|
||||
with op.batch_alter_table('user') as batch_op:
|
||||
batch_op.alter_column('enabled',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text("'false'"))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
tmp_table = sa.Table('_alembic_tmp_user', sa.MetaData())
|
||||
op.execute(sa.schema.DropTable(tmp_table, if_exists=True))
|
||||
with op.batch_alter_table('user') as batch_op:
|
||||
batch_op.alter_column('enabled',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text("'false'"))
|
||||
op.drop_column('totp', 'last_used')
|
||||
op.drop_constraint(None, 'app_token', type_='foreignkey')
|
||||
op.drop_column('app_token', 'last_used')
|
||||
op.drop_column('app_token', 'user_id')
|
||||
# ### end Alembic commands ###
|
|
@ -9,6 +9,8 @@ import pyotp
|
|||
import json
|
||||
import logging
|
||||
import crypt
|
||||
import secrets
|
||||
import string
|
||||
from flask_sqlalchemy import SQLAlchemy, orm
|
||||
from flask_migrate import Migrate
|
||||
from datetime import datetime
|
||||
|
@ -23,7 +25,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
|
||||
db = SQLAlchemy() # type: SQLAlchemy
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
|
||||
|
||||
|
@ -42,6 +44,7 @@ class Service(object):
|
|||
|
||||
def __init__(self, name: str):
|
||||
self._name = name
|
||||
self._app_token = False
|
||||
self._client_cert = False
|
||||
self._pki_config = {
|
||||
'cn': '{username}',
|
||||
|
@ -53,6 +56,8 @@ class Service(object):
|
|||
"""
|
||||
"""
|
||||
service = Service(name)
|
||||
if 'app_token' in config:
|
||||
service._app_token = bool(config['app_token'])
|
||||
if 'client_cert' in config:
|
||||
service._client_cert = bool(config['client_cert'])
|
||||
if 'pki_config' in config:
|
||||
|
@ -68,6 +73,10 @@ class Service(object):
|
|||
def client_cert(self) -> bool:
|
||||
return self._client_cert
|
||||
|
||||
@property
|
||||
def app_token(self) -> bool:
|
||||
return self._app_token
|
||||
|
||||
@property
|
||||
def pki_config(self) -> dict[str,str]:
|
||||
if not self._client_cert:
|
||||
|
@ -148,6 +157,7 @@ class User(BaseModel):
|
|||
|
||||
enabled = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
app_tokens = db.relationship('AppToken', back_populates='user')
|
||||
totps = db.relationship('Totp', back_populates='user')
|
||||
webauthn_credentials = db.relationship('WebauthnCredential', back_populates='user', cascade='delete,delete-orphan', passive_deletes=True)
|
||||
|
||||
|
@ -162,7 +172,7 @@ class User(BaseModel):
|
|||
print(f'getitem: {key}') # TODO
|
||||
|
||||
@property
|
||||
def groups(self) -> list[str]:
|
||||
def groups(self) -> list['Group']:
|
||||
if self.username == 'tuxcoder':
|
||||
return [Group(name='admin')]
|
||||
else:
|
||||
|
@ -173,23 +183,44 @@ class User(BaseModel):
|
|||
domain = current_app.config['DOMAIN']
|
||||
return f'{self.username}@{domain}'
|
||||
|
||||
def change_password(self, password_new: str) -> bool:
|
||||
password_hashed = crypt.crypt(password_new)
|
||||
return True
|
||||
def change_password(self, password_new: str) -> None:
|
||||
self.password_hashed = crypt.crypt(password_new)
|
||||
|
||||
def get_tokens_by_service(self, service: Service) -> list['AppToken']:
|
||||
return [ token for token in self.app_tokens if token.service_name == service.name ]
|
||||
|
||||
def get_token(self, service: Service, name: str) -> Optional['AppToken']:
|
||||
for token in self.app_tokens:
|
||||
if token.service_name == service.name and token.name == name:
|
||||
return token # type: ignore
|
||||
return None
|
||||
|
||||
|
||||
class AppToken(BaseModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
service_name = db.Column(db.String, nullable=False)
|
||||
user_id = db.Column(
|
||||
db.String(length=36),
|
||||
db.ForeignKey(User.id), nullable=False)
|
||||
user = db.relationship(User)
|
||||
token = db.Column(db.String, nullable=False)
|
||||
name = db.Column(db.String, nullable=False)
|
||||
last_used = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
@staticmethod
|
||||
def new(service: Service):
|
||||
app_token = AppToken()
|
||||
app_token.service_name = service.name
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
app_token.token = ''.join(secrets.choice(alphabet) for i in range(12))
|
||||
return app_token
|
||||
|
||||
class Totp(BaseModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
secret = db.Column(db.String, nullable=False)
|
||||
name = db.Column(db.String, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now, nullable=False)
|
||||
#last_used = db.Column(db.DateTime, nullable=True)
|
||||
last_used = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
user_id = db.Column(
|
||||
db.String(length=36),
|
||||
|
|
|
@ -48,7 +48,7 @@ class Pki(object):
|
|||
'''
|
||||
pki_path: str base path from the pkis
|
||||
'''
|
||||
self._pki_path = Path(os.getcwd()) / app.config['PKI_PATH']
|
||||
self._pki_path = Path(app.root_path) / app.config['PKI_PATH']
|
||||
self._domain = app.config['DOMAIN']
|
||||
|
||||
|
||||
|
|
BIN
lenticular_cloud/static/0caf4c6cf244a90efcc5.woff2
Normal file
BIN
lenticular_cloud/static/0caf4c6cf244a90efcc5.woff2
Normal file
Binary file not shown.
BIN
lenticular_cloud/static/0d03b1bbd1d62c1e1284.ttf
Normal file
BIN
lenticular_cloud/static/0d03b1bbd1d62c1e1284.ttf
Normal file
Binary file not shown.
BIN
lenticular_cloud/static/4baccb548138840fa33a.ttf
Normal file
BIN
lenticular_cloud/static/4baccb548138840fa33a.ttf
Normal file
Binary file not shown.
BIN
lenticular_cloud/static/59edf72a325ac2048d60.woff2
Normal file
BIN
lenticular_cloud/static/59edf72a325ac2048d60.woff2
Normal file
Binary file not shown.
BIN
lenticular_cloud/static/afac89562a5301459069.woff2
Normal file
BIN
lenticular_cloud/static/afac89562a5301459069.woff2
Normal file
Binary file not shown.
BIN
lenticular_cloud/static/b823fc0dbb5a5f0c21bb.ttf
Normal file
BIN
lenticular_cloud/static/b823fc0dbb5a5f0c21bb.ttf
Normal file
Binary file not shown.
BIN
lenticular_cloud/static/e615bbcb258550973c16.ttf
Normal file
BIN
lenticular_cloud/static/e615bbcb258550973c16.ttf
Normal file
Binary file not shown.
BIN
lenticular_cloud/static/ebb7a127d2d8ee6f1832.woff2
Normal file
BIN
lenticular_cloud/static/ebb7a127d2d8ee6f1832.woff2
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -4,50 +4,48 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
{#
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
{% for service in services.values() %}
|
||||
{% for service in services.values() if service.app_token %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{' active' if loop.first else ''}}" id="home-tab" data-toggle="tab" href="#{{ service.name }}" role="tab" aria-controls="home" aria-selected="true">{{ service.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="myTabContent">
|
||||
{% for service in services.values() if service.client_cert %}
|
||||
{% for service in services.values() if service.app_token %}
|
||||
|
||||
<div class="tab-pane fade{{ ' show active' if loop.first else '' }}" id="{{ service.name }}" role="tabpanel" aria-labelledby="{{ service.name }}-tab">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>not valid before</th>
|
||||
<th>not valid after</th>
|
||||
<th>serial_number<th>
|
||||
<th>name</th>
|
||||
<th>last used</th>
|
||||
<th>created at<th>
|
||||
<th> <th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cert in client_certs[service.name] %}
|
||||
<tr {{ 'class="table-warning"' if not cert.is_valid else ''}}>
|
||||
<td>{{ cert.not_valid_before }}</td>
|
||||
<td>{{ cert.not_valid_after }}</td>
|
||||
<td>{{ cert.serial_number_hex }}</td>
|
||||
{% for app_token in current_user.get_tokens_by_service(service) %}
|
||||
<tr>
|
||||
<td>{{ app_token.name }}</td>
|
||||
<td>{{ app_token.last_used }}</td>
|
||||
<td>{{ app_token.created_at }}</td>
|
||||
<td>
|
||||
<a title="{{ gettext('Download') }}" href="{{ url_for('.get_client_cert', service_name=service.name, serial_number=cert.serial_number_hex) }}"><i class="fas fa-file-download"></i></a>
|
||||
|
||||
{% if cert.is_valid %}
|
||||
<a title="{{ gettext('Revoke')}}" href="{{ url_for('.revoke_client_cert', service_name=service.name, serial_number=cert.serial_number_hex) }}" onclick="client_cert.revoke_certificate(this.href, '{{ cert.serial_number_hex }}'); return false;"><i class="fas fa-ban"></i></a>
|
||||
{% endif %}
|
||||
{{ render_form(delete_form, action_url=url_for('frontend.app_token_delete', service_name=service.name,app_token_name=app_token.name)) }}
|
||||
{#
|
||||
<a title="{{ gettext('Revoke')}}" href="{{ url_for('.app_token_revoce', id=app_token.id) }}" onclick="client_cert.revoke_certificate(this.href, '{{ cert.serial_number_hex }}'); return false;"><i class="fas fa-ban"></i></a>
|
||||
#}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<a class="btn btn-primary" href="{{ url_for('frontend.client_cert_new', service_name=service.name) }}">
|
||||
New Certificate
|
||||
<a class="btn btn-primary" href="{{ url_for('frontend.app_token_new', service_name=service.name) }}">
|
||||
New Token
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
#}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,44 +1,12 @@
|
|||
{% extends 'frontend/base.html.j2' %}
|
||||
|
||||
{% block title %}{{ gettext('new client cert - {service_name}').format(service_name=service.name) }}{% endblock %}
|
||||
{% block title %}{{ gettext('new app token for {service_name}').format(service_name=service.name) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="sign-key">
|
||||
<h4>Sign Public Key</h4>
|
||||
<div>
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
<div id="gen-key">
|
||||
<h4>Generate new key in the browser</h4>
|
||||
<div id="gen-key-sign" style="display: none">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
<form id="gen-key-form">
|
||||
<div class="form-group">
|
||||
<label for="valid_time" class="control-label ">Key Password for .p12 (optional)</label>
|
||||
<div class="">
|
||||
<input class="form-control" id="cert-password" type="password" name="password"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="valid_time" class="control-label ">Key Size</label>
|
||||
<div class="">
|
||||
<select id="key-size" class="custom-select">
|
||||
<option value="4096" selected>4096</option>
|
||||
<option value="2048">2048</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group ">
|
||||
<label for="valid_time" class="control-label ">valid time in days</label>
|
||||
<div class="">
|
||||
<input class="form-control" name="valid_time" required type="text" value="365">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<button id="generate-key" class="btn btn-primary" onclick="client_cert.generate_private_key()">Generate Key</button>
|
||||
<a style="display: none" id="save-button" download="lenticular_cloud_{{ service.name }}.p12" class="btn btn-primary">Save Keypair</a>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{% extends 'frontend/base.html.j2' %}
|
||||
|
||||
{% block title %}{{ gettext('new app token for {service_name}').format(service_name=service.name) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div>
|
||||
<p>
|
||||
Your new App Token for {{ service.name }}:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<code id="app_token_secret">{{ app_token.token }}</code><br />
|
||||
</p>
|
||||
<p>
|
||||
<button class="btn btn-primary" onclick="navigator.clipboard.writeText(document.getElementById('app_token_secret').textContent);">
|
||||
<i class="fa-solid fa-clipboard"></i>
|
||||
Copy Secret
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
{% for service in services.values() %}
|
||||
{% for service in services.values() if service.client_cert %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{' active' if loop.first else ''}}" id="home-tab" data-toggle="tab" href="#{{ service.name }}" role="tab" aria-controls="home" aria-selected="true">{{ service.name }}</a>
|
||||
</li>
|
||||
|
|
|
@ -9,6 +9,7 @@ from authlib.integrations.base_client.errors import InvalidTokenError
|
|||
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
|
||||
from collections.abc import Iterable
|
||||
import logging
|
||||
|
||||
from ..model import db, User
|
||||
|
@ -44,19 +45,19 @@ async def index() -> ResponseReturnValue:
|
|||
|
||||
@admin_views.route('/user', methods=['GET'])
|
||||
async def users():
|
||||
users = User.query.all()
|
||||
users = User.query.all() # type: Iterable[User]
|
||||
return render_template('admin/users.html.j2', users=users)
|
||||
|
||||
|
||||
@admin_views.route('/registrations', methods=['GET'])
|
||||
def registrations() -> ResponseReturnValue:
|
||||
users = User.query.filter_by(enabled=False).all()
|
||||
users = User.query.filter_by(enabled=False).all() # type: Iterable[User]
|
||||
return render_template('admin/registrations.html.j2', users=users)
|
||||
|
||||
|
||||
@admin_views.route('/registration/<registration_id>', methods=['DELETE'])
|
||||
def registration_delete(registration_id) -> ResponseReturnValue:
|
||||
user = User.query.get(registration_id)
|
||||
user = User.query.get(registration_id) # type: Optional[User]
|
||||
if user is None:
|
||||
return jsonify({}), 404
|
||||
db.session.delete(user)
|
||||
|
@ -66,7 +67,9 @@ def registration_delete(registration_id) -> ResponseReturnValue:
|
|||
|
||||
@admin_views.route('/registration/<registration_id>', methods=['PUT'])
|
||||
def registration_accept(registration_id) -> ResponseReturnValue:
|
||||
user = User.query.get(registration_id)
|
||||
user = User.query.get(registration_id) # type: Optional[User]
|
||||
if user is None:
|
||||
return jsonify({'message':'user not found'}), 404
|
||||
user.enabled = True
|
||||
db.session.commit()
|
||||
return jsonify({})
|
||||
|
|
|
@ -7,11 +7,15 @@ from flask.templating import render_template
|
|||
from flask.typing import ResponseReturnValue
|
||||
|
||||
from flask import Blueprint, render_template, request, url_for
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any
|
||||
import logging
|
||||
import httpx
|
||||
import secrets
|
||||
|
||||
from ..model import User
|
||||
from ..model import db, User
|
||||
from ..hydra import hydra_service
|
||||
from ..lenticular_services import lenticular_services
|
||||
from ory_hydra_client.api.admin import introspect_o_auth_2_token
|
||||
from ory_hydra_client.models import GenericError
|
||||
|
||||
|
@ -40,39 +44,50 @@ def user_list() -> ResponseReturnValue:
|
|||
|
||||
@api_views.route('/introspect', methods=['POST'])
|
||||
def introspect() -> ResponseReturnValue:
|
||||
token = request.form['token']
|
||||
logger.error(f'debug token: {token}')
|
||||
token = request.form['token'] # type: Optional[str]
|
||||
resp = httpx.post("https://hydra.cloud.tux.ac/oauth2/introspect", data={'token':token})
|
||||
#if token_info is None or isinstance(token_info, GenericError):
|
||||
if resp.status_code != 200:
|
||||
return jsonify({}), 500
|
||||
token_info = resp.json()
|
||||
#token_info = introspect_o_auth_2_token.sync(_client=hydra_service, token=token)
|
||||
|
||||
if not token_info['active']:
|
||||
return jsonify({'active': False})
|
||||
token_info['email'] = token_info['ext']['email']
|
||||
|
||||
logger.error(f'debug: {token_info}')
|
||||
|
||||
return jsonify(token_info)
|
||||
|
||||
|
||||
@api_views.route('email/login', methods=['POST'])
|
||||
def email_login() -> ResponseReturnValue:
|
||||
logger.error(f'{request}')
|
||||
logger.error(f'{request.headers}')
|
||||
@api_views.route('/login/<service_name>', methods=['POST'])
|
||||
def email_login(service_name: str) -> ResponseReturnValue:
|
||||
if service_name not in lenticular_services:
|
||||
return '', 404
|
||||
service = lenticular_services[service_name]
|
||||
|
||||
if not request.is_json:
|
||||
return jsonify({}), 400
|
||||
req_payload = request.get_json()
|
||||
logger.debug(f'{req_payload}')
|
||||
req_payload = request.get_json() # type: Any
|
||||
|
||||
if not isinstance(req_payload, dict):
|
||||
return 'bad request', 400
|
||||
|
||||
password = req_payload["password"]
|
||||
username = req_payload["username"]
|
||||
|
||||
if password == "123456":
|
||||
return jsonify({})
|
||||
if '@' in username:
|
||||
username = username.split('@')[0]
|
||||
|
||||
user = User.query.filter_by(username=username.lower()).first() # type: Optional[User]
|
||||
if user is None:
|
||||
logger.warning(f'login with invalid username')
|
||||
return jsonify({}), 403
|
||||
|
||||
for app_token in user.get_tokens_by_service(service):
|
||||
if secrets.compare_digest(password, app_token.token):
|
||||
app_token.last_used = datetime.now()
|
||||
db.session.commit()
|
||||
return jsonify({'username': user.username}), 200
|
||||
|
||||
logger.warning(f'login with invalid password for {username}')
|
||||
return jsonify({}), 403
|
||||
|
||||
|
|
|
@ -60,7 +60,9 @@ async def consent() -> ResponseReturnValue:
|
|||
requested_audiences = consent_request.requested_access_token_audience
|
||||
|
||||
if form.validate_on_submit() or consent_request.skip:
|
||||
user = User.query.get(consent_request.subject)
|
||||
user = User.query.get(consent_request.subject) # type: Optional[User]
|
||||
if user is None:
|
||||
return 'internal error', 500
|
||||
token_data = {
|
||||
'name': str(user.username),
|
||||
'preferred_username': str(user.username),
|
||||
|
@ -118,7 +120,7 @@ async def login() -> ResponseReturnValue:
|
|||
return redirect(resp.redirect_to)
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.data['name']).first()
|
||||
user = User.query.filter_by(username=form.data['name']).first() # type: Optional[User]
|
||||
if user:
|
||||
session['username'] = str(user.username)
|
||||
else:
|
||||
|
@ -141,7 +143,7 @@ async def login_auth() -> ResponseReturnValue:
|
|||
if 'username' not in session:
|
||||
return redirect(url_for('auth.login'))
|
||||
auth_forms = {}
|
||||
user = User.query.filter_by(username=session['username']).first()
|
||||
user = User.query.filter_by(username=session['username']).first() # Optional[User]
|
||||
for auth_provider in AUTH_PROVIDER_LIST:
|
||||
form = auth_provider.get_form()
|
||||
if auth_provider.get_name() not in session['auth_providers'] and\
|
||||
|
@ -178,7 +180,7 @@ async def login_auth() -> ResponseReturnValue:
|
|||
def webauthn_pkcro_route():
|
||||
"""login webauthn pkcro route"""
|
||||
|
||||
user = User.query.filter(User.id == session.get('webauthn_login_user_id')).one_or_none()
|
||||
user = User.query.filter(User.id == session.get('webauthn_login_user_id')).one() #type: User
|
||||
form = ButtonForm()
|
||||
if user and form.validate_on_submit():
|
||||
pkcro, state = webauthn.authenticate_begin(webauthn_credentials(user))
|
||||
|
|
|
@ -21,11 +21,13 @@ from ory_hydra_client.models import GenericError
|
|||
from urllib.parse import urlencode, parse_qs
|
||||
from random import SystemRandom
|
||||
import string
|
||||
from collections.abc import Iterable
|
||||
from typing import Optional
|
||||
|
||||
from ..model import db, User, SecurityUser, Totp, WebauthnCredential
|
||||
from ..model import db, User, SecurityUser, Totp, AppToken, WebauthnCredential
|
||||
from ..form.frontend import ClientCertForm, TOTPForm, \
|
||||
TOTPDeleteForm, PasswordChangeForm, WebauthnRegisterForm
|
||||
TOTPDeleteForm, PasswordChangeForm, WebauthnRegisterForm, \
|
||||
AppTokenForm, AppTokenDeleteForm
|
||||
from ..form.base import ButtonForm
|
||||
from ..auth_providers import PasswordAuthProvider
|
||||
from .auth import webauthn
|
||||
|
@ -111,6 +113,8 @@ def revoke_client_cert(service_name, serial_number) -> ResponseReturnValue:
|
|||
'/client_cert/<service_name>/new',
|
||||
methods=['GET', 'POST'])
|
||||
def client_cert_new(service_name) -> ResponseReturnValue:
|
||||
if service_name not in lenticular_services:
|
||||
return '', 404
|
||||
service = lenticular_services[service_name]
|
||||
form = ClientCertForm()
|
||||
if form.validate_on_submit():
|
||||
|
@ -139,16 +143,51 @@ def client_cert_new(service_name) -> ResponseReturnValue:
|
|||
|
||||
@frontend_views.route('/app_token')
|
||||
def app_token() -> ResponseReturnValue:
|
||||
delete_form = TOTPDeleteForm()
|
||||
return render_template('frontend/app_token.html.j2', delete_form=delete_form)
|
||||
delete_form = AppTokenDeleteForm()
|
||||
form = ClientCertForm()
|
||||
return render_template('frontend/app_token.html.j2',
|
||||
delete_form=delete_form,
|
||||
services=lenticular_services)
|
||||
|
||||
@frontend_views.route('/app_token/<service_name>/new')
|
||||
@frontend_views.route('/app_token/<service_name>/new', methods=['GET','POST'])
|
||||
def app_token_new(service_name: str) -> ResponseReturnValue:
|
||||
return
|
||||
if service_name not in lenticular_services:
|
||||
return '', 404
|
||||
service = lenticular_services[service_name]
|
||||
form = AppTokenForm()
|
||||
|
||||
@frontend_views.route('/app_token/<service_name>/<token_name>')
|
||||
def app_token_delete(service_name: str, token_name: str) -> ResponseReturnValue:
|
||||
return
|
||||
if form.validate_on_submit():
|
||||
app_token = AppToken.new(service)
|
||||
form.populate_obj(app_token)
|
||||
# check for duplicate names
|
||||
for user_app_token in current_user.app_tokens:
|
||||
if user_app_token.name == app_token.name:
|
||||
return 'name already exist', 400
|
||||
current_user.app_tokens.append(app_token)
|
||||
db.session.commit()
|
||||
return render_template('frontend/app_token_new_show.html.j2', service=service, app_token=app_token)
|
||||
|
||||
|
||||
return render_template('frontend/app_token_new.html.j2',
|
||||
form=form,
|
||||
service=service)
|
||||
|
||||
@frontend_views.route('/app_token/<service_name>/<app_token_name>', methods=["POST"])
|
||||
def app_token_delete(service_name: str, app_token_name: str) -> ResponseReturnValue:
|
||||
form = AppTokenDeleteForm()
|
||||
|
||||
if service_name not in lenticular_services:
|
||||
return '', 404
|
||||
|
||||
service = lenticular_services[service_name]
|
||||
if form.validate_on_submit():
|
||||
app_token = current_user.get_token(service, app_token_name)
|
||||
if app_token is None:
|
||||
return 'not found', 404
|
||||
db.session.delete(app_token)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for('frontend.app_token'))
|
||||
|
||||
@frontend_views.route('/totp')
|
||||
def totp() -> ResponseReturnValue:
|
||||
|
@ -178,7 +217,7 @@ def totp_new() -> ResponseReturnValue:
|
|||
|
||||
@frontend_views.route('/totp/<totp_name>/delete', methods=['GET', 'POST'])
|
||||
def totp_delete(totp_name) -> ResponseReturnValue:
|
||||
totp = Totp.query.filter(Totp.name == totp_name).first()
|
||||
totp = Totp.query.filter(Totp.name == totp_name).first() # type: Optional[Totp]
|
||||
db.session.delete(totp)
|
||||
db.session.commit()
|
||||
|
||||
|
@ -190,7 +229,7 @@ def totp_delete(totp_name) -> ResponseReturnValue:
|
|||
def webauthn_list_route() -> ResponseReturnValue:
|
||||
"""list registered credentials for current user"""
|
||||
|
||||
creds = WebauthnCredential.query.all()
|
||||
creds = WebauthnCredential.query.all() # type: Iterable[WebauthnCredential]
|
||||
return render_template('frontend/webauthn_list.html', creds=creds, button_form=ButtonForm())
|
||||
|
||||
|
||||
|
@ -200,7 +239,7 @@ def webauthn_delete_route(webauthn_id: str) -> ResponseReturnValue:
|
|||
|
||||
form = ButtonForm()
|
||||
if form.validate_on_submit():
|
||||
cred = WebauthnCredential.query.filter(WebauthnCredential.id == webauthn_id).one()
|
||||
cred = WebauthnCredential.query.filter(WebauthnCredential.id == webauthn_id).one() # type: WebauthnCredential
|
||||
db.session.delete(cred)
|
||||
db.session.commit()
|
||||
return redirect(url_for('app.webauthn_list_route'))
|
||||
|
@ -223,7 +262,9 @@ def random_string(length=32) -> str:
|
|||
def webauthn_pkcco_route() -> ResponseReturnValue:
|
||||
"""get publicKeyCredentialCreationOptions"""
|
||||
|
||||
user = User.query.get(current_user.id)
|
||||
user = User.query.get(current_user.id) #type: Optional[User]
|
||||
if user is None:
|
||||
return 'internal error', 500
|
||||
user_handle = random_string()
|
||||
exclude_credentials = webauthn_credentials(user)
|
||||
pkcco, state = webauthn.register_begin(
|
||||
|
@ -238,7 +279,7 @@ def webauthn_pkcco_route() -> ResponseReturnValue:
|
|||
def webauthn_register_route() -> ResponseReturnValue:
|
||||
"""register credential for current user"""
|
||||
|
||||
user = User.query.get(current_user.id)
|
||||
user = current_user # type: User
|
||||
form = WebauthnRegisterForm()
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
|
@ -279,11 +320,11 @@ def password_change_post() -> ResponseReturnValue:
|
|||
current_user, password_old):
|
||||
return jsonify(
|
||||
{'errors': {'password_old': 'Old Password is invalid'}})
|
||||
resp = current_user.change_password(password_new)
|
||||
if resp:
|
||||
|
||||
current_user.change_password(password_new)
|
||||
logger.info(f"user {current_user.username} changed password")
|
||||
db.session.commit()
|
||||
return jsonify({})
|
||||
else:
|
||||
return jsonify({'errors': {'internal': 'internal server errror'}})
|
||||
return jsonify({'errors': form.errors})
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ 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 werkzeug.wrappers.response import Response as WerkzeugResponse
|
||||
import logging
|
||||
|
||||
from ..model import User, SecurityUser
|
||||
|
@ -28,7 +29,7 @@ def redirect_login() -> ResponseReturnValue:
|
|||
session['next_url'] = request.path
|
||||
redirect_uri = url_for('oauth2.authorized', _external=True)
|
||||
response = oauth2.custom.authorize_redirect(redirect_uri)
|
||||
if isinstance(response, Response):
|
||||
if not isinstance(response, WerkzeugResponse):
|
||||
raise RuntimeError("invalid redirect")
|
||||
return response
|
||||
|
||||
|
@ -44,7 +45,7 @@ def authorized() -> ResponseReturnValue:
|
|||
return 'bad request', 400
|
||||
session['token'] = token
|
||||
userinfo = oauth2.custom.get('/userinfo').json()
|
||||
user = User.query.get(str(userinfo["sub"]))
|
||||
user = User.query.get(str(userinfo["sub"])) # type: Optional[User]
|
||||
if user is None:
|
||||
return "user not found", 404
|
||||
logger.info(f"user `{user.username}` successfully logged in")
|
||||
|
@ -60,14 +61,14 @@ def authorized() -> ResponseReturnValue:
|
|||
def login() -> ResponseReturnValue:
|
||||
redirect_uri = url_for('.authorized', _external=True)
|
||||
response = oauth2.custom.authorize_redirect(redirect_uri)
|
||||
#if type(response) != Response:
|
||||
# raise RuntimeError("invalid redirect")
|
||||
if not isinstance(response, WerkzeugResponse):
|
||||
raise RuntimeError("invalid redirect")
|
||||
return response
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def user_loader(username) -> Optional[User]:
|
||||
user = User.query.filter_by(username=username).first()
|
||||
user = User.query.filter_by(username=username).first() # type: Optional[User]
|
||||
if isinstance(user, User):
|
||||
return user
|
||||
else:
|
||||
|
|
2953
package-lock.json
generated
2953
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue