forked from Github-Mirrors/canaille
Client forms
This commit is contained in:
parent
3a1284880a
commit
d75fcb163b
18 changed files with 581 additions and 335 deletions
4
app.py
4
app.py
|
@ -1,4 +0,0 @@
|
|||
from web.app import create_app
|
||||
|
||||
|
||||
app = create_app()
|
|
@ -9,6 +9,7 @@ RUN pip install --requirement /app/requirements.txt
|
|||
WORKDIR /app
|
||||
USER oauthserver
|
||||
|
||||
ENV FLASK_APP=web
|
||||
ENV FLASK_ENV=development
|
||||
ENV AUTHLIB_INSECURE_TRANSPORT=1
|
||||
|
||||
|
|
|
@ -122,6 +122,7 @@ olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.15 NAME 'oauthClientName'
|
|||
ORDERING caseIgnoreOrderingMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
SINGLE-VALUE
|
||||
USAGE userApplications
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.16 NAME 'oauthClientContact'
|
||||
|
@ -130,6 +131,7 @@ olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.16 NAME 'oauthClientContact'
|
|||
ORDERING caseIgnoreOrderingMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
SINGLE-VALUE
|
||||
USAGE userApplications
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.17 NAME 'oauthClientURI'
|
||||
|
@ -138,6 +140,7 @@ olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.17 NAME 'oauthClientURI'
|
|||
ORDERING caseIgnoreOrderingMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
SINGLE-VALUE
|
||||
USAGE userApplications
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.18 NAME 'oauthLogoURI'
|
||||
|
@ -155,6 +158,7 @@ olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.19 NAME 'oauthTermsOfServiceURI'
|
|||
ORDERING caseIgnoreOrderingMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
SINGLE-VALUE
|
||||
USAGE userApplications
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.20 NAME 'oauthPolicyURI'
|
||||
|
@ -163,6 +167,7 @@ olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.20 NAME 'oauthPolicyURI'
|
|||
ORDERING caseIgnoreOrderingMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
SINGLE-VALUE
|
||||
USAGE userApplications
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.21 NAME 'oauthJWKURI'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
authlib
|
||||
flask
|
||||
flask - babel
|
||||
python - ldap
|
||||
flask-babel
|
||||
flask-wtf
|
||||
python-ldap
|
||||
toml
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import ldap
|
||||
import os
|
||||
import toml
|
||||
from . import routes, clients
|
||||
|
||||
from flask import Flask, g, request
|
||||
from flask_babel import Babel
|
||||
|
||||
from .oauth2utils import config_oauth
|
||||
|
||||
|
||||
def create_app(config=None):
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config.from_mapping(
|
||||
{"OAUTH2_REFRESH_TOKEN_GENERATOR": True,}
|
||||
)
|
||||
app.config.from_mapping(toml.load(os.environ.get("CONFIG", "config.toml")))
|
||||
|
||||
app.url_map.strict_slashes = False
|
||||
|
||||
setup_app(app)
|
||||
return app
|
||||
|
||||
|
||||
def setup_app(app):
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.ldap = ldap.initialize(app.config["LDAP"]["URI"])
|
||||
g.ldap.simple_bind_s(
|
||||
app.config["LDAP"]["BIND_USER"], app.config["LDAP"]["BIND_PW"]
|
||||
)
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
if "ldap" in g:
|
||||
g.ldap.unbind_s()
|
||||
return response
|
||||
|
||||
config_oauth(app)
|
||||
app.register_blueprint(routes.bp)
|
||||
app.register_blueprint(clients.bp, url_prefix="/client")
|
||||
|
||||
babel = Babel(app)
|
||||
|
||||
@app.context_processor
|
||||
def global_processor():
|
||||
return {
|
||||
"logo_url": app.config.get("LOGO"),
|
||||
"website_name": app.config.get("NAME"),
|
||||
}
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
user = getattr(g, "user", None)
|
||||
if user is not None:
|
||||
return user.locale
|
||||
|
||||
if app.config.get("LANGUAGE"):
|
||||
return app.config.get("LANGUAGE")
|
||||
|
||||
return request.accept_languages.best_match(["fr", "en"])
|
||||
|
||||
@babel.timezoneselector
|
||||
def get_timezone():
|
||||
user = getattr(g, "user", None)
|
||||
if user is not None:
|
||||
return user.timezone
|
67
web/app.py
67
web/app.py
|
@ -1,67 +0,0 @@
|
|||
import ldap
|
||||
import os
|
||||
import toml
|
||||
|
||||
from flask import Flask, g, request
|
||||
from flask_babel import Babel
|
||||
|
||||
from .oauth2 import config_oauth
|
||||
from .routes import bp
|
||||
|
||||
|
||||
def create_app(config=None):
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config.from_mapping(
|
||||
{"OAUTH2_REFRESH_TOKEN_GENERATOR": True,}
|
||||
)
|
||||
app.config.from_mapping(toml.load(os.environ.get("CONFIG", "config.toml")))
|
||||
|
||||
app.url_map.strict_slashes = False
|
||||
|
||||
setup_app(app)
|
||||
return app
|
||||
|
||||
|
||||
def setup_app(app):
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.ldap = ldap.initialize(app.config["LDAP"]["URI"])
|
||||
g.ldap.simple_bind_s(
|
||||
app.config["LDAP"]["BIND_USER"], app.config["LDAP"]["BIND_PW"]
|
||||
)
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
if "ldap" in g:
|
||||
g.ldap.unbind_s()
|
||||
return response
|
||||
|
||||
config_oauth(app)
|
||||
app.register_blueprint(bp, url_prefix="")
|
||||
|
||||
babel = Babel(app)
|
||||
|
||||
@app.context_processor
|
||||
def global_processor():
|
||||
return {
|
||||
"logo_url": app.config.get("LOGO"),
|
||||
"website_name": app.config.get("NAME"),
|
||||
}
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
user = getattr(g, "user", None)
|
||||
if user is not None:
|
||||
return user.locale
|
||||
|
||||
if app.config.get("LANGUAGE"):
|
||||
return app.config.get("LANGUAGE")
|
||||
|
||||
return request.accept_languages.best_match(["fr", "en"])
|
||||
|
||||
@babel.timezoneselector
|
||||
def get_timezone():
|
||||
user = getattr(g, "user", None)
|
||||
if user is not None:
|
||||
return user.timezone
|
195
web/clients.py
Normal file
195
web/clients.py
Normal file
|
@ -0,0 +1,195 @@
|
|||
import datetime
|
||||
import wtforms
|
||||
import wtforms.fields.html5
|
||||
from flask import Blueprint, render_template, request, flash, redirect, url_for
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_babel import gettext
|
||||
from werkzeug.security import gen_salt
|
||||
from .models import Client
|
||||
|
||||
|
||||
bp = Blueprint(__name__, "clients")
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
def index():
|
||||
clients = Client.filter()
|
||||
return render_template("client_list.html", clients=clients)
|
||||
|
||||
|
||||
class ClientAdd(FlaskForm):
|
||||
oauthClientName = wtforms.TextField(
|
||||
gettext("Name"),
|
||||
validators=[wtforms.validators.DataRequired()],
|
||||
render_kw={"placeholder": "Client Name"},
|
||||
)
|
||||
oauthClientContact = wtforms.fields.html5.EmailField(
|
||||
gettext("Contact"),
|
||||
validators=[wtforms.validators.Optional()],
|
||||
render_kw={"placeholder": "admin@mydomain.tld"},
|
||||
)
|
||||
oauthClientURI = wtforms.fields.html5.URLField(
|
||||
gettext("URI"),
|
||||
validators=[wtforms.validators.DataRequired()],
|
||||
render_kw={"placeholder": "https://mydomain.tld"},
|
||||
)
|
||||
oauthRedirectURI = wtforms.fields.html5.URLField(
|
||||
gettext("Redirect URIs"),
|
||||
validators=[wtforms.validators.DataRequired()],
|
||||
render_kw={"placeholder": "https://mydomain.tld/callback"},
|
||||
)
|
||||
oauthGrantType = wtforms.SelectMultipleField(
|
||||
gettext("Grant types"),
|
||||
validators=[wtforms.validators.DataRequired()],
|
||||
choices=[
|
||||
("password", "password"),
|
||||
("authorization_code", "authorization_code"),
|
||||
],
|
||||
default=["authorization_code"],
|
||||
)
|
||||
oauthScope = wtforms.TextField(
|
||||
gettext("Scope"),
|
||||
validators=[wtforms.validators.Optional()],
|
||||
default="openid profile",
|
||||
render_kw={"placeholder": "openid profile"},
|
||||
)
|
||||
oauthResponseType = wtforms.SelectMultipleField(
|
||||
gettext("Response types"),
|
||||
validators=[wtforms.validators.DataRequired()],
|
||||
choices=[("code", "code")],
|
||||
default=["code"],
|
||||
)
|
||||
oauthTokenEndpointAuthMethod = wtforms.SelectField(
|
||||
gettext("Token Endpoint Auth Method"),
|
||||
validators=[wtforms.validators.DataRequired()],
|
||||
choices=[
|
||||
("client_secret_basic", "client_secret_basic"),
|
||||
("client_secret_post", "client_secret_post"),
|
||||
("none", "none"),
|
||||
],
|
||||
default="client_secret_basic",
|
||||
)
|
||||
oauthLogoURI = wtforms.fields.html5.URLField(
|
||||
gettext("Logo URI"),
|
||||
validators=[wtforms.validators.Optional()],
|
||||
render_kw={"placeholder": "https://mydomain.tld/logo.png"},
|
||||
)
|
||||
oauthTermsOfServiceURI = wtforms.fields.html5.URLField(
|
||||
gettext("Terms of service URI"),
|
||||
validators=[wtforms.validators.Optional()],
|
||||
render_kw={"placeholder": "https://mydomain.tld/tos.html"},
|
||||
)
|
||||
oauthPolicyURI = wtforms.fields.html5.URLField(
|
||||
gettext("Policy URI"),
|
||||
validators=[wtforms.validators.Optional()],
|
||||
render_kw={"placeholder": "https://mydomain.tld/policy.html"},
|
||||
)
|
||||
oauthSoftwareID = wtforms.TextField(
|
||||
gettext("Software ID"),
|
||||
validators=[wtforms.validators.Optional()],
|
||||
render_kw={"placeholder": "xyz"},
|
||||
)
|
||||
oauthSoftwareVersion = wtforms.TextField(
|
||||
gettext("Software Version"),
|
||||
validators=[wtforms.validators.Optional()],
|
||||
render_kw={"placeholder": "1.0"},
|
||||
)
|
||||
oauthJWK = wtforms.TextField(
|
||||
gettext("JWK"),
|
||||
validators=[wtforms.validators.Optional()],
|
||||
render_kw={"placeholder": ""},
|
||||
)
|
||||
oauthJWKURI = wtforms.fields.html5.URLField(
|
||||
gettext("JKW URI"),
|
||||
validators=[wtforms.validators.Optional()],
|
||||
render_kw={"placeholder": ""},
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/add", methods=["GET", "POST"])
|
||||
def add():
|
||||
form = ClientAdd(request.form or None)
|
||||
|
||||
if not request.form:
|
||||
return render_template("client_add.html", form=form)
|
||||
|
||||
if not form.validate():
|
||||
flash(
|
||||
gettext("The client has not been added. Please check your information."),
|
||||
"error",
|
||||
)
|
||||
return render_template("client_add.html", form=form)
|
||||
|
||||
client_id = gen_salt(24)
|
||||
client_id_issued_at = datetime.datetime.now().strftime("%Y%m%d%H%M%SZ")
|
||||
client = Client(
|
||||
oauthClientID=client_id,
|
||||
oauthIssueDate=client_id_issued_at,
|
||||
oauthClientName=form["oauthClientName"].data,
|
||||
oauthClientContact=form["oauthClientContact"].data,
|
||||
oauthClientURI=form["oauthClientURI"].data,
|
||||
oauthGrantType=form["oauthGrantType"].data,
|
||||
oauthRedirectURI=[form["oauthRedirectURI"].data],
|
||||
oauthResponseType=form["oauthResponseType"].data,
|
||||
oauthScope=form["oauthScope"].data.split(" "),
|
||||
oauthTokenEndpointAuthMethod=form["oauthTokenEndpointAuthMethod"].data,
|
||||
oauthLogoURI=form["oauthLogoURI"].data,
|
||||
oauthTermsOfServiceURI=form["oauthTermsOfServiceURI"].data,
|
||||
oauthPolicyURI=form["oauthPolicyURI"].data,
|
||||
oauthSoftwareID=form["oauthSoftwareID"].data,
|
||||
oauthSoftwareVersion=form["oauthSoftwareVersion"].data,
|
||||
oauthJWK=form["oauthJWK"].data,
|
||||
oauthJWKURI=form["oauthJWKURI"].data,
|
||||
oauthClientSecret=""
|
||||
if form["oauthTokenEndpointAuthMethod"].data == "none"
|
||||
else gen_salt(48),
|
||||
)
|
||||
client.save()
|
||||
flash(
|
||||
gettext("The client has been created."), "success",
|
||||
)
|
||||
|
||||
return redirect(url_for("web.clients.edit", client_id=client_id))
|
||||
|
||||
|
||||
@bp.route("/edit/<client_id>", methods=["GET", "POST"])
|
||||
def edit(client_id):
|
||||
client = Client.get(client_id)
|
||||
data = dict(client)
|
||||
data["oauthScope"] = " ".join(data["oauthScope"])
|
||||
data["oauthRedirectURI"] = data["oauthRedirectURI"][0]
|
||||
form = ClientAdd(request.form or None, data=data, client=client)
|
||||
|
||||
if not request.form:
|
||||
return render_template("client_edit.html", form=form, client=client)
|
||||
|
||||
if not form.validate():
|
||||
flash(
|
||||
gettext("The client has not been edited. Please check your information."),
|
||||
"error",
|
||||
)
|
||||
|
||||
else:
|
||||
client.update(
|
||||
oauthClientName=form["oauthClientName"].data,
|
||||
oauthClientContact=form["oauthClientContact"].data,
|
||||
oauthClientURI=form["oauthClientURI"].data,
|
||||
oauthGrantType=form["oauthGrantType"].data,
|
||||
oauthRedirectURI=[form["oauthRedirectURI"].data],
|
||||
oauthResponseType=form["oauthResponseType"].data,
|
||||
oauthScope=form["oauthScope"].data.split(" "),
|
||||
oauthTokenEndpointAuthMethod=form["oauthTokenEndpointAuthMethod"].data,
|
||||
oauthLogoURI=form["oauthLogoURI"].data,
|
||||
oauthTermsOfServiceURI=form["oauthTermsOfServiceURI"].data,
|
||||
oauthPolicyURI=form["oauthPolicyURI"].data,
|
||||
oauthSoftwareID=form["oauthSoftwareID"].data,
|
||||
oauthSoftwareVersion=form["oauthSoftwareVersion"].data,
|
||||
oauthJWK=form["oauthJWK"].data,
|
||||
oauthJWKURI=form["oauthJWKURI"].data,
|
||||
)
|
||||
client.save()
|
||||
flash(
|
||||
gettext("The client has been edited."), "success",
|
||||
)
|
||||
|
||||
return render_template("client_edit.html", form=form, client=client)
|
150
web/ldaputils.py
Normal file
150
web/ldaputils.py
Normal file
|
@ -0,0 +1,150 @@
|
|||
import ldap
|
||||
from flask import g
|
||||
|
||||
|
||||
class LDAPObjectHelper:
|
||||
_object_class_by_name = None
|
||||
_attribute_type_by_name = None
|
||||
may = None
|
||||
must = None
|
||||
base = None
|
||||
id = None
|
||||
|
||||
def __init__(self, dn=None, **kwargs):
|
||||
self.attrs = {}
|
||||
for k, v in kwargs.items():
|
||||
self.attrs[k] = [v] if not isinstance(v, list) else v
|
||||
self.attrs.setdefault("objectClass", self.objectClass)
|
||||
|
||||
by_name = self.ocs_by_name()
|
||||
ocs = [by_name[name] for name in self.objectClass]
|
||||
self.may = []
|
||||
self.must = []
|
||||
for oc in ocs:
|
||||
self.may.extend(oc.may)
|
||||
self.must.extend(oc.must)
|
||||
|
||||
def keys(self):
|
||||
return self.must + self.may
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
def update(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
self.__setattr__(k, v)
|
||||
|
||||
@property
|
||||
def dn(self):
|
||||
if not self.id in self.attrs:
|
||||
return None
|
||||
return f"{self.id}={self.attrs[self.id][0]},{self.base}"
|
||||
|
||||
@classmethod
|
||||
def ocs_by_name(cls):
|
||||
if cls._object_class_by_name:
|
||||
return cls._object_class_by_name
|
||||
|
||||
res = g.ldap.search_s(
|
||||
"cn=subschema", ldap.SCOPE_BASE, "(objectclass=*)", ["*", "+"]
|
||||
)
|
||||
subschema_entry = res[0]
|
||||
subschema_subentry = ldap.cidict.cidict(subschema_entry[1])
|
||||
subschema = ldap.schema.SubSchema(subschema_subentry)
|
||||
object_class_oids = subschema.listall(ldap.schema.models.ObjectClass)
|
||||
cls._object_class_by_name = {}
|
||||
for oid in object_class_oids:
|
||||
oc = subschema.get_obj(ldap.schema.models.ObjectClass, oid)
|
||||
for name in oc.names:
|
||||
cls._object_class_by_name[name] = oc
|
||||
|
||||
return cls._object_class_by_name
|
||||
|
||||
@classmethod
|
||||
def attr_type_by_name(cls):
|
||||
if cls._attribute_type_by_name:
|
||||
return cls._attribute_type_by_name
|
||||
|
||||
res = g.ldap.search_s(
|
||||
"cn=subschema", ldap.SCOPE_BASE, "(objectclass=*)", ["*", "+"]
|
||||
)
|
||||
subschema_entry = res[0]
|
||||
subschema_subentry = ldap.cidict.cidict(subschema_entry[1])
|
||||
subschema = ldap.schema.SubSchema(subschema_subentry)
|
||||
attribute_type_oids = subschema.listall(ldap.schema.models.AttributeType)
|
||||
cls._attribute_type_by_name = {}
|
||||
for oid in attribute_type_oids:
|
||||
oc = subschema.get_obj(ldap.schema.models.AttributeType, oid)
|
||||
for name in oc.names:
|
||||
cls._attribute_type_by_name[name] = oc
|
||||
|
||||
return cls._attribute_type_by_name
|
||||
|
||||
def save(self):
|
||||
try:
|
||||
match = bool(g.ldap.search_s(self.dn, ldap.SCOPE_SUBTREE))
|
||||
except ldap.NO_SUCH_OBJECT:
|
||||
match = False
|
||||
|
||||
if match:
|
||||
attributes = [
|
||||
(ldap.MOD_REPLACE, k, [elt.encode("utf-8") for elt in v])
|
||||
for k, v in self.attrs.items()
|
||||
]
|
||||
g.ldap.modify_s(self.dn, attributes)
|
||||
|
||||
else:
|
||||
attributes = [
|
||||
(k, [elt.encode("utf-8") for elt in v]) for k, v in self.attrs.items()
|
||||
]
|
||||
g.ldap.add_s(self.dn, attributes)
|
||||
|
||||
@classmethod
|
||||
def get(cls, dn):
|
||||
if "=" not in dn:
|
||||
dn = f"{cls.id}={dn},{cls.base}"
|
||||
result = g.ldap.search_s(dn, ldap.SCOPE_SUBTREE)
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
o = cls(
|
||||
**{k: [elt.decode("utf-8") for elt in v] for k, v in result[0][1].items()}
|
||||
)
|
||||
|
||||
return o
|
||||
|
||||
@classmethod
|
||||
def filter(cls, base=None, **kwargs):
|
||||
class_filter = "".join([f"(objectClass={oc})" for oc in cls.objectClass])
|
||||
arg_filter = "".join(f"({k}={v})" for k, v in kwargs.items())
|
||||
ldapfilter = f"(&{class_filter}{arg_filter})"
|
||||
result = g.ldap.search_s(base or cls.base, ldap.SCOPE_SUBTREE, ldapfilter)
|
||||
|
||||
return [
|
||||
cls(**{k: [elt.decode("utf-8") for elt in v] for k, v in args.items()},)
|
||||
for _, args in result
|
||||
]
|
||||
|
||||
def __getattr__(self, name):
|
||||
if (not self.may or name not in self.may) and (
|
||||
not self.must or name not in self.must
|
||||
):
|
||||
return super().__getattribute__(name)
|
||||
|
||||
if (
|
||||
not self.attr_type_by_name()
|
||||
or not self.attr_type_by_name()[name].single_value
|
||||
):
|
||||
return self.attrs.get(name, [])
|
||||
|
||||
return self.attrs.get(name, [None])[0]
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
super().__setattr__(name, value)
|
||||
|
||||
if (self.may and name in self.may) or (self.must and name in self.must):
|
||||
if self.attr_type_by_name()[name].single_value:
|
||||
self.attrs[name] = [value]
|
||||
else:
|
||||
self.attrs[name] = value
|
146
web/models.py
146
web/models.py
|
@ -1,152 +1,12 @@
|
|||
import ldap
|
||||
import time
|
||||
import datetime
|
||||
from flask import g
|
||||
from authlib.common.encoding import json_loads, json_dumps
|
||||
from authlib.oauth2.rfc6749 import (
|
||||
ClientMixin,
|
||||
TokenMixin,
|
||||
AuthorizationCodeMixin,
|
||||
)
|
||||
|
||||
|
||||
class LDAPObjectHelper:
|
||||
_object_class_by_name = None
|
||||
_attribute_type_by_name = None
|
||||
may = None
|
||||
must = None
|
||||
base = None
|
||||
id = None
|
||||
|
||||
def __init__(self, dn=None, **kwargs):
|
||||
self.attrs = {}
|
||||
for k, v in kwargs.items():
|
||||
self.attrs[k] = [v] if not isinstance(v, list) else v
|
||||
self.attrs.setdefault("objectClass", self.objectClass)
|
||||
|
||||
by_name = self.ocs_by_name()
|
||||
ocs = [by_name[name] for name in self.objectClass]
|
||||
self.may = []
|
||||
self.must = []
|
||||
for oc in ocs:
|
||||
self.may.extend(oc.may)
|
||||
self.must.extend(oc.must)
|
||||
|
||||
@property
|
||||
def dn(self):
|
||||
if not self.id in self.attrs:
|
||||
return None
|
||||
return f"{self.id}={self.attrs[self.id][0]},{self.base}"
|
||||
|
||||
@classmethod
|
||||
def ocs_by_name(cls):
|
||||
if cls._object_class_by_name:
|
||||
return cls._object_class_by_name
|
||||
|
||||
res = g.ldap.search_s(
|
||||
"cn=subschema", ldap.SCOPE_BASE, "(objectclass=*)", ["*", "+"]
|
||||
)
|
||||
subschema_entry = res[0]
|
||||
subschema_subentry = ldap.cidict.cidict(subschema_entry[1])
|
||||
subschema = ldap.schema.SubSchema(subschema_subentry)
|
||||
object_class_oids = subschema.listall(ldap.schema.models.ObjectClass)
|
||||
cls._object_class_by_name = {}
|
||||
for oid in object_class_oids:
|
||||
oc = subschema.get_obj(ldap.schema.models.ObjectClass, oid)
|
||||
for name in oc.names:
|
||||
cls._object_class_by_name[name] = oc
|
||||
|
||||
return cls._object_class_by_name
|
||||
|
||||
@classmethod
|
||||
def attr_type_by_name(cls):
|
||||
if cls._attribute_type_by_name:
|
||||
return cls._attribute_type_by_name
|
||||
|
||||
res = g.ldap.search_s(
|
||||
"cn=subschema", ldap.SCOPE_BASE, "(objectclass=*)", ["*", "+"]
|
||||
)
|
||||
subschema_entry = res[0]
|
||||
subschema_subentry = ldap.cidict.cidict(subschema_entry[1])
|
||||
subschema = ldap.schema.SubSchema(subschema_subentry)
|
||||
attribute_type_oids = subschema.listall(ldap.schema.models.AttributeType)
|
||||
cls._attribute_type_by_name = {}
|
||||
for oid in attribute_type_oids:
|
||||
oc = subschema.get_obj(ldap.schema.models.AttributeType, oid)
|
||||
for name in oc.names:
|
||||
cls._attribute_type_by_name[name] = oc
|
||||
|
||||
return cls._attribute_type_by_name
|
||||
|
||||
def save(self):
|
||||
try:
|
||||
match = bool(g.ldap.search_s(self.dn, ldap.SCOPE_SUBTREE))
|
||||
except ldap.NO_SUCH_OBJECT:
|
||||
match = False
|
||||
|
||||
if match:
|
||||
attributes = [
|
||||
(ldap.MOD_REPLACE, k, [elt.encode("utf-8") for elt in v])
|
||||
for k, v in self.attrs.items()
|
||||
]
|
||||
g.ldap.modify_s(self.dn, attributes)
|
||||
|
||||
else:
|
||||
attributes = [
|
||||
(k, [elt.encode("utf-8") for elt in v]) for k, v in self.attrs.items()
|
||||
]
|
||||
g.ldap.add_s(self.dn, attributes)
|
||||
|
||||
@classmethod
|
||||
def get(cls, dn):
|
||||
if "=" not in dn:
|
||||
dn = f"{cls.id}={dn},{cls.base}"
|
||||
result = g.ldap.search_s(dn, ldap.SCOPE_SUBTREE)
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
o = cls(
|
||||
**{k: [elt.decode("utf-8") for elt in v] for k, v in result[0][1].items()}
|
||||
)
|
||||
|
||||
return o
|
||||
|
||||
@classmethod
|
||||
def filter(cls, base=None, **kwargs):
|
||||
class_filter = "".join([f"(objectClass={oc})" for oc in cls.objectClass])
|
||||
arg_filter = "".join(f"({k}={v})" for k, v in kwargs.items())
|
||||
ldapfilter = f"(&{class_filter}{arg_filter})"
|
||||
result = g.ldap.search_s(base or cls.base, ldap.SCOPE_SUBTREE, ldapfilter)
|
||||
|
||||
return [
|
||||
cls(**{k: [elt.decode("utf-8") for elt in v] for k, v in args.items()},)
|
||||
for _, args in result
|
||||
]
|
||||
|
||||
def __getattr__(self, name):
|
||||
if (not self.may or name not in self.may) and (
|
||||
not self.must or name not in self.must
|
||||
):
|
||||
return super().__getattribute__(name)
|
||||
|
||||
if (
|
||||
not self.attr_type_by_name()
|
||||
or not self.attr_type_by_name()[name].single_value
|
||||
):
|
||||
return self.attrs.get(name, [])
|
||||
|
||||
return self.attrs.get(name, [None])[0]
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
super().__setattr__(name, value)
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
if (self.may and name in self.may) or (self.must and name in self.must):
|
||||
if self.attr_type_by_name()[name].single_value:
|
||||
self.attrs[name] = [value]
|
||||
else:
|
||||
self.attrs[name] = value
|
||||
from .ldaputils import LDAPObjectHelper
|
||||
|
||||
|
||||
class User(LDAPObjectHelper):
|
||||
|
@ -166,6 +26,10 @@ class Client(LDAPObjectHelper, ClientMixin):
|
|||
base = "ou=clients,dc=mydomain,dc=tld"
|
||||
id = "oauthClientID"
|
||||
|
||||
@property
|
||||
def issue_date(self):
|
||||
return datetime.datetime.strptime(self.oauthIssueDate, "%Y%m%d%H%M%SZ")
|
||||
|
||||
def get_client_id(self):
|
||||
return self.oauthClientID
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import datetime
|
||||
from flask import Blueprint, request, session
|
||||
from flask import render_template, redirect, jsonify
|
||||
from werkzeug.security import gen_salt
|
||||
from authlib.oauth2 import OAuth2Error
|
||||
from .models import User, Client
|
||||
from .oauth2 import authorization, require_oauth
|
||||
from .oauth2utils import authorization, require_oauth
|
||||
|
||||
|
||||
bp = Blueprint(__name__, "home")
|
||||
|
@ -38,40 +36,6 @@ def home():
|
|||
return render_template("home.html", user=user, clients=clients)
|
||||
|
||||
|
||||
def split_by_crlf(s):
|
||||
return [v for v in s.splitlines() if v]
|
||||
|
||||
|
||||
@bp.route("/create_client", methods=("GET", "POST"))
|
||||
def create_client():
|
||||
user = current_user()
|
||||
if not user:
|
||||
return redirect("/")
|
||||
|
||||
if request.method == "GET":
|
||||
return render_template("create_client.html")
|
||||
|
||||
form = request.form
|
||||
client_id = gen_salt(24)
|
||||
client_id_issued_at = datetime.datetime.now().strftime("%Y%m%d%H%M%SZ")
|
||||
client = Client(
|
||||
oauthClientID=client_id,
|
||||
oauthIssueDate=client_id_issued_at,
|
||||
oauthClientName=form["client_name"],
|
||||
oauthClientURI=form["client_uri"],
|
||||
oauthGrantType=split_by_crlf(form["grant_type"]),
|
||||
oauthRedirectURI=split_by_crlf(form["redirect_uri"]),
|
||||
oauthResponseType=split_by_crlf(form["response_type"]),
|
||||
oauthScope=form["scope"],
|
||||
oauthTokenEndpointAuthMethod=form["token_endpoint_auth_method"],
|
||||
oauthClientSecret=""
|
||||
if form["token_endpoint_auth_method"] == "none"
|
||||
else gen_salt(48),
|
||||
)
|
||||
client.save()
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@bp.route("/oauth/authorize", methods=["GET", "POST"])
|
||||
def authorize():
|
||||
user = current_user()
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>{% block title %}Accueil{% endblock %}</title>
|
||||
<title>{% block title %}{% trans %}OpenID Connect LDAP Bridge{% endtrans %}{% endblock %}</title>
|
||||
<link href="/static/fomanticui/semantic.min.css" rel="stylesheet">
|
||||
<link href="/static/css/base.css" rel="stylesheet">
|
||||
{% block style %}{% endblock %}
|
||||
|
@ -28,6 +28,18 @@
|
|||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a class="item" href="{{ url_for('web.clients.index') }}">
|
||||
<i class="plug icon"></i>
|
||||
{% trans %}Clients{% endtrans %}
|
||||
</a>
|
||||
<a class="item" href="/">
|
||||
<i class="key icon"></i>
|
||||
{% trans %}Tokens{% endtrans %}
|
||||
</a>
|
||||
<a class="item" href="/">
|
||||
<i class="user secret icon"></i>
|
||||
{% trans %}Codes{% endtrans %}
|
||||
</a>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
<div class="content">
|
||||
|
|
20
web/templates/client_add.html
Normal file
20
web/templates/client_add.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends 'base.html' %}
|
||||
{% import 'fomanticui.j2' as sui %}
|
||||
|
||||
{% block content %}
|
||||
<div class="loginform">
|
||||
<h3 class="ui top attached header">
|
||||
{% trans %}Add a client{% endtrans %}
|
||||
</h3>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="ui attached message {{ category }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
<div class="ui attached clearing segment">
|
||||
{{ sui.render_form(form, _("Confirm")) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
35
web/templates/client_edit.html
Normal file
35
web/templates/client_edit.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% extends 'base.html' %}
|
||||
{% import 'fomanticui.j2' as sui %}
|
||||
|
||||
{% block content %}
|
||||
<div class="loginform">
|
||||
<h3 class="ui top attached header">
|
||||
{% trans %}Edit a client{% endtrans %}
|
||||
</h3>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="ui attached message {{ category }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
<div class="ui attached clearing segment">
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{% trans %}ID{% endtrans %}</label>
|
||||
<input type="text" value="{{ client.oauthClientID }}" readonly>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{% trans %}Secret{% endtrans %}</label>
|
||||
<input type="text" value="{{ client.oauthClientSecret }}" readonly>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{% trans %}Issued at{% endtrans %}</label>
|
||||
<input type="text" value="{{ client.issue_date }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ sui.render_form(form, _("Confirm")) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
35
web/templates/client_list.html
Normal file
35
web/templates/client_list.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% 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">
|
||||
<a class="ui primary button" href="{{ url_for('web.clients.add') }}">{% trans %}Add client{% endtrans %}</a>
|
||||
</div>
|
||||
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<th>{% trans %}Name{% endtrans %}</th>
|
||||
<th>{% trans %}URL{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
</thead>
|
||||
{% for client in clients %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('web.clients.edit', client_id=client.oauthClientID) }}">{{ client.oauthClientName }}</a></td>
|
||||
<td><a href="{{ client.oauthClientURI }}">{{ client.oauthClientURI }}</a></td>
|
||||
<td>{{ client.issue_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
|
@ -1,48 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="ui segment">
|
||||
|
||||
<style>
|
||||
label, label > span { display: block; }
|
||||
label { margin: 15px 0; }
|
||||
</style>
|
||||
|
||||
<form action="" method="post">
|
||||
<label>
|
||||
<span>Client Name</span>
|
||||
<input type="text" name="client_name">
|
||||
</label>
|
||||
<label>
|
||||
<span>Client URI</span>
|
||||
<input type="url" name="client_uri">
|
||||
</label>
|
||||
<label>
|
||||
<span>Allowed Scope</span>
|
||||
<input type="text" name="scope">
|
||||
</label>
|
||||
<label>
|
||||
<span>Redirect URIs</span>
|
||||
<textarea name="redirect_uri" cols="30" rows="10"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
<span>Allowed Grant Types</span>
|
||||
<textarea name="grant_type" cols="30" rows="10"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
<span>Allowed Response Types</span>
|
||||
<textarea name="response_type" cols="30" rows="10"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
<span>Token Endpoint Auth Method</span>
|
||||
<select name="token_endpoint_auth_method">
|
||||
<option value="client_secret_basic">client_secret_basic</option>
|
||||
<option value="client_secret_post">client_secret_post</option>
|
||||
<option value="none">none</option>
|
||||
</select>
|
||||
</label>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
49
web/templates/fomanticui.j2
Normal file
49
web/templates/fomanticui.j2
Normal file
|
@ -0,0 +1,49 @@
|
|||
{% macro render_field(field, label_visible=true) -%}
|
||||
<div class="field {{ kwargs.pop('class_', '') }} {% if field.errors %} error{% endif %}">
|
||||
{% if (field.type != 'HiddenField' and field.type !='CSRFTokenField') and label_visible %}
|
||||
{{ field.label() }}
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
<div class="ui error message">
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if field.type in ("SelectField", "SelectMultipleField") %}
|
||||
{{ field(class_="ui dropdown multiple", **kwargs) }}
|
||||
{% else %}
|
||||
{{ field(**kwargs) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro render_form(
|
||||
form,
|
||||
action_text='submit',
|
||||
class_='',
|
||||
btn_class='ui right floated primary button',
|
||||
action=none,
|
||||
id=none) -%}
|
||||
<form method="POST"
|
||||
id="{{ id or form.__class__.__name__|lower }}"
|
||||
action="{{ action or form.action }}"
|
||||
role="form"
|
||||
enctype="multipart/form-data"
|
||||
class="ui form {{ class_ }}"
|
||||
>
|
||||
{{ form.hidden_tag() if form.hidden_tag }}
|
||||
{% if caller %}
|
||||
{{ caller() }}
|
||||
{% else %}
|
||||
{% for field in form %}
|
||||
{{ render_field(field) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if action_text %}
|
||||
<button type="submit" class="{{ btn_class }}">{{ action_text }}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{%- endmacro %}
|
|
@ -2,40 +2,6 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="ui segment">
|
||||
{% if user %}
|
||||
<style>pre{white-space:wrap}</style>
|
||||
<div>{{ _('Logged in as') }} <strong>{{user}}</strong> (<a href="{{ url_for('.logout') }}">Log Out</a>)</div>
|
||||
|
||||
{% for client in clients %}
|
||||
<strong>Client</strong>
|
||||
<ul>
|
||||
{%- for key in client.must -%}
|
||||
{%- if key in client.attrs and client.attrs[key] -%}
|
||||
{%- for value in client.attrs[key] -%}
|
||||
<li><strong>{{ key }}: </strong>{{ value }}</li>
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- for key in client.may -%}
|
||||
{%- if key in client.attrs and client.attrs[key] -%}
|
||||
{%- for value in client.attrs[key] -%}
|
||||
<li><strong>{{ key }}: </strong>{{ value }}</li>
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
|
||||
</ul>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
|
||||
<br><a href="{{ url_for('.create_client') }}">{% trans %}Create Client{% endtrans %}</a>
|
||||
|
||||
{% else %}
|
||||
<form action="" method="post">
|
||||
<input type="text" name="username" placeholder="username">
|
||||
<button type="submit">Login / Signup</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
Welcome
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue