Skip to content
Snippets Groups Projects
Commit 67c682c9 authored by kaiyou's avatar kaiyou
Browse files

Add the ability to create a service

parent d46b7581
No related branches found
No related tags found
No related merge requests found
Showing
with 326 additions and 36 deletions
......@@ -14,3 +14,4 @@ PyYAML
bcrypt
pysaml2
xmlsec
cryptography
......@@ -30,10 +30,10 @@ def create_app_from_config(config):
return dict(config=app.config)
# Import views
from trurt import account, admin, sso
from trurt import account, service, sso
app.register_blueprint(account.blueprint, url_prefix='/account')
app.register_blueprint(service.blueprint, url_prefix='/service')
app.register_blueprint(sso.blueprint, url_prefix='/sso')
app.register_blueprint(admin.blueprint, url_prefix='/admin')
@app.route("/")
def index():
......
......@@ -34,3 +34,7 @@ class ProfileForm(flask_wtf.FlaskForm):
username = fields.StringField(_('Username'), [validators.DataRequired()])
comment = fields.StringField(_('Comment'))
submit = fields.SubmitField(_('Create profile'))
class ProfilePickForm(flask_wtf.FlaskForm):
profile_uuid = fields.TextField('profile', [])
......@@ -6,6 +6,17 @@ import flask_login
import flask
def pick_profile(service, **redirect_args):
form = forms.ProfilePickForm()
if form.validate_on_submit():
profile = models.Profile.query.get(form.profile_uuid.data)
if not (profile.user == flask_login.current_user and
profile.service == service):
return None
return profile
utils.force_redirect(utils.url_for("account.pick", **redirect_args))
@blueprint.route("/profiles")
def profiles():
return flask.render_template("account_profiles.html")
......@@ -20,7 +31,7 @@ def pick():
service_uuid=service.uuid,
user_uuid=flask_login.current_user.uuid
).all()
form = sso_forms.SSOValidateForm()
form = forms.ProfilePickForm()
return flask.render_template("account_pick.html",
service=service, profiles=profiles, form=form,
action_create=utils.url_for("account.create_profile", intent="account.pick"),
......
......@@ -30,7 +30,7 @@
<form method="POST" action="{{ action_pick }}" class="form">
{{ form.hidden_tag() }}
<input type="hidden" name="profile_uuid" value="{{ profile.uuid }}">
<input type="submit" value="Use this" class="btn btn-lg btn-bg-gray text-black pull-right">
<input type="submit" value="Use this" style="opacity: 0.8" class="btn btn-lg btn-flat bg-gray text-black pull-right">
</form>
<h3 class="widget-header-username">{{ profile.username }}</h3>
<h5 class="widget-header-desc">{{ profile.comment or "No profile description" }}&nbsp;</h5>
......@@ -41,9 +41,8 @@
<li><a href="#">Not shared with anyone</a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
from flask import Blueprint
blueprint = Blueprint("admin", __name__, template_folder="templates")
......@@ -133,9 +133,31 @@ class Service(db.Model):
"""
__tablename__ = "service"
APPLICATIONS = {
"generic": "Generic application",
"mastodon": "Mastodon"
}
LOCKED = "locked"
RESERVED = "reserved"
MANAGED = "managed"
BURST = "burst"
OPEN = "open"
POLICIES = {
LOCKED: "Profile creation is impossible",
RESERVED: "Profile creation is reserved to managers",
MANAGED: "Profile creation must be validated",
BURST: "Additional profiles must be validated",
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))
description = db.Column(db.String())
policy = db.Column(db.String(255))
max_profiles = db.Column(db.Integer(), nullable=False, default=1)
config = db.Column(JSONEncoded)
......@@ -199,5 +221,4 @@ class History(db.Model):
@property
def description(self):
print(History.DESCRIPTION.get(self.category, "").format(this=self))
return History.DESCRIPTION.get(self.category, "").format(this=self)
from flask import Blueprint
blueprint = Blueprint("service", __name__, template_folder="templates")
from trurt.service import admin
from trurt import models, utils
from trurt.service import blueprint, forms
from trurt.sso import protocols
import flask_login
import flask
import uuid
@blueprint.route("/list")
def list():
services = models.Service.query.all()
return flask.render_template("service_list.html", services=services)
@blueprint.route("/create")
def create():
return flask.render_template("service_create.html", protocols=protocols)
@blueprint.route("/create/<protocol_name>", methods=["GET", "POST"])
def create_protocol(protocol_name):
protocol = protocols.get(protocol_name, None) or flask.abort(404)
form = protocol.Config.derive_form(forms.ServiceForm)()
if form.validate_on_submit():
service = models.Service()
service.protocol = protocol_name
service.uuid = str(uuid.uuid4())
service.config = {}
form.populate_obj(service)
protocol.Config.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_form.html",
protocol=protocol, form=form)
@blueprint.route("/details/<service_uuid>")
def details(service_uuid):
service = models.Service.query.get(service_uuid) or flask.abort(404)
return flask.render_template("service_details.html", service=service)
from wtforms import validators, fields, widgets
from flask_babel import lazy_gettext as _
from trurt import models
import flask_wtf
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(0, 1000)])
submit = fields.SubmitField(_('Submit'))
{% extends "base.html" %}
{% block title %}Create a service{% endblock %}
{% block subtitle %}pick a protocol{% endblock %}
{% set colors = ['blue', 'green', 'orange', 'teal', 'red', 'purple', 'maroon'] %}
{% 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-{{ colors[loop.index0 % 7] }}">
<a href="{{ url_for(".create_protocol", protocol_name=protocol) }}" style="opacity: 0.8" class="btn btn-lg bg-gray text-black pull-right">
Create {{ protocol_macros.name() }}
</a>
<h3 class="widget-header-username">{{ protocol_macros.name() }}</h3>
<h5 class="widget-header-desc">{{ protocol_macros.description() }}&nbsp;</h5>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% extends "base.html" %}
{% block title %}Create a service{% endblock %}
{% block subtitle %}add a SAML service{% endblock %}
{% block content %}
{{ macros.form(form) }}
{% endblock %}
{% extends "base.html" %}
{% import "protocol_" + service.protocol + ".html" as protocol_macros %}
{% block title %}{{ service.name }}{% endblock %}
{% block subtitle %}service details{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="box box-solid">
<div class="box-body">
<dl class="dl-horizontal">
<dt>Service name</dt>
<dd>{{ service.name }}</dd>
<dt>Description</dt>
<dd>{{ service.description }}</dd>
<dt>UUID</dt>
<dd>{{ service.uuid }}</dd>
{{ protocol_macros.describe(service) }}
</dl>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "base.html" %}
{% block title %}Service list{% endblock %}
{% block subtitle %}all available services{% endblock %}
{% block content %}
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tr>
<th>Service</th>
<th>Provider</th>
<th>Policy</th>
<th>Max profiles</th>
</tr>
{% for service in services %}
<tr>
<td><a href="{{ url_for(".details", service_uuid=service.uuid) }}">{{ service.name }}</a></td>
<td>{{ service.provider }}</td>
<td>{{ service.policy }}</td>
<td>{{ service.max_profiles }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -4,3 +4,8 @@ from flask import Blueprint
blueprint = Blueprint("sso", __name__, template_folder="templates")
from trurt.sso import saml, oidc
protocols = {
"saml": saml,
"oidc": oidc
}
......@@ -4,5 +4,7 @@ from flask_babel import lazy_gettext as _
import flask_wtf
class SSOValidateForm(flask_wtf.FlaskForm):
profile_uuid = fields.TextField('profile', [])
class SAMLForm(flask_wtf.FlaskForm):
entityid = fields.StringField('SP entity id', [validators.URL()])
acs = fields.StringField('SP ACS', [validators.URL()])
submit = fields.SubmitField('Submit')
......@@ -6,9 +6,71 @@ from saml2 import sigver
sigver.security_context = security_context
from trurt.sso import blueprint, forms
from trurt import models, utils
from trurt import models, utils, account
from saml2 import server, saml, config, mdstore, assertion
import saml2, base64, flask, xmlsec, lxml.etree, flask_login
from cryptography import x509
from cryptography.hazmat import primitives, backends
import saml2, base64, datetime, flask, xmlsec, lxml.etree, flask_login
class Config(object):
""" Handles service configuration and forms.
"""
@classmethod
def derive_form(cls, form):
""" Add required fields to a form.
"""
return type('NewForm', (form, forms.SAMLForm), {})
@classmethod
def populate_service(cls, form, service):
""" Populate a service from a form
"""
service.config.update({
"acs": form.acs.data,
"entityid": form.entityid.data
})
cls.update_keys(service)
@classmethod
def populate_form(cls, service, form):
""" Populate a form from a service
"""
form.acs.data = service.config["acs"]
form.entityid.data = service.config["entityid"]
@classmethod
def update_keys(cls, service):
if "idp_cert" not in service.config:
key, cert = cls.generate_key(service.uuid + "-idp")
service.config.update({"idp_key": key, "idp_cert": cert})
if "sp_cert" not in service.config:
key, cert = cls.generate_key(service.uuid + "-sp")
service.config.update({"sp_key": key, "sp_cert": cert})
@classmethod
def generate_key(cls, cn):
key = primitives.asymmetric.rsa.generate_private_key(
key_size=2048, public_exponent=65535,
backend=backends.default_backend()
)
now = datetime.datetime.utcnow()
subject = x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, cn)])
cert = x509.CertificateBuilder().subject_name(subject)\
.issuer_name(subject).serial_number(x509.random_serial_number())\
.public_key(key.public_key())\
.not_valid_before(now)\
.not_valid_after(now + datetime.timedelta(days=3650))\
.sign(key, primitives.hashes.SHA256(), backends.default_backend())
return (
key.private_bytes(primitives.serialization.Encoding.PEM,
format=primitives.serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=primitives.serialization.NoEncryption()
).decode("ascii"),
cert.public_bytes(primitives.serialization.Encoding.PEM).decode("ascii")
)
class MetaData(mdstore.InMemoryMetaData):
......@@ -40,8 +102,8 @@ class MetaData(mdstore.InMemoryMetaData):
'name_id_format': [saml2.saml.NAMEID_FORMAT_PERSISTENT]
}
config_dict = {
'key_file': service.config["idp_key"],
'cert_file': service.config["sp_cert"],
'key_file': "".join(service.config["idp_key"].strip().split("\n")[1:-1]),
'cert_file': "".join(service.config["sp_cert"].strip().split("\n")[1:-1]),
'service':{'idp': idp_service},
'metadata':[
{'class':'trurt.sso.saml.MetaData',
......@@ -92,29 +154,20 @@ class SecurityContext(sigver.SecurityContext):
return lxml.etree.tostring(xml)
@blueprint.route('/saml/<service_uuid>/redirect')
def redirect(service_uuid):
service = models.Service.query.get(service_uuid) or flask.abort(404)
return flask.redirect(utils.url_for(
"account.pick", intent="sso.reply", service_uuid=service_uuid,
))
@blueprint.route('/saml/<service_uuid>/reply', methods=["POST"])
def reply(service_uuid):
# First check the service and picked profile
form = forms.SSOValidateForm()
form.validate() or flask.abort(403)
@blueprint.route('/saml/<service_uuid>', methods=["GET", "POST"])
def saml_redirect(service_uuid):
# Get the profile from user input (implies redirects)
service = models.Service.query.get(service_uuid) or flask.abort(404)
profile = models.Profile.query.get(form.profile_uuid.data) or flask.abort(404)
if not (profile.user == flask_login.current_user and profile.service == service):
return flask.abort(403)
# Parse the authentication request
service.protocol == "saml" or flask.abort(404)
profile = account.profiles.pick_profile(
service, intent="sso.saml_redirect", service_uuid=service_uuid
) or flask.abort(403)
# Parse the authentication request and check the ACS
idp = server.Server(config=(MetaData.get_config(service)))
xml = flask.request.args["SAMLRequest"]
request = idp.parse_authn_request(xml, saml2.BINDING_HTTP_REDIRECT)
if not service.config["acs"] == request.message.issuer.text:
return flask.abort(403)
request.message.issuer or flask.abort(403)
service.config["acs"] == request.message.issuer.text or flask.abort(403)
# Provide a SAML response
response = idp.create_authn_response(
identity={
......
{% macro name() %}OIDC{% endmacro %}
{% macro description() %}OpenID Connect (OIDC) is JWT based authentication and authorization protocol{% endmacro %}
{% macro name() %}SAML2{% endmacro %}
{% macro description() %}SAML2 is a legacy protocol based on XML security. Only redirect/post binding is supported.{% endmacro %}
{% macro describe(service) %}
<dt>Endpoint</dt>
<dd>{{ url_for("sso.saml_redirect", service_uuid=service.uuid, _external=True) }}</dd>
<dt>ACS</dt>
<dd>{{ service.config["acs"] }}</dd>
<dt>IDP certificate</dt>
<dd><pre>{{ service.config["idp_cert"] }}</pre></dd>
<dt>Short IDP certificate</dt>
<dd><pre>{{ "".join(service.config["idp_cert"].strip().split("\n")[1:-1]) }}</pre></dd>
<dt>SP certificate</dt>
<dd><pre>{{ service.config["sp_cert"] }}</pre></dd>
<dt>Short SP certificate</dt>
<dd><pre>{{ "".join(service.config["sp_cert"].strip().split("\n")[1:-1]) }}</pre></dd>
<dt>SP private key</dt>
<dd><pre>{{ service.config["sp_key"] }}</pre></dd>
<dt>Short SP private key</dt>
<dd><pre>{{ "".join(service.config["sp_key"].strip().split("\n")[1:-1]) }}</pre></dd>
{% endmacro %}
......@@ -28,6 +28,14 @@
</li>
{% endif %}
<li class="header">Management</li>
<li>
<a href="{{ url_for("service.list") }}">
<i class="fa fa-book"></i> <span>Services</span>
</a>
</li>
<li class="header">About</li>
<li>
<a href="#">
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment