manage.get.gov/src/registrar/models/user_portfolio_permission.py
2025-02-15 08:16:17 -05:00

255 lines
10 KiB
Python

from django.db import models
from registrar.models.user_domain_role import UserDomainRole
from registrar.models.utility.portfolio_helper import (
UserPortfolioPermissionChoices,
UserPortfolioRoleChoices,
DomainRequestPermissionDisplay,
MemberPermissionDisplay,
cleanup_after_portfolio_member_deletion,
get_domain_requests_display,
get_domains_display,
get_members_display,
get_role_display,
validate_user_portfolio_permission,
)
from .utility.time_stamped_model import TimeStampedModel
from django.contrib.postgres.fields import ArrayField
class UserPortfolioPermission(TimeStampedModel):
"""This is a linking table that connects a user with a role on a portfolio."""
class Meta:
unique_together = ["user", "portfolio"]
PORTFOLIO_ROLE_PERMISSIONS = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
],
# NOTE: Check FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS before adding roles here.
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
],
}
# Determines which roles are forbidden for certain role types to possess.
# Used to throw a ValidationError on clean() for UserPortfolioPermission and PortfolioInvitation.
FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS = {
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
}
user = models.ForeignKey(
"registrar.User",
null=False,
# when a user is deleted, permissions are too
on_delete=models.CASCADE,
related_name="portfolio_permissions",
)
portfolio = models.ForeignKey(
"registrar.Portfolio",
null=False,
# when a portfolio is deleted, permissions are too
on_delete=models.CASCADE,
related_name="portfolio_users",
)
roles = ArrayField(
models.CharField(
max_length=50,
choices=UserPortfolioRoleChoices.choices,
),
null=True,
blank=True,
help_text="Select one or more roles.",
)
additional_permissions = ArrayField(
models.CharField(
max_length=50,
choices=UserPortfolioPermissionChoices.choices,
),
null=True,
blank=True,
help_text="Select one or more additional permissions.",
)
def __str__(self):
readable_roles = []
if self.roles:
readable_roles = self.get_readable_roles()
return f"{self.user}" f" <Roles: {', '.join(readable_roles)}>" if self.roles else ""
def get_readable_roles(self):
"""Returns a readable list of self.roles"""
readable_roles = []
if self.roles:
readable_roles = sorted(
[UserPortfolioRoleChoices.get_user_portfolio_role_label(role) for role in self.roles]
)
return readable_roles
def get_managed_domains_count(self):
"""Return the count of domains managed by the user for this portfolio."""
# Filter the UserDomainRole model to get domains where the user has a manager role
managed_domains = UserDomainRole.objects.filter(
user=self.user, role=UserDomainRole.Roles.MANAGER, domain__domain_info__portfolio=self.portfolio
).count()
return managed_domains
def _get_portfolio_permissions(self):
"""
Retrieve the permissions for the user's portfolio roles.
"""
return self.get_portfolio_permissions(self.roles, self.additional_permissions)
@classmethod
def get_portfolio_permissions(cls, roles, additional_permissions, get_list=True):
"""Class method to return a list of permissions based on roles and addtl permissions.
Params:
roles => An array of roles
additional_permissions => An array of additional_permissions
get_list => If true, returns a list of perms. If false, returns a set of perms.
"""
# Use a set to avoid duplicate permissions
portfolio_permissions = set()
if roles:
for role in roles:
portfolio_permissions.update(cls.PORTFOLIO_ROLE_PERMISSIONS.get(role, []))
if additional_permissions:
portfolio_permissions.update(additional_permissions)
return list(portfolio_permissions) if get_list else portfolio_permissions
@classmethod
def get_domain_request_permission_display(cls, roles, additional_permissions):
"""Class method to return a readable string for domain request permissions"""
# Tracks if they can view, create requests, or not do anything
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions)
all_domain_perms = [
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
]
if all(perm in all_permissions for perm in all_domain_perms):
return DomainRequestPermissionDisplay.VIEWER_REQUESTER
elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions:
return DomainRequestPermissionDisplay.VIEWER
else:
return DomainRequestPermissionDisplay.NONE
@classmethod
def get_member_permission_display(cls, roles, additional_permissions):
"""Class method to return a readable string for member permissions"""
# Tracks if they can view, create requests, or not do anything.
# This is different than get_domain_request_permission_display because member tracks
# permissions slightly differently.
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions)
if UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions:
return MemberPermissionDisplay.MANAGER
elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions:
return MemberPermissionDisplay.VIEWER
else:
return MemberPermissionDisplay.NONE
@classmethod
def get_forbidden_permissions(cls, roles, additional_permissions):
"""Some permissions are forbidden for certain roles, like member.
This checks for conflicts between the current permission list and forbidden perms."""
# Get the portfolio permissions that the user currently possesses
portfolio_permissions = set(cls.get_portfolio_permissions(roles, additional_permissions))
# Get intersection of forbidden permissions across all roles.
# This is because if you have roles ["admin", "member"], then they can have the
# so called "forbidden" ones. But just member on their own cannot.
# The solution to this is to only grab what is only COMMONLY "forbidden".
# This will scale if we add more roles in the future.
# This is thes same as applying the `&` operator across all sets for each role.
common_forbidden_perms = (
set.intersection(*[set(cls.FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS.get(role, [])) for role in roles])
if roles
else set()
)
# Check if the users current permissions overlap with any forbidden permissions
# by getting the intersection between current user permissions, and forbidden ones.
# This is the same as portfolio_permissions & common_forbidden_perms.
return portfolio_permissions.intersection(common_forbidden_perms)
@property
def role_display(self):
"""
Returns a human-readable display name for the user's role.
Uses the `get_role_display` function to determine if the user is an "Admin",
"Basic" member, or has no role assigned.
Returns:
str: The display name of the user's role.
"""
return get_role_display(self.roles)
@property
def domains_display(self):
"""
Returns a string representation of the user's domain access level.
Uses the `get_domains_display` function to determine whether the user has
"Viewer" access (can view all domains) or "Viewer, limited" access.
Returns:
str: The display name of the user's domain permissions.
"""
return get_domains_display(self.roles, self.additional_permissions)
@property
def domain_requests_display(self):
"""
Returns a string representation of the user's access to domain requests.
Uses the `get_domain_requests_display` function to determine if the user
is a "Creator" (can create and edit requests), a "Viewer" (can only view requests),
or has "No access" to domain requests.
Returns:
str: The display name of the user's domain request permissions.
"""
return get_domain_requests_display(self.roles, self.additional_permissions)
@property
def members_display(self):
"""
Returns a string representation of the user's access to managing members.
Uses the `get_members_display` function to determine if the user is a
"Manager" (can edit members), a "Viewer" (can view members), or has "No access"
to member management.
Returns:
str: The display name of the user's member management permissions.
"""
return get_members_display(self.roles, self.additional_permissions)
def clean(self):
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
super().clean()
validate_user_portfolio_permission(self)
def delete(self, *args, **kwargs):
user = self.user # Capture the user before the instance is deleted
portfolio = self.portfolio # Capture the portfolio before the instance is deleted
# Call the superclass delete method to actually delete the instance
super().delete(*args, **kwargs)
cleanup_after_portfolio_member_deletion(portfolio=portfolio, email=user.email, user=user)