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

Add multi-step transitions and related CLI

parent 443c2469
No related branches found
No related tags found
No related merge requests found
......@@ -42,6 +42,10 @@ def create_app_from_config(config):
app.register_blueprint(captcha.blueprint, url_prefix='/captcha')
app.register_blueprint(api.blueprint, url_prefix='/api')
# Enable global CLI
from hiboo import cli
app.cli.add_command(cli.tasks)
@app.route("/")
def index():
return flask.redirect(flask.url_for("account.home"))
......
......@@ -45,7 +45,7 @@ def action(label, profile=False, quick=False):
return Register
def hook(step="pre", *transitions):
def hook(step=models.Profile.INIT, *transitions):
""" Registers a hook for given transitions
"""
class Register():
......@@ -97,7 +97,7 @@ class BaseApplication(object):
# service config, no need to pass the service object
return self.sso_protocol.fill_service(service)
def apply_hooks(self, transition, step, profile):
def apply_hooks(self, profile, transition, step):
""" Apply hooks for a given transition and step
"""
results = [
......@@ -105,10 +105,9 @@ class BaseApplication(object):
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")
@hook(models.Profile.INIT, "purge", "purge-blocked")
def manual_purge(self, profile):
""" The default behavior is to wait for manual purge
"""
......
......@@ -57,25 +57,6 @@ 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):
......@@ -88,17 +69,16 @@ class WriteFreelyApplication(base.OIDCApplication):
application_uri = fields.StringField(_("WriteFreely URL"), [validators.URL(require_tld=False)])
submit = fields.SubmitField(_('Submit'))
def populate_service(self, form, service):
def configure(self, form, service):
callback_uri = form.application_uri.data + "/oauth/callback/generic"
service.config.update({
return {
"application_uri": form.application_uri.data,
"token_endpoint_auth_method": "client_secret_basic",
"redirect_uris": [callback_uri],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"special_mappings": ["ignore_scopes"]
})
self.fill_service(service)
}
def populate_form(self, service, form):
form.process(
......@@ -118,16 +98,15 @@ class PeertubeApplication(base.OIDCApplication):
application_uri = fields.StringField(_("PeerTube URL"), [validators.URL(require_tld=False)])
submit = fields.SubmitField(_('Submit'))
def populate_service(self, form, service):
def configure(self, form, service):
callback_uri = form.application_uri.data + "/plugins/auth-openid-connect/router/code-cb"
service.config.update({
return {
"application_uri": form.application_uri.data,
"token_endpoint_auth_method": "client_secret_basic",
"redirect_uris": [callback_uri],
"grant_types": ["authorization_code"],
"response_types": ["code"],
})
self.fill_service(service)
}
def populate_form(self, service, form):
form.process(
......
import click
import flask
import time
import datetime
from flask import cli
from hiboo import models
from hiboo.profile import common
tasks = cli.AppGroup("tasks")
def run_transitions():
# Handle profile transitions
profiles = models.Profile.query.filter(
models.Profile.transition_step.in_ (
[models.Profile.INIT, models.Profile.START, models.Profile.DONE]
)
).all()
for profile in profiles:
_, _, delay, _ = models.Profile.TRANSITIONS[profile.transition]
trigger = profile.updated_at + datetime.timedelta(seconds=delay)
if profile.transition_step == models.Profile.INIT and trigger > datetime.datetime.now():
continue
common.apply_transition(profile)
models.db.session.commit()
@tasks.command("once")
def tasks_once():
""" Run regular tasks
"""
run_transitions()
@tasks.command("loop")
def tasks_loop():
""" Run regular tasks at interval
"""
while True:
run_transitions()
time.sleep(30)
......@@ -171,6 +171,7 @@ class Profile(db.Model):
"""
__tablename__ = "profile"
# Profile statuses
UNCLAIMED = "unclaimed"
REQUEST = "request"
ACTIVE = "active"
......@@ -178,6 +179,12 @@ class Profile(db.Model):
DELETED = "deleted"
PURGED = "purged"
# Transitions steps
INIT = "init"
MANUAL = "manual"
START = "start"
DONE = "done"
STATUSES = {
UNCLAIMED: ("gray", _("unclaimed")),
REQUEST: ("blue", _("requested")),
......@@ -190,14 +197,14 @@ class Profile(db.Model):
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")),
"activate": (REQUEST, ACTIVE, 30, _("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")),
"unblock": (BLOCKED, ACTIVE, 30, _("unblock")),
"delete": (ACTIVE, DELETED, 3600 * 24, _("delete")),
"delete-blocked": (BLOCKED, DELETED, 3600, _("delete")),
"purge": (ACTIVE, PURGED, 3600 * 24 * 3, _("purge")),
"purge-blocked": (BLOCKED, PURGED, 300, _("purge"))
"purge-blocked": (BLOCKED, PURGED, 3600, _("purge"))
}
user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
......@@ -210,6 +217,7 @@ class Profile(db.Model):
username = db.Column(db.String(255), nullable=False)
status = db.Column(db.String(25), nullable=False)
transition = db.Column(db.String(25))
transition_step = db.Column(db.String(25))
extra = db.Column(mutable.MutableDict.as_mutable(JSONEncoded))
@property
......@@ -220,7 +228,7 @@ class Profile(db.Model):
def transitions(self):
return {
name: transition for name, transition in Profile.TRANSITIONS.items()
if transition[0] == self.status and not self.transition
if transition[0] == self.status and not self.transition_step
}
@classmethod
......
from hiboo import models
def apply_transition(profile, current, target, delay, transition, step="pre"):
""" Handle profile transitions as single-step workflows
def apply_transition(profile):
""" Handle profile transitions as a three step workflow
First the transition is initiated (step INIT), which sets a delay before
it can actually get started. Then the transition is START, the state
is actually changed, and the transition is DONE.
For each step, hooks can be applied. During INIT phase, if the result is
negative, then transition is set on hold until manual action is applied.
Applying manual action sets the transition as DONE (it never reaches
the step START)
"""
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")
transition = profile.transition
_, target, _, _ = models.Profile.TRANSITIONS[transition]
step = profile.transition_step
manual = app.apply_hooks(profile, transition, step)
profile.transition_step = {
models.Profile.INIT:
models.Profile.MANUAL if manual else models.Profile.START,
models.Profile.MANUAL: models.Profile.DONE,
models.Profile.START: models.Profile.DONE,
models.Profile.DONE: None
}[step]
if profile.transition_step == models.Profile.DONE:
profile.status = target
......@@ -169,8 +169,9 @@ def details(profile_uuid):
@security.admin_required()
def start_transition(profile_uuid, transition):
profile = models.Profile.query.get(profile_uuid) or flask.abort(404)
current, target, delay, label = profile.transitions.get(transition) or flask.abort(403)
profile.transitions.get(transition) or flask.abort(403)
profile.transition = transition
profile.transition_step = models.Profile.INIT
models.log(
category=models.History.TRANSITION,
value=transition,
......@@ -180,7 +181,6 @@ def start_transition(profile_uuid, transition):
actor=flask_login.current_user,
public=True
)
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))
......@@ -190,10 +190,9 @@ def start_transition(profile_uuid, transition):
@security.admin_required()
def complete_transition(profile_uuid):
profile = models.Profile.query.get(profile_uuid) or flask.abort(404)
transition = profile.transition
current, target, delay, label = profile.TRANSITIONS.get(transition) or flask.abort(403)
profile.status, profile.transition = target, None
common.apply_transition(profile, current, target, delay, transition, "post")
profile.TRANSITIONS.get(profile.transition) or flask.abort(403)
profile.transition_step == models.Profile.MANUAL or flask.abort(403)
common.apply_transition(profile)
models.db.session.commit()
flask.flash(_("Profile status change was completed"), "success")
return flask.redirect(flask.url_for(".details", profile_uuid=profile_uuid))
......
......@@ -34,11 +34,19 @@
{% macro profile_status(profile) %}
{% set current = profile.STATUSES[profile.status] %}
<span class="badge bg-{{ current[0] }}">{{ current[1] }}</span>
{% if profile.transition %}
{% if profile.transition_step %}
{% set target = profile.STATUSES[profile.TRANSITIONS[profile.transition][1]] %}
<i class="fa fa-arrow-right"></i>
<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>)
{% if profile.transition_step == profile.INIT %}
<i class="fa fa-clock-o"></i>
{% elif profile.transition_step == profile.START %}
<i class="fa fa-rocket"></i>
{% elif profile.transition_step == profile.DONE %}
<i class="fa fa-check"></i>
{% elif profile.transition_step == profile.MANUAL %}
<a href="{{ url_for("profile.complete_transition", profile_uuid=profile.uuid) }}"><i class="fa fa-hand-o-up"></i></a>
{% endif %}
{% endif %}
{% endmacro %}
......
""" Make transition a mini-state-machine
Revision ID: 05d7115e90f9
Revises: 5d4ff2ca81c2
Create Date: 2020-09-21 20:16:06.541002
"""
from alembic import op
import sqlalchemy as sa
import hiboo
revision = '05d7115e90f9'
down_revision = '5d4ff2ca81c2'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('profile') as batch_op:
batch_op.add_column(sa.Column('transition_step', sa.String(length=25), nullable=True))
def downgrade():
with op.batch_alter_table('profile') as batch_op:
batch_op.drop_column('transition_step')
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