forked from Github-Mirrors/canaille
Consents page
This commit is contained in:
parent
6cb668c64a
commit
09ae01a5df
12 changed files with 182 additions and 3 deletions
2
TODO.md
2
TODO.md
|
@ -5,7 +5,5 @@
|
||||||
- Test with wrong inputs
|
- Test with wrong inputs
|
||||||
- Manage several redirect uris when adding a client
|
- Manage several redirect uris when adding a client
|
||||||
- Special page when schemas are not installed (NO_SUCH_OBJECT exception)
|
- Special page when schemas are not installed (NO_SUCH_OBJECT exception)
|
||||||
- Display a page for consents.
|
|
||||||
- Removing a consent should revoke all related tokens.
|
|
||||||
- User admin
|
- User admin
|
||||||
- Consents admin
|
- Consents admin
|
||||||
|
|
|
@ -6,6 +6,7 @@ import oidc_ldap_bridge.admin
|
||||||
import oidc_ldap_bridge.admin.tokens
|
import oidc_ldap_bridge.admin.tokens
|
||||||
import oidc_ldap_bridge.admin.authorizations
|
import oidc_ldap_bridge.admin.authorizations
|
||||||
import oidc_ldap_bridge.admin.clients
|
import oidc_ldap_bridge.admin.clients
|
||||||
|
import oidc_ldap_bridge.consents
|
||||||
import oidc_ldap_bridge.oauth
|
import oidc_ldap_bridge.oauth
|
||||||
import oidc_ldap_bridge.routes
|
import oidc_ldap_bridge.routes
|
||||||
import oidc_ldap_bridge.tokens
|
import oidc_ldap_bridge.tokens
|
||||||
|
@ -122,6 +123,7 @@ def setup_app(app):
|
||||||
setup_ldap_tree(app)
|
setup_ldap_tree(app)
|
||||||
app.register_blueprint(oidc_ldap_bridge.routes.bp)
|
app.register_blueprint(oidc_ldap_bridge.routes.bp)
|
||||||
app.register_blueprint(oidc_ldap_bridge.oauth.bp, url_prefix="/oauth")
|
app.register_blueprint(oidc_ldap_bridge.oauth.bp, url_prefix="/oauth")
|
||||||
|
app.register_blueprint(oidc_ldap_bridge.consents.bp, url_prefix="/consent")
|
||||||
app.register_blueprint(oidc_ldap_bridge.tokens.bp, url_prefix="/token")
|
app.register_blueprint(oidc_ldap_bridge.tokens.bp, url_prefix="/token")
|
||||||
app.register_blueprint(
|
app.register_blueprint(
|
||||||
oidc_ldap_bridge.well_known.bp, url_prefix="/.well-known"
|
oidc_ldap_bridge.well_known.bp, url_prefix="/.well-known"
|
||||||
|
|
33
oidc_ldap_bridge/consents.py
Normal file
33
oidc_ldap_bridge/consents.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import datetime
|
||||||
|
from flask import Blueprint, render_template, flash, redirect, url_for
|
||||||
|
from flask_babel import gettext
|
||||||
|
from oidc_ldap_bridge.models import Consent, Client
|
||||||
|
from oidc_ldap_bridge.flaskutils import user_needed
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint(__name__, "consents")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/")
|
||||||
|
@user_needed()
|
||||||
|
def consents(user):
|
||||||
|
consents = Consent.filter(oauthSubject=user.dn)
|
||||||
|
consents = [c for c in consents if not c.oauthRevokationDate]
|
||||||
|
client_dns = list(set(t.oauthClient for t in consents))
|
||||||
|
clients = {dn: Client.get(dn) for dn in client_dns}
|
||||||
|
return render_template("consent_list.html", consents=consents, clients=clients)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/delete/<consent_id>")
|
||||||
|
@user_needed()
|
||||||
|
def delete(user, consent_id):
|
||||||
|
consent = Consent.get(consent_id)
|
||||||
|
|
||||||
|
if not consent or consent.oauthSubject != user.dn:
|
||||||
|
flash(gettext("Could not delete this access"), "error")
|
||||||
|
|
||||||
|
else:
|
||||||
|
consent.revoke()
|
||||||
|
flash(gettext("The access has been revoked"), "success")
|
||||||
|
|
||||||
|
return redirect(url_for("oidc_ldap_bridge.consents.consents"))
|
|
@ -228,3 +228,29 @@ class Consent(LDAPObjectHelper):
|
||||||
kwargs["cn"] = str(uuid.uuid4())
|
kwargs["cn"] = str(uuid.uuid4())
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issue_date(self):
|
||||||
|
return datetime.datetime.strptime(self.oauthIssueDate, "%Y%m%d%H%M%SZ")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def revokation_date(self):
|
||||||
|
return datetime.datetime.strptime(self.oauthRevokationDate, "%Y%m%d%H%M%SZ")
|
||||||
|
|
||||||
|
def revoke(self):
|
||||||
|
self.oauthRevokationDate = datetime.datetime.now().strftime("%Y%m%d%H%M%SZ")
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
tokens = Token.filter(
|
||||||
|
oauthClient=self.oauthClient,
|
||||||
|
oauthSubject=self.oauthSubject,
|
||||||
|
)
|
||||||
|
for t in tokens:
|
||||||
|
a = [scope not in t.oauthScope[0] for scope in self.oauthScope]
|
||||||
|
if t.revoked or any(
|
||||||
|
scope not in t.oauthScope[0] for scope in self.oauthScope
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
t.oauthRevokationDate = self.oauthRevokationDate
|
||||||
|
t.save()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import datetime
|
||||||
from authlib.jose import jwk
|
from authlib.jose import jwk
|
||||||
from authlib.oauth2 import OAuth2Error
|
from authlib.oauth2 import OAuth2Error
|
||||||
from flask import Blueprint, request, session, redirect
|
from flask import Blueprint, request, session, redirect
|
||||||
|
@ -42,6 +43,7 @@ def authorize():
|
||||||
oauthClient=client.dn,
|
oauthClient=client.dn,
|
||||||
oauthSubject=user.dn,
|
oauthSubject=user.dn,
|
||||||
)
|
)
|
||||||
|
consents = [c for c in consents if not c.oauthRevokationDate]
|
||||||
consent = consents[0] if consents else None
|
consent = consents[0] if consents else None
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
|
@ -63,6 +65,7 @@ def authorize():
|
||||||
oauthClient=client.dn,
|
oauthClient=client.dn,
|
||||||
oauthSubject=user.dn,
|
oauthSubject=user.dn,
|
||||||
oauthScope=scopes,
|
oauthScope=scopes,
|
||||||
|
oauthIssueDate=datetime.datetime.now().strftime("%Y%m%d%H%M%SZ"),
|
||||||
)
|
)
|
||||||
|
|
||||||
consent.save()
|
consent.save()
|
||||||
|
|
|
@ -30,6 +30,10 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a class="item" href="{{ url_for('oidc_ldap_bridge.consents.consents') }}">
|
||||||
|
<i class="handshake icon"></i>
|
||||||
|
{% trans %}My consents{% endtrans %}
|
||||||
|
</a>
|
||||||
<a class="item" href="{{ url_for('oidc_ldap_bridge.tokens.tokens') }}">
|
<a class="item" href="{{ url_for('oidc_ldap_bridge.tokens.tokens') }}">
|
||||||
<i class="key icon"></i>
|
<i class="key icon"></i>
|
||||||
{% trans %}My tokens{% endtrans %}
|
{% trans %}My tokens{% endtrans %}
|
||||||
|
@ -51,6 +55,10 @@
|
||||||
<i class="user secret icon"></i>
|
<i class="user secret icon"></i>
|
||||||
{% trans %}Codes{% endtrans %}
|
{% trans %}Codes{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
|
<a class="item" href="">
|
||||||
|
<i class="handshake icon"></i>
|
||||||
|
{% trans %}Consents{% endtrans %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
70
oidc_ldap_bridge/templates/consent_list.html
Normal file
70
oidc_ldap_bridge/templates/consent_list.html
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
||||||
|
<link href="/static/datatables/dataTables.semanticui.min.css" rel="stylesheet">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script src="/static/datatables/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="/static/datatables/dataTables.semanticui.min.js"></script>
|
||||||
|
<script src="/static/js/users.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="ui segment">
|
||||||
|
<h1 class="ui header">{% trans %}My consents{% endtrans %}</h1>
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="ui attached message {{ category }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% if consents %}
|
||||||
|
<div class="ui centered cards">
|
||||||
|
{% for consent in consents %}
|
||||||
|
{% set client = clients[consent.oauthClient] %}
|
||||||
|
<div class="ui card">
|
||||||
|
<div class="content">
|
||||||
|
{% if client.oauthLogoURI %}
|
||||||
|
<img class="right floated mini ui image" src="{{ client.oauthLogoURI }}">
|
||||||
|
{% endif %}
|
||||||
|
{% if client.oauthClientURI %}
|
||||||
|
<a href="{{ client.oauthClientURI }}" class="header">{{ client.oauthClientName }}</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="header">{{ client.oauthClientName }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="meta">{% trans %}From:{% endtrans %} {{ consent.issue_date.strftime("%d/%m/%Y %H:%M:%S") }}</div>
|
||||||
|
{% if consent.oauthRevokationDate %}
|
||||||
|
<div class="meta">{% trans %}Revoked:{% endtrans %} {{ consent.revokation_date.strftime("%d/%m/%Y %H:%M:%S") }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="description">
|
||||||
|
<p>{% trans %}Has access to:{% endtrans %}</p>
|
||||||
|
<ul>
|
||||||
|
{% for s in consent.oauthScope %}
|
||||||
|
<li>{{ s }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="ui bottom attached button" href="{{ url_for('oidc_ldap_bridge.consents.delete', consent_id=consent.cn[0] ) }}">
|
||||||
|
<i class="remove icon"></i>
|
||||||
|
{% trans %}Remove access{% endtrans %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="ui center aligned">
|
||||||
|
<i class="massive smile outline icon image ui"></i>
|
||||||
|
|
||||||
|
<h2 class="ui center aligned header">
|
||||||
|
<div class="content">{% trans %}Nothing here{% endtrans %}</div>
|
||||||
|
<div class="sub header">{% trans %}You did not authorize applications yet.{% endtrans %}</div>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -50,7 +50,7 @@
|
||||||
</div>
|
</div>
|
||||||
<a class="ui bottom attached button" href="{{ url_for('oidc_ldap_bridge.tokens.delete', token_id=token.oauthAccessToken ) }}">
|
<a class="ui bottom attached button" href="{{ url_for('oidc_ldap_bridge.tokens.delete', token_id=token.oauthAccessToken ) }}">
|
||||||
<i class="remove icon"></i>
|
<i class="remove icon"></i>
|
||||||
{% trans %}Remove access{% endtrans %}
|
{% trans %}Remove token{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -364,4 +364,8 @@ olcObjectClasses: ( 1.3.6.1.4.1.56207.1.2.4 NAME 'oauthConsent'
|
||||||
oauthClient $
|
oauthClient $
|
||||||
oauthScope
|
oauthScope
|
||||||
)
|
)
|
||||||
|
MAY (
|
||||||
|
oauthIssueDate $
|
||||||
|
oauthRevokationDate
|
||||||
|
)
|
||||||
X-ORIGIN 'OAuth 2.0' )
|
X-ORIGIN 'OAuth 2.0' )
|
||||||
|
|
|
@ -361,4 +361,8 @@ objectclass ( 1.3.6.1.4.1.56207.1.2.4 NAME 'oauthConsent'
|
||||||
oauthClient $
|
oauthClient $
|
||||||
oauthScope
|
oauthScope
|
||||||
)
|
)
|
||||||
|
MAY (
|
||||||
|
oauthIssueDate $
|
||||||
|
oauthRevokationDate
|
||||||
|
)
|
||||||
X-ORIGIN 'OAuth 2.0' )
|
X-ORIGIN 'OAuth 2.0' )
|
||||||
|
|
|
@ -241,6 +241,18 @@ def token(slapd_connection, client, user):
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def consent(slapd_connection, client, user):
|
||||||
|
t = Consent(
|
||||||
|
oauthClient=client.dn,
|
||||||
|
oauthSubject=user.dn,
|
||||||
|
oauthScope=["openid", "profile"],
|
||||||
|
oauthIssueDate=datetime.datetime.now().strftime("%Y%m%d%H%M%SZ"),
|
||||||
|
)
|
||||||
|
t.save(slapd_connection)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def logged_user(user, testclient):
|
def logged_user(user, testclient):
|
||||||
with testclient.session_transaction() as sess:
|
with testclient.session_transaction() as sess:
|
||||||
|
|
19
tests/test_consent.py
Normal file
19
tests/test_consent.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
def test_no_logged_no_access(testclient):
|
||||||
|
testclient.get("/consent", status=403)
|
||||||
|
|
||||||
|
|
||||||
|
def test_client_list(testclient, slapd_connection, client, consent, logged_user, token):
|
||||||
|
res = testclient.get("/consent")
|
||||||
|
assert 200 == res.status_code
|
||||||
|
assert client.oauthClientName in res.text
|
||||||
|
assert not token.revoked
|
||||||
|
|
||||||
|
res = testclient.get(f"/consent/delete/{consent.cn[0]}")
|
||||||
|
assert 302 == res.status_code
|
||||||
|
|
||||||
|
res = res.follow()
|
||||||
|
assert 200 == res.status_code
|
||||||
|
assert client.oauthClientName not in res.text
|
||||||
|
|
||||||
|
token.reload(conn=slapd_connection)
|
||||||
|
assert token.revoked
|
Loading…
Reference in a new issue