Implement membership_required client option
User without an active membership: * will be able to log into sso.hackerspace.pl * will not be able to create/modify any of their clients * will not be able to authorize to any client that has "Active membership required" option enabled * API requests for said user will return 402 (in case a token has already been issued) Authorization errors will now be wrapped in properly rendered alerts.master
parent
c4c810cd25
commit
a57ab99014
|
@ -16,7 +16,9 @@ services:
|
|||
volumes:
|
||||
- .:/usr/src/app
|
||||
environment:
|
||||
- TESTING=1
|
||||
- TEMPLATES_AUTO_RELOAD=true
|
||||
- AUTHLIB_INSECURE_TRANSPORT=1
|
||||
- LDAP_BIND_DN
|
||||
- LDAP_BIND_PASSWORD
|
||||
- LOGGING_LEVEL=DEBUG
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
"""add client.membership_required
|
||||
|
||||
Revision ID: 59b941d00ea9
|
||||
Revises: dd58bc95a904
|
||||
Create Date: 2022-04-28 20:59:06.161062
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '59b941d00ea9'
|
||||
down_revision = 'dd58bc95a904'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('oauth2_client', sa.Column('membership_required', sa.Boolean(), server_default='1', nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('oauth2_client', 'membership_required')
|
||||
# ### end Alembic commands ###
|
|
@ -82,16 +82,20 @@ class LDAPUserProxy(object):
|
|||
def email(self):
|
||||
return self.username + "@hackerspace.pl"
|
||||
|
||||
@cached_property
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
@cached_property
|
||||
def is_membership_active(self):
|
||||
url = "https://kasownik.hackerspace.pl/api/judgement/{}.json"
|
||||
try:
|
||||
r = requests.get(url.format(self.username))
|
||||
return bool(r.json()["content"])
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
return data["status"] == "ok" and data["content"]
|
||||
except Exception as e:
|
||||
logging.error("When getting data from Kasownik: {}".format(e))
|
||||
# Fail-safe.
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_id(self):
|
||||
return self.username
|
||||
|
|
|
@ -80,3 +80,9 @@ class ClientForm(FlaskForm):
|
|||
validators=[DataRequired()],
|
||||
default=["openid"],
|
||||
)
|
||||
|
||||
membership_required = BooleanField(
|
||||
"Active membership required",
|
||||
default=True,
|
||||
description="User will be refused authorization to this client if their membership in Kasownik is not active",
|
||||
)
|
||||
|
|
|
@ -16,6 +16,10 @@ class Client(db.Model, OAuth2ClientMixin):
|
|||
|
||||
owner_id = db.Column(db.String(40), nullable=True)
|
||||
|
||||
membership_required = db.Column(
|
||||
db.Boolean, nullable=False, default=True, server_default="1"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Client %s>" % (self.client_id,)
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ WTF_CSRF_SSL_STRICT = env.bool("WTF_CSRF_SSL_STRICT", default=False)
|
|||
|
||||
SECRET_KEY = env.str("SECRET_KEY", default="randomstring")
|
||||
|
||||
TESTING = env.bool("TESTING", default=False)
|
||||
|
||||
db_username = env.str("DATABASE_USERNAME", default="postgres")
|
||||
db_password = env.str("DATABASE_PASSWORD", default="secret")
|
||||
db_hostname = env.str("DATABASE_HOSTNAME", default="postgres")
|
||||
|
|
44
sso/views.py
44
sso/views.py
|
@ -10,6 +10,7 @@ from flask import (
|
|||
current_app,
|
||||
)
|
||||
import uuid
|
||||
from functools import wraps
|
||||
from flask_login import login_required, current_user, login_user, logout_user
|
||||
from sso.extensions import csrf
|
||||
from sso.directory import LDAPUserProxy, check_credentials
|
||||
|
@ -25,6 +26,17 @@ from authlib.integrations.flask_oauth2 import current_token
|
|||
bp = Blueprint("sso", __name__)
|
||||
|
||||
|
||||
def membership_required(fn):
|
||||
@wraps(fn)
|
||||
def wrapped(*args, **kwargs):
|
||||
print(current_user.is_membership_active)
|
||||
if current_user.is_anonymous or not current_user.is_membership_active:
|
||||
return render_template("membership_required.html")
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@bp.route("/profile")
|
||||
@login_required
|
||||
|
@ -75,6 +87,7 @@ def logout():
|
|||
|
||||
@bp.route("/client/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@membership_required
|
||||
def client_create():
|
||||
form = ClientForm()
|
||||
|
||||
|
@ -95,6 +108,7 @@ def client_create():
|
|||
|
||||
@bp.route("/client/<client_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@membership_required
|
||||
def client_edit(client_id):
|
||||
client = get_object_or_404(
|
||||
Client, Client.id == client_id, Client.owner_id == current_user.get_user_id()
|
||||
|
@ -112,6 +126,8 @@ def client_edit(client_id):
|
|||
|
||||
|
||||
@bp.route("/client/<client_id>/destroy", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@membership_required
|
||||
def client_destroy(client_id):
|
||||
client = get_object_or_404(
|
||||
Client, Client.id == client_id, Client.owner_id == current_user.get_user_id()
|
||||
|
@ -128,13 +144,14 @@ def client_destroy(client_id):
|
|||
|
||||
|
||||
@bp.route("/client/<client_id>/regenerate", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@membership_required
|
||||
def client_regenerate_secret(client_id):
|
||||
client = get_object_or_404(
|
||||
Client, Client.id == client_id, Client.owner_id == current_user.get_user_id()
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
print(request.form)
|
||||
client.client_secret = generate_token()
|
||||
|
||||
if request.form.get("revoke") == "yes":
|
||||
|
@ -151,12 +168,16 @@ def client_regenerate_secret(client_id):
|
|||
@bp.route("/oauth/authorize", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def authorize():
|
||||
if request.method == "GET":
|
||||
try:
|
||||
grant = authorization.validate_consent_request(end_user=current_user)
|
||||
except OAuth2Error as error:
|
||||
return jsonify(dict(error.get_body()))
|
||||
try:
|
||||
grant = authorization.validate_consent_request(end_user=current_user)
|
||||
except OAuth2Error as error:
|
||||
return render_template("authorization_error.html", error=dict(error.get_body()))
|
||||
|
||||
print(grant)
|
||||
if grant.client.membership_required and not current_user.is_membership_active:
|
||||
return render_template("membership_required.html")
|
||||
|
||||
if request.method == "GET":
|
||||
if Token.query.filter(
|
||||
Token.client_id == grant.client.client_id,
|
||||
Token.user_id == current_user.get_user_id(),
|
||||
|
@ -193,6 +214,10 @@ def issue_token():
|
|||
@require_oauth("profile:read openid", "OR")
|
||||
def api_profile():
|
||||
user = current_token.user
|
||||
|
||||
if current_token.client.membership_required and not user.is_membership_active:
|
||||
abort(402)
|
||||
|
||||
return jsonify(
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
|
@ -206,7 +231,12 @@ def api_profile():
|
|||
@bp.route("/api/1/userinfo")
|
||||
@require_oauth("profile:read openid", "OR")
|
||||
def api_userinfo():
|
||||
return jsonify(generate_user_info(current_token.user, current_token.scope))
|
||||
user = current_token.user
|
||||
|
||||
if current_token.client.membership_required and not user.is_membership_active:
|
||||
abort(402)
|
||||
|
||||
return jsonify(generate_user_info(user, current_token.scope))
|
||||
|
||||
|
||||
@bp.route("/.well-known/openid-configuration")
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "_helpers.html" import csrf_field %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container" id="authorize-container">
|
||||
<center><img src="/static/hswaw_wht.svg" style="width: 50%;"/></center>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<p><strong>Oh snap!</strong> {{ error.error }}: {{ error.error_description }}</p>
|
||||
</div>
|
||||
<a href="/">« Go back</a>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -25,6 +25,7 @@
|
|||
{{ render_field(form.grant_types) }}
|
||||
{{ render_field(form.response_types) }}
|
||||
{{ render_field(form.scope) }}
|
||||
{{ render_field(form.membership_required) }}
|
||||
<div class="form-group">
|
||||
<div class="col-md-8 col-md-offset-4">
|
||||
<button type="submit" class="btn btn-primary btn-block">Save</button>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "_helpers.html" import csrf_field %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container" id="authorize-container">
|
||||
<center><img src="/static/hswaw_wht.svg" style="width: 50%;"/></center>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<p><strong>Oh snap!</strong> Your membership has expired. This operation requires active membership.</p>
|
||||
<p>Go to <a href="https://kasownik.hackerspace.pl">Kasownik</a> and check your pending membership fees.</p>
|
||||
</div>
|
||||
<a href="/">« Go back</a>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue