Skip to content

Commit 519881a

Browse files
Add endpoints for group management
Relocate search-users endpoint Add tests for group creation Add admin endpoints for group listing and tweak user listing Restructure group routes and add test for search-groups Refactor API routes Fix group lookup for users Improve endpoint and testing for adding user to existing group Co-Authored-By: Benjamin Charmes <BenjaminCharmes@users.noreply.github.com>
1 parent 1167072 commit 519881a

File tree

14 files changed

+755
-54
lines changed

14 files changed

+755
-54
lines changed

pydatalab/schemas/cell.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,64 @@
381381
],
382382
"type": "string"
383383
},
384+
"Group": {
385+
"title": "Group",
386+
"description": "A model that describes a group of users, for the sake\nof applying group permissions.\n\nEach `Person` can point to multiple groups.\n\nRelationships between groups can be described via the `relationships`\nfield inherited from `Entry`.",
387+
"type": "object",
388+
"properties": {
389+
"type": {
390+
"title": "Type",
391+
"default": "groups",
392+
"const": "groups",
393+
"type": "string"
394+
},
395+
"immutable_id": {
396+
"title": "Immutable ID",
397+
"format": "uuid",
398+
"type": "string"
399+
},
400+
"last_modified": {
401+
"title": "Last Modified",
402+
"type": "string",
403+
"format": "date-time"
404+
},
405+
"relationships": {
406+
"title": "Relationships",
407+
"type": "array",
408+
"items": {
409+
"$ref": "#/definitions/TypedRelationship"
410+
}
411+
},
412+
"group_id": {
413+
"title": "Group Id",
414+
"minLength": 1,
415+
"maxLength": 40,
416+
"pattern": "^(?:[a-zA-Z0-9]+|[a-zA-Z0-9][a-zA-Z0-9._-]+[a-zA-Z0-9])$",
417+
"type": "string"
418+
},
419+
"display_name": {
420+
"title": "Display Name",
421+
"minLength": 1,
422+
"maxLength": 150,
423+
"type": "string"
424+
},
425+
"description": {
426+
"title": "Description",
427+
"type": "string"
428+
},
429+
"managers": {
430+
"title": "Managers",
431+
"type": "array",
432+
"items": {
433+
"type": "string"
434+
}
435+
}
436+
},
437+
"required": [
438+
"group_id",
439+
"display_name"
440+
]
441+
},
384442
"AccountStatus": {
385443
"title": "AccountStatus",
386444
"description": "A string enum representing the account status.",
@@ -452,6 +510,13 @@
452510
}
453511
]
454512
},
513+
"groups": {
514+
"title": "Groups",
515+
"type": "array",
516+
"items": {
517+
"$ref": "#/definitions/Group"
518+
}
519+
},
455520
"account_status": {
456521
"default": "unverified",
457522
"allOf": [

pydatalab/schemas/equipment.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,64 @@
345345
],
346346
"type": "string"
347347
},
348+
"Group": {
349+
"title": "Group",
350+
"description": "A model that describes a group of users, for the sake\nof applying group permissions.\n\nEach `Person` can point to multiple groups.\n\nRelationships between groups can be described via the `relationships`\nfield inherited from `Entry`.",
351+
"type": "object",
352+
"properties": {
353+
"type": {
354+
"title": "Type",
355+
"default": "groups",
356+
"const": "groups",
357+
"type": "string"
358+
},
359+
"immutable_id": {
360+
"title": "Immutable ID",
361+
"format": "uuid",
362+
"type": "string"
363+
},
364+
"last_modified": {
365+
"title": "Last Modified",
366+
"type": "string",
367+
"format": "date-time"
368+
},
369+
"relationships": {
370+
"title": "Relationships",
371+
"type": "array",
372+
"items": {
373+
"$ref": "#/definitions/TypedRelationship"
374+
}
375+
},
376+
"group_id": {
377+
"title": "Group Id",
378+
"minLength": 1,
379+
"maxLength": 40,
380+
"pattern": "^(?:[a-zA-Z0-9]+|[a-zA-Z0-9][a-zA-Z0-9._-]+[a-zA-Z0-9])$",
381+
"type": "string"
382+
},
383+
"display_name": {
384+
"title": "Display Name",
385+
"minLength": 1,
386+
"maxLength": 150,
387+
"type": "string"
388+
},
389+
"description": {
390+
"title": "Description",
391+
"type": "string"
392+
},
393+
"managers": {
394+
"title": "Managers",
395+
"type": "array",
396+
"items": {
397+
"type": "string"
398+
}
399+
}
400+
},
401+
"required": [
402+
"group_id",
403+
"display_name"
404+
]
405+
},
348406
"AccountStatus": {
349407
"title": "AccountStatus",
350408
"description": "A string enum representing the account status.",
@@ -416,6 +474,13 @@
416474
}
417475
]
418476
},
477+
"groups": {
478+
"title": "Groups",
479+
"type": "array",
480+
"items": {
481+
"$ref": "#/definitions/Group"
482+
}
483+
},
419484
"account_status": {
420485
"default": "unverified",
421486
"allOf": [

pydatalab/schemas/sample.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,64 @@
434434
],
435435
"type": "string"
436436
},
437+
"Group": {
438+
"title": "Group",
439+
"description": "A model that describes a group of users, for the sake\nof applying group permissions.\n\nEach `Person` can point to multiple groups.\n\nRelationships between groups can be described via the `relationships`\nfield inherited from `Entry`.",
440+
"type": "object",
441+
"properties": {
442+
"type": {
443+
"title": "Type",
444+
"default": "groups",
445+
"const": "groups",
446+
"type": "string"
447+
},
448+
"immutable_id": {
449+
"title": "Immutable ID",
450+
"format": "uuid",
451+
"type": "string"
452+
},
453+
"last_modified": {
454+
"title": "Last Modified",
455+
"type": "string",
456+
"format": "date-time"
457+
},
458+
"relationships": {
459+
"title": "Relationships",
460+
"type": "array",
461+
"items": {
462+
"$ref": "#/definitions/TypedRelationship"
463+
}
464+
},
465+
"group_id": {
466+
"title": "Group Id",
467+
"minLength": 1,
468+
"maxLength": 40,
469+
"pattern": "^(?:[a-zA-Z0-9]+|[a-zA-Z0-9][a-zA-Z0-9._-]+[a-zA-Z0-9])$",
470+
"type": "string"
471+
},
472+
"display_name": {
473+
"title": "Display Name",
474+
"minLength": 1,
475+
"maxLength": 150,
476+
"type": "string"
477+
},
478+
"description": {
479+
"title": "Description",
480+
"type": "string"
481+
},
482+
"managers": {
483+
"title": "Managers",
484+
"type": "array",
485+
"items": {
486+
"type": "string"
487+
}
488+
}
489+
},
490+
"required": [
491+
"group_id",
492+
"display_name"
493+
]
494+
},
437495
"AccountStatus": {
438496
"title": "AccountStatus",
439497
"description": "A string enum representing the account status.",
@@ -505,6 +563,13 @@
505563
}
506564
]
507565
},
566+
"groups": {
567+
"title": "Groups",
568+
"type": "array",
569+
"items": {
570+
"$ref": "#/definitions/Group"
571+
}
572+
},
508573
"account_status": {
509574
"default": "unverified",
510575
"allOf": [

pydatalab/schemas/startingmaterial.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,64 @@
487487
],
488488
"type": "string"
489489
},
490+
"Group": {
491+
"title": "Group",
492+
"description": "A model that describes a group of users, for the sake\nof applying group permissions.\n\nEach `Person` can point to multiple groups.\n\nRelationships between groups can be described via the `relationships`\nfield inherited from `Entry`.",
493+
"type": "object",
494+
"properties": {
495+
"type": {
496+
"title": "Type",
497+
"default": "groups",
498+
"const": "groups",
499+
"type": "string"
500+
},
501+
"immutable_id": {
502+
"title": "Immutable ID",
503+
"format": "uuid",
504+
"type": "string"
505+
},
506+
"last_modified": {
507+
"title": "Last Modified",
508+
"type": "string",
509+
"format": "date-time"
510+
},
511+
"relationships": {
512+
"title": "Relationships",
513+
"type": "array",
514+
"items": {
515+
"$ref": "#/definitions/TypedRelationship"
516+
}
517+
},
518+
"group_id": {
519+
"title": "Group Id",
520+
"minLength": 1,
521+
"maxLength": 40,
522+
"pattern": "^(?:[a-zA-Z0-9]+|[a-zA-Z0-9][a-zA-Z0-9._-]+[a-zA-Z0-9])$",
523+
"type": "string"
524+
},
525+
"display_name": {
526+
"title": "Display Name",
527+
"minLength": 1,
528+
"maxLength": 150,
529+
"type": "string"
530+
},
531+
"description": {
532+
"title": "Description",
533+
"type": "string"
534+
},
535+
"managers": {
536+
"title": "Managers",
537+
"type": "array",
538+
"items": {
539+
"type": "string"
540+
}
541+
}
542+
},
543+
"required": [
544+
"group_id",
545+
"display_name"
546+
]
547+
},
490548
"AccountStatus": {
491549
"title": "AccountStatus",
492550
"description": "A string enum representing the account status.",
@@ -558,6 +616,13 @@
558616
}
559617
]
560618
},
619+
"groups": {
620+
"title": "Groups",
621+
"type": "array",
622+
"items": {
623+
"$ref": "#/definitions/Group"
624+
}
625+
},
561626
"account_status": {
562627
"default": "unverified",
563628
"allOf": [

pydatalab/src/pydatalab/login.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from flask_login import LoginManager, UserMixin
1010

1111
from pydatalab.models import Person
12-
from pydatalab.models.people import AccountStatus, Identity, IdentityType
12+
from pydatalab.models.people import AccountStatus, Group, Identity, IdentityType
1313
from pydatalab.models.utils import UserRole
1414
from pydatalab.mongo import flask_mongo
1515

@@ -72,6 +72,11 @@ def identity_types(self) -> list[IdentityType]:
7272
"""Returns a list of the identity types associated with the user."""
7373
return [_.identity_type for _ in self.person.identities]
7474

75+
@property
76+
def groups(self) -> list[Group]:
77+
"""Returns the list of groups that the user is a member of."""
78+
return self.person.groups
79+
7580
def refresh(self) -> None:
7681
"""Reconstruct the user object from their database entry, to be used when,
7782
e.g., a new identity has been associated with them.
@@ -87,6 +92,19 @@ def get_by_id_cached(user_id):
8792
return get_by_id(user_id)
8893

8994

95+
def groups_lookup() -> dict:
96+
return {
97+
"from": "groups",
98+
"let": {"group_ids": "$groups.immutable_id"},
99+
"pipeline": [
100+
{"$match": {"$expr": {"$in": ["$_id", {"$ifNull": ["$$group_ids", []]}]}}},
101+
{"$sort": {"__order": 1}},
102+
{"$project": {"_id": 1, "display_name": 1, "group_id": 1}},
103+
],
104+
"as": "groups",
105+
}
106+
107+
90108
def get_by_id(user_id: str) -> LoginUser | None:
91109
"""Lookup the user database ID and create a new `LoginUser`
92110
with the relevant metadata.
@@ -100,7 +118,12 @@ def get_by_id(user_id: str) -> LoginUser | None:
100118
101119
"""
102120

103-
user = flask_mongo.db.users.find_one({"_id": ObjectId(user_id)})
121+
user = flask_mongo.db.users.aggregate(
122+
[
123+
{"$match": {"_id": ObjectId(user_id)}},
124+
{"$lookup": groups_lookup()},
125+
]
126+
).next()
104127
if not user:
105128
return None
106129

0 commit comments

Comments
 (0)