diff --git a/trurt/account/forms.py b/trurt/account/forms.py index c6a3d729feeda6193c97a16153ce00d26d41893a..26598641c924b510ef18bc68185d616e1f46a20b 100644 --- a/trurt/account/forms.py +++ b/trurt/account/forms.py @@ -38,3 +38,7 @@ class ProfileForm(flask_wtf.FlaskForm): class ProfilePickForm(flask_wtf.FlaskForm): profile_uuid = fields.TextField('profile', []) + + +class AvatarForm(flask_wtf.FlaskForm): + submit = fields.SubmitField(_('Sign up'), []) diff --git a/trurt/account/profiles.py b/trurt/account/profiles.py index b65c390ab717997425266432ab3826e47300ca1b..f23a7539341050d6ade16b17162cae96a28bbb13 100644 --- a/trurt/account/profiles.py +++ b/trurt/account/profiles.py @@ -3,54 +3,61 @@ from trurt.sso import forms as sso_forms from trurt import models, utils, security import flask +import flask_login -def pick_profile(service, **redirect_args): +def get_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): + profile.service == service and + profile.status == models.Profile.ACTIVE): return None return profile - utils.force_redirect(utils.url_for("account.pick", **redirect_args)) + next = ("account.pick_avatar" if service.max_profiles == 0 + else "account.pick_profile") + utils.force_redirect(utils.url_for(next, **redirect_args)) -@blueprint.route("/pick") +@blueprint.route("/profile/pick") @security.authentication_required() -def pick(): +def pick_profile(): service_uuid = flask.request.args.get("service_uuid") or flask.abort(404) service = models.Service.query.get(service_uuid) or flask.abort(404) - profiles = models.Profile.query.filter_by( - service_uuid=service.uuid, - user_uuid=flask_login.current_user.uuid - ).all() + profiles = models.Profile.filter(service, flask_login.current_user).all() form = forms.ProfilePickForm() - return flask.render_template("account_pick.html", + return flask.render_template("profile_pick.html", service=service, profiles=profiles, form=form, - action_create=utils.url_for("account.create_profile", intent="account.pick"), - action_pick=utils.url_or_intent("account.status")) + action_create=utils.url_for("account.create_profile", intent="account.pick_profile"), + action_pick=utils.url_or_intent("account.home")) @blueprint.route("/profile/create", methods=["GET", "POST"]) -@security.admin_required() +@security.authentication_required() def create_profile(): service_uuid = flask.request.args.get("service_uuid") or flask.abort(404) service = models.Service.query.get(service_uuid) or flask.abort(404) - profiles = models.Profile.query.filter_by( - service_uuid=service.uuid, - user_uuid=flask_login.current_user.uuid - ).all() - if len(profiles) >= service.max_profiles: - flask.flash("You have reached the maximum number of profiles for that\ - account", "error") - return flask.redirect(utils.url_or_intent("account.home")) + status = models.Profile.ACTIVE + profiles = models.Profile.filter(service, flask_login.current_user).all() + # Do not create profile for reserved or locked services + if service.policy in (models.Service.RESERVED, models.Service.LOCKED): + flask.flash("You cannot request a profile for this service", "danger") + return flask.redirect(flask.url_for("account.home")) + # Only burst services are allowed to exceed profile count + elif len(profiles) >= service.max_profiles and service.policy != models.Service.BURST: + flask.flash("Your reached the maximum number of profiles", "danger") + return flask.redirect(flask.url_for("account.home")) + # Managed services and bursting accounts require approval + elif len(profiles) >= service.max_profiles or service.policy == models.Service.MANAGED: + flask.flash("Your profile creation requires approval", "warning") + status = models.Profile.REQUEST + # Actually display the form form = forms.ProfileForm() if form.validate_on_submit(): conflict = models.Profile.query.filter_by( service_uuid=service_uuid, username=form.username.data ).first() - print(conflict) if conflict: flask.flash("A profile with that username exists already", "danger") else: @@ -59,11 +66,66 @@ def create_profile(): profile.user = flask_login.current_user profile.service = service profile.comment = form.comment.data + profile.status = status models.db.session.add(profile) models.log(models.History.CREATE, profile.username, profile.comment, user=flask_login.current_user, service=service, profile=profile) models.db.session.commit() return flask.redirect(utils.url_or_intent("account.home")) - return flask.render_template("profile_create.html", - title="Create a profile", subtitle="for {}".format(service.name), - form=form, service=service) + return flask.render_template("profile_create.html", form=form, service=service) + + +@blueprint.route("/avatar/pick") +@security.authentication_required() +def pick_avatar(): + service_uuid = flask.request.args.get("service_uuid") or flask.abort(404) + service = models.Service.query.get(service_uuid) or flask.abort(404) + avatar = models.Profile.filter(service, flask_login.current_user).first() + form = forms.ProfilePickForm() + if avatar: + if avatar.status == models.Profile.REQUEST: + flask.flash("Your account request is awaiting approval", "warning") + return flask.redirect(fflask.url_for("account.home")) + elif avatar.status == models.Profile.BLOCKED: + flask.flash("You are currently blocked", "danger") + return flask.redirect(fflask.url_for("account.home")) + elif avatar.status in (models.Profile.DELETED, models.Profile.UNCLAIMED): + flask.flash("Your avatar is unavailable", "danger") + return flask.redirect(fflask.url_for("account.home")) + elif service.policy not in (models.Service.OPEN, models.Service.MANAGED): + flask.flash("You cannot access this service", "danger") + return flask.redirect(fflask.url_for("account.home")) + else: + return flask.redirect(utils.url_for("account.create_avatar", intent="account.pick_avatar")) + return flask.render_template("avatar_pick.html", + service=service, avatar=avatar, form=form, + action_pick=utils.url_or_intent("account.home"), + action_create=utils.url_for("account.create_avatar", intent="account.pick_avatar")) + + +@blueprint.route("/avatar/create", methods=["GET", "POST"]) +@security.authentication_required() +def create_avatar(): + service_uuid = flask.request.args.get("service_uuid") or flask.abort(404) + service = models.Service.query.get(service_uuid) or flask.abort(404) + # Cannot create an avatar if one exists already + existing = models.Profile.filter(service, flask_login.current_user).first() + existing and flask.abort(403) + # Cannot create an avatar for anything but a managed or open service + if service.policy == models.Service.OPEN: + status = models.Profile.ACTIVE + elif service.policy == models.Service.MANAGED: + status = models.Profile.REQUEST + else: + flask.abort(403) + form = forms.AvatarForm() + if form.validate_on_submit(): + avatar = models.Profile() + avatar.username = flask_login.current_user.username + avatar.user = flask_login.current_user + avatar.service = service + avatar.status = models.Profile.ACTIVE + models.db.session.add(avatar) + models.db.session.commit() + return flask.redirect(utils.url_or_intent("account.home")) + return flask.render_template("avatar_create.html", form=form, service=service) diff --git a/trurt/account/templates/account_home.html b/trurt/account/templates/account_home.html index 02dbb6e282710f54a608cbd48930277e698481bf..771733d5118462e5d6387ea039ad0b0e61818d94 100644 --- a/trurt/account/templates/account_home.html +++ b/trurt/account/templates/account_home.html @@ -20,7 +20,7 @@ {{ macros.infobox("Pending requests", "0", "green", "hourglass") }} </div> <div class="col-md-6 col-xs-12"> - {{ macros.infobox("Role", "registered user", "yellow", "lock") }} + {{ macros.infobox("Role", "administrator" if current_user.is_admin else "registered user", "yellow", "lock") }} </div> </div> </section> diff --git a/trurt/account/templates/avatar_create.html b/trurt/account/templates/avatar_create.html new file mode 100644 index 0000000000000000000000000000000000000000..ca723579acf2e3569134abff960d5d39c9335f2b --- /dev/null +++ b/trurt/account/templates/avatar_create.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}Sign-up{% endblock %} +{% block subtitle %}for the service {{ service.name }}{% endblock %} + +{% block content %} +<div class="box"> + <div class="box-body"> + <p>Your are about to sign up for {{ service.name }}.</p> + </div> + <form method="POST" action="{{ action_pick }}" class="form"> + {{ form.hidden_tag() }} + <input type="submit" value="Sign up" style="opacity: 0.8" class="btn btn-lg btn-flat bg-gray text-black"> + </form> +</div> +{% endblock %} diff --git a/trurt/account/templates/avatar_pick.html b/trurt/account/templates/avatar_pick.html new file mode 100644 index 0000000000000000000000000000000000000000..b6314dbbdbe06a355dcca6f41dd19952cd16f03c --- /dev/null +++ b/trurt/account/templates/avatar_pick.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}Sign-in{% endblock %} +{% block subtitle %}for the service {{ service.name }}{% endblock %} + +{% block content %} +<div class="box"> + <div class="box-body"> + <p>Please confirm that you wish to sign in to {{ service.name }}.</p> + </div> + <form method="POST" action="{{ action_pick }}" class="form"> + {{ form.hidden_tag() }} + <input type="hidden" name="profile_uuid" value="{{ avatar.uuid }}"> + <input type="submit" value="Sign in" style="opacity: 0.8" class="btn btn-lg btn-flat bg-gray text-black"> + </form> +</div> +{% endblock %} diff --git a/trurt/account/templates/profile_create.html b/trurt/account/templates/profile_create.html index a2d63c0f35212e83a93f7e1f01abdd50c8df7351..1219e0dfa51f232d9e72b4ff2abd281ad1cda2b3 100644 --- a/trurt/account/templates/profile_create.html +++ b/trurt/account/templates/profile_create.html @@ -1,5 +1,8 @@ {% extends "base.html" %} +{% block title %}New profile{% endblock %} +{% block subtitle %}for the service {{ service.name }}{% endblock %} + {% block content %} <div class="box"> <div class="box-body"> diff --git a/trurt/account/templates/account_pick.html b/trurt/account/templates/profile_pick.html similarity index 78% rename from trurt/account/templates/account_pick.html rename to trurt/account/templates/profile_pick.html index 4f6e81647f661763c54d0ec634eff8d09843d76d..ebd3ec51656f4df2ccd36092dcac81a842a149a5 100644 --- a/trurt/account/templates/account_pick.html +++ b/trurt/account/templates/profile_pick.html @@ -27,11 +27,17 @@ <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] }}"> + {% if profile.status == "active" %} <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" style="opacity: 0.8" class="btn btn-lg btn-flat bg-gray text-black pull-right"> + <input type="submit" value="Sign in" style="opacity: 0.8" class="btn btn-lg btn-flat bg-gray text-black pull-right"> </form> + {% elif profile.status == "blocked" %} + <span class="btn btn-lg btn-flat bg-red text-black pull-right">Blocked</span> + {% elif profile.status == "request" %} + <span class="btn btn-lg btn-flat bg-orange text-black pull-right">Awaiting approval</span> + {% endif %} <h3 class="widget-header-username">{{ profile.username }}</h3> <h5 class="widget-header-desc">{{ profile.comment or "No profile description" }} </h5> </div> diff --git a/trurt/models.py b/trurt/models.py index 7f94c2ec43f24bac02106fb38be521bfca65b2d1..79ee76c2d7197eca0e049a5a71e6e7dac358ffa9 100644 --- a/trurt/models.py +++ b/trurt/models.py @@ -168,7 +168,10 @@ class Profile(db.Model): """ __tablename__ = "profile" + UNCLAIMED = "unclaimed" + REQUEST = "request" ACTIVE = "active" + BLOCKED = "blocked" DELETED = "deleted" user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid)) @@ -179,12 +182,19 @@ class Profile(db.Model): backref=db.backref('profiles', cascade='all, delete-orphan')) username = db.Column(db.String(255), nullable=False) - status = db.Column(db.String(25), nullable=False, default=ACTIVE) + status = db.Column(db.String(25), nullable=False) @property def email(self): return "{}@{}".format(self.uuid, app.config.get("MAIL_DOMAIN")) + @classmethod + def filter(cls, service, user): + return cls.query.filter_by( + service_uuid=service.uuid, + user_uuid=user.uuid, + ).filter(cls.status.in_((cls.ACTIVE, cls.BLOCKED, cls.REQUEST))) + class History(db.Model): """ Records an even in an account's or profile's lifetime. diff --git a/trurt/sso/saml.py b/trurt/sso/saml.py index f9191da325837ac56bd240aa3c349f3b96f9fa28..ffd137d34f0ad3d2fd1853865e0598ee5bf79027 100644 --- a/trurt/sso/saml.py +++ b/trurt/sso/saml.py @@ -159,7 +159,7 @@ def saml_redirect(service_uuid): # Get the profile from user input (implies redirects) service = models.Service.query.get(service_uuid) or flask.abort(404) service.protocol == "saml" or flask.abort(404) - profile = account.profiles.pick_profile( + profile = account.profiles.get_profile( service, intent="sso.saml_redirect", service_uuid=service_uuid ) or flask.abort(403) # Parse the authentication request and check the ACS