diff --git a/hiboo/__init__.py b/hiboo/__init__.py index 6033fb783dd2b77cbfc80bc8f727f7553566162b..93c9c49dd32e3c069ff71a0f762070fb8b4315cd 100644 --- a/hiboo/__init__.py +++ b/hiboo/__init__.py @@ -32,11 +32,12 @@ def create_app_from_config(config): return dict(config=app.config, utils=utils) # Import views - from hiboo import account, user, profile, service, sso, captcha, api + from hiboo import account, user, profile, service, application, sso, captcha, api app.register_blueprint(account.blueprint, url_prefix='/account') app.register_blueprint(user.blueprint, url_prefix='/user') app.register_blueprint(profile.blueprint, url_prefix='/profile') app.register_blueprint(service.blueprint, url_prefix='/service') + app.register_blueprint(application.blueprint, url_prefix='/application') app.register_blueprint(sso.blueprint, url_prefix='/sso') app.register_blueprint(captcha.blueprint, url_prefix='/captcha') app.register_blueprint(api.blueprint, url_prefix='/api') diff --git a/hiboo/application/__init__.py b/hiboo/application/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8343071bfb5329dc4f1c03f9866b295f22794f2c --- /dev/null +++ b/hiboo/application/__init__.py @@ -0,0 +1,12 @@ +import flask + + +blueprint = flask.Blueprint("application", __name__, template_folder="templates") + + +from hiboo.application import base + +register = base.BaseApplication.register +registry = base.BaseApplication.registry + +from hiboo.application import sso, social, storage \ No newline at end of file diff --git a/hiboo/application/base.py b/hiboo/application/base.py new file mode 100644 index 0000000000000000000000000000000000000000..3b1bbb4c44fcc3fae5c53350823d16a24f12dda5 --- /dev/null +++ b/hiboo/application/base.py @@ -0,0 +1,39 @@ +from hiboo.service.forms import ServiceForm as BaseForm +from flask_babel import lazy_gettext as _ + +import flask + + +class BaseApplication(object): + """ Base application class, that provides basic behavior and registry + """ + + registry = dict() + sso_protocol = None + name = None + + @classmethod + def register(cls, application_id): + def register_function(application): + application.application_id = application_id + cls.registry[application_id] = application() + return application + return register_function + + def render_details(self, service): + return flask.render_template( + "application_{}.html".format(self.application_id), + service=service + ) + + def fill_service(self, service): + return self.sso_protocol.fill_service(service) + + +class OIDCApplication(BaseApplication): + sso_protocol = "oidc" + +class SAMLApplication(BaseApplication): + sso_protocol = "saml" + + diff --git a/hiboo/application/social.py b/hiboo/application/social.py new file mode 100644 index 0000000000000000000000000000000000000000..c023e433b2b9ff0155cdd2d0654b58723a58b790 --- /dev/null +++ b/hiboo/application/social.py @@ -0,0 +1,32 @@ +from hiboo.application import register, base +from wtforms import validators, fields +from flask_babel import lazy_gettext as _ + + +@register("mastodon") +class MastodonApplication(base.SAMLApplication): + """ Mastodon social network is an ActivityPub micro-blogging platform + """ + + name = _("Mastodon social network") + + class Form(base.BaseForm): + application_uri = fields.StringField(_("Mastodon URL"), [validators.URL(require_tld=False)]) + submit = fields.SubmitField(_('Submit')) + + def populate_service(self, form, service): + service.profile_regex = "[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?" + callback_uri = form.application_uri.data + "/auth/auth/callback" + service.config.update({ + "application_uri": form.application_uri.data, + "acs": callback_uri, + "entityid": callback_uri, + "sign_mode": "assertion" + }) + self.fill_service(service) + + def populate_form(self, service, form): + form.process( + obj=service, + application_uri=service.config.get("application_uri") + ) diff --git a/hiboo/application/sso.py b/hiboo/application/sso.py new file mode 100644 index 0000000000000000000000000000000000000000..9f7feebbf9135ac902f3b4de95060a25fe05ac2d --- /dev/null +++ b/hiboo/application/sso.py @@ -0,0 +1,97 @@ +from hiboo.application import base, register +from wtforms import validators, fields +from flask_babel import lazy_gettext as _ + + +@register("oidc") +class GenericOIDCApplication(base.OIDCApplication): + """ OpenID Connect (OIDC) is JWT based authentication and authorization protocol + """ + + name = _("Generic OIDC") + + class Form(base.BaseForm): + redirect_uri = fields.StringField(_("Redirect URI"), [validators.URL(require_tld=False)]) + token_endpoint_auth_method = fields.SelectField( + _('Token Endpoint Auth Method'), choices=[ + ("client_secret_post", _("HTTP POST data")), + ("client_secret_basic", _("HTTP basic authorization")), + ("none", _("No authentication")) + ] + ) + grant_types = fields.SelectMultipleField( + _('OpenID Connect grant type'), choices=[ + ("authorization_code", _("Authorization Code")), + ("implicit", _("Implicit")), + ("hybrid", _("Hybrid")) + ] + ) + response_types = fields.SelectMultipleField( + _('Allowed response types'), choices=[ + ("code", _("Authorization code only")), + ("id_token", _("Id token only")), + ("id_token token", _("Id token and token")) + ] + ) + special_mappings = fields.SelectMultipleField( + _('Enabled special claim mappings'), choices=[ + ("mask_sub_uuid", _("Mask the profile uuid")), + ("original_email", _("Return the actual user email")) + ] + ) + submit = fields.SubmitField(_('Submit')) + + def populate_service(self, form, service): + service.config.update({ + "token_endpoint_auth_method": form.token_endpoint_auth_method.data, + "redirect_uris": [form.redirect_uri.data], + "grant_types": form.grant_types.data, + "response_types": form.response_types.data, + "special_mappings": form.special_mappings.data + }) + self.fill_service(service) + + def populate_form(self, service, form): + form.process( + obj=service, + token_endpoint_auth_method=service.config.get("token_endpoint_auth_method"), + redirect_uri=service.config.get("redirect_uris", [""])[0], + grant_types=service.config.get("grant_types", ["authorization_code"]), + response_types=service.config.get("response_types", ["code"]), + special_mappings=service.config.get("special_mappings", []) + ) + + +@register("saml") +class GenericSAMLApplication(base.SAMLApplication): + """ SAML2 is a legacy protocol based on XML security. Only redirect/post binding is supported + """ + + name = _("Generic SAML2") + + class Form(base.BaseForm): + entityid = fields.StringField(_('SP entity id'), [validators.URL(require_tld=False)]) + acs = fields.StringField(_('SP ACS'), [validators.URL(require_tld=False)]) + sign_mode = fields.SelectField( + _('Signature mode'), choices=[ + ('response', _('Sign the full response')), + ('assertion', _('Sign only the assertion')) + ] + ) + submit = fields.SubmitField(_('Submit')) + + def populate_service(self, form, service): + service.config.update({ + "acs": form.acs.data, + "entityid": form.entityid.data, + "sign_mode": form.sign_mode.data + }) + self.fill_service(service) + + def populate_form(self, service, form): + form.process( + obj=service, + acs=service.config.get("acs"), + entityid=service.config.get("entityid"), + sign_mode=service.config.get("sign_mode") + ) \ No newline at end of file diff --git a/hiboo/application/storage.py b/hiboo/application/storage.py new file mode 100644 index 0000000000000000000000000000000000000000..1d5c45fb8c061dccfd65dac2c7430d217da62e95 --- /dev/null +++ b/hiboo/application/storage.py @@ -0,0 +1,34 @@ +from hiboo.application import register, base +from wtforms import validators, fields +from flask_babel import lazy_gettext as _ + + +@register("gitlab") +class GitlabApplication(base.OIDCApplication): + """ Gitlab is a source code and project management plaform, largely based on Git + """ + + name = _("Gitlab repository manager") + + class Form(base.BaseForm): + application_uri = fields.StringField(_("Gitlab URL"), [validators.URL(require_tld=False)]) + submit = fields.SubmitField(_('Submit')) + + def populate_service(self, form, service): + service.profile_regex = "[a-z0-9_.\-]*" + callback_uri = form.application_uri.data + "/users/auth/openid_connect/callback" + service.config.update({ + "application_uri": form.application_uri.data, + "token_endpoint_auth_method": "client_secret_post", + "redirect_uris": [callback_uri], + "grant_types": ["authorization_code"], + "response_types": ["code"], + "special_mappings": ["mask_sub_uuid"] + }) + self.fill_service(service) + + def populate_form(self, service, form): + form.process( + obj=service, + application_uri=service.config.get("application_uri") + ) diff --git a/hiboo/application/templates/application_gitlab.html b/hiboo/application/templates/application_gitlab.html new file mode 100644 index 0000000000000000000000000000000000000000..9d28220dee4cc6ccf9e51e26c2f74612ea351540 --- /dev/null +++ b/hiboo/application/templates/application_gitlab.html @@ -0,0 +1,38 @@ +<h3>Setting up Gitlab</h3> +<p>Gitlab supports OIDC authentication using the Omniauth::oidc module.</p> +<p>If you are using Omnibus, you may paste the following config directly in your `gitlab.rb`.</p> +<pre> +# Authentication +gitlab_rails['omniauth_allow_single_sign_on'] = ['openid_connect'] +gitlab_rails['omniauth_block_auto_created_users'] = false +gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'openid_connect' +gitlab_rails['omniauth_providers'] = [ + { 'name' => 'openid_connect', + 'label' => 'Hiboo', + 'args' => { + 'name' => 'openid_connect', + 'scope' => ['openid','profile','email'], + 'issuer' => '{{ url_for("sso.oidc_authorize", service_uuid=service.uuid, _external=True) }}', + 'response_type' => 'code', + 'discovery' => false, + 'client_auth_method' => 'query', + 'client_options' => { + 'identifier' => '{{ service.config["client_id"] }}', + 'secret' => '{{ service.config["client_secret"] }}', + 'redirect_uri' => '{{ service.config["redirect_uris"][0] }}', + 'authorization_endpoint' => '{{ url_for("sso.oidc_authorize", service_uuid=service.uuid, _external=True) }}', + 'token_endpoint' => '{{ url_for("sso.oidc_token", service_uuid=service.uuid, _external=True) }}', + 'userinfo_endpoint' => '{{ url_for("sso.oidc_userinfo", service_uuid=service.uuid, _external=True) }}' + } + } + } +] +</pre> + +<p></p>You will also need to provision your users Omniauth bindings, by running the following SQL query against your Gitlab database.</p> + +<pre> +insert into identities (extern_uid,provider,user_id,created_at,updated_at) (select users.username as extern_uid, 'openid_connect' as provider, users.id as user_id, now() created_at, now() updated_at from users); +</pre> + +{% include "application_oidc.html" %} \ No newline at end of file diff --git a/hiboo/application/templates/application_mastodon.html b/hiboo/application/templates/application_mastodon.html new file mode 100644 index 0000000000000000000000000000000000000000..60d1047924011ffdc642a016bb41c75a2723a494 --- /dev/null +++ b/hiboo/application/templates/application_mastodon.html @@ -0,0 +1,22 @@ +<h3>Setting up Mastodon</h3> +<p>Mastodon uses SAML for SSO authentication. The example configuration is available at the following URL : <a href="https://github.com/tootsuite/mastodon/blob/master/.env.production.sample">https://github.com/tootsuite/mastodon/blob/master/.env.production.sample</a>.</p> +<p>In order to configure SAML for Mastodon, you may copy then paste the following lines directly into your Mastodon environment.</p> +<pre> +# Authentication +OAUTH_REDIRECT_AT_SIGN_IN=true +SAML_ENABLED=true +SAML_ISSUER={{ service.config["sp_entityid"] }} +SAML_ATTRIBUTES_STATEMENTS_UID=urn:oid:0.9.2342.19200300.100.1.1 +SAML_ATTRIBUTES_STATEMENTS_EMAIL=urn:oid:1.2.840.113549.1.9.1.1 +SAML_UID_ATTRIBUTE=urn:oid:0.9.2342.19200300.100.1.1 +SAML_ALLOWED_CLOCK_DRIFT=60 +SAML_SECURITY_WANT_ASSERTION_SIGNED=true +SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true +SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true +SAML_IDP_SSO_TARGET_URL={{ url_for("sso.saml_redirect", service_uuid=service.uuid, _external=True) }} +SAML_IDP_CERT={{ "".join(service.config["idp_cert"].strip().split("\n")[1:-1]) }} +SAML_CERT={{ "".join(service.config["sp_cert"].strip().split("\n")[1:-1]) }} +SAML_PRIVATE_KEY={{ "".join(service.config["sp_key"].strip().split("\n")[1:-1]) }} +</pre> + +{% include "application_saml.html" %} \ No newline at end of file diff --git a/hiboo/application/templates/application_oidc.html b/hiboo/application/templates/application_oidc.html new file mode 100644 index 0000000000000000000000000000000000000000..00e406337911690aec0ced28bd4f434c07d909d7 --- /dev/null +++ b/hiboo/application/templates/application_oidc.html @@ -0,0 +1,15 @@ +<h3>Detailed OpenID Connect settings</h3> +<dt>{% trans %}Authorization endpoint{% endtrans %}</dt> +<dd><pre>{{ url_for("sso.oidc_authorize", service_uuid=service.uuid, _external=True) }}</pre></dd> + +<dt>{% trans %}Token endpoint{% endtrans %}</dt> +<dd><pre>{{ url_for("sso.oidc_token", service_uuid=service.uuid, _external=True) }}</pre></dd> + +<dt>{% trans %}Userinfo endpoint{% endtrans %}</dt> +<dd><pre>{{ url_for("sso.oidc_userinfo", service_uuid=service.uuid, _external=True) }}</pre></dd> + +<dt>{% trans %}Client ID{% endtrans %}</dt> +<dd><pre>{{ service.config["client_id"] }}</pre></dd> + +<dt>{% trans %}Client secret{% endtrans %}</dt> +<dd><pre>{{ service.config["client_secret"] }}</dd> diff --git a/hiboo/application/templates/application_pick.html b/hiboo/application/templates/application_pick.html new file mode 100644 index 0000000000000000000000000000000000000000..2e5dc6754c60ef6192119a17c01a1efe6dfca968 --- /dev/null +++ b/hiboo/application/templates/application_pick.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block title %}{% trans %}Create a service{% endtrans %}{% endblock %} +{% block subtitle %}{% trans %}pick an application{% endtrans %}{% endblock %} + +{% block content %} +<div class="row"> +{% for application_id, application in applications.items() %} +<div class="col-md-4 col-s-6 col-xs-12"> + <div class="box box-widget widget-user-2"> + <div class="widget-user-header bg-{{ macros.colors[loop.index0 % 7] }}"> + <a href="{{ url_for(route, application_id=application_id) }}" style="opacity: 0.8" class="btn btn-lg bg-gray text-black pull-right"> + {% trans %}Create{% endtrans %} + </a> + <h3 class="widget-header-username">{{ application.name }}</h3> + <h5 class="widget-header-desc">{{ application.__doc__ }} </h5> + </div> + </div> +</div> +{% endfor %} +</div> +{% endblock %} diff --git a/hiboo/application/templates/application_saml.html b/hiboo/application/templates/application_saml.html new file mode 100644 index 0000000000000000000000000000000000000000..090e9e4768ff47859d89d6b7c9d09b85276ab219 --- /dev/null +++ b/hiboo/application/templates/application_saml.html @@ -0,0 +1,18 @@ +<h3>Detailed SAML settings</h3> +<dt>{% trans %}SAML Metadata{% endtrans %}</dt> +<dd><pre>{{ url_for("sso.saml_metadata", service_uuid=service.uuid, _external=True) }}</pre></dd> + +<dt>{% trans %}SSO redirect binding{% endtrans %}</dt> +<dd><pre>{{ url_for("sso.saml_redirect", service_uuid=service.uuid, _external=True) }}</pre></dd> + +<dt>{% trans %}ACS{% endtrans %}</dt> +<dd><pre>{{ service.config["acs"] }}</pre></dd> + +<dt>{% trans %}IDP certificate{% endtrans %}</dt> +<dd><pre>{{ service.config["idp_cert"] }}</pre></dd> + +<dt{% trans %}>SP certificate{% endtrans %}</dt> +<dd><pre>{{ service.config["sp_cert"] }}</pre></dd> + +<dt>{% trans %}SP private key{% endtrans %}</dt> +<dd><pre>{{ service.config["sp_key"] }}</pre></dd> diff --git a/hiboo/models.py b/hiboo/models.py index 51543ed2e78df897d464a6441752ee8e61c9e7f8..51bde2dd79ae1195f868d0e115ad965af7c75674 100644 --- a/hiboo/models.py +++ b/hiboo/models.py @@ -138,11 +138,6 @@ class Service(db.Model): """ __tablename__ = "service" - APPLICATIONS = { - "generic": "Generic application", - "mastodon": "Mastodon" - } - LOCKED = "locked" RESERVED = "reserved" MANAGED = "managed" @@ -157,10 +152,9 @@ class Service(db.Model): OPEN: _("No validation is required") } - protocol = db.Column(db.String(25)) name = db.Column(db.String(255)) provider = db.Column(db.String(255)) - application = db.Column(db.String(255)) + application_id = db.Column(db.String(255)) description = db.Column(db.String()) policy = db.Column(db.String(255)) max_profiles = db.Column(db.Integer(), nullable=False, default=1) diff --git a/hiboo/service/admin.py b/hiboo/service/admin.py index 9c70d664e1d4eb497317cc37fbf2220424b21c17..e7219517974bb5f84a0bc296311edcf35acf2758 100644 --- a/hiboo/service/admin.py +++ b/hiboo/service/admin.py @@ -1,6 +1,5 @@ -from hiboo import models, utils, security +from hiboo import models, utils, security, application from hiboo.service import blueprint, forms -from hiboo.sso import protocols from flask_babel import lazy_gettext as _ import flask @@ -15,26 +14,29 @@ def list(): @blueprint.route("/create") -@blueprint.route("/create/<protocol_name>", methods=["GET", "POST"]) +@blueprint.route("/create/<application_id>", methods=["GET", "POST"]) @security.admin_required() -def create(protocol_name=None): - if protocol_name is None: - return flask.render_template("protocol_pick.html", protocols=protocols) - protocol = protocols.get(protocol_name, None) or flask.abort(404) - form = protocol.Config.derive_form(forms.ServiceForm)() +def create(application_id=None): + if application_id is None: + return flask.render_template( + "application_pick.html", + applications=application.registry, + route="service.create" + ) + app = application.registry.get(application_id) or flask.abort(404) + form = app.Form() if form.validate_on_submit(): service = models.Service() - service.protocol = protocol_name + service.application_id = application_id service.uuid = str(uuid.uuid4()) service.config = {} form.populate_obj(service) - protocol.Config.populate_service(form, service) + app.populate_service(form, service) models.db.session.add(service) models.db.session.commit() flask.flash(_("Service successfully created"), "success") return flask.redirect(flask.url_for(".list")) - return flask.render_template("service_create.html", - protocol=protocol, form=form) + return flask.render_template("service_create.html", application=app, form=form) @blueprint.route("/edit") @@ -42,15 +44,15 @@ def create(protocol_name=None): @security.admin_required() def edit(service_uuid): service = models.Service.query.get(service_uuid) or flask.abort(404) - protocol = protocols.get(service.protocol, None) or flask.abort(404) - form = protocol.Config.derive_form(forms.ServiceForm)() + app = application.registry.get(service.application_id) or flask.abort(404) + form = app.Form() if form.validate_on_submit(): form.populate_obj(service) - protocol.Config.populate_service(form, service) + app.populate_service(form, service) models.db.session.commit() flask.flash(_("Service successfully updated"), "success") return flask.redirect(flask.url_for(".details", service_uuid=service_uuid)) - protocol.Config.populate_form(service, form) + app.populate_form(service, form) return flask.render_template("service_edit.html", service=service, form=form) @@ -58,7 +60,8 @@ def edit(service_uuid): @security.admin_required() def details(service_uuid): service = models.Service.query.get(service_uuid) or flask.abort(404) - return flask.render_template("service_details.html", service=service) + app = application.registry.get(service.application_id) or flask.abort(404) + return flask.render_template("service_details.html", service=service, application=app) @blueprint.route("/delete/<service_uuid>", methods=["GET", "POST"]) diff --git a/hiboo/service/forms.py b/hiboo/service/forms.py index 86efd8232dd8916a5593e1511b559dbc7b8f02d8..ee17f4a076eacc46e7da8ef458bda7aae91c77e4 100644 --- a/hiboo/service/forms.py +++ b/hiboo/service/forms.py @@ -1,7 +1,7 @@ from wtforms import validators, fields, widgets from flask_babel import lazy_gettext as _ -from hiboo import models +from hiboo import models, application import flask_wtf @@ -10,12 +10,10 @@ class ServiceForm(flask_wtf.FlaskForm): name = fields.StringField(_('Service name'), [validators.DataRequired()]) provider = fields.StringField(_('Provider'), [validators.DataRequired()]) description = fields.StringField(_('Description')) - application = fields.SelectField(_('Application'), - choices=list(models.Service.APPLICATIONS.items())) policy = fields.SelectField(_('Profile policy'), choices=list(models.Service.POLICIES.items())) max_profiles = fields.IntegerField(_('Maximum profile count'), [validators.NumberRange(1, 1000)]) - profile_regex = fields.StringField(_('Profile usenrame regex')) + profile_regex = fields.StringField(_('Profile username regex')) same_username = fields.BooleanField(_('Disable per-profile username')) submit = fields.SubmitField(_('Submit')) diff --git a/hiboo/service/templates/protocol_pick.html b/hiboo/service/templates/protocol_pick.html deleted file mode 100644 index 788a833d023091f74b4f85ff8b725cd29225e6c7..0000000000000000000000000000000000000000 --- a/hiboo/service/templates/protocol_pick.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{% trans %}Create a service{% endtrans %}{% endblock %} -{% block subtitle %}{% trans %}pick a protocol{% endtrans %}{% endblock %} - -{% block content %} -<div class="row"> -{% for protocol in protocols %} -{% import "protocol_" + protocol + ".html" as protocol_macros %} -<div class="col-md-4 col-s-6 col-xs-12"> - <div class="box box-widget widget-user-2"> - <div class="widget-user-header bg-{{ macros.colors[loop.index0 % 7] }}"> - <a href="{{ url_for(".create", protocol_name=protocol) }}" style="opacity: 0.8" class="btn btn-lg bg-gray text-black pull-right"> - {% trans %}Create {% endtrans %}{{ protocol_macros.name() }} - </a> - <h3 class="widget-header-username">{{ protocol_macros.name() }}</h3> - <h5 class="widget-header-desc">{{ protocol_macros.description() }} </h5> - </div> - </div> -</div> -{% endfor %} -</div> -{% endblock %} diff --git a/hiboo/service/templates/service_create.html b/hiboo/service/templates/service_create.html index 05f4402315eef9fdb5f3d952de28aa7fba058bd6..ef13bdb687ed8a345f9f8402936b07c7a9fc8bf3 100644 --- a/hiboo/service/templates/service_create.html +++ b/hiboo/service/templates/service_create.html @@ -1,5 +1,5 @@ {% extends "form.html" %} -{% set protocol_name = protocol.__name__ %} +{% set application_name = application.name %} {% block title %}{% trans %}Create a service{% endtrans %}{% endblock %} -{% block subtitle %}{% trans protocol_name %}add a {{ protocol_name }} service{% endtrans %}{% endblock %} +{% block subtitle %}{% trans application_name %}add a {{ application_name }} service{% endtrans %}{% endblock %} diff --git a/hiboo/service/templates/service_details.html b/hiboo/service/templates/service_details.html index cdd73733cc78a052b299ea36cdad09cc0d885c13..3a9963dcb4b5d2f6a4f5578ac2e4d63f3bf5fc1c 100644 --- a/hiboo/service/templates/service_details.html +++ b/hiboo/service/templates/service_details.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% import "protocol_" + service.protocol + ".html" as protocol_macros %} {% block title %}{{ service.name }}{% endblock %} {% block subtitle %}{% trans %}service details{% endtrans %}{% endblock %} @@ -16,15 +15,30 @@ <dt>{% trans %}Description{% endtrans %}</dt> <dd>{{ service.description }}</dd> + <dt>{% trans %}Provider{% endtrans %}</dt> + <dd>{{ service.provider }}</dd> + + <dt>{% trans %}Application{% endtrans %}</dt> + <dd>{{ application.name }}</dd> + + <dt>{% trans %}Application destriction{% endtrans %}</dt> + <dd>{{ application.__doc__ }}</dd> + <dt>{% trans %}UUID{% endtrans %}</dt> <dd><pre>{{ service.uuid }}</pre></dd> - - {{ protocol_macros.describe(service) }} </dl> </div> </div> </div> </div> +<div class="row"> + <div class="col-xs-12"> + <div class="box"> + <div class="box-body"> + {{ application.render_details(service) | safe }} + </div> + </div> +</div> {% endblock %} {% block actions %} diff --git a/hiboo/service/templates/service_list.html b/hiboo/service/templates/service_list.html index 6aa928365840b034c3fe9036279fae2a665ae686..d6bc62ba15647fcbf341a2c5765968d0681d6eb4 100644 --- a/hiboo/service/templates/service_list.html +++ b/hiboo/service/templates/service_list.html @@ -12,7 +12,7 @@ <tr> <th>{% trans %}Service{% endtrans %}</th> <th>{% trans %}Provider{% endtrans %}</th> - <th>{% trans %}Type{% endtrans %}</th> + <th>{% trans %}Application{% endtrans %}</th> <th>{% trans %}Policy{% endtrans %}</th> <th>{% trans %}Max profiles{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th> @@ -21,11 +21,10 @@ <tr> <td><a href="{{ url_for(".details", service_uuid=service.uuid) }}">{{ service.name }}</a></td> <td>{{ service.provider }}</td> - <td>{{ service.protocol }}</td> - <td>{{ service.policy }}</td> + <td>{{ service.application_id }}</td> + <td>{{ service.POLICIES[service.policy] }}</td> <td>{{ service.max_profiles }}</td> <td> - <a href="{{ url_for(".details", service_uuid=service.uuid)}}">Details</a> <a href="{{ url_for("profile.list_for_service", service_uuid=service.uuid)}}">Profiles</a> <a href="{{ url_for(".edit", service_uuid=service.uuid)}}">Edit</a> </td> diff --git a/hiboo/sso/__init__.py b/hiboo/sso/__init__.py index 504ae9128498376348dfd4d28738825113730714..8e7d3f21e3c8539623ba8b49fc076006ee983aad 100644 --- a/hiboo/sso/__init__.py +++ b/hiboo/sso/__init__.py @@ -1,11 +1,18 @@ +from hiboo import models, application + import flask blueprint = flask.Blueprint("sso", __name__, template_folder="templates") -from hiboo.sso import saml, oidc -protocols = { - "saml": saml, - "oidc": oidc -} +def get_service(service_uuid, expected_protocol): + """ Get a service by uuid, while checking for the application sso protocol + """ + service = models.Service.query.get(service_uuid) or flask.abort(404) + app = application.registry.get(service.application_id) or flask.abort(404) + app.sso_protocol == expected_protocol or flask.abort(404) + return service + + +from hiboo.sso import saml, oidc diff --git a/hiboo/sso/forms.py b/hiboo/sso/forms.py deleted file mode 100644 index 98a7994af18ee7df0b1f63ac47a881f0cc6d95c1..0000000000000000000000000000000000000000 --- a/hiboo/sso/forms.py +++ /dev/null @@ -1,48 +0,0 @@ -from wtforms import validators, fields, widgets -from flask_babel import lazy_gettext as _ - -import flask_wtf - - -class SAMLForm(flask_wtf.FlaskForm): - entityid = fields.StringField(_('SP entity id'), [validators.URL(require_tld=False)]) - acs = fields.StringField(_('SP ACS'), [validators.URL(require_tld=False)]) - sign_mode = fields.SelectField( - _('Signature mode'), choices=[ - ('response', _('Sign the full response')), - ('assertion', _('Sign only the assertion')) - ] - ) - submit = fields.SubmitField(_('Submit')) - - -class OIDCForm(flask_wtf.FlaskForm): - redirect_uri = fields.StringField(_("Redirect URI"), [validators.URL(require_tld=False)]) - token_endpoint_auth_method = fields.SelectField( - _('Token Endpoint Auth Method'), choices=[ - ("client_secret_post", _("HTTP POST data")), - ("client_secret_basic", _("HTTP basic authorization")), - ("none", _("No authentication")) - ] - ) - grant_types = fields.SelectMultipleField( - _('OpenID Connect grant type'), choices=[ - ("authorization_code", _("Authorization Code")), - ("implicit", _("Implicit")), - ("hybrid", _("Hybrid")) - ] - ) - response_types = fields.SelectMultipleField( - _('Allowed response types'), choices=[ - ("code", _("Authorization code only")), - ("id_token", _("Id token only")), - ("id_token token", _("Id token and token")) - ] - ) - special_mappings = fields.SelectMultipleField( - _('Enabled special claim mappings'), choices=[ - ("mask_sub_uuid", _("Mask the profile uuid")), - ("original_email", _("Return the actual user email")) - ] - ) - submit = fields.SubmitField(_('Submit')) diff --git a/hiboo/sso/oidc.py b/hiboo/sso/oidc.py index 55253ad63c45e8bce8ea9c2d1cf5d3b8ada85428..9a08ce1f549a57f65983d4d477659f497faba62a 100644 --- a/hiboo/sso/oidc.py +++ b/hiboo/sso/oidc.py @@ -9,7 +9,7 @@ from authlib.oauth2 import rfc6749 as oauth2 from authlib.oidc import core as oidc from authlib.common import security -from hiboo.sso import forms, blueprint +from hiboo.sso import blueprint, get_service from hiboo import models, utils, profile import flask @@ -17,54 +17,17 @@ import time import inspect -class Config(object): - """ Handles service configuration and forms. - - Settings are: - - token_endpoint_auth_method: the method for authenticating clients - - redirect_url: the (single) supported redirect uri for the client - - grant_types: supported grant types - - response_types: supported response types (the order matters) +def fill_service(service): + """ If necessary, prepare the client with cryptographic material. """ - - @classmethod - def derive_form(cls, form): - return type('DerivedOIDCForm', (forms.OIDCForm, form), {}) - - @classmethod - def populate_service(cls, form, service): - service.config.update({ - "token_endpoint_auth_method": form.token_endpoint_auth_method.data, - "redirect_uris": [form.redirect_uri.data], - "grant_types": form.grant_types.data, - "response_types": form.response_types.data, - "special_mappings": form.special_mappings.data - }) - cls.update_client(service) - - @classmethod - def populate_form(cls, service, form): - form.process( - obj=service, - token_endpoint_auth_method=service.config.get("token_endpoint_auth_method"), - redirect_uri=service.config.get("redirect_uris", [""])[0], - grant_types=service.config.get("grant_types", ["authorization_code"]), - response_types=service.config.get("response_types", ["code"]), - special_mappings=service.config.get("special_mappings", []) + if "client_id" not in service.config: + service.config.update( + client_id=security.generate_token(24), + client_secret=security.generate_token(48), + jwt_key=security.generate_token(24), + jwt_alg="HS256" ) - @classmethod - def update_client(cls, service): - """ If necessary, prepare the client with cryptographic material. - """ - if "client_id" not in service.config: - service.config.update( - client_id=security.generate_token(24), - client_secret=security.generate_token(48), - jwt_key=security.generate_token(24), - jwt_alg="HS256" - ) - class AuthorizationCodeMixin(object): """ Mixin for defining oauth grants @@ -146,12 +109,6 @@ class Client(sqla_oauth2.OAuth2ClientMixin): self.authorization.register_grant(Client.ImplicitGrant) self.authorization.register_grant(Client.HybridGrant) - @classmethod - def get_by_service(cls, service_uuid): - service = models.Service.query.get(service_uuid) - if service and service.protocol == "oidc": - return cls(service) - def query_client(self, client_id): return self if client_id == self.client_id else None @@ -208,20 +165,20 @@ class Client(sqla_oauth2.OAuth2ClientMixin): @blueprint.route("/oidc/authorize/<service_uuid>", methods=["GET", "POST"]) def oidc_authorize(service_uuid): - client = Client.get_by_service(service_uuid) or flask.abort(404) + client = Client(get_service(service_uuid, "oidc")) picked = profile.get_profile(client.service, intent=True) or flask.abort(403) return client.authorization.create_authorization_response(grant_user=picked) @blueprint.route("/oidc/token/<service_uuid>", methods=["POST"]) def oidc_token(service_uuid): - client = Client.get_by_service(service_uuid) or flask.abort(404) + client = Client(get_service(service_uuid, "oidc")) return client.authorization.create_token_response() @blueprint.route("/oidc/userinfo/<service_uuid>", methods=["GET", "POST"]) def oidc_userinfo(service_uuid): - client = Client.get_by_service(service_uuid) or flask.abort(404) + client = Client(get_service(service_uuid, "oidc")) token = client.validate_token(flask.request) profile = models.Profile.query.get(token["profile_uuid"]) return client.generate_user_info(profile, token["scope"])