add more registration stuff, admin interface

This commit is contained in:
TuxCoder 2020-06-01 23:43:10 +02:00
parent c1c8876b63
commit 67b69104d6
23 changed files with 369 additions and 92 deletions

View file

@ -47,6 +47,54 @@ window.$(document).ready(function () {
};
});
window.admin = {
registration: {
delete: function(href, registration_id, username) {
var dialog = new ConfirmDialog('Reject user registration', `Are you sure to reject the registration request from "${username}"?`);
dialog.show().then(()=>{
fetch(href, {
method: 'DELETE'
});
});
return false;
},
accept: function(href, registration_id, username) {
var dialog = new ConfirmDialog('Accept user registration', `Are you sure to accept the registration request from "${username}"?`);
dialog.show().then(()=>{
fetch(href, {
method: 'PUT'
});
});
return false;
}
}
};
window.auth = {
sign_up: {
submit: function(form) {
SimpleFormSubmit.submitForm(form.action, form)
.then(response =>{
response.json().then(function(data) {
if (data.errors) {
var msg ='<ul>';
for( var field in data.errors) {
msg += `<li>${field}: ${data.errors[field]}</li>`;
}
msg += '</ul>';
new Dialog('Registration Error', `Error Happend: ${msg}`).show()
} else {
new Dialog('Registration successfully', 'Wait until an administrator has aproved your account').show();
}
});
});
return false;
}
}
};
window.totp = {
init_list: function(){
},

View file

@ -10,6 +10,10 @@
width: 100%;
margin-top: 30px;
z-index: 500;
}
nav.sidebar {
top: 44px;
}

View file

@ -1,9 +1,13 @@
import logging
from lenticular_cloud.app import oidc_provider_init_app
from lenticular_cloud.app import init_app
from lenticular_cloud.model import User
name = 'oidc_provider'
app = oidc_provider_init_app(name)
app = init_app(name)
logging.basicConfig(level=logging.DEBUG)
with app.context():
with app.app_context():
for ldap_user in User.query_().all():
user = User.query.filter_by(username=str(ldap_user.username)).first()
print(user)

View file

@ -54,12 +54,13 @@ def init_app(name=None):
hydra_client = hydra.ApiClient(hydra_config)
app.hydra_api = hydra.AdminApi(hydra_client)
from .views import auth_views, frontend_views, init_login_manager, api_views, pki_views
from .views import auth_views, frontend_views, init_login_manager, api_views, pki_views, admin_views
init_login_manager(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.before_request
def befor_request():

View file

@ -6,7 +6,7 @@ from wtforms import StringField, SubmitField, TextField, \
SelectField, Form as NoCsrfForm, SelectMultipleField
from wtforms.fields.html5 import EmailField
from wtforms.widgets.html5 import NumberInput, DateInput
from wtforms.validators import DataRequired, NumberRange, Optional, NoneOf, Length
from wtforms.validators import DataRequired, NumberRange, Optional, NoneOf, Length, Regexp
from datetime import datetime
@ -33,12 +33,18 @@ class Fido2Form(FlaskForm):
class ConsentForm(FlaskForm):
# scopes = SelectMultipleField(gettext('scopes'))
# audiences = SelectMultipleField(gettext('audiences'))
remember = BooleanField(gettext('remember me'))
remember = BooleanField(gettext('remember'))
submit = SubmitField()
class RegistrationForm(FlaskForm):
username = StringField(gettext('Username'), validators=[DataRequired()])
password = PasswordField(gettext('Password'), validators=[DataRequired()])
alternative_email = EmailField(gettext('Alternative Email'))
username = StringField(gettext('Username'), validators=[
DataRequired(),
Regexp('^[a-zA-Z0-9-.]+$', message=gettext('Only `a-z`, `A-Z`, `0-9`, `-.` is allowed for username'))
])
password = PasswordField(gettext('Password'), validators=[
DataRequired(),
Length(min=6)
])
alternative_email = EmailField(gettext('Alternative Email'), render_kw={"placeholder": "Optional"})
submit = SubmitField()

View file

@ -28,6 +28,14 @@ base_dn = ''
db = SQLAlchemy() # type: SQLAlchemy
class UserSignUp(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, nullable=False)
password = db.Column(db.String, nullable=False)
alternative_email = db.Column(db.String)
created_at = db.Column(db.DateTime, nullable=False,
default=datetime.now)
class SecurityUser(UserMixin):
@ -51,7 +59,7 @@ class EntryBase(db.Model):
__abstract__ = True # for sqlalchemy
_type = None # will get replaced by the local type
_query_object = None # will get replaced by the local type
_ldap_query_object = None # will get replaced by the local type
_base_dn = LambdaStr(lambda: base_dn)
# def __init__(self, ldap_object=None, **kwargs):
@ -60,39 +68,45 @@ class EntryBase(db.Model):
# else:
# self._ldap_object = ldap_object
def __str__(self):
def __str__(self) -> str:
return str(self._ldap_object)
@classmethod
def get_object_def(cls):
def get_object_def(cls) -> ObjectDef:
return ObjectDef(cls.object_classes, ldap_conn)
@classmethod
def get_base(cls):
def get_entry_type(cls) -> EntryType:
return EntryType(cls.get_dn(), cls.object_classes, ldap_conn)
@classmethod
def get_base(cls) -> str:
return cls.base_dn.format(_base_dn=base_dn)
@classmethod
def get_dn(cls) -> str:
return cls.dn.replace('{base_dn}', cls.get_base())
@classmethod
def get_type(cls):
if cls._type is None:
cls._type = EntryType(cls.dn.replace('{base_dn}',cls.get_base()), cls.object_classes, ldap_conn)
cls._type = EntryType(cls.get_dn(), cls.object_classes, ldap_conn)
return cls._type
def commit(self):
def ldap_commit(self):
self._ldap_object.entry_commit_changes()
def add(self):
print(self._ldap_object.entry_attributes_as_dict)
def ldap_add(self):
ret = ldap_conn.add(
self.dn, self.object_classes, self._ldap_object.entry_attributes_as_dict)
logger.debug(ret)
pass
self.entry_dn, self.object_classes, self._ldap_object.entry_attributes_as_dict)
if not ret:
raise Exception('ldap error')
@classmethod
def query_(cls):
if cls._query_object is None:
cls._query_object = cls._query(cls)
return cls._query_object
if cls._ldap_query_object is None:
cls._ldap_query_object = cls._query(cls)
return cls._ldap_query_object
class _query(object):
def __init__(self, clazz):
@ -114,8 +128,6 @@ class EntryBase(db.Model):
return self._query(None)
class Service(object):
def __init__(self, name):
@ -213,16 +225,20 @@ class User(EntryBase):
id = db.Column(
db.String(length=36), primary_key=True, default=generate_uuid)
username = db.Column(
db.String, unique=True)
db.String, unique=True, nullable=False)
created_at = db.Column(db.DateTime, nullable=False,
default=datetime.now)
modified_at = db.Column(db.DateTime, nullable=False,
default=datetime.now, onupdate=datetime.now)
last_login = db.Column(db.DateTime, nullable=True)
totps = db.relationship('Totp', back_populates='user')
dn = "uid={uid},{base_dn}"
base_dn = "ou=users,{_base_dn}"
object_classes = ["top", "inetOrgPerson", "LenticularUser"]
def __init__(self,**kwargs):
def __init__(self, **kwargs):
self._ldap_object = None
super(db.Model).__init__(**kwargs)
@ -236,6 +252,13 @@ class User(EntryBase):
def make_writeable(self):
self._ldap_object = self._ldap_object.entry_writable()
@property
def groups(self):
if self.username == 'tuxcoder':
return [Group(name='admin')]
else:
return []
@property
def entry_dn(self):
return self._ldap_object.entry_dn
@ -274,7 +297,7 @@ class User(EntryBase):
self.make_writeable()
password_hashed = crypt.crypt(password_new)
self._ldap_object.userPassword = ('{CRYPT}' + password_hashed).encode()
self.commit()
self.ldap_commit()
class _query(EntryBase._query):
@ -296,6 +319,21 @@ class User(EntryBase):
else:
return None
@staticmethod
def new(user_data: UserSignUp):
user = User()
user.username = user_data.username.lower()
domain = current_app.config['DOMAIN']
ldap_object = User.get_entry_type()(
uid=user_data.username.lower(),
sn=user_data.username,
cn=user_data.username,
userPassword='{CRYPT}' + user_data.password,
mail=f'{user_data.username}@{domain}')
user._ldap_object = ldap_object
user.ldap_add()
return user
class Totp(db.Model):
@ -321,9 +359,6 @@ class Group(EntryBase):
fullname = AttrDef("cn")
class UserSignUp(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, nullable=False)
password = db.Column(db.String, nullable=False)
alternative_email = db.Column(db.String)
name = db.Column(db.String(), nullable=False, unique=True)

View file

@ -2,5 +2,6 @@
from .auth import auth_views
from .frontend import frontend_views, init_login_manager
from .admin import admin_views
from .api import api_views
from .pki import pki_views

View file

@ -0,0 +1,66 @@
import flask
from flask import Blueprint, redirect, request, url_for, render_template
from flask import current_app, session
from flask import jsonify
from flask_login import current_user, logout_user
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
from ..model import db, User, UserSignUp
admin_views = Blueprint('admin', __name__, url_prefix='/admin')
def before_request():
try:
resp = current_app.oauth.session.get('/userinfo')
data = resp.json()
if not current_user.is_authenticated or resp.status_code is not 200:
logout_user()
return redirect(url_for('oauth.login'))
if 'admin' not in data['groups']:
return 'Not an admin', 403
except TokenExpiredError:
logout_user()
return redirect(url_for('oauth.login'))
admin_views.before_request(before_request)
@admin_views.route('/', methods=['GET', 'POST'])
def index():
return render_template('admin/index.html.j2')
@admin_views.route('/user', methods=['GET'])
def users():
users = User.query.all()
return render_template('admin/users.html.j2', users=users)
@admin_views.route('/registrations', methods=['GET'])
def registrations():
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):
user_data = UserSignUp.query.get(registration_id)
if user_data is None:
return jsonify({}), 404
db.session.delete(user_data)
db.session.commit()
return jsonify({})
@admin_views.route('/registration/<registration_id>', methods=['PUT'])
def registration_accept(registration_id):
user_data = UserSignUp.query.get(registration_id)
#create user
user = User.new(user_data)
db.session.add(user)
db.session.delete(user_data)
db.session.commit()
return jsonify({})

View file

@ -7,13 +7,15 @@ from flask import current_app, session
from flask.templating import render_template
from flask_babel import gettext
from flask import request, url_for
from flask import request, url_for, jsonify
from flask_login import login_required, login_user, logout_user, current_user
import logging
from urllib.parse import urlparse
from base64 import b64decode, b64encode
import http
import crypt
import ory_hydra_client
from datetime import datetime
from ..model import db, User, SecurityUser, UserSignUp
from ..form.auth import ConsentForm, LoginForm, RegistrationForm
@ -31,8 +33,11 @@ def consent():
form = ConsentForm()
remember_for = 60*60*24*30 # remember for 7 days
try:
consent_request = current_app.hydra_api.get_consent_request(
request.args['consent_challenge'])
except ory_hydra_client.exceptions.ApiException:
return redirect(url_for('frontend.index'))
requested_scope = consent_request.requested_scope
requested_audiences = consent_request.requested_access_token_audience
@ -40,9 +45,11 @@ def consent():
if form.validate_on_submit() or consent_request.skip:
user = User.query.get(consent_request.subject)
token_data = {
'name': str(user.username),
'preferred_username': str(user.username),
'email': str(user.email),
'email_verified': True,
'groups': [group.name for group in user.groups]
}
id_token_data = {}
if 'openid' in requested_scope:
@ -70,7 +77,10 @@ def consent():
@auth_views.route('/login', methods=['GET', 'POST'])
def login():
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:
return redirect(url_for('frontend.index'))
if login_request.skip:
resp = current_app.hydra_api.accept_login_request(
@ -93,7 +103,11 @@ def login():
@auth_views.route('/login/auth', methods=['GET', 'POST'])
def login_auth():
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:
return redirect(url_for('frontend.index'))
if 'username' not in session:
return redirect(url_for('auth.login'))
auth_forms = {}
@ -115,7 +129,8 @@ def login_auth():
# db.session.commit()
subject = user.id
user.last_login = datetime.now()
db.session.commit()
resp = current_app.hydra_api.accept_login_request(
login_challenge, body={
'subject': subject,
@ -135,8 +150,13 @@ def logout():
@auth_views.route("/sign_up", methods=["GET", "POST"])
@auth_views.route("/sign_up", methods=["GET"])
def sign_up():
form = RegistrationForm()
return render_template('auth/sign_up.html.j2', form=form)
@auth_views.route("/sign_up", methods=["POST"])
def sign_up_submit():
form = RegistrationForm()
if form.validate_on_submit():
user = UserSignUp()
@ -145,5 +165,8 @@ def sign_up():
user.alternative_email = form.data['alternative_email']
db.session.add(user)
db.session.commit()
return render_template('auth/sign_up.html.j2', form=form)
return jsonify({})
return jsonify({
'status': 'error',
'errors': form.errors
})

View file

@ -27,8 +27,10 @@ def before_request():
try:
resp = current_app.oauth.session.get('/userinfo')
if not current_user.is_authenticated or resp.status_code is not 200:
logout_user()
return redirect(url_for('oauth.login'))
except TokenExpiredError:
logout_user()
return redirect(url_for('oauth.login'))

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

View file

@ -0,0 +1,37 @@
{% extends 'base.html.j2' %}
{% block body %}
<div class="container-fluid">
<div class="row">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">&times;</button>
{{ message }}
</div>
{% endfor %}
</div>
<div class="row">
<nav class="col-md-2 d-none d-md-block bg-light sidebar fixed-top">
<div class="sidebar-sticky active">
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.users') }}">{{ gettext('users') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.registrations') }}">{{ gettext('registrations') }}</a></li>
</div>
</nav>
<main class="col-md-9 ml-sm-auto col-lg-10 px-4" role="main">
<h1>{% block title %}{% endblock %}</h1>
<div class="card">
<div class="card-body mt-5 mb-5">
<div class="tab-content">
{% block content %}{% endblock %}
</div>
</div>
</div>
</main>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends 'admin/base.html.j2' %}

View file

@ -0,0 +1,28 @@
{% extends 'admin/base.html.j2' %}
{% block title %}{{ gettext('registrations') }}{% endblock %}
{% block content %}
<table class="table">
<thead>
<tr>
<th>username</th>
<th>created_at</th>
<th>action<th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td>{{ user.created_at }}</td>
<td>
<a title="{{ gettext('Reject')}}" href="{{ url_for('.registration_delete', registration_id=user.id) }}" onclick="admin.registration.delete(this.href, '{{ user.username }}'); return false;"><i class="fas fa-ban"></i></a>
<a title="{{ gettext('Reject')}}" href="{{ url_for('.registration_accept', registration_id=user.id) }}" onclick="admin.registration.accept(this.href, '{{ user.username }}'); return false;"><i class="fas fa-check"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock%}

View file

@ -0,0 +1,26 @@
{% extends 'admin/base.html.j2' %}
{% block title %}{{ gettext('users') }}{% endblock %}
{% block content %}
<table class="table">
<thead>
<tr>
<th>username</th>
<th>created_at</th>
<th>modified_at<th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td>{{ user.created_at }}</td>
<td>{{ user.modified_at }}</td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock%}

View file

@ -2,12 +2,7 @@
{% block body %}
<nav class="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
<div class="col-xs-1"><button id="sidebarCollapse" class="btn btn-primary d-xs-block d-md-none" ><i class="fa fa-bars fa-2x"></i></button></div>
<div class="col-xs-11"><a class="navbar-brand col-xs-11 col-sm-3 col-md-2 mr-0" href="/">Lenticular Cloud</a></div>
</nav>
<div style="height: 40px"></div>
<div class="container-fluid">
<div class="row">
{% for message in get_flashed_messages() %}

View file

@ -5,7 +5,7 @@
{% block content %}
{{ render_form(form) }}
{{ render_form(form, onsubmit="return auth.sign_up.submit(this)", action_url=url_for('auth.sign_up_submit')) }}
{% endblock %}

View file

@ -153,9 +153,9 @@
action_text - text of submit button
class_ - sets a class for form
#}
{% macro render_form(form, action_url='', class_='', method='post') -%}
{% macro render_form(form, action_url='', class_='', method='post', onsubmit='') -%}
<form method="{{ method }}" {% if action_url %}action="{{ action_url }}" {% endif %}role="form" class="{{ class_ }}">
<form method="{{ method }}" {% if action_url %}action="{{ action_url }}" {% endif %}role="form" class="{{ class_ }}" {% if onsubmit %}onsubmit="{{ onsubmit }}"{% endif %}>
<input name="form" type="hidden" value="{{ form.__class__.__name__ }}">
{{ _render_form(form) }}
</form>

View file

@ -3,13 +3,6 @@
{% block body %}
<nav class="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
<div class="col-xs-1"><button id="sidebarCollapse" class="btn btn-primary d-xs-block d-md-none" ><i class="fa fa-bars fa-2x"></i></button></div>
<div class="col-xs-11"><a class="navbar-brand col-xs-11 col-sm-3 col-md-2 mr-0" href="/">Lenticular Cloud</a></div>
</nav>
<div style="height: 40px"></div>
<div class="container-fluid">
<div class="row">
{% for message in get_flashed_messages() %}

View file

@ -7,11 +7,14 @@
<table class="table">
<thead>
<tr>
<th>{{ gettext('Client ID') }}</th>
<th>{{ gettext('Remember me') }}</th>
<th>{{ gettext('Remember') }}</th>
<th>{{ gettext('Created at') }}
<th>{{ gettext('Action') }}
</tr>
</thead>
<tbody>
{% for consent_session in consent_sessions %}
<tr>
<td>{{ consent_session.consent_request.client.client_id }}</td>
@ -21,6 +24,8 @@
<a title="{{ gettext('Revoke')}}" href="{{ url_for('.oauth2_token_revoke', client_id=consent_session.consent_request.client.client_id) }}" onclick="oauth2_token.revoke(this.href, '{{ consent_session.consent_request.client.client_id }}'); return false;"><i class="fas fa-ban"></i></a>
</td>
</tr>
</tbody>
{% endfor %}
</table>
{% endblock %}

View file

@ -10,26 +10,27 @@
<body>
<div class='messages-box'>
</div>
<template id='confirm-dialog-template'>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<nav class="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
{% if current_user.is_authenticated %}
<a class="nav-link" href="{{ url_for('frontend.logout') }}">{{ gettext('Logout') }}</a>
Hallo {{ current_user.username }}
{% endif %}
<div class="col-xs-1"><button id="sidebarCollapse" class="btn btn-primary d-xs-block d-md-none" ><i class="fa fa-bars fa-2x"></i></button></div>
<div class="col-xs-11"><a class="navbar-brand col-xs-11 col-sm-3 col-md-2 mr-0" href="/">Lenticular Cloud</a></div>
</nav>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger close" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-ok process">Process</button>
</div>
</div>
</div>
</template>
<div style="height: 40px"></div>
{% block body %}{% endblock %}
<div class='container'>
<div class="mt-5 row justify-content-center">
<footer>
<span class="text-muted">Render Time: {{ g.request_time() }}</span> | <span class="text-muted">{{ gettext('All right reserved. &copy;') + '2020' }}</span>
</footer>
</div>
</div>
<script type="application/javascript" src="/static/main.js?v={{ GIT_HASH }}" ></script>
<script type="application/javascript" >
{% block script_js %}{% endblock %}