Merge pull request #746 from dod-ccpo/application_roles
Application roles
This commit is contained in:
@@ -2,11 +2,19 @@ from atst.utils import first_or_none
|
||||
from atst.models.permissions import Permissions
|
||||
from atst.domain.exceptions import UnauthorizedError
|
||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
||||
from atst.models.application_role import Status as ApplicationRoleStatus
|
||||
|
||||
|
||||
class Authorization(object):
|
||||
@classmethod
|
||||
def has_atat_permission(cls, user, permission):
|
||||
return permission in user.permissions
|
||||
|
||||
@classmethod
|
||||
def has_portfolio_permission(cls, user, portfolio, permission):
|
||||
if Authorization.has_atat_permission(user, permission):
|
||||
return True
|
||||
|
||||
port_role = first_or_none(
|
||||
lambda pr: pr.portfolio == portfolio, user.portfolio_roles
|
||||
)
|
||||
@@ -16,22 +24,37 @@ class Authorization(object):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def has_atat_permission(cls, user, permission):
|
||||
return permission in user.permissions
|
||||
def has_application_permission(cls, user, application, permission):
|
||||
if Authorization.has_portfolio_permission(
|
||||
user, application.portfolio, permission
|
||||
):
|
||||
return True
|
||||
|
||||
app_role = first_or_none(
|
||||
lambda app_role: app_role.application == application, user.application_roles
|
||||
)
|
||||
if app_role and app_role.status is not ApplicationRoleStatus.DISABLED:
|
||||
return permission in app_role.permissions
|
||||
else:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def check_portfolio_permission(cls, user, portfolio, permission, message):
|
||||
if not (
|
||||
Authorization.has_atat_permission(user, permission)
|
||||
or Authorization.has_portfolio_permission(user, portfolio, permission)
|
||||
):
|
||||
def check_atat_permission(cls, user, permission, message):
|
||||
if not Authorization.has_atat_permission(user, permission):
|
||||
raise UnauthorizedError(user, message)
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def check_atat_permission(cls, user, permission, message):
|
||||
if not Authorization.has_atat_permission(user, permission):
|
||||
def check_portfolio_permission(cls, user, portfolio, permission, message):
|
||||
if not Authorization.has_portfolio_permission(user, portfolio, permission):
|
||||
raise UnauthorizedError(user, message)
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def check_application_permission(cls, user, portfolio, permission, message):
|
||||
if not Authorization.has_application_permission(user, portfolio, permission):
|
||||
raise UnauthorizedError(user, message)
|
||||
|
||||
return True
|
||||
@@ -70,8 +93,12 @@ class Authorization(object):
|
||||
raise UnauthorizedError(user, message)
|
||||
|
||||
|
||||
def user_can_access(user, permission, portfolio=None, message=None):
|
||||
if portfolio:
|
||||
def user_can_access(user, permission, portfolio=None, application=None, message=None):
|
||||
if application:
|
||||
Authorization.check_application_permission(
|
||||
user, application, permission, message
|
||||
)
|
||||
elif portfolio:
|
||||
Authorization.check_portfolio_permission(user, portfolio, permission, message)
|
||||
else:
|
||||
Authorization.check_atat_permission(user, permission, message)
|
||||
|
@@ -15,6 +15,7 @@ def check_access(permission, message, override, *args, **kwargs):
|
||||
|
||||
if "application_id" in kwargs:
|
||||
application = Applications.get(kwargs["application_id"])
|
||||
access_args["application"] = application
|
||||
access_args["portfolio"] = application.portfolio
|
||||
|
||||
elif "task_order_id" in kwargs:
|
||||
|
@@ -18,6 +18,11 @@ class PermissionSets(object):
|
||||
PORTFOLIO_POC = "portfolio_poc"
|
||||
VIEW_AUDIT_LOG = "view_audit_log"
|
||||
|
||||
VIEW_APPLICATION = "view_application"
|
||||
EDIT_APPLICATION_ENVIRONMENTS = "edit_application_environments"
|
||||
EDIT_APPLICATION_TEAM = "edit_application_team"
|
||||
DELETE_APPLICATION_ENVIRONMENTS = "delete_application_environments"
|
||||
|
||||
@classmethod
|
||||
def get(cls, perms_set_name):
|
||||
try:
|
||||
@@ -85,6 +90,8 @@ _PORTFOLIO_APP_MGMT_PERMISSION_SETS = [
|
||||
Permissions.CREATE_APPLICATION_MEMBER,
|
||||
Permissions.EDIT_ENVIRONMENT,
|
||||
Permissions.CREATE_ENVIRONMENT,
|
||||
Permissions.DELETE_ENVIRONMENT,
|
||||
Permissions.ASSIGN_ENVIRONMENT_MEMBER,
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -167,3 +174,51 @@ PORTFOLIO_PERMISSION_SETS = (
|
||||
+ _PORTFOLIO_ADMIN_PERMISSION_SETS
|
||||
+ _PORTFOLIO_POC_PERMISSION_SETS
|
||||
)
|
||||
|
||||
_APPLICATION_BASIC_PERMISSION_SET = {
|
||||
"name": PermissionSets.VIEW_APPLICATION,
|
||||
"description": "View application data",
|
||||
"display_name": "View applications",
|
||||
"permissions": [
|
||||
Permissions.VIEW_APPLICATION,
|
||||
Permissions.VIEW_APPLICATION_MEMBER,
|
||||
Permissions.VIEW_ENVIRONMENT,
|
||||
],
|
||||
}
|
||||
|
||||
# need perm to assign and unassign users to environments
|
||||
_APPLICATION_ENVIRONMENTS_PERMISSION_SET = {
|
||||
"name": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
|
||||
"description": "Manage environments for an application",
|
||||
"display_name": "Manage environments",
|
||||
"permissions": [
|
||||
Permissions.EDIT_ENVIRONMENT,
|
||||
Permissions.CREATE_ENVIRONMENT,
|
||||
Permissions.ASSIGN_ENVIRONMENT_MEMBER,
|
||||
],
|
||||
}
|
||||
|
||||
_APPLICATION_TEAM_PERMISSION_SET = {
|
||||
"name": PermissionSets.EDIT_APPLICATION_TEAM,
|
||||
"description": "Manage team members for an application",
|
||||
"display_name": "Manage team",
|
||||
"permissions": [
|
||||
Permissions.EDIT_APPLICATION_MEMBER,
|
||||
Permissions.CREATE_APPLICATION_MEMBER,
|
||||
Permissions.ASSIGN_ENVIRONMENT_MEMBER,
|
||||
],
|
||||
}
|
||||
|
||||
_APPLICATION_ENVIRONMENT_DELETE_PERMISSION_SET = {
|
||||
"name": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
|
||||
"description": "Delete environments within an application",
|
||||
"display_name": "Delete environments",
|
||||
"permissions": [Permissions.DELETE_ENVIRONMENT],
|
||||
}
|
||||
|
||||
APPLICATION_PERMISSION_SETS = [
|
||||
_APPLICATION_BASIC_PERMISSION_SET,
|
||||
_APPLICATION_TEAM_PERMISSION_SET,
|
||||
_APPLICATION_ENVIRONMENTS_PERMISSION_SET,
|
||||
_APPLICATION_ENVIRONMENT_DELETE_PERMISSION_SET,
|
||||
]
|
||||
|
@@ -6,6 +6,8 @@ from .permissions import Permissions
|
||||
from .permission_set import PermissionSet
|
||||
from .user import User
|
||||
from .portfolio_role import PortfolioRole
|
||||
from .application_role import ApplicationRole
|
||||
from .environment_role import EnvironmentRole
|
||||
from .portfolio import Portfolio
|
||||
from .application import Application
|
||||
from .environment import Environment
|
||||
|
@@ -16,10 +16,11 @@ class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False)
|
||||
portfolio = relationship("Portfolio")
|
||||
environments = relationship("Environment", back_populates="application")
|
||||
roles = relationship("ApplicationRole")
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
return set([user for env in self.environments for user in env.users])
|
||||
return set(role.user for role in self.roles)
|
||||
|
||||
@property
|
||||
def num_users(self):
|
||||
|
68
atst/models/application_role.py
Normal file
68
atst/models/application_role.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from enum import Enum
|
||||
from sqlalchemy import Index, ForeignKey, Column, Enum as SQLAEnum, Table
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.event import listen
|
||||
|
||||
from atst.models import Base, mixins
|
||||
from atst.models.mixins.auditable import record_permission_sets_updates
|
||||
from .types import Id
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
ACTIVE = "active"
|
||||
DISABLED = "disabled"
|
||||
PENDING = "pending"
|
||||
|
||||
|
||||
application_roles_permission_sets = Table(
|
||||
"application_roles_permission_sets",
|
||||
Base.metadata,
|
||||
Column(
|
||||
"application_role_id", UUID(as_uuid=True), ForeignKey("application_roles.id")
|
||||
),
|
||||
Column("permission_set_id", UUID(as_uuid=True), ForeignKey("permission_sets.id")),
|
||||
)
|
||||
|
||||
|
||||
class ApplicationRole(
|
||||
Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.PermissionsMixin
|
||||
):
|
||||
__tablename__ = "application_roles"
|
||||
|
||||
id = Id()
|
||||
application_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("applications.id"), index=True, nullable=False
|
||||
)
|
||||
application = relationship("Application", back_populates="roles")
|
||||
|
||||
user_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=False
|
||||
)
|
||||
|
||||
status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING)
|
||||
|
||||
permission_sets = relationship(
|
||||
"PermissionSet", secondary=application_roles_permission_sets
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<ApplicationRole(application='{}', user_id='{}', id='{}', permissions={})>".format(
|
||||
self.application.name, self.user_id, self.id, self.permissions
|
||||
)
|
||||
|
||||
|
||||
Index(
|
||||
"application_role_user_application",
|
||||
ApplicationRole.user_id,
|
||||
ApplicationRole.application_id,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
|
||||
listen(
|
||||
ApplicationRole.permission_sets,
|
||||
"bulk_replace",
|
||||
record_permission_sets_updates,
|
||||
raw=True,
|
||||
)
|
@@ -98,3 +98,18 @@ class AuditableMixin(object):
|
||||
@property
|
||||
def displayname(self):
|
||||
return None
|
||||
|
||||
|
||||
def record_permission_sets_updates(instance_state, permission_sets, initiator):
|
||||
old_perm_sets = instance_state.attrs.get("permission_sets").value
|
||||
if instance_state.persistent and old_perm_sets != permission_sets:
|
||||
connection = instance_state.session.connection()
|
||||
old_state = [p.name for p in old_perm_sets]
|
||||
new_state = [p.name for p in permission_sets]
|
||||
changed_state = {"permission_sets": (old_state, new_state)}
|
||||
instance_state.object.create_audit_event(
|
||||
connection,
|
||||
instance_state.object,
|
||||
ACTION_UPDATE,
|
||||
changed_state=changed_state,
|
||||
)
|
||||
|
@@ -14,6 +14,8 @@ class Permissions(object):
|
||||
VIEW_ENVIRONMENT = "view_environment"
|
||||
EDIT_ENVIRONMENT = "edit_environment"
|
||||
CREATE_ENVIRONMENT = "create_environment"
|
||||
DELETE_ENVIRONMENT = "delete_environment"
|
||||
ASSIGN_ENVIRONMENT_MEMBER = "assign_environment_member"
|
||||
|
||||
# funding
|
||||
VIEW_PORTFOLIO_FUNDING = "view_portfolio_funding" # TO summary page
|
||||
|
@@ -1,7 +1,8 @@
|
||||
from enum import Enum
|
||||
from sqlalchemy import Index, ForeignKey, Column, Enum as SQLAEnum, Table, event
|
||||
from sqlalchemy import Index, ForeignKey, Column, Enum as SQLAEnum, Table
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.event import listen
|
||||
|
||||
from atst.models import Base, mixins
|
||||
from .types import Id
|
||||
@@ -11,7 +12,7 @@ from atst.utils import first_or_none
|
||||
from atst.models.environment_role import EnvironmentRole
|
||||
from atst.models.application import Application
|
||||
from atst.models.environment import Environment
|
||||
from atst.models.mixins.auditable import ACTION_UPDATE as AUDIT_ACTION_UPDATE
|
||||
from atst.models.mixins.auditable import record_permission_sets_updates
|
||||
|
||||
|
||||
MEMBER_STATUSES = {
|
||||
@@ -168,17 +169,9 @@ Index(
|
||||
)
|
||||
|
||||
|
||||
@event.listens_for(PortfolioRole.permission_sets, "bulk_replace", raw=True)
|
||||
def record_permission_sets_updates(instance_state, permission_sets, initiator):
|
||||
old_perm_sets = instance_state.attrs.get("permission_sets").value
|
||||
if instance_state.persistent and old_perm_sets != permission_sets:
|
||||
connection = instance_state.session.connection()
|
||||
old_state = [p.name for p in old_perm_sets]
|
||||
new_state = [p.name for p in permission_sets]
|
||||
changed_state = {"permission_sets": (old_state, new_state)}
|
||||
instance_state.object.create_audit_event(
|
||||
connection,
|
||||
instance_state.object,
|
||||
AUDIT_ACTION_UPDATE,
|
||||
changed_state=changed_state,
|
||||
)
|
||||
listen(
|
||||
PortfolioRole.permission_sets,
|
||||
"bulk_replace",
|
||||
record_permission_sets_updates,
|
||||
raw=True,
|
||||
)
|
||||
|
@@ -25,6 +25,7 @@ class User(
|
||||
permission_sets = relationship("PermissionSet", secondary=users_permission_sets)
|
||||
|
||||
portfolio_roles = relationship("PortfolioRole", backref="user")
|
||||
application_roles = relationship("ApplicationRole", backref="user")
|
||||
|
||||
email = Column(String, unique=True)
|
||||
dod_id = Column(String, unique=True, nullable=False)
|
||||
|
Reference in New Issue
Block a user