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

View file

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

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

View file

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

View file

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

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

View file

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

View file

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

View file

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