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
|
||||
- Manage several redirect uris when adding a client
|
||||
- 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
|
||||
- Consents admin
|
||||
|
|
|
@ -6,6 +6,7 @@ import oidc_ldap_bridge.admin
|
|||
import oidc_ldap_bridge.admin.tokens
|
||||
import oidc_ldap_bridge.admin.authorizations
|
||||
import oidc_ldap_bridge.admin.clients
|
||||
import oidc_ldap_bridge.consents
|
||||
import oidc_ldap_bridge.oauth
|
||||
import oidc_ldap_bridge.routes
|
||||
import oidc_ldap_bridge.tokens
|
||||
|
@ -122,6 +123,7 @@ def setup_app(app):
|
|||
setup_ldap_tree(app)
|
||||
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.consents.bp, url_prefix="/consent")
|
||||
app.register_blueprint(oidc_ldap_bridge.tokens.bp, url_prefix="/token")
|
||||
app.register_blueprint(
|
||||
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())
|
||||
|
||||
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.oauth2 import OAuth2Error
|
||||
from flask import Blueprint, request, session, redirect
|
||||
|
@ -42,6 +43,7 @@ def authorize():
|
|||
oauthClient=client.dn,
|
||||
oauthSubject=user.dn,
|
||||
)
|
||||
consents = [c for c in consents if not c.oauthRevokationDate]
|
||||
consent = consents[0] if consents else None
|
||||
|
||||
if request.method == "GET":
|
||||
|
@ -63,6 +65,7 @@ def authorize():
|
|||
oauthClient=client.dn,
|
||||
oauthSubject=user.dn,
|
||||
oauthScope=scopes,
|
||||
oauthIssueDate=datetime.datetime.now().strftime("%Y%m%d%H%M%SZ"),
|
||||
)
|
||||
|
||||
consent.save()
|
||||
|
|
|
@ -30,6 +30,10 @@
|
|||
</a>
|
||||
</div>
|
||||
{% 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') }}">
|
||||
<i class="key icon"></i>
|
||||
{% trans %}My tokens{% endtrans %}
|
||||
|
@ -51,6 +55,10 @@
|
|||
<i class="user secret icon"></i>
|
||||
{% trans %}Codes{% endtrans %}
|
||||
</a>
|
||||
<a class="item" href="">
|
||||
<i class="handshake icon"></i>
|
||||
{% trans %}Consents{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% 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>
|
||||
<a class="ui bottom attached button" href="{{ url_for('oidc_ldap_bridge.tokens.delete', token_id=token.oauthAccessToken ) }}">
|
||||
<i class="remove icon"></i>
|
||||
{% trans %}Remove access{% endtrans %}
|
||||
{% trans %}Remove token{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -364,4 +364,8 @@ olcObjectClasses: ( 1.3.6.1.4.1.56207.1.2.4 NAME 'oauthConsent'
|
|||
oauthClient $
|
||||
oauthScope
|
||||
)
|
||||
MAY (
|
||||
oauthIssueDate $
|
||||
oauthRevokationDate
|
||||
)
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
|
|
|
@ -361,4 +361,8 @@ objectclass ( 1.3.6.1.4.1.56207.1.2.4 NAME 'oauthConsent'
|
|||
oauthClient $
|
||||
oauthScope
|
||||
)
|
||||
MAY (
|
||||
oauthIssueDate $
|
||||
oauthRevokationDate
|
||||
)
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
|
|
|
@ -241,6 +241,18 @@ def token(slapd_connection, client, user):
|
|||
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
|
||||
def logged_user(user, testclient):
|
||||
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