From d0ff3930f31afdde421a65e0a6e356ea48cd5824 Mon Sep 17 00:00:00 2001 From: Julian Rother <julianr@fsmpi.rwth-aachen.de> Date: Sat, 17 Apr 2021 00:43:43 +0200 Subject: [PATCH] Refactored invite page and increased invite/signup token entropy --- uffd/invite/models.py | 2 +- uffd/invite/templates/invite/list.html | 127 ++++++++++++++++++------- uffd/signup/models.py | 2 +- 3 files changed, 93 insertions(+), 38 deletions(-) diff --git a/uffd/invite/models.py b/uffd/invite/models.py index a46c387f..fcfc948f 100644 --- a/uffd/invite/models.py +++ b/uffd/invite/models.py @@ -19,7 +19,7 @@ invite_roles = db.Table('invite_roles', class Invite(db.Model): __tablename__ = 'invite' id = Column(Integer(), primary_key=True, autoincrement=True) - token = Column(String(128), unique=True, nullable=False, default=lambda: secrets.token_hex(20)) + token = Column(String(128), unique=True, nullable=False, default=lambda: secrets.token_urlsafe(32)) created = Column(DateTime, default=datetime.datetime.now, nullable=False) creator_dn = Column(String(128), nullable=True) creator = DBRelationship('creator_dn', User) diff --git a/uffd/invite/templates/invite/list.html b/uffd/invite/templates/invite/list.html index 44b48371..bb6ad283 100644 --- a/uffd/invite/templates/invite/list.html +++ b/uffd/invite/templates/invite/list.html @@ -10,11 +10,10 @@ <thead> <tr> <th scope="col">Link</th> - <th scope="col">Status</th> - <th scope="col">Created on</th> - <th scope="col">Expires after</th> + <th scope="col">Created by</th> <th scope="col">Permissions</th> <th scope="col">Usages</th> + <th scope="col">Status</th> <th scope="col"></th> </tr> </thead> @@ -22,12 +21,31 @@ {% for invite in invites|sort(attribute='created', reverse=True)|sort(attribute='active', reverse=True) %} <tr> <td> - {% if invite.creator == get_current_user() %} + {% if invite.creator == get_current_user() and invite.active %} <a href="{{ url_for('invite.use', token=invite.token) }}"><code>{{ invite.short_token }}</code></a> + <button type="button" class="btn btn-link btn-sm p-0 copy-clipboard" data-copy="{{ url_for('invite.use', token=invite.token, _external=True) }}" title="Copy link to clipboard"><i class="fas fa-clipboard"></i></button> + <button type="button" class="btn btn-link btn-sm p-0" data-toggle="modal" data-target="#modal-{{ invite.id }}-qrcode" title="Show link as QR code"><i class="fas fa-qrcode"></i></button> {% else %} <code>{{ invite.short_token }}</code> {% endif %} </td> + <td> + {% if not invite.creator_dn %} + {{ '<admin>' }} + {% elif not invite.creator %} + {{ '<deleted user>' }} + {% else %} + {{ invite.creator.loginname }} + {% endif %} + </td> + <td> + {{ 'Signup' if invite.allow_signup }}{{ ', ' if invite.allow_signup and invite.roles }} + {% for role in invite.roles %}{{ ', ' if loop.index != 1 }}<i class="fas fa-key"></i> {{ role.name }}{% endfor %} + </td> + <td> + <span style="white-space: nowrap;">{{ invite.signups|selectattr('completed')|list|length }} <i class="fas fa-users" title="user registrations"></i></span>, + <span style="white-space: nowrap;">{{ invite.grants|length }} <i class="fas fa-key" title="role grants"></i></span> + </td> <td> {% if invite.disabled %} Disabled @@ -35,36 +53,18 @@ Voided {% elif invite.expired %} Expired + {% elif not invite.permitted %} + Invalid, unpermitted creator {% elif not invite.active %} Invalid {% elif invite.single_use %} - Valid once + Valid once, expires {{ invite.valid_until.strftime('%Y-%m-%d') }} {% else %} - Valid + Valid, expires {{ invite.valid_until.strftime('%Y-%m-%d') }} {% endif %} </td> - <td>{{ invite.created.strftime('%Y-%m-%d %H:%M') }}</td> - <td>{{ invite.valid_until.strftime('%Y-%m-%d %H:%M') }}</td> - <td> - {{ 'Signup' if invite.allow_signup }}{{ ', ' if invite.allow_signup and invite.roles }} - {% for role in invite.roles %}{{ ', ' if loop.index != 1 }}<a href="{{ url_for('role.show', roleid=role.id) }}" style="white-space: nowrap;"><i class="fas fa-key"></i> {{ role.name }}</a>{% endfor %} - </td> - <td> - <a href="#" data-toggle="modal" data-target="#modal-{{ invite.id }}"> - <span style="white-space: nowrap;">{{ invite.signups|selectattr('completed')|list|length }} <i class="fas fa-users" title="user registrations"></i></span>, - <span style="white-space: nowrap;">{{ invite.grants|length }} <i class="fas fa-key" title="role grants"></i></span> - </a> - </td> <td class="text-right"> - {% if invite.active %} - <form action="{{ url_for('invite.disable', invite_id=invite.id) }}" method="POST"> - <button type="submit" class="btn btn-link btn-sm py-0" title="Disable"><i class="fas fa-ban" style="width: 1.5em;"></i></button> - </form> - {% elif invite.creator == get_current_user() and not invite.expired and invite.permitted %} - <form action="{{ url_for('invite.reset', invite_id=invite.id) }}" method="POST"> - <button type="submit" class="btn btn-link btn-sm py-0" title="Reenable"><i class="fas fa-redo" style="width: 1.5em;"></i></button> - </form> - {% endif %} + <button type="button" class="btn btn-link btn-sm p-0" data-toggle="modal" data-target="#modal-{{ invite.id }}"><i class="fas fa-ellipsis-h"></i></button> </td> </tr> {% endfor %} @@ -77,27 +77,82 @@ <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> - <h5 class="modal-title">Invite Usages</h5> + <h5 class="modal-title">Invite Link Details</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> - <ul> - {% for signup in invite.signups if signup.completed %} - <li>Registration of user <a href="{{ url_for('user.show', uid=signup.user.uid) }}">{{ signup.user.loginname }}</a></li> - {% endfor %} - {% for grant in invite.grants if grant.user %} - <li>Roles granted to <a href="{{ url_for('user.show', uid=grant.user.uid) }}">{{ grant.user.loginname }}</a></li> - {% endfor %} + <ul class="list-unstyled"> + <li><b>Type:</b> {% if invite.single_use %}Single-use{% else %}Multi-use{% endif %}</li> + <li><b>Created:</b> {{ invite.created.strftime('%Y-%m-%d %H:%M:%S') }}</li> + <li><b>Expires:</b> {{ invite.valid_until.strftime('%Y-%m-%d %H:%M:%S') }}</li> + <li><b>Permissions:</b> + <ul> + {% if invite.signup %} + <li>Link allows account registration</li> + {% else %} + <li>No account registration allowed</li> + {% endif %} + {% for role in invite.roles %} + <li>Link grants users the role "{{ role.name }}"</li> + {% endfor %} + </ul> + </li> + <li><b>Usages:</b> + {% if not invite.signups and not invite.grants %} + Never used + {% else %} + <ul> + {% for signup in invite.signups if signup.completed %} + <li>Registration of user <a href="{{ url_for('user.show', uid=signup.user.uid) }}">{{ signup.user.loginname }}</a></li> + {% endfor %} + {% for grant in invite.grants if grant.user %} + <li>Roles granted to <a href="{{ url_for('user.show', uid=grant.user.uid) }}">{{ grant.user.loginname }}</a></li> + {% endfor %} + </ul> + {% endif %} + </li> </ul> </div> <div class="modal-footer"> - <button type="button" class="btn btn-primary" data-dismiss="modal">Close</button> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> + {% if invite.active %} + <form action="{{ url_for('invite.disable', invite_id=invite.id) }}" method="POST"> + <button type="submit" class="btn btn-primary">Disable Link</button> + </form> + {% elif invite.creator == get_current_user() and not invite.expired and invite.permitted %} + <form action="{{ url_for('invite.reset', invite_id=invite.id) }}" method="POST"> + <button type="submit" class="btn btn-primary">Reenable Link</button> + </form> + {% endif %} + </div> + </div> + </div> +</div> +{% endfor %} + +{% for invite in invites if invite.creator == get_current_user() %} +<div class="modal" tabindex="-1" id="modal-{{ invite.id }}-qrcode"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Invite</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + {{ url_for('invite.use', token=invite.token, _external=True)|qrcode_svg(width='100%', height='100%') }} </div> </div> </div> </div> {% endfor %} +<script> +$(".copy-clipboard").on("click", function() { + navigator.clipboard.writeText($(this).data("copy")); +}); +</script> {% endblock %} diff --git a/uffd/signup/models.py b/uffd/signup/models.py index 82e89ced..0a7532ae 100644 --- a/uffd/signup/models.py +++ b/uffd/signup/models.py @@ -28,7 +28,7 @@ class Signup(db.Model): As long as they are not completed, signup requests have no effect each other or different parts of the application.''' __tablename__ = 'signup' - token = Column(String(128), primary_key=True, default=lambda: secrets.token_hex(20)) + token = Column(String(128), primary_key=True, default=lambda: secrets.token_urlsafe(32)) created = Column(DateTime, default=datetime.datetime.now, nullable=False) loginname = Column(Text) displayname = Column(Text) -- GitLab