use invite token instead of id for invitation url

This commit is contained in:
dandds 2018-10-29 09:59:34 -04:00
parent b81a831c85
commit 151d5be5ea
7 changed files with 53 additions and 20 deletions

View File

@ -0,0 +1,30 @@
"""add token to invitation
Revision ID: 81e6babf5136
Revises: 67955a4abaef
Create Date: 2018-10-29 09:26:30.728348
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '81e6babf5136'
down_revision = '67955a4abaef'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('invitations', sa.Column('token', sa.String(), nullable=True))
op.create_index(op.f('ix_invitations_token'), 'invitations', ['token'], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_invitations_token'), table_name='invitations')
op.drop_column('invitations', 'token')
# ### end Alembic commands ###

View File

@ -21,9 +21,9 @@ class Invitations(object):
EXPIRATION_LIMIT_MINUTES = 360
@classmethod
def _get(cls, invite_id):
def _get(cls, token):
try:
invite = db.session.query(Invitation).filter_by(id=invite_id).one()
invite = db.session.query(Invitation).filter_by(token=token).one()
except NoResultFound:
raise NotFoundError("invite")
@ -58,8 +58,8 @@ class Invitations(object):
return invite
@classmethod
def accept(cls, invite_id):
invite = Invitations._get(invite_id)
def accept(cls, token):
invite = Invitations._get(token)
if invite.is_expired:
invite.status = InvitationStatus.REJECTED

View File

@ -1,7 +1,8 @@
import datetime
from enum import Enum
import secrets
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum, TIMESTAMP
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum, TIMESTAMP, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
@ -34,6 +35,8 @@ class Invitation(Base, TimestampsMixin):
expiration_time = Column(TIMESTAMP(timezone=True))
token = Column(String(), index=True, default=lambda: secrets.token_urlsafe())
def __repr__(self):
return "<Invitation(user='{}', workspace='{}', id='{}')>".format(
self.user.id, self.workspace.id, self.id

View File

@ -220,10 +220,8 @@ def new_member(workspace_id):
)
def send_invite_email(owner_name, invite_id, new_member_email):
body = render_template(
"emails/invitation.txt", owner=owner_name, invite_id=invite_id
)
def send_invite_email(owner_name, token, new_member_email):
body = render_template("emails/invitation.txt", owner=owner_name, token=token)
queue.send_mail(
[new_member_email],
"{} has invited you to a JEDI Cloud Workspace".format(owner_name),
@ -241,7 +239,7 @@ def create_member(workspace_id):
new_member = Workspaces.create_member(g.current_user, workspace, form.data)
invite = Invitations.create(workspace, g.current_user, new_member.user)
send_invite_email(
g.current_user.full_name, invite.id, new_member.user.email
g.current_user.full_name, invite.token, new_member.user.email
)
return redirect(
@ -338,11 +336,11 @@ def update_member(workspace_id, member_id):
)
@bp.route("/workspaces/invitation/<invite_id>", methods=["GET"])
def accept_invitation(invite_id):
@bp.route("/workspaces/invitation/<token>", methods=["GET"])
def accept_invitation(token):
# TODO: check that the current_user DOD ID matches the user associated with
# the invitation
invite = Invitations.accept(invite_id)
invite = Invitations.accept(token)
return redirect(
url_for("workspaces.show_workspace", workspace_id=invite.workspace.id)

View File

@ -1,7 +1,7 @@
Join this JEDI Cloud Workspace
{{ owner }} has invited you to join a JEDI Cloud Workspace. Login now to view or use your JEDI Cloud resources.
{{ url_for("workspaces.accept_invitation", invite_id=invite_id, _external=True) }}
{{ url_for("workspaces.accept_invitation", token=token, _external=True) }}
What is JEDI Cloud?
JEDI Cloud is a DoD enterprise-wide solution for commercial cloud services.

View File

@ -1,5 +1,6 @@
import datetime
import pytest
import re
from atst.domain.invitations import Invitations, InvitationError
from atst.models.invitation import Status
@ -15,6 +16,7 @@ def test_create_invitation():
assert invite.workspace == workspace
assert invite.inviter == workspace.owner
assert invite.status == Status.PENDING
assert re.match(r"^[\w\-_]+$", invite.token)
def test_accept_invitation():
@ -22,7 +24,7 @@ def test_accept_invitation():
user = UserFactory.create()
invite = Invitations.create(workspace, workspace.owner, user)
assert invite.is_pending
accepted_invite = Invitations.accept(invite.id)
accepted_invite = Invitations.accept(invite.token)
assert accepted_invite.is_accepted
@ -38,7 +40,7 @@ def test_accept_expired_invitation():
status=Status.PENDING,
)
with pytest.raises(InvitationError):
Invitations.accept(invite.id)
Invitations.accept(invite.token)
assert invite.is_rejected
@ -50,7 +52,7 @@ def test_accept_rejected_invite():
workspace_id=workspace.id, user_id=user.id, status=Status.REJECTED
)
with pytest.raises(InvitationError):
Invitations.accept(invite.id)
Invitations.accept(invite.token)
def test_accept_revoked_invite():
@ -60,4 +62,4 @@ def test_accept_revoked_invite():
workspace_id=workspace.id, user_id=user.id, status=Status.REVOKED
)
with pytest.raises(InvitationError):
Invitations.accept(invite.id)
Invitations.accept(invite.token)

View File

@ -308,7 +308,7 @@ def test_new_member_accepts_valid_invite(client, user_session):
assert len(Workspaces.for_user(user)) == 0
user_session(user)
response = client.get(url_for("workspaces.accept_invitation", invite_id=invite.id))
response = client.get(url_for("workspaces.accept_invitation", token=invite.token))
# user is redirected to the workspace view
assert response.status_code == 302
@ -333,7 +333,7 @@ def test_new_member_accept_invalid_invite(client, user_session):
status=InvitationStatus.REJECTED,
)
user_session(user)
response = client.get(url_for("workspaces.accept_invitation", invite_id=invite.id))
response = client.get(url_for("workspaces.accept_invitation", token=invite.token))
assert response.status_code == 404