Consents page

This commit is contained in:
Éloi Rivard 2020-09-17 12:01:21 +02:00
parent 6cb668c64a
commit 09ae01a5df
12 changed files with 182 additions and 3 deletions

View file

@ -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

View file

@ -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"

View 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"))

View file

@ -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()

View file

@ -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()

View file

@ -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 %}

View 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 %}

View file

@ -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 %}

View file

@ -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' )

View file

@ -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' )

View file

@ -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
View 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