mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-06-12 23:44:51 +02:00
basic infra
This commit is contained in:
parent
b331f61d0d
commit
b1ae220602
7 changed files with 244 additions and 6 deletions
|
@ -652,6 +652,9 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
"is_superuser",
|
"is_superuser",
|
||||||
"groups",
|
"groups",
|
||||||
"user_permissions",
|
"user_permissions",
|
||||||
|
"portfolio",
|
||||||
|
"portfolio_roles",
|
||||||
|
"portfolio_permissions",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-07-15 22:07
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0112_remove_contact_registrar_c_user_id_4059c4_idx_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="portfolio",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="user",
|
||||||
|
to="registrar.portfolio",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="portfolio_permissions",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("view_domains", "View all domains and domain reports"),
|
||||||
|
("edit_domains", "User is a manager on a domain"),
|
||||||
|
("view_member", "View members"),
|
||||||
|
("edit_member", "Create and edit members"),
|
||||||
|
("view_requests", "View requests"),
|
||||||
|
("edit_requests", "Create and edit requests"),
|
||||||
|
("view_portfolio", "View organization"),
|
||||||
|
("edit_portfolio", "Edit organization"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more permissions.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="portfolio_roles",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("organization_admin", "Admin"),
|
||||||
|
("organization_admin_read_only", "Admin read only"),
|
||||||
|
("organization_member", "Member"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more roles.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="portfolio",
|
||||||
|
name="creator",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text="Associated user",
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="creator",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -23,7 +23,7 @@ class Portfolio(TimeStampedModel):
|
||||||
|
|
||||||
# Stores who created this model. If no creator is specified in DJA,
|
# Stores who created this model. If no creator is specified in DJA,
|
||||||
# then the creator will default to the current request user"""
|
# then the creator will default to the current request user"""
|
||||||
creator = models.ForeignKey("registrar.User", on_delete=models.PROTECT, help_text="Associated user", unique=False)
|
creator = models.ForeignKey("registrar.User", on_delete=models.PROTECT, help_text="Associated user", related_name="creator", unique=False)
|
||||||
|
|
||||||
notes = models.TextField(
|
notes = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .transition_domain import TransitionDomain
|
||||||
from .verified_by_staff import VerifiedByStaff
|
from .verified_by_staff import VerifiedByStaff
|
||||||
from .domain import Domain
|
from .domain import Domain
|
||||||
from .domain_request import DomainRequest
|
from .domain_request import DomainRequest
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
|
||||||
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||||
|
|
||||||
|
@ -60,6 +61,56 @@ class User(AbstractUser):
|
||||||
# after they login.
|
# after they login.
|
||||||
FIXTURE_USER = "fixture_user", "Created by fixtures"
|
FIXTURE_USER = "fixture_user", "Created by fixtures"
|
||||||
|
|
||||||
|
class UserPortfolioRoleChoices(models.TextChoices):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
ORGANIZATION_ADMIN = "organization_admin", "Admin"
|
||||||
|
ORGANIZATION_ADMIN_READ_ONLY = "organization_admin_read_only", "Admin read only"
|
||||||
|
ORGANIZATION_MEMBER = "organization_member", "Member"
|
||||||
|
|
||||||
|
class UserPortfolioPermissionChoices(models.TextChoices):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
VIEW_DOMAINS = "view_domains", "View all domains and domain reports"
|
||||||
|
# EDIT_DOMAINS is really self.domains. We add is hear and leverage it in has_permission
|
||||||
|
# so we have one way to test for portfolio and domain edit permissions
|
||||||
|
# Do we need to check for portfolio domains specifically?
|
||||||
|
EDIT_DOMAINS = "edit_domains", "User is a manager on a domain"
|
||||||
|
|
||||||
|
VIEW_MEMBER = "view_member", "View members"
|
||||||
|
EDIT_MEMBER = "edit_member", "Create and edit members"
|
||||||
|
|
||||||
|
VIEW_REQUESTS = "view_requests", "View requests"
|
||||||
|
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
||||||
|
|
||||||
|
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
||||||
|
EDIT_PORTFOLIO = "edit_portfolio", "Edit organization"
|
||||||
|
|
||||||
|
|
||||||
|
PORTFOLIO_ROLE_PERMISSIONS = {
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
||||||
|
UserPortfolioPermissionChoices.VIEW_DOMAINS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBER,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBER,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
||||||
|
],
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
|
||||||
|
UserPortfolioPermissionChoices.VIEW_DOMAINS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBER,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
],
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# #### Constants for choice fields ####
|
# #### Constants for choice fields ####
|
||||||
RESTRICTED = "restricted"
|
RESTRICTED = "restricted"
|
||||||
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
|
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
|
||||||
|
@ -80,6 +131,34 @@ class User(AbstractUser):
|
||||||
related_name="users",
|
related_name="users",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
portfolio = models.ForeignKey(
|
||||||
|
"registrar.Portfolio",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="user",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio_roles = ArrayField(
|
||||||
|
models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=UserPortfolioRoleChoices.choices,
|
||||||
|
),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more roles.",
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio_permissions = ArrayField(
|
||||||
|
models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=UserPortfolioPermissionChoices.choices,
|
||||||
|
),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more permissions.",
|
||||||
|
)
|
||||||
|
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -168,6 +247,31 @@ class User(AbstractUser):
|
||||||
|
|
||||||
def has_contact_info(self):
|
def has_contact_info(self):
|
||||||
return bool(self.title or self.email or self.phone)
|
return bool(self.title or self.email or self.phone)
|
||||||
|
|
||||||
|
def has_role(self, role):
|
||||||
|
"""Do not rely on roles when testing for perms in views"""
|
||||||
|
return role in self.portfolio_roles if self.portfolio_roles else False
|
||||||
|
|
||||||
|
def has_portfolio_permissions(self, portfolio_permission):
|
||||||
|
"""The views should only call this guy when testing for perms and not rely on roles"""
|
||||||
|
|
||||||
|
if portfolio_permission == self.UserPortfolioPermissionChoices.EDIT_DOMAINS and self.domains.exists():
|
||||||
|
return self.domains
|
||||||
|
|
||||||
|
return portfolio_permission in self.portfolio_permissions if self.portfolio_permissions else False
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.update_permissions_from_roles()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def update_permissions_from_roles(self):
|
||||||
|
print('update permissions when saving')
|
||||||
|
new_portfolio_permissions = set(self.portfolio_permissions or [])
|
||||||
|
print(f'new_portfolio_permissions {new_portfolio_permissions}')
|
||||||
|
for role in self.portfolio_roles or []:
|
||||||
|
print(f'role {role}')
|
||||||
|
new_portfolio_permissions.update(self.PORTFOLIO_ROLE_PERMISSIONS.get(role, []))
|
||||||
|
self.portfolio_permissions = list(new_portfolio_permissions)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def needs_identity_verification(cls, email, uuid):
|
def needs_identity_verification(cls, email, uuid):
|
||||||
|
|
|
@ -146,9 +146,15 @@ class CheckPortfolioMiddleware:
|
||||||
if current_path == self.home:
|
if current_path == self.home:
|
||||||
if has_organization_feature_flag:
|
if has_organization_feature_flag:
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
user_portfolios = Portfolio.objects.filter(creator=request.user)
|
# user_portfolios = Portfolio.objects.filter(creator=request.user)
|
||||||
if user_portfolios.exists():
|
|
||||||
first_portfolio = user_portfolios.first()
|
required_permission = User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO
|
||||||
home_with_portfolio = reverse("portfolio-domains", kwargs={"portfolio_id": first_portfolio.id})
|
|
||||||
|
if request.user.has_portfolio_permissions(required_permission):
|
||||||
|
print('user has portfolio')
|
||||||
|
portfolio = request.user.portfolio
|
||||||
|
home_with_portfolio = reverse("portfolio-domains", kwargs={"portfolio_id": portfolio.id})
|
||||||
return HttpResponseRedirect(home_with_portfolio)
|
return HttpResponseRedirect(home_with_portfolio)
|
||||||
|
|
||||||
|
print('user does not have a portfolio')
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -9,6 +9,7 @@ from registrar.models import (
|
||||||
DomainInformation,
|
DomainInformation,
|
||||||
UserDomainRole,
|
UserDomainRole,
|
||||||
)
|
)
|
||||||
|
from registrar.models.user import User
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
@ -398,3 +399,28 @@ class UserProfilePermission(PermissionsLoginMixin):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioBasePermission(PermissionsLoginMixin):
|
||||||
|
"""Permission mixin that redirects to portfolio pages if user
|
||||||
|
has access, otherwise 403"""
|
||||||
|
|
||||||
|
def has_permission(self):
|
||||||
|
"""Check if this user has access to this portfolio.
|
||||||
|
|
||||||
|
The user is in self.request.user and the portfolio can be looked
|
||||||
|
up from the portfolio's primary key in self.kwargs["pk"]
|
||||||
|
"""
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# portfolio_id = self.kwargs["pk"]
|
||||||
|
# portfolio = Portfolio.objects.get(pk=portfolio_id)
|
||||||
|
|
||||||
|
# The 'Base' portfolio permission is VIEW_PORTFOLIO
|
||||||
|
required_permission = User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO
|
||||||
|
|
||||||
|
if not self.request.user.has_portfolio_permissions(required_permission):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import abc # abstract base class
|
import abc # abstract base class
|
||||||
|
|
||||||
from django.views.generic import DetailView, DeleteView, TemplateView
|
from django.views.generic import DetailView, DeleteView, TemplateView
|
||||||
from registrar.models import Domain, DomainRequest, DomainInvitation
|
from registrar.models import Domain, DomainRequest, DomainInvitation, Portfolio
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ from .mixins import (
|
||||||
DomainRequestWizardPermission,
|
DomainRequestWizardPermission,
|
||||||
UserDeleteDomainRolePermission,
|
UserDeleteDomainRolePermission,
|
||||||
UserProfilePermission,
|
UserProfilePermission,
|
||||||
|
PortfolioBasePermission,
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -163,3 +164,23 @@ class UserProfilePermissionView(UserProfilePermission, DetailView, abc.ABC):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def template_name(self):
|
def template_name(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioPermissionView(PortfolioBasePermission, DetailView, abc.ABC):
|
||||||
|
"""Abstract base view for portfolio views that enforces permissions.
|
||||||
|
|
||||||
|
This abstract view cannot be instantiated. Actual views must specify
|
||||||
|
`template_name`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# DetailView property for what model this is viewing
|
||||||
|
model = Portfolio
|
||||||
|
# variable name in template context for the model object
|
||||||
|
context_object_name = "portfolio"
|
||||||
|
|
||||||
|
# Abstract property enforces NotImplementedError on an attribute.
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def template_name(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue