Merge pull request #436 from dod-ccpo/revoke-invitation-#160300315
Revoke invitation #160300315
This commit is contained in:
commit
42ecdaa275
@ -99,3 +99,8 @@ class Invitations(object):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return invite
|
return invite
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def revoke(cls, token):
|
||||||
|
invite = Invitations._get(token)
|
||||||
|
return Invitations._update_status(invite, InvitationStatus.REVOKED)
|
||||||
|
@ -50,6 +50,18 @@ class Workspaces(object):
|
|||||||
|
|
||||||
return workspace
|
return workspace
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_update_member(cls, user, workspace_id):
|
||||||
|
workspace = WorkspacesQuery.get(workspace_id)
|
||||||
|
Authorization.check_workspace_permission(
|
||||||
|
user,
|
||||||
|
workspace,
|
||||||
|
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
|
||||||
|
"update a workspace member",
|
||||||
|
)
|
||||||
|
|
||||||
|
return workspace
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_request(cls, request):
|
def get_by_request(cls, request):
|
||||||
return WorkspacesQuery.get_by_request(request)
|
return WorkspacesQuery.get_by_request(request)
|
||||||
|
@ -51,7 +51,9 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
|||||||
if self.status == Status.ACTIVE:
|
if self.status == Status.ACTIVE:
|
||||||
return "Active"
|
return "Active"
|
||||||
elif self.latest_invitation:
|
elif self.latest_invitation:
|
||||||
if self.latest_invitation.is_rejected_expired:
|
if self.latest_invitation.is_revoked:
|
||||||
|
return "Revoked"
|
||||||
|
elif self.latest_invitation.is_rejected_expired:
|
||||||
return "Invite expired"
|
return "Invite expired"
|
||||||
elif self.latest_invitation.is_rejected_wrong_user:
|
elif self.latest_invitation.is_rejected_wrong_user:
|
||||||
return "Error on invite"
|
return "Error on invite"
|
||||||
|
@ -357,10 +357,18 @@ def update_member(workspace_id, member_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/workspaces/invitation/<token>", methods=["GET"])
|
@bp.route("/workspaces/invitations/<token>", methods=["GET"])
|
||||||
def accept_invitation(token):
|
def accept_invitation(token):
|
||||||
invite = Invitations.accept(g.current_user, token)
|
invite = Invitations.accept(g.current_user, token)
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("workspaces.show_workspace", workspace_id=invite.workspace.id)
|
url_for("workspaces.show_workspace", workspace_id=invite.workspace.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/workspaces/<workspace_id>/invitations/<token>/revoke", methods=["POST"])
|
||||||
|
def revoke_invitation(workspace_id, token):
|
||||||
|
workspace = Workspaces.get_for_update_member(g.current_user, workspace_id)
|
||||||
|
Invitations.revoke(token)
|
||||||
|
|
||||||
|
return redirect(url_for("workspaces.workspace_members", workspace_id=workspace.id))
|
||||||
|
19
templates/components/confirmation_button.html
Normal file
19
templates/components/confirmation_button.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% macro ConfirmationButton(btn_text, action, csrf_token, confirm_msg="Are you sure?", confirm_btn="Confirm", cancel_btn="Cancel") -%}
|
||||||
|
<v-popover placement='top-start'>
|
||||||
|
<template slot="popover">
|
||||||
|
<p>{{ confirm_msg }}</p>
|
||||||
|
<div class='action-group'>
|
||||||
|
<form method="POST" action="{{ action }}">
|
||||||
|
{{ csrf_token }}
|
||||||
|
<button class='usa-button usa-button-primary' type='submit'>
|
||||||
|
{{ confirm_btn }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<button class='usa-button usa-button-secondary' v-close-popover>
|
||||||
|
{{ cancel_btn }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<button class="tooltip-target" type="button">{{ btn_text }}</button>
|
||||||
|
</v-popover>
|
||||||
|
{%- endmacro %}
|
@ -5,6 +5,7 @@
|
|||||||
{% from "components/selector.html" import Selector %}
|
{% from "components/selector.html" import Selector %}
|
||||||
{% from "components/options_input.html" import OptionsInput %}
|
{% from "components/options_input.html" import OptionsInput %}
|
||||||
{% from "components/alert.html" import Alert %}
|
{% from "components/alert.html" import Alert %}
|
||||||
|
{% from "components/confirmation_button.html" import ConfirmationButton %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -38,6 +39,13 @@
|
|||||||
{% if editable %}
|
{% if editable %}
|
||||||
<a href='{{ url_for("users.user") }}' class='icon-link'>edit account details</a>
|
<a href='{{ url_for("users.user") }}' class='icon-link'>edit account details</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if member.latest_invitation.is_pending %}
|
||||||
|
{{ ConfirmationButton(
|
||||||
|
"Revoke Invitation",
|
||||||
|
url_for("workspaces.revoke_invitation", workspace_id=workspace.id, token=member.latest_invitation.token),
|
||||||
|
form.csrf_token
|
||||||
|
) }}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -93,3 +93,13 @@ def test_accept_invitation_twice():
|
|||||||
Invitations.accept(user, invite.token)
|
Invitations.accept(user, invite.token)
|
||||||
with pytest.raises(InvitationError):
|
with pytest.raises(InvitationError):
|
||||||
Invitations.accept(user, invite.token)
|
Invitations.accept(user, invite.token)
|
||||||
|
|
||||||
|
|
||||||
|
def test_revoke_invitation():
|
||||||
|
workspace = WorkspaceFactory.create()
|
||||||
|
user = UserFactory.create()
|
||||||
|
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||||
|
invite = Invitations.create(ws_role, workspace.owner, user)
|
||||||
|
assert invite.is_pending
|
||||||
|
Invitations.revoke(invite.token)
|
||||||
|
assert invite.is_revoked
|
||||||
|
@ -432,3 +432,28 @@ def test_user_accepts_expired_invite(client, user_session):
|
|||||||
response = client.get(url_for("workspaces.accept_invitation", token=invite.token))
|
response = client.get(url_for("workspaces.accept_invitation", token=invite.token))
|
||||||
|
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_revoke_invitation(client, user_session):
|
||||||
|
workspace = WorkspaceFactory.create()
|
||||||
|
user = UserFactory.create()
|
||||||
|
ws_role = WorkspaceRoleFactory.create(
|
||||||
|
user=user, workspace=workspace, status=WorkspaceRoleStatus.PENDING
|
||||||
|
)
|
||||||
|
invite = InvitationFactory.create(
|
||||||
|
user_id=user.id,
|
||||||
|
workspace_role_id=ws_role.id,
|
||||||
|
status=InvitationStatus.REJECTED_EXPIRED,
|
||||||
|
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
||||||
|
)
|
||||||
|
user_session(workspace.owner)
|
||||||
|
response = client.post(
|
||||||
|
url_for(
|
||||||
|
"workspaces.revoke_invitation",
|
||||||
|
workspace_id=workspace.id,
|
||||||
|
token=invite.token,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert invite.is_revoked
|
||||||
|
Loading…
x
Reference in New Issue
Block a user