Merge pull request #746 from dod-ccpo/application_roles

Application roles
This commit is contained in:
dandds
2019-04-08 14:22:52 -04:00
committed by GitHub
17 changed files with 406 additions and 60 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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,
]

View File

@@ -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

View File

@@ -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):

View 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,
)

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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)