Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • acides/hiboo
  • frju365/hiboo
  • pascoual/hiboo
  • thedarky/hiboo
  • jeremy/hiboo
  • cyrinux/hiboo
  • a.f/hiboo
  • mickge/hiboo
  • llaq/hiboo
  • vaguelysalaried/hiboo
  • felinn-glotte/hiboo
  • AntoninDelFabbro/hiboo
  • docemmetbrown/hiboo
13 results
Show changes
Showing
with 480 additions and 312 deletions
{% extends "base.html" %}
{% from 'bootstrap5/pagination.html' import render_pagination %}
{% block title %}{% trans %}Moderation{% endtrans %}{% endblock %}
{% block subtitle %}{% trans %}Pending profiles{% endtrans %}{% endblock %}
{% block content %}
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-head-fixed text-nowrap">
<thead>
<tr>
<th>{% trans %}Service{% endtrans %}</th>
<th>{% trans %}Profile username{% endtrans %}</th>
<th>{% trans %}Owned by{% endtrans %}</th>
<th>{% trans %}Created on{% endtrans %}</th>
<th>{% trans %}Status{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for profile in profiles %}
<tr>
<td>{{ profile.service.name }}</td>
<td><a href="{{ url_for("profile.details", profile_uuid=profile.uuid, service_uuid=profile.service.uuid) }}">{{ profile.name }}</a></td>
{% if profile.user_uuid %}
<td><a href="{{ url_for("user.details", user_uuid=profile.user_uuid) }}">{{ profile.user.name }}</a></td>
{% else %}
<td>-</td>
{% endif %}
<td>{{ profile.created_at.date() }}</td>
<td>{{ macros.profile_status(profile) }}</td>
<td>
{% for action in profile.actions %}
<a href="{{ action.url(profile=profile) }}">{{ action.label | capitalize }}</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if (profiles.page) and (profiles.has_next or profiles.has_prev) %}
{{ render_pagination(profiles, align="center") }}
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block actions %}
<a href="{{ url_for(".activity") }}" class="btn btn-outline-primary">
{% trans %}Activity logs{% endtrans %}</a>
{% endblock %}
from hiboo.moderation import blueprint
from hiboo import security, models
import flask
@blueprint.route("/")
@security.admin_required()
def pending_profiles():
page = flask.request.args.get('page', 1, type=int)
profiles = models.Profile.query.filter(
models.db.or_(
models.Profile.status==models.Profile.REQUEST,
models.Profile.status==models.Profile.BLOCKED,
models.Profile.status==models.Profile.UNCLAIMED,
models.Profile.transition.is_not(None),
)).order_by(models.Profile.status.desc()).paginate(page=page, per_page=25)
return flask.render_template("moderation_profiles.html", profiles=profiles)
@blueprint.route("/activity")
@security.admin_required()
def activity():
page = flask.request.args.get('page', 1, type=int)
events = models.History.query.order_by(
models.History.created_at.desc()).paginate(page=page, per_page=25)
return flask.render_template("moderation_activity.html", events=events)
...@@ -9,65 +9,65 @@ import csv ...@@ -9,65 +9,65 @@ import csv
@blueprint.cli.command() @blueprint.cli.command()
@click.argument("service_uuid") @click.argument("service_uuid")
@click.option("-s", "--status") @click.option("-s", "--status")
@click.option("-u", "--username") @click.option("-u", "--profilename")
def list(service_uuid, status=None, username=None, ): def list(service_uuid, status=None, profilename=None, ):
""" Select and list profiles. """ Select and list profiles.
""" """
criteria = [models.Profile.service_uuid == service_uuid] criteria = [models.Profile.service_uuid == service_uuid]
if status is not None: if status is not None:
criteria.append(models.Profile.status == status) criteria.append(models.Profile.status == status)
if username is not None: if profilename is not None:
criteria.append(models.Profile.username.ilike('%{}%'.format(username))) criteria.append(models.profile.name.ilike('%{}%'.format(profilename)))
click.echo(terminaltables.SingleTable([('uuid', 'username', 'status', 'claim_names')]+ click.echo(terminaltables.SingleTable([('uuid', 'profilename', 'status', 'claim_names')]+
[ [
[profile.uuid, profile.username, profile.status, ','.join([item.username for item in profile.claimnames])] [profile.uuid, profile.name, profile.status, ','.join([item.name for item in profile.claimnames])]
for profile in models.Profile.query.filter(*criteria).all() for profile in models.Profile.query.filter(*criteria).all()
], title='Profiles').table) ], title='Profiles').table)
@blueprint.cli.command() @blueprint.cli.command()
@click.argument("service_uuid") @click.argument("service_uuid")
@click.argument("username") @click.argument("profilename")
@click.argument("claim_names", nargs=-1) @click.argument("claim_names", nargs=-1)
@click.option("-p", "--password-hash", "password_hash") @click.option("-p", "--password-hash", "password_hash")
def unclaimed(service_uuid, username, claim_names, password_hash=None): def unclaimed(service_uuid, profilename, claim_names, password_hash=None):
""" This creates or updates an unclaimed profile. """ This creates or updates an unclaimed profile.
""" """
assert models.Service.query.get(service_uuid) assert models.Service.query.get(service_uuid)
profile = models.Profile.query.filter_by(service_uuid=service_uuid, username=username).first() profile = models.Profile.query.filter_by(service_uuid=service_uuid, name=profilename).first()
# Do nothing if the profile is already claimed # Do nothing if the profile is already claimed
if profile and not profile.status == models.Profile.UNCLAIMED: if profile and not profile.status == models.Profile.UNCLAIMED:
click.echo("Profile {} was claimed already".format(username)) click.echo("Profile {} was claimed already".format(profilename))
return return
# Create the profile if necessary # Create the profile if necessary
if profile: if profile:
click.echo("Profile {} exists already".format(username)) click.echo("Profile {} exists already".format(profilename))
else: else:
profile = models.Profile( profile = models.Profile(
service_uuid=service_uuid, service_uuid=service_uuid,
username=username, name=profilename,
status=models.Profile.UNCLAIMED, status=models.Profile.UNCLAIMED,
) )
click.echo("Profile {} was created".format(username)) click.echo("Profile {} was created".format(profilename))
models.db.session.add(profile) models.db.session.add(profile)
# Update the profile password if one was provided # Update the profile password if one was provided
if password_hash is not None: if password_hash is not None:
profile.extra={"password": password_hash} profile.extra={"password": password_hash}
click.echo("Password updated for profile {}".format(username)) click.echo("Password updated for profile {}".format(profilename))
# Update profile claim names if any is provided (never delete an existing claim name) # Update profile claim names if any is provided (never delete an existing claim name)
for username in claim_names: for claimname in claim_names:
claim_name = models.ClaimName.query.filter_by(service_uuid=service_uuid, username=username).first() claim_name = models.ClaimName.query.filter_by(service_uuid=service_uuid, name=claimname).first()
if claim_name and not claim_name.profile == profile: if claim_name and not claim_name.profile == profile:
click.echo("Claim name conflict for {}, already belongs to {}".format(username, claim_name.profile.username)) click.echo("Claim name conflict for {}, already belongs to {}".format(claimname, claim_name.profile.name))
elif claim_name: elif claim_name:
click.echo("Claim {} already exists for {}".format(username, profile.username)) click.echo("Claim {} already exists for {}".format(claimname, profile.name))
else: else:
claim_name = models.ClaimName( claim_name = models.ClaimName(
service_uuid=service_uuid, service_uuid=service_uuid,
profile=profile, profile=profile,
username=username name=claimname
) )
click.echo("Claim created for {}, belongs to {}".format(username, profile.username)) click.echo("Claim created for {}, belongs to {}".format(claimname, profile.name))
models.db.session.add(claim_name) models.db.session.add(claim_name)
models.db.session.commit() models.db.session.commit()
......
...@@ -24,7 +24,7 @@ def apply_transition(profile): ...@@ -24,7 +24,7 @@ def apply_transition(profile):
the step START) the step START)
""" """
print("Applying {}/{} to profile {}@{}".format( print("Applying {}/{} to profile {}@{}".format(
profile.transition, profile.transition_step, profile.username, profile.service.name)) profile.transition, profile.transition_step, profile.name, profile.service.name))
app = profile.service.application app = profile.service.application
transition = models.Profile.TRANSITIONS[profile.transition] transition = models.Profile.TRANSITIONS[profile.transition]
manual = app.apply_hooks(profile, profile.transition, profile.transition_step) manual = app.apply_hooks(profile, profile.transition, profile.transition_step)
...@@ -37,4 +37,6 @@ def apply_transition(profile): ...@@ -37,4 +37,6 @@ def apply_transition(profile):
}[profile.transition_step] }[profile.transition_step]
if profile.transition_step == models.Profile.DONE: if profile.transition_step == models.Profile.DONE:
profile.status = transition.to profile.status = transition.to
if profile.transition_step is None:
profile.transition = None
...@@ -7,6 +7,11 @@ import flask_wtf ...@@ -7,6 +7,11 @@ import flask_wtf
class ProfileForm(flask_wtf.FlaskForm): class ProfileForm(flask_wtf.FlaskForm):
username = fields.StringField(_('Username'), [validators.DataRequired()]) username = fields.StringField(_('Username'), [validators.DataRequired()])
comment = fields.StringField(_('Comment')) comment = fields.StringField(_('Comment'))
username_spoof_protection = fields.BooleanField(
_('Username spoof protection'),
description=_("Prevent to register a profile username that case-insensitively exists in user database"),
default=True
)
submit = fields.SubmitField(_('Create profile')) submit = fields.SubmitField(_('Create profile'))
def force_username(self, username): def force_username(self, username):
......
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ profile.username }}{% endblock %} {% block title %}{{ profile.name }}{% endblock %}
{% block subtitle %}{{ label.lower() }}{% endblock %} {% block subtitle %}{{ label.lower() }}{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">{{ result | safe }}</div>
{{ result | safe }}
</div> </div>
</div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block actions %} {% block actions %}
<a href="{{ url_for("profile.details", profile_uuid=profile.uuid) }}" class="btn btn-info">{% trans %}Show profile{% endtrans %}</a> <a href="{{ url_for("profile.details", profile_uuid=profile.uuid) }}"
class="btn btn-info">{% trans %}Show profile{% endtrans %}</a>
{% endblock %} {% endblock %}
{% extends "base.html" %} {% extends "form.html" %}
{% set service_name = service.name %} {% set service_name = service.name %}
{% block title %}{% trans %}Claim profile{% endtrans %}{% endblock %}
{% block subtitle %}{% trans service_name %}for the service {{ service_name }}{% endtrans %}{% endblock %}
{% block content %}
{{ macros.form(form) }}
{% block title %}
{% trans %}Claim profile{% endtrans %}
{% endblock %}
{% block subtitle %}
{% trans service_name %}for the service {{ service_name }}{% endtrans %}
{% endblock %} {% endblock %}
{% extends "base.html" %} {% extends "form.html" %}
{% set service_name = service.name %} {% set service_name = service.name %}
{% block title %}{% trans %}New profile{% endtrans %}{% endblock %}
{% block title %}
{% trans %}New profile{% endtrans %}
{% endblock %}
{% block subtitle %} {% block subtitle %}
{% trans service_name %}for the service {{ service_name }}{% endtrans %} {% trans service_name %}for the service {{ service_name }}{% endtrans %}
{% if create_for %}{% trans %}and user{% endtrans %} {{ user.username }}{% endif %} {% if create_for %}
{% endblock %} {% trans %}and user{% endtrans %} {{ user.name }}
{% endif %}
{% block content %}
{{ macros.form(form) }}
{% endblock %} {% endblock %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ profile.username }}{% endblock %} {% block title %}{{ profile.name }}{% endblock %}
{% block subtitle %}{% trans %}profile details{% endtrans %}{% endblock %} {% block subtitle %}
{% trans %}profile details{% endtrans %}
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-6 col"> <div class="col-md-6 col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-lg-3">{% trans %}Username{% endtrans %}</dt> <dt class="col-lg-3">{% trans %}Username{% endtrans %}</dt>
<dd class="col-lg-9">{{ profile.username }}</dd> <dd class="col-lg-9">
{{ profile.name }}
{% if profile.user %} </dd>
<dt class="col-lg-3">{% trans %}Owner{% endtrans %}</dt> {% if profile.user %}
<dd class="col-lg-9"><a href="{{ url_for("user.details", user_uuid=profile.user_uuid) }}">{{ profile.user.username }}</a></dd> <dt class="col-lg-3">{% trans %}Owner{% endtrans %}</dt>
{% endif %} <dd class="col-lg-9">
<a href="{{ url_for("user.details", user_uuid=profile.user_uuid) }}">{{ profile.user.name }}</a>
<dt class="col-lg-3">{% trans %}UUID{% endtrans %}</dt> </dd>
<dd class="col-lg-9"><pre>{{ profile.uuid }}</pre></dd> {% endif %}
<dt class="col-lg-3">{% trans %}UUID{% endtrans %}</dt>
<dt class="col-3">{% trans %}Status{% endtrans %}</dt> <dd class="col-lg-9">
<dd class="col-9">{{ macros.profile_status(profile) }}</dd> <code>{{ profile.uuid }}</code>
</dd>
<dt class="col-lg-3">{% trans %}Created at{% endtrans %}</dt> <dt class="col-3">{% trans %}Status{% endtrans %}</dt>
<dd class="col-lg-9">{{ profile.created_at }}</dd> <dd class="col-9">
</dl> {{ macros.profile_status(profile) }}
</dd>
<dt class="col-lg-3">{% trans %}Created at{% endtrans %}</dt>
<dd class="col-lg-9">
{{ profile.created_at }}
</dd>
</dl>
</div>
</div> </div>
</div> </div>
<div class="col-md-6 col">{{ macros.timeline(profile.history, public_only=False) }}</div>
</div> </div>
<div class="col-md-6 col">
{{ macros.timeline(profile.history, public_only=False) }}
</div>
</div>
{% endblock %} {% endblock %}
{% block actions %} {% block actions %}
{% for action in profile.actions %} {% for action in profile.actions %}
<a href="{{ action.url(profile) }}" class="btn btn-info">{{ action.label | capitalize }}</a> <a href="{{ action.url(profile) }}" class="btn btn-outline-info">{{ action.label | capitalize }}</a>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
{% extends "form.html" %}
{% set service_name = service.name %}
{% set profile_name = profile.name %}
{% block title %}
{% trans %}Edit profile {{ profile_name }}{% endtrans %}
{% endblock %}
{% block subtitle %}
{% trans service_name %}for the service {{ service_name }}{% endtrans %}
{% trans %}and user{% endtrans %} {{ profile.user.name }}
{% endblock %}
{% extends "base.html" %} {% extends "base.html" %}
{% from 'bootstrap5/pagination.html' import render_pagination %}
{% block title %} {% block title %}
{% if service %} {{ service.name }}
{{ service.name }}
{% elif status %}
{{ status[1] | capitalize }} {% trans %}profiles{% endtrans %}
{% else %}
All profiles
{% endif %}
{% endblock %} {% endblock %}
{% block subtitle %}profile list{% endblock %} {% block subtitle %}{% trans %}profile list{% endtrans %}{% endblock %}
{% block content %} {% block content %}
<div class="row"> {{ render_form(form, button_size="sm", form_type="inline") }}
<div class="col">
<div class="card"> <div class="row">
<div class="card-body table-responsive p-0"> <div class="col">
<table class="table table-striped table-head-fixed text-nowrap"> <div class="table-responsive">
<table class="table table-striped table-head-fixed table-hover">
<thead> <thead>
<tr> <tr>
{% if not service %} {% if not service %}
<th>{% trans %}Service{% endtrans %}</th> <th>{% trans %}Service{% endtrans %}</th>
{% endif %} {% endif %}
<th>{% trans %}Profile username{% endtrans %}</th> <th>{% trans %}Profile username{% endtrans %}</th>
<th>{% trans %}Owned by{% endtrans %}</th> <th>{% trans %}Owned by{% endtrans %}</th>
...@@ -32,37 +28,41 @@ ...@@ -32,37 +28,41 @@
</thead> </thead>
<tbody> <tbody>
{% for profile in profiles %} {% for profile in profiles %}
<tr> <tr>
{% if not service %} {% if not service %}
<td>{{ profile.service.name }}</td> <td>{{ profile.service.name }}</td>
{% endif %} {% endif %}
<td><a href="{{ url_for("profile.details", profile_uuid=profile.uuid) }}">{{ profile.username }}</a></td> <td><a href="{{ url_for("profile.details", profile_uuid=profile.uuid) }}">{{ profile.name }}</a></td>
{% if profile.user_uuid %} {% if profile.user_uuid %}
<td><a href="{{ url_for("user.details", user_uuid=profile.user_uuid) }}">{{ profile.user.username }}</a></td> <td><a href="{{ url_for("user.details", user_uuid=profile.user_uuid) }}">{{ profile.user.name }}</a></td>
{% else %} {% else %}
<td>-</td> <td>-</td>
{% endif %} {% endif %}
<td>{{ profile.uuid }}</td> <td><code>{{ profile.uuid }}</code></td>
<td>{{ macros.profile_status(profile) }}</td> <td>{{ macros.profile_status(profile) }}</td>
<td>{{ profile.created_at.date() }}</td> <td>{{ profile.created_at.date() }}</td>
<td> <td>
{% for action in profile.actions %} <a href="{{ url_for("profile.edit", profile_uuid=profile.uuid) }}" class="gx-1">{% trans %}Edit{% endtrans %}</a>
<a href="{{ action.url(profile) }}">{{ action.label | capitalize }}</a> {% for action in profile.actions %}
{% endfor %} <a href="{{ action.url(profile) }}" class="gx-1">{{ action.label | capitalize }}</a>
</td> {% endfor %}
</tr> </td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if (profiles.page) and (profiles.has_next or profiles.has_prev) %}
{{ render_pagination(profiles, align="center") }}
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block actions %} {% block actions %}
{% if service %} <a href="{{ url_for(".unclaimed_export_for_service", service_uuid=service.uuid) }}"
<a href="{{ url_for(".unclaimed_export_for_service", service_uuid=service.uuid) }}" class="btn btn-success" download>{% trans %}Export unclaimed profiles{% endtrans %}</a> class="btn btn-outline-primary"
<a href="{{ url_for(".create_for", service_uuid=service.uuid) }}" class="btn btn-success">{% trans %}Create profile{% endtrans %}</a> download>{% trans %}Export unclaimed profiles{% endtrans %}</a>
{% endif %} <a href="{{ url_for(".create_for", service_uuid=service.uuid) }}"
class="btn btn-outline-success">{% trans %}Create profile{% endtrans %}</a>
{% endblock %} {% endblock %}
{% extends "base.html" %} {% extends "base.html" %}
{% set service_name = service.name %} {% set service_name = service.name %}
{% block title %}{% trans %}Pick a profile{% endtrans %}{% endblock %}
{% block subtitle %}{% trans service_name %}for the service {{ service_name }}{% endtrans %}{% endblock %} {% block title %}
{% trans %}Pick a profile{% endtrans %}
{% endblock %}
{% block subtitle %}
{% trans service_name %}for the service {{ service_name }}{% endtrans %}
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
{% for profile in profiles %} {% for profile in profiles %}
<div class="col-xs col-md-6 col-lg-4"> <div class="col-xs col-md-6 col-lg-4">
<div class="card card-widget widget-user-2"> <div class="card">
<div class="widget-user-header bg-{{ macros.colors[loop.index0 % 7] }}"> <div class="card-header"
<div class="float-right"> style="background-color: var(--bs-{{ macros.colors[loop.index0 % 7] }}">
{% if profile.status == profile.ACTIVE %} <h3>{{ profile.name }}</h3>
<form method="POST" action="{{ utils.url_or_intent("account.home") }}" class="form"> {% if profile.comment %}
{{ form.hidden_tag() }} <h5>{{ profile.comment }}&nbsp;</h5>
<input type="hidden" name="profile_uuid" value="{{ profile.uuid }}"> {% endif %}
<input type="submit" value="{% trans %}Sign in{% endtrans %}" class="btn btn-lg btn-flat bg-gray text-black"> {% set created_on = profile.created_at.date() %}
{% trans %}Created on {{ created_on }}{% endtrans %}
</div>
<div class="card-body">
{% if profile.status == profile.ACTIVE %}
<form method="POST"
action="{{ utils.url_or_intent("account.home") }}"
class="form">
{{ form.hidden_tag() }}
<input type="hidden" name="profile_uuid" value="{{ profile.uuid }}"/>
<input type="submit"
value="{% trans %}Sign in{% endtrans %}"
class="btn btn-lg btn-outline-success"/>
</form> </form>
{% else %} {% else %}
<span class="btn btn-lg btn-flat bg-secondary text-black">{{ profile.STATUSES[profile.status][1] | capitalize }}</span> <button disabled class="btn btn-lg">{{ profile.STATUSES[profile.status][1] | capitalize }}</button>
{% endif %} {% endif %}
</div> </div>
<h3 class="widget-header-username">{{ profile.username }}</h3>
{% if profile.comment %}<h5>{{ profile.comment }}&nbsp;</h5>
{% else %}<h5>{% trans %}No profile description{% endtrans %}&nbsp;</h5>
{% endif %}
</div>
<div class="card-footer p-0">
<ul class="nav flex-column">
{% set created_on = profile.created_at.date() %}
<li class="nav-item"><a href="#" class="nav-link">{% trans %}Created on {{ created_on }}{% endtrans %}</a></li>
<li class="nav-item"><a href="#" class="nav-link">{% trans %}Not shared with anyone{% endtrans %}</a></li>
</ul>
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
{% endfor %}
</div>
{% endblock %} {% endblock %}
{% block actions %} {% block actions %}
{% if service.policy not in ("locked",) %} {% if service.policy not in ("locked",) %}
<a href="{{ utils.url_for(".claim", intent=True) }}" class="btn btn-primary">{% trans %}Claim a profile{% endtrans %}</a> <a href="{{ utils.url_for(".claim", intent=True) }}"
{% endif %} class="btn btn-outline-primary">{% trans %}Claim a profile{% endtrans %}</a>
{% if service.policy in ("open", "burst") and profiles.__len__() < service.max_profiles %} {% endif %}
<a href="{{ utils.url_for(".create", intent=True) }}" class="btn btn-success">{% trans %}Create another profile{% endtrans %}</a> {% if service.policy in ("open", "burst") and profiles.__len__() < service.max_profiles %}
{% elif service.policy in ("managed", "burst") %} <a href="{{ utils.url_for(".create", intent=True) }}"
<a href="{{ utils.url_for(".create", intent=True) }}" class="btn btn-warning">{% trans %}Request another profile{% endtrans %}</a> class="btn btn-outline-success">{% trans %}Create another profile{% endtrans %}</a>
{% else %} {% elif service.policy in ("managed", "burst") %}
<a href="#" class="btn btn-success" disabled>{% trans %}Create another profile{% endtrans %}</a> <a href="{{ utils.url_for(".create", intent=True) }}"
{% endif %} class="btn btn-outline-warning">{% trans %}Request another profile{% endtrans %}</a>
{% else %}
<a href="#" class="btn btn-outline-success" disabled>{% trans %}Create another profile{% endtrans %}</a>
{% endif %}
{% endblock %} {% endblock %}
{% extends "base.html" %} {% extends "base.html" %}
{% set service_name = service.name %} {% set service_name = service.name %}
{% set username = form.username.data %} {% set username = form.username.data %}
{% block title %}{% trans %}Sign up{% endtrans %}{% endblock %} {% block title %}
{% trans %}Sign up{% endtrans %}
{% endblock %}
{% block subtitle %} {% block subtitle %}
{% trans service_name %}for the service {{ service_name }}{% endtrans %} {% trans service_name %}for the service {{ service_name }}{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-xs col-md-6 col-lg-4"> <div class="col-xs col-md-6 col-lg-4">
<div class="card card-widget widget-user-2"> <div class="card">
<div class="widget-user-header bg-primary"> <div class="card-header bg-light-subtle">
<div class="float-right"> <h3>{{ form.username.data }}</h3>
<form method="POST" class="form"> <h5>{% trans service_name %}Your new {{ service_name }} profile{% endtrans %}</h5>
{{ form.hidden_tag() }} </div>
<input type="hidden" name="username" value="{{ form.username.data }}"> <div class="card-body">
<input type="submit" value="{% trans %}Sign up{% endtrans %}" class="btn btn-lg btn-flat bg-gray text-black"> <p>
</form> {% trans service_name %}Please click the "Sign up" button to initialize your {{ service_name }} account.{% endtrans %}
</div> </p>
<h3 class="widget-header-username">{{ form.username.data }}</h3> {% if not service.single_profile %}
<h5 class="widget-header-desc">{% trans service_name %}Your new {{ service_name }} profile{% endtrans %}</h5> <p>{% trans %}If you wish to pick a different username, please click the "Create a custom profile" button.{% endtrans %}</p>
</div> {% endif %}
<div class="card-footer"> <form method="POST" class="form text-center">
<p>{% trans service_name %}Please click the "Sign up" button to initialize your {{ service_name }} account.{% endtrans %}</p> {{ form.hidden_tag() }}
{% if not service.single_profile %} <input type="hidden" name="username" value="{{ form.username.data }}"/>
<p>{% trans %}If you wish to pick a different username, please click the "Custom profile" button.{% endtrans %}</p> <input type="submit"
{% endif %} value="{% trans %}Sign up{% endtrans %}"
</div> class="btn btn-lg btn-outline-success"/>
</form>
</div> </div>
</div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block actions %} {% block actions %}
{% if service.policy not in ("locked",) %} {% if service.policy not in ("locked",) %}
<a href="{{ utils.url_for(".claim") }}" class="btn btn-primary">{% trans %}Claim a profile{% endtrans %}</a> <a href="{{ utils.url_for(".claim") }}" class="btn btn-outline-primary">{% trans %}Claim a profile{% endtrans %}</a>
{% endif %} {% endif %}
{% if not service.single_profile %} {% if not service.single_profile %}
<a href="{{ utils.url_for(".create") }}" class="btn btn-success">{% trans %}Create a custom profile{% endtrans %}</a> <a href="{{ utils.url_for(".create") }}" class="btn btn-outline-success">{% trans %}Create a custom profile{% endtrans %}</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
service_client_id,profile_name,profile_uuid service_client_id,profile_name,profile_uuid
{% for profile in profiles %}{% if profile.user_uuid %}{% else %}{{service.config["client_id"]}},{{ profile.username }},{{ profile.uuid }} {% for profile in profiles %}
{% endif %}{% endfor %} {% if profile.user_uuid %}
\ No newline at end of file {% else %}
{{ service.config["client_id"] }},{{ profile.name }},{{ profile.uuid }}
{% endif %}
{% endfor %}
...@@ -26,7 +26,7 @@ def create(service_uuid, create_for=False, quick=False): ...@@ -26,7 +26,7 @@ def create(service_uuid, create_for=False, quick=False):
service = models.Service.query.get(service_uuid) or flask.abort(404) service = models.Service.query.get(service_uuid) or flask.abort(404)
status = models.Profile.ACTIVE status = models.Profile.ACTIVE
is_admin = flask_login.current_user.is_admin is_admin = flask_login.current_user.is_admin
formatter = format.ProfileFormat.registry[service.profile_format] formatter = format.NameFormat.registry[service.profile_format]
# If the admin passed a user uuid, use that one, otherwise ignore it # If the admin passed a user uuid, use that one, otherwise ignore it
user = hiboo_user.get_user(intent="profile.create_for", create_for=None) if (create_for and is_admin) else flask_login.current_user user = hiboo_user.get_user(intent="profile.create_for", create_for=None) if (create_for and is_admin) else flask_login.current_user
# Check that profile creation is allowed # Check that profile creation is allowed
...@@ -35,43 +35,50 @@ def create(service_uuid, create_for=False, quick=False): ...@@ -35,43 +35,50 @@ def create(service_uuid, create_for=False, quick=False):
(service.policy == models.Service.LOCKED, _("You cannot request a profile for this service")), (service.policy == models.Service.LOCKED, _("You cannot request a profile for this service")),
(len(profiles) > 0 and service.single_profile, _("You already own a profile for this service")), (len(profiles) > 0 and service.single_profile, _("You already own a profile for this service")),
(not is_admin and service.policy == models.Service.RESERVED, _("You cannot request a profile for this service")), (not is_admin and service.policy == models.Service.RESERVED, _("You cannot request a profile for this service")),
(not is_admin and len(profiles) >= service.max_profiles and service.policy != models.Service.BURST, _("Your reached the maximum number of profiles")) (not is_admin and len(profiles) >= service.max_profiles and service.policy != models.Service.BURST, _("You have reached the maximum number of profiles"))
) if check] ) if check]
if errors: if errors:
flask.flash(errors[0], "danger") flask.flash(errors[0], "danger")
return flask.redirect(flask.url_for("account.home")) return flask.redirect(flask.url_for("account.home"))
# Managed services and bursting accounts require approval # Managed services and bursting accounts require approval
if len(profiles) >= service.max_profiles or service.policy == models.Service.MANAGED: if len(profiles) >= service.max_profiles or service.policy == models.Service.MANAGED:
flask.flash(_("Your profile creation requires approval, please contact us!"), "warning") flask.flash(_("The creation of your profile requires approval, so don't forget to contact us after filling in the form"), "warning")
status = models.Profile.REQUEST status = models.Profile.REQUEST
# Initialize and validate the form before applying overrides # Initialize and validate the form before applying overrides
form = forms.ProfileForm() form = forms.ProfileForm()
form.username.validators = formatter.validators() form.username.validators = formatter.validators(blocklist=not is_admin)
form.username.description = (_("The username can be between 3 and 30 characters long. {}".format(formatter.message)))
if not is_admin:
del form.username_spoof_protection
submit = form.validate_on_submit() submit = form.validate_on_submit()
# If this is a quick creation or the service prevents custom profile creation, force the username # If this is a quick creation or the service prevents custom profile creation, force the username
if quick or service.single_profile: if quick or service.single_profile:
for username in formatter.alternatives(formatter.coalesce(user.username)): for username in formatter.alternatives(formatter.coalesce(user.name)):
if not service.check_username(username): if user.available_username(username, service=service):
form.force_username(username) form.force_username(username)
break break
if quick: if quick:
form.hide_fields() form.hide_fields()
# Handle the creation form # Handle the creation form
if submit: if submit:
if service.check_username(form.username.data): if user.available_username(
flask.flash(_("A profile with that username exists already"), "danger") username=form.username.data,
else: service=service,
spoof_allowed=not form.username_spoof_protection.data if is_admin else False,
):
profile = models.Profile( profile = models.Profile(
user_uuid=user.uuid, service_uuid=service.uuid, user_uuid=user.uuid, service_uuid=service.uuid,
username=form.username.data, name=form.username.data,
comment=form.comment.data, comment=form.comment.data,
status=status status=status
) )
models.db.session.add(profile) models.db.session.add(profile)
models.log(models.History.CREATE, profile.username, profile.comment, models.log(models.History.CREATE, profile.name, profile.comment,
user=user, service=service, profile=profile) user=user, actor=flask_login.current_user, service=service, profile=profile)
models.db.session.commit() models.db.session.commit()
return flask.redirect(utils.url_or_intent("account.home")) return flask.redirect(utils.url_or_intent("account.home"))
else:
flask.flash(_("A profile with that username exists already"), "danger")
# Display either the quick version or the full version (one can switch from one to another) # Display either the quick version or the full version (one can switch from one to another)
return flask.render_template("profile_quick.html" if quick else "profile_create.html", return flask.render_template("profile_quick.html" if quick else "profile_create.html",
form=form, service=service, user=user, create_for=create_for) form=form, service=service, user=user, create_for=create_for)
...@@ -84,19 +91,19 @@ def claim(service_uuid): ...@@ -84,19 +91,19 @@ def claim(service_uuid):
form = forms.ClaimForm() form = forms.ClaimForm()
if form.validate_on_submit(): if form.validate_on_submit():
# A claim may either be a direct claim, ie. the user types in the # A claim may either be a direct claim, ie. the user types in the
# profile username directly, or an indirect claim, ie. the user types # profilename directly, or an indirect claim, ie. the user types in one
# in one of the profile alternate claim names. Whichever comes first # of the profile alternate claim names. Whichever comes first wins.
# wins. Unicity must be handled somewhere else. # Unicity must be handled somewhere else.
claim_names = models.ClaimName.query.filter_by( claim_names = models.ClaimName.query.filter_by(
service_uuid=service_uuid, service_uuid=service_uuid,
username=form.username.data name=form.username.data
).options(models.sqlalchemy.orm.load_only("profile_uuid")).all() ).options(models.db.orm.load_only(models.ClaimName.profile_uuid)).all()
claim_names_uuid = [claim_name.profile_uuid for claim_name in claim_names] claim_names_uuid = [claim_name.profile_uuid for claim_name in claim_names]
profile = models.Profile.query.filter( profile = models.Profile.query.filter(
models.Profile.service_uuid==service.uuid, models.Profile.service_uuid==service.uuid,
models.Profile.status==models.Profile.UNCLAIMED, models.Profile.status==models.Profile.UNCLAIMED,
models.sqlalchemy.or_( models.db.or_(
models.Profile.username==form.username.data, models.Profile.name==form.username.data,
models.Profile.uuid.in_(claim_names_uuid) models.Profile.uuid.in_(claim_names_uuid)
) )
).first() ).first()
...@@ -134,20 +141,20 @@ def claim(service_uuid): ...@@ -134,20 +141,20 @@ def claim(service_uuid):
return flask.render_template("profile_claim.html", form=form, service=service) return flask.render_template("profile_claim.html", form=form, service=service)
@blueprint.route("/list/status/<status>") @blueprint.route("/list/service/<service_uuid>", methods=["GET", "POST"])
@security.admin_required()
def list_for_status(status):
profiles = models.Profile.query.filter_by(status=status).all()
return flask.render_template("profile_list.html", profiles=profiles,
status=models.Profile.STATUSES[status])
@blueprint.route("/list/service/<service_uuid>")
@security.admin_required() @security.admin_required()
def list_for_service(service_uuid): def list_for_service(service_uuid):
service = models.Service.query.get(service_uuid) or flask.abort(404) form = utils.SearchForm()
return flask.render_template("profile_list.html", profiles=service.profiles, page = flask.request.args.get('page', 1, type=int)
service=service) service = models.Service.query.get(service_uuid)
if form.validate_on_submit():
profiles = service.profiles.where(
models.Profile.name.contains(form.query.data)
).paginate(page=1, per_page=25)
else:
profiles = service.profiles.paginate(page=page, per_page=25)
return flask.render_template("profile_list.html", profiles=profiles or flask.abort(404),
service=service, form=form)
@blueprint.route("/unclaimedexport/service/<service_uuid>.csv") @blueprint.route("/unclaimedexport/service/<service_uuid>.csv")
...@@ -165,6 +172,31 @@ def details(profile_uuid): ...@@ -165,6 +172,31 @@ def details(profile_uuid):
return flask.render_template("profile_details.html", profile=profile) return flask.render_template("profile_details.html", profile=profile)
@blueprint.route("/edit/<profile_uuid>", methods=["GET", "POST"])
@security.authentication_required()
def edit(profile_uuid):
profile = models.Profile.query.get(profile_uuid) or flask.abort(404)
service = profile.service or flask.abort(404)
user = profile.user or flask.abort(404)
form = forms.ProfileForm()
form.submit.label.text = (_("Edit profile"))
del form.username, form.username_spoof_protection
if not flask_login.current_user.is_admin and profile.user_uuid != flask_login.current_user.uuid:
flask.abort(403)
if form.validate_on_submit():
profile.comment = form.comment.data
models.db.session.add(profile)
models.log(models.History.EDIT, profile.name, profile.comment,
user=user, actor=flask_login.current_user, service=service, profile=profile)
models.db.session.commit()
flask.flash(_("Profile successfully updated"), "success")
return flask.redirect(flask.url_for("account.home"))
form.process(obj=profile)
return flask.render_template("profile_edit.html", profile=profile, service=service, form=form)
@blueprint.route("/action/<profile_uuid>/<action>", methods=["GET", "POST"]) @blueprint.route("/action/<profile_uuid>/<action>", methods=["GET", "POST"])
@security.admin_required() @security.admin_required()
def action(profile_uuid, action): def action(profile_uuid, action):
...@@ -179,11 +211,12 @@ def action(profile_uuid, action): ...@@ -179,11 +211,12 @@ def action(profile_uuid, action):
@blueprint.route("/transition/<profile_uuid>/<transition_id>", methods=["GET", "POST"]) @blueprint.route("/transition/<profile_uuid>/<transition_id>", methods=["GET", "POST"])
@security.confirmation_required("change the profile status") @security.authentication_required()
@security.confirmation_required(_("change the profile status"))
def start_transition(profile_uuid, transition_id): def start_transition(profile_uuid, transition_id):
profile = models.Profile.query.get(profile_uuid) or flask.abort(404) profile = models.Profile.query.get(profile_uuid) or flask.abort(404)
transition = profile.TRANSITIONS.get(transition_id) or flask.abort(404) transition = profile.TRANSITIONS.get(transition_id) or flask.abort(404)
transition.authorized(profile) authorized = transition.authorized(profile) or flask.abort(403)
profile.transition = transition_id profile.transition = transition_id
profile.transition_step = models.Profile.INIT profile.transition_step = models.Profile.INIT
profile.transition_time = datetime.datetime.now() + datetime.timedelta(seconds=transition.delay) profile.transition_time = datetime.datetime.now() + datetime.timedelta(seconds=transition.delay)
...@@ -196,6 +229,9 @@ def start_transition(profile_uuid, transition_id): ...@@ -196,6 +229,9 @@ def start_transition(profile_uuid, transition_id):
actor=flask_login.current_user, actor=flask_login.current_user,
public=True public=True
) )
if transition_id == "assign":
user = hiboo_user.get_user(intent="profile.start_transition", profile_uuid=profile_uuid)
profile.user_uuid = user.uuid
models.db.session.commit() models.db.session.commit()
flask.flash(_("Profile status change was requested"), "success") flask.flash(_("Profile status change was requested"), "success")
if transition_id == "delete" and flask_login.current_user.is_admin == False or transition_id == "purge" and not flask_login.current_user.is_admin: if transition_id == "delete" and flask_login.current_user.is_admin == False or transition_id == "purge" and not flask_login.current_user.is_admin:
...@@ -205,7 +241,7 @@ def start_transition(profile_uuid, transition_id): ...@@ -205,7 +241,7 @@ def start_transition(profile_uuid, transition_id):
@blueprint.route("/transition/<profile_uuid>/cancel", methods=["GET", "POST"]) @blueprint.route("/transition/<profile_uuid>/cancel", methods=["GET", "POST"])
@security.confirmation_required("cancel the profile status change") @security.confirmation_required(_("cancel the profile status change"))
@security.admin_required() @security.admin_required()
def cancel_transition(profile_uuid): def cancel_transition(profile_uuid):
profile = models.Profile.query.get(profile_uuid) or flask.abort(404) profile = models.Profile.query.get(profile_uuid) or flask.abort(404)
...@@ -227,25 +263,3 @@ def complete_transition(profile_uuid): ...@@ -227,25 +263,3 @@ def complete_transition(profile_uuid):
models.db.session.commit() models.db.session.commit()
flask.flash(_("Profile status change was completed"), "success") flask.flash(_("Profile status change was completed"), "success")
return flask.redirect(flask.url_for(".details", profile_uuid=profile_uuid)) return flask.redirect(flask.url_for(".details", profile_uuid=profile_uuid))
@blueprint.route("/assign/<profile_uuid>", methods=["GET", "POST"])
@security.admin_required()
def assign(profile_uuid):
profile = models.Profile.query.get(profile_uuid) or flask.abort(404)
assert profile.status == models.Profile.UNCLAIMED
user = hiboo_user.get_user(intent="profile.assign", profile_uuid=profile_uuid)
profile.user_uuid = user.uuid
profile.status = models.Profile.ACTIVE
models.log(
category=models.History.STATUS,
value=models.Profile.ACTIVE,
user=user,
service=profile.service,
profile=profile,
actor=flask_login.current_user,
public=True
)
models.db.session.commit()
flask.flash(_("Successfully assigned the profile"), "success")
return flask.redirect(flask.url_for(".details", profile_uuid=profile_uuid))
...@@ -62,3 +62,18 @@ def confirmation_required(action): ...@@ -62,3 +62,18 @@ def confirmation_required(action):
) )
return wrapper return wrapper
return inner return inner
def username_blocklist():
""" Return an opinionated set of unsafe usernames
"""
return set.union(
# linux common users and groups
{"abuild", "adm", "_apt", "at", "audio", "avahi", "backup", "bin", "bluetooth", "cdrom", "cdrw", "console", "cron", "crontab", "cyrus", "daemon", "dbus", "dhcp", "dialout", "dip", "disk", "dnsmasq", "docker", "fax", "floppy", "ftp", "games", "gnats", "guest", "halt", "http", "input", "irc", "kmem", "kvm", "libvirt", "list", "locate", "lock", "log", "lp", "lpadmin", "mail", "man", "mem", "messagebus", "netdev", "network", "news", "nobody", "nofiles", "nogroup", "ntp", "nullmail", "nvpd", "openvpn", "operator", "optical", "pcap", "ping", "plugdev", "polkitd", "portage", "postfix", "postmaster", "power", "proc", "proxy", "qemu", "radvd", "readproc", "render", "rfkill", "root", "saned", "sasl", "scanner", "sgx", "shadow", "shutdown", "smmsp", "squid", "src", "ssh", "sshd", "ssl-cert", "staff", "storage", "sudo", "sync", "sys", "systemd", "systemd-coredump", "systemd-journal", "systemd-network", "systemd-resolve", "systemd-timesync", "tape", "tcpdump", "tss", "tty", "usb", "users", "utmp", "uucp", "uuidd", "video", "voice", "vpopmail", "wheel", "www", "www-data", "xfs"},
# linux reserved uids
{uid for uid in range(1000)},
# web stack commons
{"admin", "administrator", "git", "localhost", "localdomain", "mariadb", "master", "mysql", "nginx", "no-reply", "noreply", "passwd", "password", "postgres", "python", "sqlite", "user", "users", "username"},
# Hiboo specifics
{"account", "action", "api", "app", "application_id", "assign", "auth", "authentication", "authorize", "cancel", "check", "claim", "complete", "contact", "create", "create_for", "create_quick", "delete", "details", "disable", "edit", "email", "enable", "flask", "home", "invite", "jwks", "list", "login", "logout", "metadata", "oidc", "openid-configuration", "password", "pick", "profile", "profiles", "profile_uuid", "redirect", "reset", "saml", "service", "service_uuid", "setapp", "signin", "signout", "signup", "sso", "static", "status", "token", "totp", "transition", "transition_id", "unclaimedexport", "user", "userinfo", "user_uuid"}
)
...@@ -4,16 +4,15 @@ ...@@ -4,16 +4,15 @@
{% block subtitle %}{{ label.lower() }}{% endblock %} {% block subtitle %}{{ label.lower() }}{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">{{ result | safe }}</div>
{{ result | safe }}
</div> </div>
</div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block actions %} {% block actions %}
<a href="{{ url_for("service.details", service_uuid=service.uuid) }}" class="btn btn-info">{% trans %}Show service{% endtrans %}</a> <a href="{{ url_for("service.details", service_uuid=service.uuid) }}"
class="btn btn-info">{% trans %}Show service{% endtrans %}</a>
{% endblock %} {% endblock %}
{% extends "form.html" %} {% extends "form.html" %}
{% set application_name = application.name %} {% set application_name = application.name %}
{% block title %}{% trans %}Create a service{% endtrans %}{% endblock %}
{% block subtitle %}{% trans application_name %}add a {{ application_name }} service{% endtrans %}{% endblock %} {% block title %}
{% trans %}Create a service{% endtrans %}
{% endblock %}
{% block subtitle %}
{% trans application_name %}add a {{ application_name }} service{% endtrans %}
{% endblock %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ service.name }}{% endblock %} {% block title %}{{ service.name }}{% endblock %}
{% block subtitle %}{% trans %}service details{% endtrans %}{% endblock %} {% block subtitle %}
{% trans %}service details{% endtrans %}
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h4>{% trans %}Attributes{% endtrans %}</h4> <h3>{% trans %}Attributes{% endtrans %}</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-lg-3">{% trans %}Service name{% endtrans %}</dt> <dt class="col-lg-3">{% trans %}Service name{% endtrans %}</dt>
<dd class="col-lg-9">{{ service.name }}</dd> <dd class="col-lg-9">
{{ service.name }}
<dt class="col-lg-3">{% trans %}Description{% endtrans %}</dt> </dd>
<dd class="col-lg-9">{{ service.description }}</dd> <dt class="col-lg-3">{% trans %}Description{% endtrans %}</dt>
<dd class="col-lg-9">
<dt class="col-lg-3">{% trans %}Provider{% endtrans %}</dt> {{ service.description }}
<dd class="col-lg-9">{{ service.provider }}</dd> </dd>
<dt class="col-lg-3">{% trans %}Provider{% endtrans %}</dt>
<dt class="col-lg-3">{% trans %}Application{% endtrans %}</dt> <dd class="col-lg-9">
<dd class="col-lg-9">{{ application.name }}</dd> {{ service.provider }}
</dd>
<dt class="col-lg-3">{% trans %}Application destriction{% endtrans %}</dt> <dt class="col-lg-3">{% trans %}Application{% endtrans %}</dt>
<dd class="col-lg-9">{{ application.__doc__ }}</dd> <dd class="col-lg-9">
{{ application.name }}
<dt class="col-lg-3">{% trans %}UUID{% endtrans %}</dt> </dd>
<dd class="col-lg-9"><pre>{{ service.uuid }}</pre></dd> <dt class="col-lg-3">{% trans %}Application destriction{% endtrans %}</dt>
</dl> <dd class="col-lg-9">
{{ application.__doc__ }}
</dd>
<dt class="col-lg-3">{% trans %}UUID{% endtrans %}</dt>
<dd class="col-lg-9">
<code>{{ service.uuid }}</code>
</dd>
</dl>
</div>
</div> </div>
</div> </div>
</div> <div class="col-md-6">
<div class="col-md-6"> {% if action %}
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h4>{% trans %}Actions{% endtrans %}</h4> <h3>{% trans %}Actions{% endtrans %}</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
{% for action, (label, profile, _, function) in application.actions.items() %} {% for action, (label, profile, _, function) in application.actions.items() %}
{% if not profile %} {% if not profile %}
<dt class="col-lg-3"><a href="{{ url_for(".action", service_uuid=service.uuid, action=action) }}">{{ label }}</a></dt> <dt class="col-lg-3">
<dd class="col-lg-9">{{ function.__doc__ }}</dd> <a href="{{ url_for(".action", service_uuid=service.uuid, action=action) }}">{{ label }}</a>
{% endif %} </dt>
{% endfor %} <dd class="col-lg-9">
</dl> {{ function.__doc__ }}
</div> </dd>
{% endif %}
{% endfor %}
</dl>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> <div class="row">
<div class="row"> <div class="col">
<div class="col"> <div class="card">
<div class="card"> <div class="card-header">
<div class="card-body"> <h3>Setting up {{ application.name }}</h3>
{{ application.render_details(service) | safe }} </div>
<div class="card-body">{{ application.render_details(service) | safe }}</div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block actions %} {% block actions %}
{% for action, (label, profile, quick, function) in application.actions.items() %} {% for action, (label, profile, quick, function) in application.actions.items() %}
{% if quick and not profile %} {% if quick and not profile %}
<a href="{{ url_for(".action", service_uuid=service.uuid, action=action) }}" class="btn btn-info">{{ label }}</a> <a href="{{ url_for(".action", service_uuid=service.uuid, action=action) }}"
{% endif %} class="btn btn-outline-info">{{ label }}</a>
{% endif %}
{% endfor %} {% endfor %}
<a href="{{ url_for("profile.list_for_service", service_uuid=service.uuid) }}" class="btn btn-primary">{% trans %}View profiles{% endtrans %}</a> <a href="{{ url_for("profile.list_for_service", service_uuid=service.uuid) }}"
<a href="{{ url_for(".setapp_select", service_uuid=service.uuid) }}" class="btn btn-primary">{% trans %}Change application{% endtrans %}</a> class="btn btn-outline-primary">{% trans %}View profiles{% endtrans %}</a>
<a href="{{ url_for(".edit", service_uuid=service.uuid) }}" class="btn btn-primary">{% trans %}Edit this service{% endtrans %}</a> <a href="{{ url_for(".edit", service_uuid=service.uuid) }}"
<a href="{{ url_for(".delete", service_uuid=service.uuid) }}" class="btn btn-danger">{% trans %}Delete this service{% endtrans %}</a> class="btn btn-outline-warning">{% trans %}Edit this service{% endtrans %}</a>
{% endblock %} <a href="{{ url_for(".setapp_select", service_uuid=service.uuid) }}"
class="btn btn-outline-warning">{% trans %}Change application{% endtrans %}</a>
<a href="{{ url_for(".delete", service_uuid=service.uuid) }}"
class="btn btn-outline-danger">{% trans %}Delete this service{% endtrans %}</a>
{% endblock %}
{% extends "form.html" %} {% extends "form.html" %}
{% block title %}{% trans %}Edit a service{% endtrans %}{% endblock %} {% block title %}
{% trans %}Edit a service{% endtrans %}
{% endblock %}
{% block subtitle %}{{ service.name }}{% endblock %} {% block subtitle %}{{ service.name }}{% endblock %}