summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPiotr Dobrowolski <admin@tastycode.pl>2022-04-29 00:24:35 +0200
committerPiotr Dobrowolski <admin@tastycode.pl>2022-04-29 00:24:35 +0200
commita57ab990148f6337017cd2ccdf9ce38b0da99cc2 (patch)
tree4c0addb229f690f60cc50ab73dc0335307c882f8
parentc4c810cd255a7bfcab5ced3fb88c8b311b518c34 (diff)
downloadsso-v2-a57ab990148f6337017cd2ccdf9ce38b0da99cc2.tar.gz
sso-v2-a57ab990148f6337017cd2ccdf9ce38b0da99cc2.tar.bz2
sso-v2-a57ab990148f6337017cd2ccdf9ce38b0da99cc2.tar.xz
sso-v2-a57ab990148f6337017cd2ccdf9ce38b0da99cc2.zip
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.
-rw-r--r--docker-compose.yml2
-rw-r--r--migrations/versions/59b941d00ea9_add_client_membership_required.py28
-rw-r--r--sso/directory.py12
-rw-r--r--sso/forms.py6
-rw-r--r--sso/models.py4
-rw-r--r--sso/settings.py2
-rw-r--r--sso/views.py44
-rw-r--r--templates/authorization_error.html12
-rw-r--r--templates/client_edit.html1
-rw-r--r--templates/membership_required.html13
10 files changed, 113 insertions, 11 deletions
diff --git a/docker-compose.yml b/docker-compose.yml
index a249133..c6a225a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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
diff --git a/migrations/versions/59b941d00ea9_add_client_membership_required.py b/migrations/versions/59b941d00ea9_add_client_membership_required.py
new file mode 100644
index 0000000..8fb966d
--- /dev/null
+++ b/migrations/versions/59b941d00ea9_add_client_membership_required.py
@@ -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 ###
diff --git a/sso/directory.py b/sso/directory.py
index 25a259f..8053dbc 100644
--- a/sso/directory.py
+++ b/sso/directory.py
@@ -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
diff --git a/sso/forms.py b/sso/forms.py
index 499d13a..22a022d 100644
--- a/sso/forms.py
+++ b/sso/forms.py
@@ -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",
+ )
diff --git a/sso/models.py b/sso/models.py
index 9c9e8fa..f62278e 100644
--- a/sso/models.py
+++ b/sso/models.py
@@ -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,)
diff --git a/sso/settings.py b/sso/settings.py
index 1f75b70..2e56f59 100644
--- a/sso/settings.py
+++ b/sso/settings.py
@@ -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")
diff --git a/sso/views.py b/sso/views.py
index 10c3b43..343f24e 100644
--- a/sso/views.py
+++ b/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")
diff --git a/templates/authorization_error.html b/templates/authorization_error.html
new file mode 100644
index 0000000..6dba999
--- /dev/null
+++ b/templates/authorization_error.html
@@ -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="/">&laquo; Go back</a>
+ </div>
+{% endblock %}
diff --git a/templates/client_edit.html b/templates/client_edit.html
index 7eac5d9..4265ec1 100644
--- a/templates/client_edit.html
+++ b/templates/client_edit.html
@@ -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>
diff --git a/templates/membership_required.html b/templates/membership_required.html
new file mode 100644
index 0000000..e029a33
--- /dev/null
+++ b/templates/membership_required.html
@@ -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="/">&laquo; Go back</a>
+ </div>
+{% endblock %}