Skip to content

Commit 7bde325

Browse files
committed
initial commit
1 parent 263fe35 commit 7bde325

File tree

6 files changed

+180
-2
lines changed

6 files changed

+180
-2
lines changed

mwdb/app.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@
4545
FileItemResource,
4646
FileResource,
4747
)
48-
from mwdb.resources.group import GroupListResource, GroupMemberResource, GroupResource
48+
from mwdb.resources.group import (
49+
GroupListResource,
50+
GroupMemberResource,
51+
GroupResource,
52+
RequestGroupInviteLinkResource,
53+
)
4954
from mwdb.resources.karton import KartonAnalysisResource, KartonObjectResource
5055
from mwdb.resources.metakey import (
5156
MetakeyDefinitionManageResource,
@@ -340,6 +345,7 @@ def require_auth():
340345
api.add_resource(GroupListResource, "/group")
341346
api.add_resource(GroupResource, "/group/<name>")
342347
api.add_resource(GroupMemberResource, "/group/<name>/member/<login>")
348+
api.add_resource(RequestGroupInviteLinkResource, "/group/<name>/invite/<invited_user>")
343349

344350
# OAuth endpoints
345351
if app_config.mwdb.enable_oidc:

mwdb/core/auth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class AuthScope(Enum):
1212
api_key = "api_key"
1313
set_password = "set_password"
1414
download_file = "download_file"
15+
group_invite = "group_invite"
1516

1617

1718
def generate_token(fields, scope, expiration=None):

mwdb/model/user.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,24 @@ def generate_set_password_token(self):
200200
expiration=14 * 24 * 3600,
201201
)
202202

203+
def generate_group_invite_token(self, group_id, inviter):
204+
return self._generate_token(
205+
user_fields=[],
206+
scope=AuthScope.group_invite,
207+
expiration=7 * 24 * 3600, # valid for 1 week
208+
group_id=group_id,
209+
inviter=inviter,
210+
)
211+
212+
@staticmethod
213+
def verify_group_invite_token(token):
214+
result = User._verify_token(
215+
token=token,
216+
fields=[],
217+
scope=AuthScope.group_invite,
218+
)
219+
return None if result is None else result[0]
220+
203221
@staticmethod
204222
def verify_session_token(token) -> Optional[Tuple["User", Optional[str]]]:
205223
return User._verify_token(

mwdb/resources/group.py

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
from flask_restful import Resource
33
from sqlalchemy import exists
44
from sqlalchemy.orm import joinedload
5-
from werkzeug.exceptions import Conflict, Forbidden, NotFound
5+
from werkzeug.exceptions import Conflict, Forbidden, InternalServerError, NotFound
66

77
from mwdb.core.capabilities import Capabilities
8+
from mwdb.core.config import app_config
9+
from mwdb.core.mail import MailError, send_email_notification
810
from mwdb.core.plugins import hooks
911
from mwdb.core.rate_limit import rate_limited_resource
1012
from mwdb.model import Group, Member, User, db
@@ -583,3 +585,145 @@ def delete(self, name, login):
583585
)
584586
schema = GroupSuccessResponseSchema()
585587
return schema.dump({"name": name})
588+
589+
590+
@rate_limited_resource
591+
class RequestGroupInviteLinkResource(Resource):
592+
@requires_authorization
593+
def post(self, name, invited_user):
594+
"""
595+
---
596+
summary: Request invitation link
597+
description: |
598+
Creates invitation link and sends an email to the invited user.
599+
600+
Invitation link works only for secified group and specified user
601+
602+
Requires `manage_users` capability or group_admin membership.
603+
security:
604+
- bearerAuth: []
605+
tags:
606+
- group
607+
parameters:
608+
- in: path
609+
name: name
610+
schema:
611+
type: string
612+
description: Group name
613+
- in: path
614+
name: invited_user
615+
schema:
616+
type: string
617+
description: Invited user login
618+
responses:
619+
200:
620+
description: When link was created successfully
621+
400:
622+
description: When request body is invalid
623+
403:
624+
description: |
625+
When user doesn't have enough permissions,
626+
group is immutable or invited user is pending
627+
404:
628+
description: When invited user or group doesn't exist
629+
409:
630+
description: When user is already a member of this group
631+
503:
632+
description: |
633+
Request canceled due to database statement timeout.
634+
"""
635+
group_obj = (db.session.query(Group).filter(Group.name == name)).first()
636+
637+
if group_obj is None:
638+
raise NotFound("Group does not exist or you are not it's member")
639+
640+
member_obj = (
641+
db.session.query(Member)
642+
.filter(Member.group_id == group_obj.id)
643+
.filter(Member.user_id == g.auth_user.id)
644+
).first()
645+
646+
if member_obj is None:
647+
raise NotFound("Group does not exist or you are not it's member")
648+
649+
if not member_obj.group_admin:
650+
raise Forbidden("You do not have group_admin role")
651+
652+
if group_obj.private or group_obj.immutable: # should it be more strict?
653+
raise Forbidden("You cannot invite users to this group")
654+
655+
invited_user_obj = (
656+
db.session.query(User).filter(User.login == invited_user)
657+
).first()
658+
659+
if invited_user_obj is None:
660+
raise NotFound("Inveted user does not exist")
661+
662+
if invited_user_obj.pending:
663+
raise Forbidden("Invited user is pending")
664+
665+
test_obj = (
666+
db.session.query(Member)
667+
.filter(Member.group_id == group_obj.id)
668+
.filter(Member.user_id == invited_user_obj.id)
669+
).first()
670+
if test_obj is not None:
671+
raise Conflict("Invited user is already a member of this group")
672+
673+
token = invited_user_obj.generate_group_invite_token(
674+
group_obj.id, g.auth_user.login
675+
)
676+
677+
try:
678+
send_email_notification(
679+
"invitation",
680+
"MWDB: You have been invited to a new group",
681+
invited_user_obj.email,
682+
base_url=app_config.mwdb.base_url,
683+
login=invited_user_obj.login,
684+
group_invite_token=token,
685+
)
686+
except MailError:
687+
logger.exception("Can't send e-mail notification")
688+
raise InternalServerError(
689+
"SMTP server needed to fulfill this request is"
690+
" not configured or unavailable."
691+
)
692+
693+
694+
@rate_limited_resource
695+
class JoinGroupInviteLinkResource(Resource):
696+
@requires_authorization
697+
def post(selt):
698+
"""
699+
---
700+
summary: Join group using invitation link
701+
description: |
702+
Join group using link
703+
704+
security:
705+
- bearerAuth: []
706+
tags:
707+
- group
708+
responses:
709+
200:
710+
description: When user joined group successfully
711+
400:
712+
description: When request body is invalid
713+
403:
714+
description: When there was a problem with the token
715+
409:
716+
description: When user is already a member of this group
717+
503:
718+
description: |
719+
Request canceled due to database statement timeout.
720+
"""
721+
token = request.args.get("token")
722+
723+
if token is None:
724+
raise Forbidden("Token not found")
725+
726+
invite_data = User.verify_group_invite_token(token)
727+
728+
if invite_data is None:
729+
raise Forbidden("There was a problem while decoding your token")

mwdb/templates/mail/invitation.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Hi {login}
2+
3+
You have been invited to join new group.
4+
5+
To view the invitation click this link: {base_url}/.../?token={group_invite_token}

mwdb/web/src/commons/api/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@ function setGroupAdmin(
412412
return axios.put(`/group/${name}/member/${member}`, { group_admin });
413413
}
414414

415+
function requestGroupInviteLink(name: string, invited_user: string){
416+
return axios.post(`/group/${name}/invite/${invited_user}`)
417+
}
418+
415419
function getUsers(): GetUsersResponse {
416420
return axios.get("/user", { timeout: undefined });
417421
}

0 commit comments

Comments
 (0)