Skip to content

Commit b8a777c

Browse files
25249 - Add/remove Contact Centre Staff and Maximus Staff (#3276)
1 parent 854ef99 commit b8a777c

File tree

11 files changed

+119
-19
lines changed

11 files changed

+119
-19
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Rename CONTACT_CENTER_STAFF to CC_STAFF in org_types
2+
3+
Revision ID: 61eb9f696d5e
4+
Revises: 7f48833011c3
5+
Create Date: 2025-03-03 11:28:30.538876
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '61eb9f696d5e'
14+
down_revision = '7f48833011c3'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.execute("""
21+
UPDATE org_types
22+
SET code = 'CC_STAFF'
23+
WHERE code = 'CONTACT_CENTRE_STAFF';
24+
""")
25+
26+
27+
def downgrade():
28+
op.execute("""
29+
UPDATE org_types
30+
SET code = 'CONTACT_CENTRE_STAFF'
31+
WHERE code = 'CC_STAFF';
32+
""")

auth-api/src/auth_api/exceptions/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ class Error(Enum):
6565
DELETE_FAILED_INACTIVE_USER = "User is already inactive", HTTPStatus.BAD_REQUEST
6666
CHANGE_ROLE_FAILED_ONLY_OWNER = "User is only Account Administrator in org", HTTPStatus.BAD_REQUEST
6767
OWNER_CANNOT_BE_REMOVED = "Account Administrator cannot be removed by anyone", HTTPStatus.BAD_REQUEST
68+
MAX_NUMBER_OF_STAFF_ORGS_LIMIT = (
69+
"Maximum number of staff organizations reached for this user",
70+
HTTPStatus.BAD_REQUEST,
71+
)
6872
MAX_NUMBER_OF_ORGS_LIMIT = "Maximum number of organisations reached", HTTPStatus.BAD_REQUEST
6973
ALREADY_CLAIMED_PASSCODE = "Passcode you entered has already been claimed", HTTPStatus.NOT_ACCEPTABLE
7074
ORG_CANNOT_BE_DISSOLVED = "Organization cannot be dissolved", HTTPStatus.NOT_ACCEPTABLE

auth-api/src/auth_api/models/membership.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,18 @@ def find_memberships_by_user_ids(cls, user_id: int) -> List[Membership]:
174174

175175
return records
176176

177+
@classmethod
178+
def find_memberships_by_user_id_and_status(cls, user_id: int, status: str) -> List[Membership]:
179+
"""Get the memberships for the specified user ids."""
180+
records = (
181+
cls.query.filter(cls.user_id == int(user_id or -1))
182+
.filter(cls.status == status)
183+
.order_by(desc(Membership.created))
184+
.all()
185+
)
186+
187+
return records
188+
177189
@classmethod
178190
def find_membership_by_user_and_org_all_status(cls, user_id: int, org_id: int) -> Membership:
179191
"""Get the membership for the specified user and org with all membership statuses."""

auth-api/src/auth_api/models/org.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ def find_by_org_id(cls, org_id: int):
124124
"""Find an Org instance that matches the provided id."""
125125
return cls.query.filter_by(id=int(org_id or -1)).first()
126126

127+
@classmethod
128+
def find_by_org_ids_and_org_types(cls, org_ids: List[int], org_types: List[str]):
129+
"""Find all Org instances that match the provided ids and org types."""
130+
return cls.query.filter(Org.id.in_(org_ids), Org.type_code.in_(org_types)).all()
131+
127132
@classmethod
128133
def find_by_bcol_id(cls, bcol_account_id):
129134
"""Find an Org instance that matches the provided id and not in INACTIVE status."""

auth-api/src/auth_api/services/keycloak.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ def remove_user_from_group(user_id: str, group_name: str):
353353
# Get the '$group_name' group
354354
group_id = KeycloakService._get_group_id(admin_token, group_name)
355355

356-
# Add user to the keycloak group '$group_name'
356+
# Remove user from keycloak group '$group_name'
357357
headers = {"Content-Type": ContentType.JSON.value, "Authorization": f"Bearer {admin_token}"}
358358
remove_group_url = f"{base_url}/auth/admin/realms/{realm}/users/{user_id}/groups/{group_id}"
359359
response = requests.delete(remove_group_url, headers=headers, timeout=timeout)

auth-api/src/auth_api/services/membership.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@
3232
from auth_api.models import Org as OrgModel
3333
from auth_api.models.dataclass import Activity
3434
from auth_api.schemas import MembershipSchema
35-
from auth_api.utils.enums import ActivityAction, LoginSource, NotificationType, Status
36-
from auth_api.utils.roles import ADMIN, ALL_ALLOWED_ROLES, COORDINATOR, STAFF
35+
from auth_api.utils.constants import GROUP_CONTACT_CENTRE_STAFF, GROUP_MAXIMUS_STAFF
36+
from auth_api.utils.enums import ActivityAction, LoginSource, NotificationType, OrgType, Status
37+
from auth_api.utils.roles import ADMIN, ALL_ALLOWED_ROLES, COORDINATOR, STAFF, USER
3738
from auth_api.utils.user_context import UserContext, user_context
3839

3940
from ..utils.account_mailer import publish_to_mailer
@@ -47,6 +48,11 @@
4748
CONFIG = get_named_config()
4849
logger = StructuredLogging.get_logger()
4950

51+
org_type_to_group_mapping = {
52+
OrgType.MAXIMUS_STAFF.value: GROUP_MAXIMUS_STAFF,
53+
OrgType.CONTACT_CENTRE_STAFF.value: GROUP_CONTACT_CENTRE_STAFF,
54+
}
55+
5056

5157
class Membership: # pylint: disable=too-many-instance-attributes,too-few-public-methods
5258
"""Manages all aspects of the Membership Entity.
@@ -214,6 +220,10 @@ def update_membership(self, updated_fields, **kwargs):
214220
logger.debug("<update_membership")
215221
user_from_context: UserContext = kwargs["user_context"]
216222
check_auth(org_id=self._model.org_id, one_of_roles=(COORDINATOR, ADMIN, STAFF))
223+
updated_membership_status = updated_fields.get("membership_status")
224+
225+
# When adding to organization, check if user is already in an ACTIVE STAFF org, if so raise exception
226+
Membership._check_if_add_user_has_active_staff_org(updated_membership_status, self._model.user.id)
217227

218228
# bceid Members cant be ADMIN's.Unless they have an affidavit approved.
219229
# TODO when multiple teams for bceid are present , do if the user has affidavit present check
@@ -225,8 +235,8 @@ def update_membership(self, updated_fields, **kwargs):
225235
if self._model.membership_type.code == COORDINATOR and updated_fields.get("membership_type", None) == ADMIN:
226236
check_auth(org_id=self._model.org_id, one_of_roles=(ADMIN, STAFF))
227237

228-
updated_membership_status = updated_fields.get("membership_status")
229238
admin_getting_removed: bool = False
239+
230240
# Admin can be removed by other admin or staff. #4909
231241
if (
232242
updated_membership_status
@@ -317,14 +327,41 @@ def _add_or_remove_group(model: MembershipModel):
317327
"""Add or remove the user from/to account holders / product keycloak group."""
318328
if model.membership_status.id == Status.ACTIVE.value:
319329
KeycloakService.join_account_holders_group(model.user.keycloak_guid)
320-
elif (
321-
model.membership_status.id == Status.INACTIVE.value
322-
and len(MembershipModel.find_orgs_for_user(model.user.id)) == 0
323-
):
330+
elif model.membership_status.id == Status.INACTIVE.value:
324331
# Check if the user has any other active org membership, if none remove from the group
325-
KeycloakService.remove_from_account_holders_group(model.user.keycloak_guid)
332+
if len(MembershipModel.find_orgs_for_user(model.user.id)) == 0:
333+
KeycloakService.remove_from_account_holders_group(model.user.keycloak_guid)
334+
335+
# Add or Remove from STAFF group in keycloak
336+
Membership._add_or_remove_group_for_staff(model)
326337
ProductService.update_users_products_keycloak_groups([model.user.id])
327338

339+
@staticmethod
340+
def _add_or_remove_group_for_staff(model: MembershipModel):
341+
mapping_group = org_type_to_group_mapping.get(model.org.type_code)
342+
if not mapping_group:
343+
return
344+
345+
user_groups = KeycloakService.get_user_groups(model.user.keycloak_guid)
346+
is_in_group = any(group["name"] == mapping_group for group in user_groups)
347+
348+
if model.membership_status.id == Status.ACTIVE.value and not is_in_group:
349+
KeycloakService.add_user_to_group(model.user.keycloak_guid, mapping_group)
350+
elif model.membership_status.id == Status.INACTIVE.value and is_in_group:
351+
KeycloakService.remove_user_from_group(model.user.keycloak_guid, mapping_group)
352+
353+
@staticmethod
354+
def _check_if_add_user_has_active_staff_org(updated_membership_status, user_id):
355+
"""Check if user is already associated with an active STAFF org."""
356+
staff_org_types = list(org_type_to_group_mapping.keys())
357+
memberships = MembershipModel.find_memberships_by_user_id_and_status(user_id, Status.ACTIVE.value)
358+
staff_orgs = OrgModel.find_by_org_ids_and_org_types(
359+
org_ids=[membership.org_id for membership in memberships], org_types=staff_org_types
360+
)
361+
362+
if updated_membership_status and updated_membership_status.id == Status.ACTIVE.value and len(staff_orgs) > 0:
363+
raise BusinessException(Error.MAX_NUMBER_OF_STAFF_ORGS_LIMIT, None)
364+
328365
@staticmethod
329366
def get_membership_for_org_and_user(org_id, user_id):
330367
"""Get the membership for the given org and user id."""

auth-api/src/auth_api/utils/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
GROUP_GOV_ACCOUNT_USERS = "gov_account_users"
2121
GROUP_API_GW_USERS = "api_gateway_users"
2222
GROUP_API_GW_SANDBOX_USERS = "api_gateway_sandbox_users"
23+
GROUP_MAXIMUS_STAFF = "maximus_staff"
24+
GROUP_CONTACT_CENTRE_STAFF = "contact_centre_staff"
2325

2426
# Affidavit folder
2527
AFFIDAVIT_FOLDER_NAME = "Affidavits"

auth-api/src/auth_api/utils/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class OrgType(Enum):
103103
STAFF = "STAFF"
104104
SBC_STAFF = "SBC_STAFF"
105105
MAXIMUS_STAFF = "MAXIMUS_STAFF"
106-
CONTACT_CENTRE_STAFF = "CONTACT_CENTRE_STAFF"
106+
CONTACT_CENTRE_STAFF = "CC_STAFF"
107107

108108

109109
class DocumentType(Enum):

auth-web/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

auth-web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "auth-web",
3-
"version": "2.8.15",
3+
"version": "2.8.16",
44
"appName": "Auth Web",
55
"sbcName": "SBC Common Components",
66
"private": true,

0 commit comments

Comments
 (0)