diff --git a/hiboo/application/__init__.py b/hiboo/application/__init__.py index b2de80b5a1bbeaf8e41b0212266a6725050ea4b1..062c565b1f7bed2d41aa6d98b6972aef123ec2ee 100644 --- a/hiboo/application/__init__.py +++ b/hiboo/application/__init__.py @@ -1,3 +1,7 @@ +# TODO: remove profile_regex +# This allows to avoid passing the service object anywhere, and manipulate +# the service config dictionary instead + import flask diff --git a/hiboo/application/base.py b/hiboo/application/base.py index eb5b64d417ce978d64b97752239219dbfe7036a4..4aeeed1bd8f406f68e338dea4afd08fc5d511b6f 100644 --- a/hiboo/application/base.py +++ b/hiboo/application/base.py @@ -31,12 +31,43 @@ class BaseForm(flask_wtf.FlaskForm): submit = fields.SubmitField(_('Submit')) +def action(label, profile=False, quick=False): + """ Registers a profile or application action + """ + class Register(): + def __init__(self, function): + self.function = function + def __set_name__(self, owner, name): + if "actions" not in owner.__dict__: + owner.actions = dict() + owner.actions[name] = (label, profile, quick) + setattr(owner, name, self.function) + return Register + + +def hook(step="pre", *transitions): + """ Registers a hook for given transitions + """ + class Register(): + def __init__(self, function): + self.function = function + def __set_name__(self, owner, name): + if not "hooks" in owner.__dict__: + owner.hooks = owner.hooks[:] + owner.hooks.append(( + step, transitions, self.function + )) + setattr(owner, name, self.function) + return Register + + class BaseApplication(object): """ Base application class, that provides basic behavior and registry """ registry = dict() actions = dict() + hooks = list() sso_protocol = None name = None @@ -62,31 +93,24 @@ class BaseApplication(object): """ Updates the service options dictionary based on SSO parameters """ + # TODO: replace this by returning a dict and updating + # service config, no need to pass the service object return self.sso_protocol.fill_service(service) - def activate(self, profile): - """ Default activation behavior is doing nothing + def apply_hooks(self, transition, step, profile): + """ Apply hooks for a given transition and step """ - return True - - def block(self, profile): - """ Default blocking behavior is doing nothing - """ - return True - - def welcome(self, profile): - """ Defaul welcome behavior is doing nothing - """ - return True - - def notify(self, profile): - """ Default notifying behavior is doing nothing - """ - return True - - def delete(self, profile): - """ Deleting an account generally requires application - implementation, so this needs to be overridden + results = [ + function(self, profile) + for transition_step, transitions, function in self.hooks + if step == transition_step and transition in transitions + ] + print(results) + return False in results and not True in results + + @hook("pre", "purge", "purge-blocked") + def manual_purge(self, profile): + """ The default behavior is to wait for manual purge """ return False @@ -97,17 +121,3 @@ class OIDCApplication(BaseApplication): class SAMLApplication(BaseApplication): sso_protocol = sso.saml - - -def action(label, profile=False, quick=False): - """ Registers a profile or application action - """ - class Register(): - def __init__(self, function): - self.function = function - def __set_name__(self, owner, name): - if not owner.actions: - owner.actions = dict() - owner.actions[name] = (label, profile, quick) - setattr(owner, name, self.function) - return Register \ No newline at end of file diff --git a/hiboo/application/social.py b/hiboo/application/social.py index cdbd9999acf7107afa8721bf564b938a20d6985a..638b872a15457d6b12a5c873750c9966844b59c0 100644 --- a/hiboo/application/social.py +++ b/hiboo/application/social.py @@ -59,6 +59,25 @@ class SynapseApplication(base.SAMLApplication): application_uri=service.config.get("application_uri") ) + @base.hook("pre", "block", "delete") + def test_hooks(self, profile): + """ TODO: testing hooks + """ + print("Hoooooked!") + + @base.hook("post", "block", "purge") + def test_hooks2(self, profile): + """ TODO: testing hooks + """ + print("Hooked after") + + @base.hook("pre", "purge") + def test_hooks3(self, profile): + """ TODO: testing hooks + """ + print("Purging the account") + return True + @register("writefreely") class WriteFreelyApplication(base.OIDCApplication): diff --git a/hiboo/models.py b/hiboo/models.py index 20f5d67db663322cf754471026c162c8a5bbf9d9..999ca7be785fb4d60cc76a9f34df2b7dafe024e3 100644 --- a/hiboo/models.py +++ b/hiboo/models.py @@ -176,13 +176,28 @@ class Profile(db.Model): ACTIVE = "active" BLOCKED = "blocked" DELETED = "deleted" + PURGED = "purged" STATUSES = { UNCLAIMED: ("gray", _("unclaimed")), REQUEST: ("blue", _("requested")), ACTIVE: ("green", _("active")), BLOCKED: ("orange", _("blocked")), - DELETED: ("red", _("deleted")) + DELETED: ("red", _("deleted")), + PURGED: ("red", _("purged")) + } + + TRANSITIONS = { + # Assigning or claiming is not a generic transition + # so it is not listed here (it requires chosing a user) + "activate": (REQUEST, ACTIVE, 300, _("activate")), + "reject": (REQUEST, DELETED, 3600 * 24 * 3, _("reject")), + "block": (ACTIVE, BLOCKED, 0, _("block")), + "unblock": (BLOCKED, ACTIVE, 300, _("unblock")), + "delete": (ACTIVE, DELETED, 300, _("delete")), + "delete-blocked": (BLOCKED, DELETED, 300, _("delete")), + "purge": (ACTIVE, PURGED, 3600 * 24 * 3, _("purge")), + "purge-blocked": (BLOCKED, PURGED, 300, _("purge")) } user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid)) @@ -201,6 +216,13 @@ class Profile(db.Model): def email(self): return "{}@{}".format(self.uuid, app.config.get("MAIL_DOMAIN")) + @property + def transitions(self): + return { + name: transition for name, transition in Profile.TRANSITIONS.items() + if transition[0] == self.status and not self.transition + } + @classmethod def filter(cls, service, user): return cls.query.filter_by( @@ -242,13 +264,15 @@ class History(db.Model): DELETE = "delete" NOTE = "note" STATUS = "status" + TRANSITION = "transition" PASSWORD = "password" DESCRIPTION = { SIGNUP: _("signed up for this account"), CREATE: _("created the profile {this.profile.username} on {this.service.name}"), PASSWORD: _("changed this account password"), - STATUS: _("set the {this.service.name} profile {this.profile.username} as {this.value}") + STATUS: _("set the {this.service.name} profile {this.profile.username} as {this.value}"), + TRANSITION: _("did {this.transition[3]} the profile {this.profile.username} on {this.service.name}") } user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid)) @@ -268,6 +292,10 @@ class History(db.Model): category = db.Column(db.String(25)) value = db.Column(db.String()) + @property + def transition(self): + return Profile.TRANSITIONS[self.value] + @property def description(self): return History.DESCRIPTION.get(self.category, "").format(this=self) diff --git a/hiboo/profile/common.py b/hiboo/profile/common.py new file mode 100644 index 0000000000000000000000000000000000000000..ad187a4f96aa2105efc260fd7cf8639fa90a0005 --- /dev/null +++ b/hiboo/profile/common.py @@ -0,0 +1,11 @@ +from hiboo import models + + +def apply_transition(profile, current, target, delay, transition, step="pre"): + """ Handle profile transitions as single-step workflows + """ + app = profile.service.application + manual = app.apply_hooks(transition, step, profile) + if step == "pre" and not manual: + profile.status, profile.transition = target, None + apply_transition(profile, current, target, delay, transition, "post") diff --git a/hiboo/profile/templates/profile_details.html b/hiboo/profile/templates/profile_details.html index 1c9c8839d7be03ee53994ba9d92fc132a0378af0..50e36864dbb1e347aaff11783b80e321e3e4aa29 100644 --- a/hiboo/profile/templates/profile_details.html +++ b/hiboo/profile/templates/profile_details.html @@ -36,13 +36,7 @@ {% endblock %} {% block actions %} -{% if profile.status == "active" %} -<a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="blocked") }}" class="btn btn-warning">{% trans %}Block profile{% endtrans %}</a> -{% elif profile.status == "blocked" %} -<a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="active") }}" class="btn btn-success">{% trans %}Unblock profile{% endtrans %}</a> -{% elif profile.status == "request" %} -<a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="active") }}" class="btn btn-success">{% trans %}Validate profile{% endtrans %}</a> -{% elif profile.status == "unclaimed" %} -<a href="{{ url_for("profile.assign", profile_uuid=profile.uuid) }}" class="btn btn-info">{% trans %}Validate profile{% endtrans %}</a> -{% endif %} +{% for transition, (_, _, _, label) in profile.transitions.items() %} +<a href="{{ url_for("profile.start_transition", profile_uuid=profile.uuid, transition=transition) }}" class="btn btn-info">{{ label | capitalize }}</a> +{% endfor %} {% endblock %} diff --git a/hiboo/profile/templates/profile_list.html b/hiboo/profile/templates/profile_list.html index ec9436e60b619ca808a17e07af68ae40dd44195e..8f44f2197921b500e7431d5097e83e65cc343ba1 100644 --- a/hiboo/profile/templates/profile_list.html +++ b/hiboo/profile/templates/profile_list.html @@ -44,16 +44,9 @@ <td>{{ macros.profile_status(profile) }}</td> <td>{{ profile.created_at.date() }}</td> <td> - {% if profile.status == "active" %} - <a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="blocked") }}">{% trans %}Block profile{% endtrans %}</a> - {% elif profile.status == "blocked" %} - <a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="active") }}">{% trans %}Unblock profile{% endtrans %}</a> - {% elif profile.status == "request" %} - <a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="active") }}">{% trans %}Validate profile{% endtrans %}</a> - {% elif profile.status == "unclaimed" %} - <a href="{{ url_for("profile.assign", profile_uuid=profile.uuid) }}">{% trans %}Assign profile{% endtrans %}</a> - {% endif %} - <a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="deleted") }}">{% trans %}Delete profile{% endtrans %}</a> + {% for transition, (_, _, _, label) in profile.transitions.items() %} + <a href="{{ url_for("profile.start_transition", profile_uuid=profile.uuid, transition=transition) }}">{{ label | capitalize }}</a> + {% endfor %} </td> </tr> {% endfor %} diff --git a/hiboo/profile/transition.py b/hiboo/profile/transition.py deleted file mode 100644 index 846be0efc42999223a2da81ad67d7e039b3a32de..0000000000000000000000000000000000000000 --- a/hiboo/profile/transition.py +++ /dev/null @@ -1,45 +0,0 @@ -from hiboo import models - -# These are coded as (current, target), None being a wildcard. -# All matches apply, including wildcards. -# Values are pairs of lists, first actions being applied before, -# then actions being applied after the transition. -TRANSITIONS = { - (None, models.Profile.BLOCKED): ( - ["block"], - ["notify"] - ), - (None, models.Profile.DELETED): ( - ["delete"], - ["notify"] - ), - (None, models.Profile.ACTIVE): ( - ["activate"], - ["notify"] - ), - (models.Profile.REQUEST, models.Profile.ACTIVE): ( - ["welcome"], - [] - ), -} - - -def apply(profile, current, target, done=False): - """ Handle profile transitions as single-step workflows - """ - app = profile.service.application - index = 1 if done else 0 - empty = ([], []) - functions = ( - TRANSITIONS.get((current, target), empty)[index] + - TRANSITIONS.get((None, target), empty)[index] + - TRANSITIONS.get((current, None), empty)[index] - ) - valid = any([ - hasattr(app, function) and getattr(app, function)(profile) - for function in functions - ]) - if valid and not done: - profile.status = profile.transition - profile.transition = None - transition(profile, current, target, True) diff --git a/hiboo/profile/views.py b/hiboo/profile/views.py index 219aac62769613997c93f1fdf9a2daa3a66ea775..052b8529bd20b8e77ad2badc85d0aa82a57d0fd0 100644 --- a/hiboo/profile/views.py +++ b/hiboo/profile/views.py @@ -1,4 +1,4 @@ -from hiboo.profile import blueprint, forms, transition +from hiboo.profile import blueprint, forms, common from hiboo import security, models, utils, format from hiboo import user as hiboo_user from passlib import context, hash @@ -164,22 +164,23 @@ def details(profile_uuid): return flask.render_template("profile_details.html", profile=profile) -@blueprint.route("/status/<profile_uuid>/<status>", methods=["GET", "POST"]) +@blueprint.route("/transition/<profile_uuid>/<transition>", methods=["GET", "POST"]) @security.confirmation_required("change the profile status") @security.admin_required() -def set_status(profile_uuid, status): +def start_transition(profile_uuid, transition): profile = models.Profile.query.get(profile_uuid) or flask.abort(404) - profile.transition = status + current, target, delay, label = profile.transitions.get(transition) or flask.abort(403) + profile.transition = transition models.log( - category=models.History.STATUS, - value=status, + category=models.History.TRANSITION, + value=transition, user=profile.user, service=profile.service, profile=profile, actor=flask_login.current_user, public=True ) - transition.apply(profile, profile.status, profile.transition) + common.apply_transition(profile, profile.status, target, delay, transition) models.db.session.commit() flask.flash(_("Profile status change was requested"), "success") return flask.redirect(flask.url_for(".details", profile_uuid=profile_uuid)) @@ -189,9 +190,10 @@ def set_status(profile_uuid, status): @security.admin_required() def complete_transition(profile_uuid): profile = models.Profile.query.get(profile_uuid) or flask.abort(404) - current, target = profile.status, profile.transition + transition = profile.transition + current, target, delay, label = profile.TRANSITIONS.get(transition) or flask.abort(403) profile.status, profile.transition = target, None - transition.apply(profile, current, target, True) + common.apply_transition(profile, current, target, delay, transition, "post") models.db.session.commit() flask.flash(_("Profile status change was completed"), "success") return flask.redirect(flask.url_for(".details", profile_uuid=profile_uuid)) diff --git a/hiboo/templates/macros.html b/hiboo/templates/macros.html index 6450780b7c014c299f73ed975e15e1d1c9cd8ad6..0280e174765f40ebbc35b37f85d361dbf759b7a1 100644 --- a/hiboo/templates/macros.html +++ b/hiboo/templates/macros.html @@ -32,12 +32,12 @@ {% endmacro %} {% macro profile_status(profile) %} - {% set status = profile.STATUSES[profile.status] %} - <span class="badge bg-{{ status[0] }}">{{ status[1] }}</span> + {% set current = profile.STATUSES[profile.status] %} + <span class="badge bg-{{ current[0] }}">{{ current[1] }}</span> {% if profile.transition %} - {% set transition = profile.STATUSES[profile.transition] %} + {% set target = profile.STATUSES[profile.TRANSITIONS[profile.transition][1]] %} <i class="fa fa-arrow-right"></i> - <span class="badge bg-{{ transition[0] }}">{{ transition[1] }}</span> + <span class="badge bg-{{ target[0] }}">{{ target[1] }}</span> (<a href="{{ url_for("profile.complete_transition", profile_uuid=profile.uuid) }}">{% trans %}complete manually{% endtrans %}</a>) {% endif %} {% endmacro %}