Merge pull request #701 from dod-ccpo/portfolio-perms

Portfolio perms
This commit is contained in:
dandds 2019-03-18 08:40:35 -04:00 committed by GitHub
commit 13bc7f56b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 940 additions and 738 deletions

View File

@ -0,0 +1,30 @@
"""remove portfolio_roles role_id
Revision ID: 0e71ab219ada
Revises: 938a31795096
Create Date: 2019-03-12 05:58:17.029899
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '0e71ab219ada'
down_revision = '938a31795096'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('workspace_roles_role_id_fkey', 'portfolio_roles', type_='foreignkey')
op.drop_column('portfolio_roles', 'role_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('portfolio_roles', sa.Column('role_id', postgresql.UUID(), autoincrement=False, nullable=True))
op.create_foreign_key('workspace_roles_role_id_fkey', 'portfolio_roles', 'roles', ['role_id'], ['id'])
# ### end Alembic commands ###

View File

@ -0,0 +1,33 @@
"""add permission sets to portfolio_role
Revision ID: 938a31795096
Revises: db161adbafdf
Create Date: 2019-03-07 06:13:05.400911
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '938a31795096'
down_revision = 'db161adbafdf'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('portfolio_roles_roles',
sa.Column('portfolio_role_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('role_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.ForeignKeyConstraint(['portfolio_role_id'], ['portfolio_roles.id'], ),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('portfolio_roles_roles')
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""rename roles table to permission_sets
Revision ID: a19138e386c4
Revises: 0e71ab219ada
Create Date: 2019-03-13 10:18:35.770296
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'a19138e386c4'
down_revision = '0e71ab219ada'
branch_labels = None
depends_on = None
def upgrade():
op.rename_table("roles", "permission_sets")
op.rename_table("portfolio_roles_roles", "portfolio_roles_permission_sets")
op.alter_column("portfolio_roles_permission_sets", "role_id", new_column_name="permission_set_id")
def downgrade():
op.rename_table("permission_sets", "roles")
op.alter_column("portfolio_roles_permission_sets", "permission_set_id", new_column_name="role_id")
op.rename_table("portfolio_roles_permission_sets", "portfolio_roles_roles")

View File

@ -27,7 +27,7 @@ class Applications(object):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user,
portfolio, portfolio,
Permissions.VIEW_APPLICATION_IN_PORTFOLIO, Permissions.VIEW_APPLICATION,
"view application in portfolio", "view application in portfolio",
) )
@ -56,7 +56,7 @@ class Applications(object):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user,
portfolio, portfolio,
Permissions.VIEW_APPLICATION_IN_PORTFOLIO, Permissions.VIEW_APPLICATION,
"view application in portfolio", "view application in portfolio",
) )

View File

@ -36,6 +36,7 @@ class AuditLog(object):
@classmethod @classmethod
def get_all_events(cls, user, pagination_opts=None): def get_all_events(cls, user, pagination_opts=None):
# TODO: general audit log permissions
Authorization.check_atat_permission( Authorization.check_atat_permission(
user, Permissions.VIEW_AUDIT_LOG, "view audit log" user, Permissions.VIEW_AUDIT_LOG, "view audit log"
) )
@ -46,7 +47,7 @@ class AuditLog(object):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user,
portfolio, portfolio,
Permissions.VIEW_PORTFOLIO_AUDIT_LOG, Permissions.VIEW_PORTFOLIO_ACTIVITY_LOG,
"view portfolio audit log", "view portfolio audit log",
) )
return AuditEventQuery.get_ws_events(portfolio.id, pagination_opts) return AuditEventQuery.get_ws_events(portfolio.id, pagination_opts)

View File

@ -1,4 +1,4 @@
from atst.domain.portfolio_roles import PortfolioRoles from atst.utils import first_or_none
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
@ -6,9 +6,13 @@ from atst.domain.exceptions import UnauthorizedError
class Authorization(object): class Authorization(object):
@classmethod @classmethod
def has_portfolio_permission(cls, user, portfolio, permission): def has_portfolio_permission(cls, user, portfolio, permission):
return permission in PortfolioRoles.portfolio_role_permissions( port_role = first_or_none(
portfolio, user lambda pr: pr.portfolio == portfolio, user.portfolio_roles
) or Authorization.is_ccpo(user) )
if port_role:
return permission in port_role.permissions
else:
return False
@classmethod @classmethod
def has_atat_permission(cls, user, permission): def has_atat_permission(cls, user, permission):

View File

@ -64,7 +64,7 @@ class Environments(object):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user,
portfolio, portfolio,
Permissions.ADD_AND_ASSIGN_CSP_ROLES, Permissions.EDIT_APPLICATION_MEMBER,
"assign environment roles", "assign environment roles",
) )
updated = False updated = False
@ -104,7 +104,7 @@ class Environments(object):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user,
environment.portfolio, environment.portfolio,
Permissions.REMOVE_CSP_ROLES, Permissions.EDIT_APPLICATION_MEMBER,
"revoke environment access", "revoke environment access",
) )
EnvironmentRoles.delete(environment.id, target_user.id) EnvironmentRoles.delete(environment.id, target_user.id)

View File

@ -119,7 +119,7 @@ class Invitations(object):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user,
portfolio, portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, Permissions.CREATE_PORTFOLIO_USERS,
"resend a portfolio invitation", "resend a portfolio invitation",
) )

View File

@ -0,0 +1,169 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models import PermissionSet, Permissions
from .exceptions import NotFoundError
class PermissionSets(object):
VIEW_PORTFOLIO = "view_portfolio"
VIEW_PORTFOLIO_APPLICATION_MANAGEMENT = "view_portfolio_application_management"
VIEW_PORTFOLIO_FUNDING = "view_portfolio_funding"
VIEW_PORTFOLIO_REPORTS = "view_portfolio_reports"
VIEW_PORTFOLIO_ADMIN = "view_portfolio_admin"
EDIT_PORTFOLIO_APPLICATION_MANAGEMENT = "edit_portfolio_application_management"
EDIT_PORTFOLIO_FUNDING = "edit_portfolio_funding"
EDIT_PORTFOLIO_REPORTS = "edit_portfolio_reports"
EDIT_PORTFOLIO_ADMIN = "edit_portfolio_admin"
PORTFOLIO_POC = "portfolio_poc"
@classmethod
def get(cls, perms_set_name):
try:
role = db.session.query(PermissionSet).filter_by(name=perms_set_name).one()
except NoResultFound:
raise NotFoundError("permission_set")
return role
@classmethod
def get_all(cls):
return db.session.query(PermissionSet).all()
@classmethod
def get_many(cls, perms_set_names):
return (
db.session.query(PermissionSet)
.filter(PermissionSet.name.in_(perms_set_names))
.all()
)
ATAT_ROLES = [
{
"name": "ccpo",
"display_name": "CCPO",
"description": "",
"permissions": [Permissions.VIEW_AUDIT_LOG],
},
{
"name": "default",
"display_name": "Default",
"description": "",
"permissions": [],
},
]
_PORTFOLIO_BASIC_PERMISSION_SETS = [
{
"name": PermissionSets.VIEW_PORTFOLIO,
"description": "View basic portfolio info",
"display_name": "View Portfolio",
"permissions": [Permissions.VIEW_PORTFOLIO],
}
]
_PORTFOLIO_APP_MGMT_PERMISSION_SETS = [
{
"name": PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
"description": "View applications and related resources",
"display_name": "Application Management",
"permissions": [
Permissions.VIEW_APPLICATION,
Permissions.VIEW_APPLICATION_MEMBER,
Permissions.VIEW_ENVIRONMENT,
],
},
{
"name": PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
"description": "Edit applications and related resources",
"display_name": "Application Management",
"permissions": [
Permissions.EDIT_APPLICATION,
Permissions.CREATE_APPLICATION,
Permissions.EDIT_APPLICATION_MEMBER,
Permissions.CREATE_APPLICATION_MEMBER,
Permissions.EDIT_ENVIRONMENT,
Permissions.CREATE_ENVIRONMENT,
],
},
]
_PORTFOLIO_FUNDING_PERMISSION_SETS = [
{
"name": PermissionSets.VIEW_PORTFOLIO_FUNDING,
"description": "View a portfolio's task orders",
"display_name": "Funding",
"permissions": [
Permissions.VIEW_PORTFOLIO_FUNDING,
Permissions.VIEW_TASK_ORDER_DETAILS,
],
},
{
"name": PermissionSets.EDIT_PORTFOLIO_FUNDING,
"description": "Edit a portfolio's task orders and add new ones",
"display_name": "Funding",
"permissions": [
Permissions.CREATE_TASK_ORDER,
Permissions.EDIT_TASK_ORDER_DETAILS,
],
},
]
_PORTFOLIO_REPORTS_PERMISSION_SETS = [
{
"name": PermissionSets.VIEW_PORTFOLIO_REPORTS,
"description": "View a portfolio's reports",
"display_name": "Reporting",
"permissions": [Permissions.VIEW_PORTFOLIO_REPORTS],
},
{
"name": PermissionSets.EDIT_PORTFOLIO_REPORTS,
"description": "Edit a portfolio's reports (no-op)",
"display_name": "Reporting",
"permissions": [],
},
]
_PORTFOLIO_ADMIN_PERMISSION_SETS = [
{
"name": PermissionSets.VIEW_PORTFOLIO_ADMIN,
"description": "View a portfolio's admin options",
"display_name": "Portfolio Administration",
"permissions": [
Permissions.VIEW_PORTFOLIO_ADMIN,
Permissions.VIEW_PORTFOLIO_NAME,
Permissions.VIEW_PORTFOLIO_USERS,
Permissions.VIEW_PORTFOLIO_ACTIVITY_LOG,
Permissions.VIEW_PORTFOLIO_POC,
],
},
{
"name": PermissionSets.EDIT_PORTFOLIO_ADMIN,
"description": "Edit a portfolio's admin options",
"display_name": "Portfolio Administration",
"permissions": [
Permissions.EDIT_PORTFOLIO_NAME,
Permissions.EDIT_PORTFOLIO_USERS,
Permissions.CREATE_PORTFOLIO_USERS,
],
},
]
_PORTFOLIO_POC_PERMISSION_SETS = [
{
"name": "portfolio_poc",
"description": "Permissions belonging to the Portfolio POC",
"display_name": "Portfolio Point of Contact",
"permissions": [Permissions.EDIT_PORTFOLIO_POC, Permissions.ARCHIVE_PORTFOLIO],
}
]
PORTFOLIO_PERMISSION_SETS = (
_PORTFOLIO_BASIC_PERMISSION_SETS
+ _PORTFOLIO_APP_MGMT_PERMISSION_SETS
+ _PORTFOLIO_FUNDING_PERMISSION_SETS
+ _PORTFOLIO_REPORTS_PERMISSION_SETS
+ _PORTFOLIO_ADMIN_PERMISSION_SETS
+ _PORTFOLIO_POC_PERMISSION_SETS
)

View File

@ -8,8 +8,7 @@ from atst.models.portfolio_role import (
) )
from atst.models.user import User from atst.models.user import User
from .roles import Roles from .permission_sets import PermissionSets
from .users import Users
from .exceptions import NotFoundError from .exceptions import NotFoundError
@ -53,17 +52,6 @@ class PortfolioRoles(object):
except NoResultFound: except NoResultFound:
return None return None
@classmethod
def portfolio_role_permissions(cls, portfolio, user):
portfolio_role = PortfolioRoles._get_active_portfolio_role(
portfolio.id, user.id
)
atat_permissions = set(user.atat_role.permissions)
portfolio_permissions = (
[] if portfolio_role is None else portfolio_role.role.permissions
)
return set(portfolio_permissions).union(atat_permissions)
@classmethod @classmethod
def _get_portfolio_role(cls, user, portfolio_id): def _get_portfolio_role(cls, user, portfolio_id):
try: try:
@ -80,9 +68,7 @@ class PortfolioRoles(object):
raise NotFoundError("portfolio role") raise NotFoundError("portfolio role")
@classmethod @classmethod
def add(cls, user, portfolio_id, role_name): def add(cls, user, portfolio_id, permission_sets=None):
role = Roles.get(role_name)
new_portfolio_role = None new_portfolio_role = None
try: try:
existing_portfolio_role = ( existing_portfolio_role = (
@ -94,13 +80,14 @@ class PortfolioRoles(object):
.one() .one()
) )
new_portfolio_role = existing_portfolio_role new_portfolio_role = existing_portfolio_role
new_portfolio_role.role = role
except NoResultFound: except NoResultFound:
new_portfolio_role = PortfolioRole( new_portfolio_role = PortfolioRole(
user=user, user=user, portfolio_id=portfolio_id, status=PortfolioRoleStatus.PENDING
role_id=role.id, )
portfolio_id=portfolio_id,
status=PortfolioRoleStatus.PENDING, if permission_sets:
new_portfolio_role.permission_sets = PortfolioRoles._permission_sets_for_names(
permission_sets
) )
user.portfolio_roles.append(new_portfolio_role) user.portfolio_roles.append(new_portfolio_role)
@ -109,56 +96,41 @@ class PortfolioRoles(object):
return new_portfolio_role return new_portfolio_role
DEFAULT_PORTFOLIO_PERMISSION_SETS = {
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
PermissionSets.VIEW_PORTFOLIO_FUNDING,
PermissionSets.VIEW_PORTFOLIO_REPORTS,
PermissionSets.VIEW_PORTFOLIO_ADMIN,
PermissionSets.VIEW_PORTFOLIO,
}
PORTFOLIO_PERMISSION_SETS = DEFAULT_PORTFOLIO_PERMISSION_SETS.union(
{
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
PermissionSets.EDIT_PORTFOLIO_FUNDING,
PermissionSets.EDIT_PORTFOLIO_REPORTS,
PermissionSets.EDIT_PORTFOLIO_ADMIN,
PermissionSets.PORTFOLIO_POC,
}
)
@classmethod @classmethod
def update_role(cls, portfolio_role, role_name): def _permission_sets_for_names(cls, set_names):
new_role = Roles.get(role_name) perms_set_names = PortfolioRoles.DEFAULT_PORTFOLIO_PERMISSION_SETS.union(
portfolio_role.role = new_role set(set_names)
)
return PermissionSets.get_many(perms_set_names)
@classmethod
def update(cls, portfolio_role, set_names):
new_permission_sets = PortfolioRoles._permission_sets_for_names(set_names)
portfolio_role.permission_sets = new_permission_sets
db.session.add(portfolio_role) db.session.add(portfolio_role)
db.session.commit() db.session.commit()
return portfolio_role return portfolio_role
@classmethod
def add_many(cls, portfolio_id, portfolio_role_dicts):
portfolio_roles = []
for user_dict in portfolio_role_dicts:
try:
user = Users.get(user_dict["id"])
except NoResultFound:
default_role = Roles.get("developer")
user = User(id=user_dict["id"], atat_role=default_role)
try:
role = Roles.get(user_dict["portfolio_role"])
except NoResultFound:
raise NotFoundError("role")
try:
existing_portfolio_role = (
db.session.query(PortfolioRole)
.filter(
PortfolioRole.user == user,
PortfolioRole.portfolio_id == portfolio_id,
)
.one()
)
new_portfolio_role = existing_portfolio_role
new_portfolio_role.role = role
except NoResultFound:
new_portfolio_role = PortfolioRole(
user=user, role_id=role.id, portfolio_id=portfolio_id
)
user.portfolio_roles.append(new_portfolio_role)
portfolio_roles.append(new_portfolio_role)
db.session.add(user)
db.session.commit()
return portfolio_roles
@classmethod @classmethod
def enable(cls, portfolio_role): def enable(cls, portfolio_role):
portfolio_role.status = PortfolioRoleStatus.ACTIVE portfolio_role.status = PortfolioRoleStatus.ACTIVE

View File

@ -1,4 +1,4 @@
from atst.domain.roles import Roles from atst.domain.permission_sets import PermissionSets
from atst.domain.authz import Authorization from atst.domain.authz import Authorization
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.domain.users import Users from atst.domain.users import Users
@ -20,8 +20,12 @@ class Portfolios(object):
portfolio = PortfoliosQuery.create( portfolio = PortfoliosQuery.create(
name=name, defense_component=defense_component name=name, defense_component=defense_component
) )
perms_sets = PermissionSets.get_many(PortfolioRoles.PORTFOLIO_PERMISSION_SETS)
Portfolios._create_portfolio_role( Portfolios._create_portfolio_role(
user, portfolio, "owner", status=PortfolioRoleStatus.ACTIVE user,
portfolio,
status=PortfolioRoleStatus.ACTIVE,
permission_sets=perms_sets,
) )
PortfoliosQuery.add_and_commit(portfolio) PortfoliosQuery.add_and_commit(portfolio)
return portfolio return portfolio
@ -39,7 +43,7 @@ class Portfolios(object):
def get_for_update_applications(cls, user, portfolio_id): def get_for_update_applications(cls, user, portfolio_id):
portfolio = PortfoliosQuery.get(portfolio_id) portfolio = PortfoliosQuery.get(portfolio_id)
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, portfolio, Permissions.ADD_APPLICATION_IN_PORTFOLIO, "add application" user, portfolio, Permissions.CREATE_APPLICATION, "add application"
) )
return portfolio return portfolio
@ -50,7 +54,7 @@ class Portfolios(object):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user,
portfolio, portfolio,
Permissions.EDIT_PORTFOLIO_INFORMATION, Permissions.EDIT_PORTFOLIO_NAME,
"update portfolio information", "update portfolio information",
) )
@ -62,7 +66,7 @@ class Portfolios(object):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user,
portfolio, portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, Permissions.EDIT_PORTFOLIO_USERS,
"update a portfolio member", "update a portfolio member",
) )
@ -72,10 +76,7 @@ class Portfolios(object):
def get_with_members(cls, user, portfolio_id): def get_with_members(cls, user, portfolio_id):
portfolio = PortfoliosQuery.get(portfolio_id) portfolio = PortfoliosQuery.get(portfolio_id)
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user, portfolio, Permissions.VIEW_PORTFOLIO_USERS, "view portfolio members"
portfolio,
Permissions.VIEW_PORTFOLIO_MEMBERS,
"view portfolio members",
) )
return portfolio return portfolio
@ -91,10 +92,7 @@ class Portfolios(object):
@classmethod @classmethod
def create_member(cls, user, portfolio, data): def create_member(cls, user, portfolio, data):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user, portfolio, Permissions.EDIT_PORTFOLIO_USERS, "create portfolio member"
portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"create portfolio member",
) )
new_user = Users.get_or_create_by_dod_id( new_user = Users.get_or_create_by_dod_id(
@ -105,31 +103,34 @@ class Portfolios(object):
atat_role_name="default", atat_role_name="default",
provisional=True, provisional=True,
) )
return Portfolios.add_member(portfolio, new_user, data["portfolio_role"]) permission_sets = data.get("permission_sets", [])
return Portfolios.add_member(
portfolio, new_user, permission_sets=permission_sets
)
@classmethod @classmethod
def add_member(cls, portfolio, member, role_name): def add_member(cls, portfolio, member, permission_sets=None):
portfolio_role = PortfolioRoles.add(member, portfolio.id, role_name) portfolio_role = PortfolioRoles.add(member, portfolio.id, permission_sets)
return portfolio_role return portfolio_role
@classmethod @classmethod
def update_member(cls, user, portfolio, member, role_name): def update_member(cls, user, portfolio, member, permission_sets):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user, portfolio, Permissions.EDIT_PORTFOLIO_USERS, "edit portfolio member"
portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"edit portfolio member",
) )
return PortfolioRoles.update_role(member, role_name) # need to update perms sets here
return PortfolioRoles.update(member, permission_sets)
@classmethod @classmethod
def _create_portfolio_role( def _create_portfolio_role(
cls, user, portfolio, role_name, status=PortfolioRoleStatus.PENDING cls, user, portfolio, status=PortfolioRoleStatus.PENDING, permission_sets=None
): ):
role = Roles.get(role_name) if permission_sets is None:
permission_sets = []
portfolio_role = PortfoliosQuery.create_portfolio_role( portfolio_role = PortfoliosQuery.create_portfolio_role(
user, role, portfolio, status=status user, portfolio, status=status, permission_sets=permission_sets
) )
PortfoliosQuery.add_and_commit(portfolio_role) PortfoliosQuery.add_and_commit(portfolio_role)
return portfolio_role return portfolio_role
@ -152,10 +153,7 @@ class Portfolios(object):
def revoke_access(cls, user, portfolio_id, portfolio_role_id): def revoke_access(cls, user, portfolio_id, portfolio_role_id):
portfolio = PortfoliosQuery.get(portfolio_id) portfolio = PortfoliosQuery.get(portfolio_id)
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user, portfolio, Permissions.EDIT_PORTFOLIO_USERS, "revoke portfolio access"
portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"revoke portfolio access",
) )
portfolio_role = PortfolioRoles.get_by_id(portfolio_role_id) portfolio_role = PortfolioRoles.get_by_id(portfolio_role_id)

View File

@ -18,5 +18,5 @@ class PortfoliosQuery(Query):
) )
@classmethod @classmethod
def create_portfolio_role(cls, user, role, portfolio, **kwargs): def create_portfolio_role(cls, user, portfolio, **kwargs):
return PortfolioRole(user=user, role=role, portfolio=portfolio, **kwargs) return PortfolioRole(user=user, portfolio=portfolio, **kwargs)

View File

@ -31,7 +31,7 @@ class ScopedPortfolio(ScopedResource):
@property @property
def applications(self): def applications(self):
can_view_all_applications = Authorization.has_portfolio_permission( can_view_all_applications = Authorization.has_portfolio_permission(
self.user, self.resource, Permissions.VIEW_APPLICATION_IN_PORTFOLIO self.user, self.resource, Permissions.VIEW_APPLICATION
) )
if can_view_all_applications: if can_view_all_applications:
@ -54,9 +54,7 @@ class ScopedApplication(ScopedResource):
@property @property
def environments(self): def environments(self):
can_view_all_environments = Authorization.has_portfolio_permission( can_view_all_environments = Authorization.has_portfolio_permission(
self.user, self.user, self.resource.portfolio, Permissions.VIEW_ENVIRONMENT
self.resource.portfolio,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
) )
if can_view_all_environments: if can_view_all_environments:

View File

@ -1,177 +0,0 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models import Role, Permissions
from .exceptions import NotFoundError
ATAT_ROLES = [
{
"name": "ccpo",
"display_name": "CCPO",
"description": "",
"permissions": [
Permissions.VIEW_ORIGINAL_JEDI_REQEUST,
Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST,
Permissions.MODIFY_ATAT_ROLE_PERMISSIONS,
Permissions.CREATE_CSP_ROLE,
Permissions.DELETE_CSP_ROLE,
Permissions.DEACTIVE_CSP_ROLE,
Permissions.MODIFY_CSP_ROLE_PERMISSIONS,
Permissions.VIEW_USAGE_REPORT,
Permissions.VIEW_USAGE_DOLLARS,
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
Permissions.REMOVE_CSP_ROLES,
Permissions.REQUEST_NEW_CSP_ROLE,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
Permissions.DEACTIVATE_PORTFOLIO,
Permissions.VIEW_ATAT_PERMISSIONS,
Permissions.TRANSFER_OWNERSHIP_OF_PORTFOLIO,
Permissions.VIEW_PORTFOLIO,
Permissions.VIEW_PORTFOLIO_MEMBERS,
Permissions.ADD_APPLICATION_IN_PORTFOLIO,
Permissions.DELETE_APPLICATION_IN_PORTFOLIO,
Permissions.DEACTIVATE_APPLICATION_IN_PORTFOLIO,
Permissions.VIEW_APPLICATION_IN_PORTFOLIO,
Permissions.RENAME_APPLICATION_IN_PORTFOLIO,
Permissions.ADD_ENVIRONMENT_IN_APPLICATION,
Permissions.DELETE_ENVIRONMENT_IN_APPLICATION,
Permissions.DEACTIVATE_ENVIRONMENT_IN_APPLICATION,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
Permissions.RENAME_ENVIRONMENT_IN_APPLICATION,
Permissions.ADD_TAG_TO_PORTFOLIO,
Permissions.REMOVE_TAG_FROM_PORTFOLIO,
Permissions.VIEW_AUDIT_LOG,
Permissions.VIEW_PORTFOLIO_AUDIT_LOG,
],
},
{
"name": "default",
"display_name": "Default",
"description": "",
"permissions": [Permissions.REQUEST_JEDI_PORTFOLIO],
},
]
PORTFOLIO_ROLES = [
{
"name": "owner",
"display_name": "Portfolio Owner",
"description": "Adds, edits, deactivates access to all applications, environments, and members. Views budget reports. Initiates and edits JEDI Cloud requests.",
"permissions": [
Permissions.REQUEST_JEDI_PORTFOLIO,
Permissions.VIEW_ORIGINAL_JEDI_REQEUST,
Permissions.VIEW_USAGE_REPORT,
Permissions.VIEW_USAGE_DOLLARS,
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
Permissions.REMOVE_CSP_ROLES,
Permissions.REQUEST_NEW_CSP_ROLE,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
Permissions.DEACTIVATE_PORTFOLIO,
Permissions.VIEW_ATAT_PERMISSIONS,
Permissions.VIEW_PORTFOLIO,
Permissions.VIEW_PORTFOLIO_MEMBERS,
Permissions.EDIT_PORTFOLIO_INFORMATION,
Permissions.ADD_APPLICATION_IN_PORTFOLIO,
Permissions.DELETE_APPLICATION_IN_PORTFOLIO,
Permissions.DEACTIVATE_APPLICATION_IN_PORTFOLIO,
Permissions.VIEW_APPLICATION_IN_PORTFOLIO,
Permissions.RENAME_APPLICATION_IN_PORTFOLIO,
Permissions.ADD_ENVIRONMENT_IN_APPLICATION,
Permissions.DELETE_ENVIRONMENT_IN_APPLICATION,
Permissions.DEACTIVATE_ENVIRONMENT_IN_APPLICATION,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
Permissions.RENAME_ENVIRONMENT_IN_APPLICATION,
Permissions.VIEW_PORTFOLIO_AUDIT_LOG,
Permissions.VIEW_TASK_ORDER,
Permissions.UPDATE_TASK_ORDER,
Permissions.ADD_TASK_ORDER_OFFICER,
],
},
{
"name": "admin",
"display_name": "Administrator",
"description": "Adds and edits applications, environments, members, but cannot deactivate. Cannot view budget reports or JEDI Cloud requests.",
"permissions": [
Permissions.VIEW_USAGE_REPORT,
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
Permissions.REMOVE_CSP_ROLES,
Permissions.REQUEST_NEW_CSP_ROLE,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
Permissions.VIEW_PORTFOLIO,
Permissions.VIEW_PORTFOLIO_MEMBERS,
Permissions.EDIT_PORTFOLIO_INFORMATION,
Permissions.ADD_APPLICATION_IN_PORTFOLIO,
Permissions.DELETE_APPLICATION_IN_PORTFOLIO,
Permissions.DEACTIVATE_APPLICATION_IN_PORTFOLIO,
Permissions.VIEW_APPLICATION_IN_PORTFOLIO,
Permissions.RENAME_APPLICATION_IN_PORTFOLIO,
Permissions.ADD_ENVIRONMENT_IN_APPLICATION,
Permissions.DELETE_ENVIRONMENT_IN_APPLICATION,
Permissions.DEACTIVATE_ENVIRONMENT_IN_APPLICATION,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
Permissions.RENAME_ENVIRONMENT_IN_APPLICATION,
Permissions.VIEW_PORTFOLIO_AUDIT_LOG,
Permissions.VIEW_TASK_ORDER,
Permissions.UPDATE_TASK_ORDER,
Permissions.ADD_TASK_ORDER_OFFICER,
],
},
{
"name": "developer",
"display_name": "Developer",
"description": "Views only the applications and environments they are granted access to. Can also view members associated with each environment.",
"permissions": [Permissions.VIEW_USAGE_REPORT, Permissions.VIEW_PORTFOLIO],
},
{
"name": "billing_auditor",
"display_name": "Billing Auditor",
"description": "Views only the applications and environments they are granted access to. Can also view budgets and reports associated with the portfolio.",
"permissions": [
Permissions.VIEW_USAGE_REPORT,
Permissions.VIEW_USAGE_DOLLARS,
Permissions.VIEW_PORTFOLIO,
],
},
{
"name": "security_auditor",
"description": "Views only the applications and environments they are granted access to. Can also view activity logs.",
"display_name": "Security Auditor",
"permissions": [
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
Permissions.VIEW_ATAT_PERMISSIONS,
Permissions.VIEW_PORTFOLIO,
],
},
{
"name": "officer",
"description": "Officer involved with setting up a Task Order",
"display_name": "Task Order Officer",
"permissions": [
Permissions.VIEW_PORTFOLIO,
Permissions.VIEW_USAGE_REPORT,
Permissions.VIEW_USAGE_DOLLARS,
],
},
]
class Roles(object):
@classmethod
def get(cls, role_name):
try:
role = db.session.query(Role).filter_by(name=role_name).one()
except NoResultFound:
raise NotFoundError("role")
return role
@classmethod
def get_all(cls):
return db.session.query(Role).all()

View File

@ -7,6 +7,7 @@ from atst.models.permissions import Permissions
from atst.models.dd_254 import DD254 from atst.models.dd_254 import DD254
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.domain.authz import Authorization from atst.domain.authz import Authorization
from atst.domain.permission_sets import PermissionSets
from .exceptions import NotFoundError from .exceptions import NotFoundError
@ -57,7 +58,7 @@ class TaskOrders(object):
try: try:
task_order = db.session.query(TaskOrder).filter_by(id=task_order_id).one() task_order = db.session.query(TaskOrder).filter_by(id=task_order_id).one()
Authorization.check_task_order_permission( Authorization.check_task_order_permission(
user, task_order, Permissions.VIEW_TASK_ORDER, "view task order" user, task_order, Permissions.VIEW_TASK_ORDER_DETAILS, "view task order"
) )
return task_order return task_order
@ -67,7 +68,7 @@ class TaskOrders(object):
@classmethod @classmethod
def create(cls, creator, portfolio): def create(cls, creator, portfolio):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
creator, portfolio, Permissions.UPDATE_TASK_ORDER, "add task order" creator, portfolio, Permissions.CREATE_TASK_ORDER, "add task order"
) )
task_order = TaskOrder(portfolio=portfolio, creator=creator) task_order = TaskOrder(portfolio=portfolio, creator=creator)
@ -79,7 +80,7 @@ class TaskOrders(object):
@classmethod @classmethod
def update(cls, user, task_order, **kwargs): def update(cls, user, task_order, **kwargs):
Authorization.check_task_order_permission( Authorization.check_task_order_permission(
user, task_order, Permissions.UPDATE_TASK_ORDER, "update task order" user, task_order, Permissions.EDIT_TASK_ORDER_DETAILS, "update task order"
) )
for key, value in kwargs.items(): for key, value in kwargs.items():
@ -150,7 +151,7 @@ class TaskOrders(object):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
user, user,
task_order.portfolio, task_order.portfolio,
Permissions.ADD_TASK_ORDER_OFFICER, Permissions.EDIT_TASK_ORDER_DETAILS,
"add task order officer", "add task order officer",
) )
@ -170,7 +171,12 @@ class TaskOrders(object):
portfolio_user = existing_member.user portfolio_user = existing_member.user
else: else:
member = Portfolios.create_member( member = Portfolios.create_member(
user, portfolio, {**officer_data, "portfolio_role": "officer"} user,
portfolio,
{
**officer_data,
"permission_sets": [PermissionSets.EDIT_PORTFOLIO_FUNDING],
},
) )
portfolio_user = member.user portfolio_user = member.user

View File

@ -4,7 +4,7 @@ from sqlalchemy.exc import IntegrityError
from atst.database import db from atst.database import db
from atst.models import User from atst.models import User
from .roles import Roles from .permission_sets import PermissionSets
from .exceptions import NotFoundError, AlreadyExistsError, UnauthorizedError from .exceptions import NotFoundError, AlreadyExistsError, UnauthorizedError
@ -29,7 +29,7 @@ class Users(object):
@classmethod @classmethod
def create(cls, dod_id, atat_role_name=None, **kwargs): def create(cls, dod_id, atat_role_name=None, **kwargs):
atat_role = Roles.get(atat_role_name) atat_role = PermissionSets.get(atat_role_name)
try: try:
user = User(dod_id=dod_id, atat_role=atat_role, **kwargs) user = User(dod_id=dod_id, atat_role=atat_role, **kwargs)
@ -56,7 +56,7 @@ class Users(object):
def update_role(cls, user_id, atat_role_name): def update_role(cls, user_id, atat_role_name):
user = Users.get(user_id) user = Users.get(user_id)
atat_role = Roles.get(atat_role_name) atat_role = PermissionSets.get(atat_role_name)
user.atat_role = atat_role user.atat_role = atat_role
db.session.add(user) db.session.add(user)

View File

@ -1,4 +1,3 @@
from atst.domain.roles import PORTFOLIO_ROLES as PORTFOLIO_ROLE_DEFINITIONS
from atst.utils.localization import translate, translate_duration from atst.utils.localization import translate, translate_duration
@ -107,12 +106,6 @@ COMPLETION_DATE_RANGES = [
("Above 12 months", "Above 12 months"), ("Above 12 months", "Above 12 months"),
] ]
PORTFOLIO_ROLES = [
(role["name"], {"name": role["display_name"], "description": role["description"]})
for role in PORTFOLIO_ROLE_DEFINITIONS
if role["name"] is not "officer"
]
ENVIRONMENT_ROLES = [ ENVIRONMENT_ROLES = [
( (
"developer", "developer",

View File

@ -1,18 +0,0 @@
from wtforms.validators import Required
from .forms import BaseForm
from atst.forms.fields import SelectField
from atst.utils.localization import translate
from .data import PORTFOLIO_ROLES
class EditMemberForm(BaseForm):
# This form also accepts a field for each environment in each application
# that the user is a member of
portfolio_role = SelectField(
translate("forms.edit_member.portfolio_role_label"),
choices=PORTFOLIO_ROLES,
validators=[Required()],
)

View File

@ -1,34 +0,0 @@
from wtforms.fields import StringField
from wtforms.fields.html5 import EmailField
from wtforms.validators import Required, Email, Length
from .forms import BaseForm
from atst.forms.validators import IsNumber
from atst.forms.fields import SelectField
from atst.utils.localization import translate
from .data import PORTFOLIO_ROLES
class NewMemberForm(BaseForm):
first_name = StringField(
label=translate("forms.new_member.first_name_label"), validators=[Required()]
)
last_name = StringField(
label=translate("forms.new_member.last_name_label"), validators=[Required()]
)
email = EmailField(
translate("forms.new_member.email_label"), validators=[Required(), Email()]
)
dod_id = StringField(
translate("forms.new_member.dod_id_label"),
validators=[Required(), Length(min=10), IsNumber()],
)
portfolio_role = SelectField(
translate("forms.new_member.portfolio_role_label"),
choices=PORTFOLIO_ROLES,
validators=[Required()],
default="",
description=translate("forms.new_member.portfolio_role_description"),
)

View File

@ -0,0 +1,72 @@
from wtforms.fields import StringField
from wtforms.fields.html5 import EmailField
from wtforms.validators import Required, Email, Length
from atst.domain.permission_sets import PermissionSets
from .forms import BaseForm
from atst.forms.validators import IsNumber
from atst.forms.fields import SelectField
from atst.utils.localization import translate
class PermissionsForm(BaseForm):
perms_app_mgmt = SelectField(
None,
choices=[
(PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT, "View Only"),
(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT, "Edit Access"),
],
)
perms_funding = SelectField(
None,
choices=[
(PermissionSets.VIEW_PORTFOLIO_FUNDING, "View Only"),
(PermissionSets.EDIT_PORTFOLIO_FUNDING, "Edit Access"),
],
)
perms_reporting = SelectField(
None,
choices=[
(PermissionSets.VIEW_PORTFOLIO_REPORTS, "View Only"),
(PermissionSets.EDIT_PORTFOLIO_REPORTS, "Edit Access"),
],
)
perms_portfolio_mgmt = SelectField(
None,
choices=[
(PermissionSets.VIEW_PORTFOLIO_ADMIN, "View Only"),
(PermissionSets.EDIT_PORTFOLIO_ADMIN, "Edit Access"),
],
)
@property
def data(self):
_data = super().data
_data["permission_sets"] = []
for field in _data:
if "perms" in field:
_data["permission_sets"].append(_data[field])
return _data
class EditForm(PermissionsForm):
# This form also accepts a field for each environment in each application
# that the user is a member of
pass
class NewForm(PermissionsForm):
first_name = StringField(
label=translate("forms.new_member.first_name_label"), validators=[Required()]
)
last_name = StringField(
label=translate("forms.new_member.last_name_label"), validators=[Required()]
)
email = EmailField(
translate("forms.new_member.email_label"), validators=[Required(), Email()]
)
dod_id = StringField(
translate("forms.new_member.dod_id_label"),
validators=[Required(), Length(min=10), IsNumber()],
)

View File

@ -3,7 +3,7 @@ from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base() Base = declarative_base()
from .permissions import Permissions from .permissions import Permissions
from .role import Role from .permission_set import PermissionSet
from .user import User from .user import User
from .portfolio_role import PortfolioRole from .portfolio_role import PortfolioRole
from .portfolio import Portfolio from .portfolio import Portfolio

View File

@ -0,0 +1,19 @@
from sqlalchemy import String, Column
from sqlalchemy.dialects.postgresql import ARRAY
from atst.models import Base, types, mixins
class PermissionSet(Base, mixins.TimestampsMixin):
__tablename__ = "permission_sets"
id = types.Id()
name = Column(String, index=True, unique=True, nullable=False)
display_name = Column(String, nullable=False)
description = Column(String, nullable=False)
permissions = Column(ARRAY(String), index=True, server_default="{}", nullable=False)
def __repr__(self):
return "<PermissionSet(name='{}', description='{}', permissions='{}', id='{}')>".format(
self.name, self.description, self.permissions, self.id
)

View File

@ -1,49 +1,41 @@
class Permissions(object): class Permissions(object):
VIEW_AUDIT_LOG = "view_audit_log" VIEW_AUDIT_LOG = "view_audit_log"
VIEW_PORTFOLIO_AUDIT_LOG = "view_portfolio_audit_log"
REQUEST_JEDI_PORTFOLIO = "request_jedi_portfolio"
VIEW_ORIGINAL_JEDI_REQEUST = "view_original_jedi_request"
REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST = (
"review_and_approve_jedi_portfolio_request"
)
MODIFY_ATAT_ROLE_PERMISSIONS = "modify_atat_role_permissions"
CREATE_CSP_ROLE = "create_csp_role"
DELETE_CSP_ROLE = "delete_csp_role"
DEACTIVE_CSP_ROLE = "deactivate_csp_role"
MODIFY_CSP_ROLE_PERMISSIONS = "modify_csp_role_permissions"
VIEW_USAGE_REPORT = "view_usage_report" # base portfolio perms
VIEW_USAGE_DOLLARS = "view_usage_dollars"
ADD_AND_ASSIGN_CSP_ROLES = "add_and_assign_csp_roles"
REMOVE_CSP_ROLES = "remove_csp_roles"
REQUEST_NEW_CSP_ROLE = "request_new_csp_role"
ASSIGN_AND_UNASSIGN_ATAT_ROLE = "assign_and_unassign_atat_role"
VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS = "view_assigned_atat_role_configurations"
VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS = "view_assigned_csp_role_configurations"
EDIT_PORTFOLIO_INFORMATION = "edit_portfolio_information"
DEACTIVATE_PORTFOLIO = "deactivate_portfolio"
VIEW_ATAT_PERMISSIONS = "view_atat_permissions"
TRANSFER_OWNERSHIP_OF_PORTFOLIO = "transfer_ownership_of_portfolio"
VIEW_PORTFOLIO_MEMBERS = "view_portfolio_members"
VIEW_PORTFOLIO = "view_portfolio" VIEW_PORTFOLIO = "view_portfolio"
ADD_APPLICATION_IN_PORTFOLIO = "add_application_in_portfolio" # application management
DELETE_APPLICATION_IN_PORTFOLIO = "delete_application_in_portfolio" VIEW_APPLICATION = "view_application"
DEACTIVATE_APPLICATION_IN_PORTFOLIO = "deactivate_application_in_portfolio" EDIT_APPLICATION = "edit_application"
VIEW_APPLICATION_IN_PORTFOLIO = "view_application_in_portfolio" CREATE_APPLICATION = "create_application"
RENAME_APPLICATION_IN_PORTFOLIO = "rename_application_in_portfolio" VIEW_APPLICATION_MEMBER = "view_application_member"
EDIT_APPLICATION_MEMBER = "edit_application_member"
CREATE_APPLICATION_MEMBER = "create_application_member"
VIEW_ENVIRONMENT = "view_environment"
EDIT_ENVIRONMENT = "edit_environment"
CREATE_ENVIRONMENT = "create_environment"
ADD_ENVIRONMENT_IN_APPLICATION = "add_environment_in_application" # funding
DELETE_ENVIRONMENT_IN_APPLICATION = "delete_environment_in_application" VIEW_PORTFOLIO_FUNDING = "view_portfolio_funding" # TO summary page
DEACTIVATE_ENVIRONMENT_IN_APPLICATION = "deactivate_environment_in_application" CREATE_TASK_ORDER = "create_task_order" # create a new TO
VIEW_ENVIRONMENT_IN_APPLICATION = "view_environment_in_application" VIEW_TASK_ORDER_DETAILS = "view_task_order_details" # individual TO page
RENAME_ENVIRONMENT_IN_APPLICATION = "rename_environment_in_application" EDIT_TASK_ORDER_DETAILS = (
"edit_task_order_details"
) # edit TO that has not been finalized
ADD_TAG_TO_PORTFOLIO = "add_tag_to_portfolio" # reporting
REMOVE_TAG_FROM_PORTFOLIO = "remove_tag_from_portfolio" VIEW_PORTFOLIO_REPORTS = "view_portfolio_reports"
VIEW_TASK_ORDER = "view_task_order" # portfolio admin
UPDATE_TASK_ORDER = "update_task_order" VIEW_PORTFOLIO_ADMIN = "view_portfolio_admin"
ADD_TASK_ORDER_OFFICER = "add_task_order_officers" VIEW_PORTFOLIO_NAME = "view_portfolio_name"
EDIT_PORTFOLIO_NAME = "edit_portfolio_name"
VIEW_PORTFOLIO_USERS = "view_portfolio_users"
EDIT_PORTFOLIO_USERS = "edit_portfolio_users"
CREATE_PORTFOLIO_USERS = "create_portfolio_users"
VIEW_PORTFOLIO_ACTIVITY_LOG = "view_portfolio_activity_log"
VIEW_PORTFOLIO_POC = "view_portfolio_poc"
# portfolio POC
EDIT_PORTFOLIO_POC = "edit_portfolio_poc"
ARCHIVE_PORTFOLIO = "archive_portfolio"

View File

@ -4,6 +4,7 @@ from itertools import chain
from atst.models import Base, mixins, types from atst.models import Base, mixins, types
from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
from atst.domain.permission_sets import PermissionSets
from atst.utils import first_or_none from atst.utils import first_or_none
from atst.database import db from atst.database import db
@ -23,7 +24,9 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
@property @property
def owner(self): def owner(self):
def _is_portfolio_owner(portfolio_role): def _is_portfolio_owner(portfolio_role):
return portfolio_role.role.name == "owner" return PermissionSets.PORTFOLIO_POC in [
perms_set.name for perms_set in portfolio_role.permission_sets
]
owner = first_or_none(_is_portfolio_owner, self.roles) owner = first_or_none(_is_portfolio_owner, self.roles)
return owner.user if owner else None return owner.user if owner else None

View File

@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from sqlalchemy import Index, ForeignKey, Column, Enum as SQLAEnum from sqlalchemy import Index, ForeignKey, Column, Enum as SQLAEnum, Table
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@ -10,7 +10,6 @@ from atst.database import db
from atst.models.environment_role import EnvironmentRole from atst.models.environment_role import EnvironmentRole
from atst.models.application import Application from atst.models.application import Application
from atst.models.environment import Environment from atst.models.environment import Environment
from atst.models.role import Role
MEMBER_STATUSES = { MEMBER_STATUSES = {
@ -30,6 +29,14 @@ class Status(Enum):
PENDING = "pending" PENDING = "pending"
portfolio_roles_permission_sets = Table(
"portfolio_roles_permission_sets",
Base.metadata,
Column("portfolio_role_id", UUID(as_uuid=True), ForeignKey("portfolio_roles.id")),
Column("permission_set_id", UUID(as_uuid=True), ForeignKey("permission_sets.id")),
)
class PortfolioRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin): class PortfolioRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
__tablename__ = "portfolio_roles" __tablename__ = "portfolio_roles"
@ -39,29 +46,32 @@ class PortfolioRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
) )
portfolio = relationship("Portfolio", back_populates="roles") portfolio = relationship("Portfolio", back_populates="roles")
role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"), nullable=False)
role = relationship("Role")
user_id = Column( user_id = Column(
UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=False UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=False
) )
status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING)
permission_sets = relationship(
"PermissionSet", secondary=portfolio_roles_permission_sets
)
@property
def permissions(self):
return [
perm for permset in self.permission_sets for perm in permset.permissions
]
def __repr__(self): def __repr__(self):
return "<PortfolioRole(role='{}', portfolio='{}', user_id='{}', id='{}')>".format( return "<PortfolioRole(portfolio='{}', user_id='{}', id='{}', permissions={})>".format(
self.role.name, self.portfolio.name, self.user_id, self.id self.portfolio.name, self.user_id, self.id, self.permissions
) )
@property @property
def history(self): def history(self):
previous_state = self.get_changes() previous_state = self.get_changes()
change_set = {} change_set = {}
if "role_id" in previous_state: # TODO: need to update to include permission_sets
from_role_id = previous_state["role_id"][0]
from_role = db.session.query(Role).filter(Role.id == from_role_id).one()
to_role = self.role_name
change_set["role"] = [from_role.name, to_role]
if "status" in previous_state: if "status" in previous_state:
from_status = previous_state["status"][0].value from_status = previous_state["status"][0].value
to_status = self.status.value to_status = self.status.value
@ -105,18 +115,10 @@ class PortfolioRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
def has_dod_id_error(self): def has_dod_id_error(self):
return self.latest_invitation and self.latest_invitation.is_rejected_wrong_user return self.latest_invitation and self.latest_invitation.is_rejected_wrong_user
@property
def role_name(self):
return self.role.name
@property @property
def user_name(self): def user_name(self):
return self.user.full_name return self.user.full_name
@property
def role_displayname(self):
return self.role.display_name
@property @property
def is_active(self): def is_active(self):
return self.status == Status.ACTIVE return self.status == Status.ACTIVE

View File

@ -1,32 +0,0 @@
from sqlalchemy import String, Column
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.orm.attributes import flag_modified
from atst.models import Base, types, mixins
class Role(Base, mixins.TimestampsMixin):
__tablename__ = "roles"
id = types.Id()
name = Column(String, index=True, unique=True, nullable=False)
display_name = Column(String, nullable=False)
description = Column(String, nullable=False)
permissions = Column(ARRAY(String), index=True, server_default="{}", nullable=False)
def add_permission(self, permission):
perms_set = set(self.permissions)
perms_set.add(permission)
self.permissions = list(perms_set)
flag_modified(self, "permissions")
def remove_permission(self, permission):
perms_set = set(self.permissions)
perms_set.discard(permission)
self.permissions = list(perms_set)
flag_modified(self, "permissions")
def __repr__(self):
return "<Role(name='{}', description='{}', permissions='{}', id='{}')>".format(
self.name, self.description, self.permissions, self.id
)

View File

@ -11,9 +11,9 @@ class User(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
id = types.Id() id = types.Id()
username = Column(String) username = Column(String)
atat_role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id")) atat_role_id = Column(UUID(as_uuid=True), ForeignKey("permission_sets.id"))
atat_role = relationship("Role") atat_role = relationship("PermissionSet")
portfolio_roles = relationship("PortfolioRole", backref="user") portfolio_roles = relationship("PortfolioRole", backref="user")
email = Column(String, unique=True) email = Column(String, unique=True)

View File

@ -64,7 +64,9 @@ def home():
elif num_portfolios == 1: elif num_portfolios == 1:
portfolio_role = user.portfolio_roles[0] portfolio_role = user.portfolio_roles[0]
portfolio_id = portfolio_role.portfolio.id portfolio_id = portfolio_role.portfolio.id
is_portfolio_owner = portfolio_role.role.name == "owner" is_portfolio_owner = "portfolio_poc" in [
ps.name for ps in portfolio_role.permission_sets
]
if is_portfolio_owner: if is_portfolio_owner:
return redirect( return redirect(

View File

@ -64,7 +64,7 @@ def portfolio_reports(portfolio_id):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
g.current_user, g.current_user,
portfolio, portfolio,
Permissions.VIEW_USAGE_DOLLARS, Permissions.VIEW_PORTFOLIO_REPORTS,
"view portfolio reports", "view portfolio reports",
) )

View File

@ -10,13 +10,8 @@ from atst.domain.portfolio_roles import PortfolioRoles, MEMBER_STATUS_CHOICES
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.services.invitation import Invitation as InvitationService from atst.services.invitation import Invitation as InvitationService
from atst.forms.new_member import NewMemberForm import atst.forms.portfolio_member as member_forms
from atst.forms.edit_member import EditMemberForm from atst.forms.data import ENVIRONMENT_ROLES, ENV_ROLE_MODAL_DESCRIPTION
from atst.forms.data import (
ENVIRONMENT_ROLES,
ENV_ROLE_MODAL_DESCRIPTION,
PORTFOLIO_ROLE_DEFINITIONS,
)
from atst.domain.authz import Authorization from atst.domain.authz import Authorization
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
@ -28,7 +23,7 @@ def serialize_portfolio_role(portfolio_role):
"name": portfolio_role.user_name, "name": portfolio_role.user_name,
"status": portfolio_role.display_status, "status": portfolio_role.display_status,
"id": portfolio_role.user_id, "id": portfolio_role.user_id,
"role": portfolio_role.role_displayname, "role": "admin",
"num_env": portfolio_role.num_environment_roles, "num_env": portfolio_role.num_environment_roles,
"edit_link": url_for( "edit_link": url_for(
"portfolios.view_member", "portfolios.view_member",
@ -46,7 +41,6 @@ def portfolio_members(portfolio_id):
return render_template( return render_template(
"portfolios/members/index.html", "portfolios/members/index.html",
portfolio=portfolio, portfolio=portfolio,
role_choices=PORTFOLIO_ROLE_DEFINITIONS,
status_choices=MEMBER_STATUS_CHOICES, status_choices=MEMBER_STATUS_CHOICES,
members=members_list, members=members_list,
) )
@ -70,7 +64,7 @@ def application_members(portfolio_id, application_id):
@portfolios_bp.route("/portfolios/<portfolio_id>/members/new") @portfolios_bp.route("/portfolios/<portfolio_id>/members/new")
def new_member(portfolio_id): def new_member(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id) portfolio = Portfolios.get(g.current_user, portfolio_id)
form = NewMemberForm() form = member_forms.NewForm()
return render_template( return render_template(
"portfolios/members/new.html", portfolio=portfolio, form=form "portfolios/members/new.html", portfolio=portfolio, form=form
) )
@ -79,7 +73,7 @@ def new_member(portfolio_id):
@portfolios_bp.route("/portfolios/<portfolio_id>/members/new", methods=["POST"]) @portfolios_bp.route("/portfolios/<portfolio_id>/members/new", methods=["POST"])
def create_member(portfolio_id): def create_member(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id) portfolio = Portfolios.get(g.current_user, portfolio_id)
form = NewMemberForm(http_request.form) form = member_forms.NewForm(http_request.form)
if form.validate(): if form.validate():
try: try:
@ -110,12 +104,12 @@ def view_member(portfolio_id, member_id):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
g.current_user, g.current_user,
portfolio, portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, Permissions.EDIT_PORTFOLIO_USERS,
"edit this portfolio user", "edit this portfolio user",
) )
member = PortfolioRoles.get(portfolio_id, member_id) member = PortfolioRoles.get(portfolio_id, member_id)
applications = Applications.get_all(g.current_user, member, portfolio) applications = Applications.get_all(g.current_user, member, portfolio)
form = EditMemberForm(portfolio_role=member.role_name) form = member_forms.EditForm(portfolio_role="admin")
editable = g.current_user == member.user editable = g.current_user == member.user
can_revoke_access = Portfolios.can_revoke_access_for(portfolio, member) can_revoke_access = Portfolios.can_revoke_access_for(portfolio, member)
@ -144,7 +138,7 @@ def update_member(portfolio_id, member_id):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(
g.current_user, g.current_user,
portfolio, portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, Permissions.EDIT_PORTFOLIO_USERS,
"edit this portfolio user", "edit this portfolio user",
) )
member = PortfolioRoles.get(portfolio_id, member_id) member = PortfolioRoles.get(portfolio_id, member_id)
@ -157,20 +151,11 @@ def update_member(portfolio_id, member_id):
env_role = form_dict[entry] or None env_role = form_dict[entry] or None
ids_and_roles.append({"id": env_id, "role": env_role}) ids_and_roles.append({"id": env_id, "role": env_role})
form = EditMemberForm(http_request.form) form = member_forms.EditForm(http_request.form)
if form.validate(): if form.validate():
new_role_name = None member = Portfolios.update_member(
if form.data["portfolio_role"] != member.role.name: g.current_user, portfolio, member, form.data["permission_sets"]
member = Portfolios.update_member( )
g.current_user, portfolio, member, form.data["portfolio_role"]
)
new_role_name = member.role_displayname
flash(
"portfolio_role_updated",
member_name=member.user_name,
updated_role=new_role_name,
)
updated_roles = Environments.update_environment_roles( updated_roles = Environments.update_environment_roles(
g.current_user, portfolio, member, ids_and_roles g.current_user, portfolio, member, ids_and_roles
) )

View File

@ -16,7 +16,7 @@ from atst.models.audit_event import AuditEvent
from atst.models.environment import Environment from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole from atst.models.environment_role import EnvironmentRole
from atst.models.application import Application from atst.models.application import Application
from atst.models.role import Role from atst.models.permission_set import PermissionSet
from atst.models.user import User from atst.models.user import User
from atst.models.portfolio_role import PortfolioRole from atst.models.portfolio_role import PortfolioRole
from atst.models.portfolio import Portfolio from atst.models.portfolio import Portfolio

View File

@ -9,23 +9,31 @@ sys.path.append(parent_dir)
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from atst.app import make_config, make_app from atst.app import make_config, make_app
from atst.database import db from atst.database import db
from atst.models import Role, Permissions from atst.models import PermissionSet, Permissions
from atst.domain.roles import ATAT_ROLES, PORTFOLIO_ROLES from atst.domain.permission_sets import ATAT_ROLES, PORTFOLIO_PERMISSION_SETS
def seed_roles(): def seed_roles():
for role_info in ATAT_ROLES + PORTFOLIO_ROLES: for permission_set_info in ATAT_ROLES + PORTFOLIO_PERMISSION_SETS:
role = Role(**role_info) permission_set = PermissionSet(**permission_set_info)
try: try:
existing_role = db.session.query(Role).filter_by(name=role.name).one() existing_permission_set = (
existing_role.description = role.description db.session.query(PermissionSet)
existing_role.permissions = role.permissions .filter_by(name=permission_set.name)
existing_role.display_name = role.display_name .one()
db.session.add(existing_role) )
print("Updated existing role {}".format(existing_role.name)) existing_permission_set.description = permission_set.description
existing_permission_set.permissions = permission_set.permissions
existing_permission_set.display_name = permission_set.display_name
db.session.add(existing_permission_set)
print(
"Updated existing permission_set {}".format(
existing_permission_set.name
)
)
except NoResultFound: except NoResultFound:
db.session.add(role) db.session.add(permission_set)
print("Added new role {}".format(role.name)) print("Added new permission_set {}".format(permission_set.name))
db.session.commit() db.session.commit()

View File

@ -3,7 +3,7 @@
{% extends "portfolios/base.html" %} {% extends "portfolios/base.html" %}
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %} {% set can_create_applications = user_can(permissions.CREATE_APPLICATION) %}
{% block portfolio_content %} {% block portfolio_content %}
@ -41,13 +41,13 @@
<h3 class='icon-link accordian__title' v-on:click="props.toggle">{{ application.name }}</h3> <h3 class='icon-link accordian__title' v-on:click="props.toggle">{{ application.name }}</h3>
<span class='accordian__description'>{{ application.description }}</span> <span class='accordian__description'>{{ application.description }}</span>
<div class='accordian__actions'> <div class='accordian__actions'>
{% if user_can(permissions.RENAME_APPLICATION_IN_PORTFOLIO) %} {% if user_can(permissions.EDIT_APPLICATION) %}
<a class='icon-link' href='{{ url_for("portfolios.edit_application", portfolio_id=portfolio.id, application_id=application.id) }}'> <a class='icon-link' href='{{ url_for("portfolios.edit_application", portfolio_id=portfolio.id, application_id=application.id) }}'>
<span>{{ "portfolios.applications.app_settings_text" | translate }}</span> <span>{{ "portfolios.applications.app_settings_text" | translate }}</span>
</a> </a>
<div class='separator'></div> <div class='separator'></div>
{% endif %} {% endif %}
{% if user_can(permissions.VIEW_PORTFOLIO_MEMBERS) %} {% if user_can(permissions.VIEW_PORTFOLIO_USERS) %}
<a class='icon-link' href='{{ url_for("portfolios.application_members", portfolio_id=portfolio.id, application_id=application.id) }}'> <a class='icon-link' href='{{ url_for("portfolios.application_members", portfolio_id=portfolio.id, application_id=application.id) }}'>
<span>{{ "portfolios.applications.team_text" | translate }}</span> <span>{{ "portfolios.applications.team_text" | translate }}</span>
<span class='counter'>{{ application.num_users }}</span> <span class='counter'>{{ application.num_users }}</span>

View File

@ -11,7 +11,7 @@
{% if not portfolio.members %} {% if not portfolio.members %}
{% set user_can_invite = user_can(permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE) %} {% set user_can_invite = user_can(permissions.CREATE_PORTFOLIO_USERS) %}
{{ EmptyState( {{ EmptyState(
'There are currently no members in this Portfolio.', 'There are currently no members in this Portfolio.',

View File

@ -56,7 +56,7 @@
</div> </div>
</div> </div>
<div class='row links'> <div class='row links'>
{% if user_can(permissions.VIEW_USAGE_DOLLARS) %} {% if user_can(permissions.VIEW_PORTFOLIO_REPORTS) %}
{{ Link( {{ Link(
icon='chart-pie', icon='chart-pie',
text='navigation.portfolio_navigation.breadcrumbs.reports' | translate, text='navigation.portfolio_navigation.breadcrumbs.reports' | translate,
@ -70,7 +70,7 @@
url=url_for("portfolios.portfolio_funding", portfolio_id=portfolio.id), url=url_for("portfolios.portfolio_funding", portfolio_id=portfolio.id),
active=request.url_rule.endpoint == "portfolios.portfolio_funding", active=request.url_rule.endpoint == "portfolios.portfolio_funding",
) }} ) }}
{% if user_can(permissions.EDIT_PORTFOLIO_INFORMATION) %} {% if user_can(permissions.EDIT_PORTFOLIO_NAME) %}
{{ Link( {{ Link(
icon='cog', icon='cog',
text='navigation.portfolio_navigation.breadcrumbs.admin' | translate, text='navigation.portfolio_navigation.breadcrumbs.admin' | translate,

View File

@ -21,7 +21,30 @@
<h1 class='member-card__heading'>{{ member.user.full_name }}</h1> <h1 class='member-card__heading'>{{ member.user.full_name }}</h1>
<div class="usa-input member-card__input"> <div class="usa-input member-card__input">
{{ Selector(form.portfolio_role) }} <table>
<thead>
<tr>
<th>{{ "portfolios.members.permissions.app_mgmt" | translate }}</th>
<th>{{ "portfolios.members.permissions.funding" | translate }}</th>
<th>{{ "portfolios.members.permissions.reporting" | translate }}</th>
<th>{{ "portfolios.members.permissions.portfolio_mgmt" | translate }}</th>
</tr>
</thead>
<tbody>
<td>
{{ form.perms_app_mgmt() }}
</td>
<td>
{{ form.perms_funding() }}
</td>
<td>
{{ form.perms_reporting() }}
</td>
<td>
{{ form.perms_portfolio_mgmt() }}
</td>
</tbody>
</table>
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@
{% if not portfolio.members %} {% if not portfolio.members %}
{% set user_can_invite = user_can(permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE) %} {% set user_can_invite = user_can(permissions.CREATE_PORTFOLIO_USERS) %}
{{ EmptyState( {{ EmptyState(
'There are currently no members in this Portfolio.', 'There are currently no members in this Portfolio.',
@ -29,7 +29,6 @@
id="search-template" id="search-template"
class='member-list' class='member-list'
v-bind:members='{{ members | tojson}}' v-bind:members='{{ members | tojson}}'
v-bind:role_choices='{{ role_choices | tojson}}'
v-bind:status_choices='{{ status_choices | tojson}}'> v-bind:status_choices='{{ status_choices | tojson}}'>
<div> <div>
<form class='search-bar' @submit.prevent> <form class='search-bar' @submit.prevent>
@ -50,17 +49,6 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class='usa-input'>
<label for='filter-role'>Filter members by role</label>
<select v-model="role" id="filter-role" name="filter-role">
<option value="" selected disabled>Filter by role</option>
<option value="all">View All</option>
{% for role in role_choices %}
<option value='{{ role.name }}'>{{ role.display_name }}</option>
{% endfor %}
</select>
</div>
</div> </div>
</form> </form>

View File

@ -22,11 +22,33 @@
{{ TextInput(form.last_name) }} {{ TextInput(form.last_name) }}
{{ TextInput(form.email,placeholder='jane@mail.mil', validation='email') }} {{ TextInput(form.email,placeholder='jane@mail.mil', validation='email') }}
{{ TextInput(form.dod_id,placeholder='10-digit number on the back of the CAC', validation='dodId') }} {{ TextInput(form.dod_id,placeholder='10-digit number on the back of the CAC', validation='dodId') }}
{{ Selector(form.portfolio_role) }} <table>
<thead>
<tr>
<th>{{ "portfolios.members.permissions.app_mgmt" | translate }}</th>
<th>{{ "portfolios.members.permissions.funding" | translate }}</th>
<th>{{ "portfolios.members.permissions.reporting" | translate }}</th>
<th>{{ "portfolios.members.permissions.portfolio_mgmt" | translate }}</th>
</tr>
</thead>
<tbody>
<td>
{{ form.perms_app_mgmt() }}
</td>
<td>
{{ form.perms_funding() }}
</td>
<td>
{{ form.perms_reporting() }}
</td>
<td>
{{ form.perms_portfolio_mgmt() }}
</td>
</tbody>
</table>
</div> </div>
</div> </div>
<div class='action-group'> <div class='action-group'>
<button class="usa-button usa-button-big usa-button-primary" tabindex="0">Add User</button> <button class="usa-button usa-button-big usa-button-primary" tabindex="0">Add User</button>
<a href='{{ url_for("portfolios.portfolio_members", portfolio_id=portfolio.id) }}' class='action-group__action icon-link'> <a href='{{ url_for("portfolios.portfolio_members", portfolio_id=portfolio.id) }}' class='action-group__action icon-link'>

View File

@ -134,7 +134,7 @@
{% if not portfolio.applications %} {% if not portfolio.applications %}
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %} {% set can_create_applications = user_can(permissions.CREATE_APPLICATION) %}
{% set message = 'This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.' {% set message = 'This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.'
if can_create_applications if can_create_applications
else 'This portfolio has no cloud environments set up, so there is no spending data to report. Contact the portfolio owner to set up some cloud environments.' else 'This portfolio has no cloud environments set up, so there is no spending data to report. Contact the portfolio owner to set up some cloud environments.'

View File

@ -2,7 +2,7 @@ import pytest
from atst.domain.audit_log import AuditLog from atst.domain.audit_log import AuditLog
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
from atst.domain.roles import Roles from atst.domain.permission_sets import PermissionSets
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from tests.factories import ( from tests.factories import (
UserFactory, UserFactory,
@ -19,7 +19,7 @@ def ccpo():
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def developer(): def developer():
return UserFactory.from_atat_role("default") return UserFactory.create()
def test_non_admin_cannot_view_audit_log(developer): def test_non_admin_cannot_view_audit_log(developer):
@ -27,6 +27,7 @@ def test_non_admin_cannot_view_audit_log(developer):
AuditLog.get_all_events(developer) AuditLog.get_all_events(developer)
@pytest.mark.skip(reason="no ccpo access yet")
def test_ccpo_can_view_audit_log(ccpo): def test_ccpo_can_view_audit_log(ccpo):
events = AuditLog.get_all_events(ccpo) events = AuditLog.get_all_events(ccpo)
assert len(events) > 0 assert len(events) > 0
@ -41,6 +42,7 @@ def test_paginate_audit_log(ccpo):
assert len(events) == 25 assert len(events) == 25
@pytest.mark.skip(reason="no ccpo access yet")
def test_ccpo_can_view_ws_audit_log(ccpo): def test_ccpo_can_view_ws_audit_log(ccpo):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
events = AuditLog.get_portfolio_events(ccpo, portfolio) events = AuditLog.get_portfolio_events(ccpo, portfolio)
@ -51,10 +53,7 @@ def test_ws_admin_can_view_ws_audit_log():
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
admin = UserFactory.create() admin = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
portfolio=portfolio, portfolio=portfolio, user=admin, status=PortfolioRoleStatus.ACTIVE
user=admin,
role=Roles.get("admin"),
status=PortfolioRoleStatus.ACTIVE,
) )
events = AuditLog.get_portfolio_events(admin, portfolio) events = AuditLog.get_portfolio_events(admin, portfolio)
assert len(events) > 0 assert len(events) > 0
@ -66,6 +65,7 @@ def test_ws_owner_can_view_ws_audit_log():
assert len(events) > 0 assert len(events) > 0
@pytest.mark.skip(reason="all portfolio users can view audit log")
def test_other_users_cannot_view_ws_audit_log(): def test_other_users_cannot_view_ws_audit_log():
with pytest.raises(UnauthorizedError): with pytest.raises(UnauthorizedError):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()

View File

@ -1,8 +1,10 @@
import pytest import pytest
from tests.factories import TaskOrderFactory, UserFactory from tests.factories import TaskOrderFactory, UserFactory, PortfolioRoleFactory
from atst.domain.authz import Authorization from atst.domain.authz import Authorization
from atst.domain.permission_sets import PermissionSets
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
from atst.models.permissions import Permissions
@pytest.fixture @pytest.fixture
@ -40,3 +42,19 @@ def test_check_is_ko_or_cor(task_order, invalid_user):
with pytest.raises(UnauthorizedError): with pytest.raises(UnauthorizedError):
Authorization.check_is_ko_or_cor(invalid_user, task_order) Authorization.check_is_ko_or_cor(invalid_user, task_order)
def test_has_portfolio_permission():
role_one = PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING)
role_two = PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_REPORTS)
port_role = PortfolioRoleFactory.create(permission_sets=[role_one, role_two])
different_user = UserFactory.create()
assert Authorization.has_portfolio_permission(
port_role.user, port_role.portfolio, Permissions.VIEW_PORTFOLIO_REPORTS
)
assert not Authorization.has_portfolio_permission(
port_role.user, port_role.portfolio, Permissions.CREATE_TASK_ORDER
)
assert not Authorization.has_portfolio_permission(
different_user, port_role.portfolio, Permissions.VIEW_PORTFOLIO_REPORTS
)

View File

@ -14,7 +14,7 @@ def test_create_environments():
def test_create_environment_role_creates_cloud_id(session): def test_create_environment_role_creates_cloud_id(session):
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") developer = UserFactory.create()
portfolio = PortfolioFactory.create( portfolio = PortfolioFactory.create(
owner=owner, owner=owner,
@ -38,7 +38,7 @@ def test_create_environment_role_creates_cloud_id(session):
def test_update_environment_roles(): def test_update_environment_roles():
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") developer = UserFactory.create()
portfolio = PortfolioFactory.create( portfolio = PortfolioFactory.create(
owner=owner, owner=owner,
@ -81,7 +81,7 @@ def test_update_environment_roles():
def test_remove_environment_role(): def test_remove_environment_role():
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") developer = UserFactory.create()
portfolio = PortfolioFactory.create( portfolio = PortfolioFactory.create(
owner=owner, owner=owner,
members=[{"user": developer, "role_name": "developer"}], members=[{"user": developer, "role_name": "developer"}],
@ -132,7 +132,7 @@ def test_remove_environment_role():
def test_no_update_to_environment_roles(): def test_no_update_to_environment_roles():
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") developer = UserFactory.create()
portfolio = PortfolioFactory.create( portfolio = PortfolioFactory.create(
owner=owner, owner=owner,

View File

@ -0,0 +1,32 @@
import pytest
from atst.domain.permission_sets import PermissionSets
from atst.domain.exceptions import NotFoundError
from atst.utils import first_or_none
def test_get_all():
roles = PermissionSets.get_all()
assert roles
def test_get_existing_permission_set():
role = PermissionSets.get("portfolio_poc")
assert role.name == "portfolio_poc"
def test_get_nonexistent_permission_set():
with pytest.raises(NotFoundError):
PermissionSets.get("nonexistent")
def test_get_many():
perms_sets = PermissionSets.get_many(
[PermissionSets.VIEW_PORTFOLIO_FUNDING, PermissionSets.EDIT_PORTFOLIO_FUNDING]
)
assert len(perms_sets) == 2
assert first_or_none(
lambda p: p.name == PermissionSets.VIEW_PORTFOLIO_FUNDING, perms_sets
)
assert first_or_none(
lambda p: p.name == PermissionSets.EDIT_PORTFOLIO_FUNDING, perms_sets
)

View File

@ -1,7 +1,7 @@
from atst.domain.portfolio_roles import PortfolioRoles from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.users import Users from atst.domain.users import Users
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from atst.domain.roles import Roles from atst.domain.permission_sets import PermissionSets
from tests.factories import ( from tests.factories import (
PortfolioFactory, PortfolioFactory,
@ -11,55 +11,21 @@ from tests.factories import (
) )
def test_can_create_new_portfolio_role(): def test_add_portfolio_role_with_permission_sets():
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
new_user = UserFactory.create() new_user = UserFactory.create()
permission_sets = [PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT]
portfolio_role_dicts = [{"id": new_user.id, "portfolio_role": "owner"}] port_role = PortfolioRoles.add(
portfolio_roles = PortfolioRoles.add_many(portfolio.id, portfolio_role_dicts) new_user, portfolio.id, permission_sets=permission_sets
assert portfolio_roles[0].user_id == new_user.id
assert portfolio_roles[0].user.atat_role.name == new_user.atat_role.name
assert portfolio_roles[0].role.name == new_user.portfolio_roles[0].role.name
def test_can_update_existing_portfolio_role():
portfolio = PortfolioFactory.create()
new_user = UserFactory.create()
PortfolioRoles.add_many(
portfolio.id, [{"id": new_user.id, "portfolio_role": "owner"}]
)
portfolio_roles = PortfolioRoles.add_many(
portfolio.id, [{"id": new_user.id, "portfolio_role": "developer"}]
)
assert portfolio_roles[0].user.atat_role.name == new_user.atat_role.name
assert portfolio_roles[0].role.name == new_user.portfolio_roles[0].role.name
def test_portfolio_role_permissions():
portfolio_one = PortfolioFactory.create()
portfolio_two = PortfolioFactory.create()
new_user = UserFactory.create()
PortfolioRoleFactory.create(
portfolio=portfolio_one,
user=new_user,
role=Roles.get("developer"),
status=PortfolioRoleStatus.ACTIVE,
)
PortfolioRoleFactory.create(
portfolio=portfolio_two,
user=new_user,
role=Roles.get("developer"),
status=PortfolioRoleStatus.PENDING,
)
default_perms = set(new_user.atat_role.permissions)
assert len(
PortfolioRoles.portfolio_role_permissions(portfolio_one, new_user)
) > len(default_perms)
assert (
PortfolioRoles.portfolio_role_permissions(portfolio_two, new_user)
== default_perms
) )
assert len(port_role.permission_sets) == 6
expected_names = [
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
PermissionSets.VIEW_PORTFOLIO_FUNDING,
PermissionSets.VIEW_PORTFOLIO_REPORTS,
PermissionSets.VIEW_PORTFOLIO_ADMIN,
PermissionSets.VIEW_PORTFOLIO,
]
actual_names = [prms.name for prms in port_role.permission_sets]
assert expected_names == expected_names

View File

@ -6,9 +6,15 @@ from atst.domain.portfolios import Portfolios, PortfolioError
from atst.domain.portfolio_roles import PortfolioRoles from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.permission_sets import PermissionSets, PORTFOLIO_PERMISSION_SETS
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from tests.factories import UserFactory, PortfolioRoleFactory, PortfolioFactory from tests.factories import (
UserFactory,
PortfolioRoleFactory,
PortfolioFactory,
get_all_portfolio_permission_sets,
)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -52,7 +58,7 @@ def test_get_for_update_applications_allows_owner(portfolio, portfolio_owner):
def test_get_for_update_applications_blocks_developer(portfolio): def test_get_for_update_applications_blocks_developer(portfolio):
developer = UserFactory.create() developer = UserFactory.create()
PortfolioRoles.add(developer, portfolio.id, "developer") PortfolioRoles.add(developer, portfolio.id)
with pytest.raises(UnauthorizedError): with pytest.raises(UnauthorizedError):
Portfolios.get_for_update_applications(developer, portfolio.id) Portfolios.get_for_update_applications(developer, portfolio.id)
@ -113,13 +119,12 @@ def test_update_portfolio_role_role(portfolio, portfolio_owner):
} }
PortfolioRoleFactory._meta.sqlalchemy_session_persistence = "flush" PortfolioRoleFactory._meta.sqlalchemy_session_persistence = "flush"
member = PortfolioRoleFactory.create(portfolio=portfolio) member = PortfolioRoleFactory.create(portfolio=portfolio)
role_name = "admin" permission_sets = [PermissionSets.EDIT_PORTFOLIO_FUNDING]
updated_member = Portfolios.update_member( updated_member = Portfolios.update_member(
portfolio_owner, portfolio, member, role_name portfolio_owner, portfolio, member, permission_sets=permission_sets
) )
assert updated_member.portfolio == portfolio assert updated_member.portfolio == portfolio
assert updated_member.role_name == role_name
def test_need_permission_to_update_portfolio_role_role(portfolio, portfolio_owner): def test_need_permission_to_update_portfolio_role_role(portfolio, portfolio_owner):
@ -144,18 +149,40 @@ def test_owner_can_view_portfolio_members(portfolio, portfolio_owner):
assert portfolio assert portfolio
@pytest.mark.skip(reason="no ccpo access yet")
def test_ccpo_can_view_portfolio_members(portfolio, portfolio_owner): def test_ccpo_can_view_portfolio_members(portfolio, portfolio_owner):
ccpo = UserFactory.from_atat_role("ccpo") ccpo = UserFactory.from_atat_role("ccpo")
assert Portfolios.get_with_members(ccpo, portfolio.id) assert Portfolios.get_with_members(ccpo, portfolio.id)
def test_random_user_cannot_view_portfolio_members(portfolio): def test_random_user_cannot_view_portfolio_members(portfolio):
developer = UserFactory.from_atat_role("developer") developer = UserFactory.create()
with pytest.raises(UnauthorizedError): with pytest.raises(UnauthorizedError):
portfolio = Portfolios.get_with_members(developer, portfolio.id) portfolio = Portfolios.get_with_members(developer, portfolio.id)
def test_scoped_portfolio_for_admin_missing_view_apps_perms(portfolio_owner, portfolio):
Applications.create(
portfolio_owner,
portfolio,
"My Application 2",
"My application 2",
["dev", "staging", "prod"],
)
restricted_admin = UserFactory.create()
PortfolioRoleFactory.create(
portfolio=portfolio,
user=restricted_admin,
permission_sets=[PermissionSets.get(PermissionSets.VIEW_PORTFOLIO)],
)
scoped_portfolio = Portfolios.get(restricted_admin, portfolio.id)
assert scoped_portfolio.id == portfolio.id
assert len(portfolio.applications) == 1
assert len(scoped_portfolio.applications) == 0
@pytest.mark.skip(reason="should be reworked pending application member changes")
def test_scoped_portfolio_only_returns_a_users_applications_and_environments( def test_scoped_portfolio_only_returns_a_users_applications_and_environments(
portfolio, portfolio_owner portfolio, portfolio_owner
): ):
@ -173,7 +200,7 @@ def test_scoped_portfolio_only_returns_a_users_applications_and_environments(
"My application 2", "My application 2",
["dev", "staging", "prod"], ["dev", "staging", "prod"],
) )
developer = UserFactory.from_atat_role("developer") developer = UserFactory.create()
dev_environment = Environments.add_member( dev_environment = Environments.add_member(
new_application.environments[0], developer, "developer" new_application.environments[0], developer, "developer"
) )
@ -198,9 +225,10 @@ def test_scoped_portfolio_returns_all_applications_for_portfolio_admin(
["dev", "staging", "prod"], ["dev", "staging", "prod"],
) )
admin = UserFactory.from_atat_role("default") admin = UserFactory.create()
Portfolios._create_portfolio_role( perm_sets = get_all_portfolio_permission_sets()
admin, portfolio, "admin", status=PortfolioRoleStatus.ACTIVE PortfolioRoleFactory.create(
user=admin, portfolio=portfolio, permission_sets=perm_sets
) )
scoped_portfolio = Portfolios.get(admin, portfolio.id) scoped_portfolio = Portfolios.get(admin, portfolio.id)
@ -227,7 +255,7 @@ def test_scoped_portfolio_returns_all_applications_for_portfolio_owner(
def test_for_user_returns_active_portfolios_for_user(portfolio, portfolio_owner): def test_for_user_returns_active_portfolios_for_user(portfolio, portfolio_owner):
bob = UserFactory.from_atat_role("default") bob = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
user=bob, portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE user=bob, portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE
) )
@ -239,14 +267,15 @@ def test_for_user_returns_active_portfolios_for_user(portfolio, portfolio_owner)
def test_for_user_does_not_return_inactive_portfolios(portfolio, portfolio_owner): def test_for_user_does_not_return_inactive_portfolios(portfolio, portfolio_owner):
bob = UserFactory.from_atat_role("default") bob = UserFactory.create()
Portfolios.add_member(portfolio, bob, "developer") Portfolios.add_member(portfolio, bob)
PortfolioFactory.create() PortfolioFactory.create()
bobs_portfolios = Portfolios.for_user(bob) bobs_portfolios = Portfolios.for_user(bob)
assert len(bobs_portfolios) == 0 assert len(bobs_portfolios) == 0
@pytest.mark.skip(reason="CCPO status not fully implemented")
def test_for_user_returns_all_portfolios_for_ccpo(portfolio, portfolio_owner): def test_for_user_returns_all_portfolios_for_ccpo(portfolio, portfolio_owner):
sam = UserFactory.from_atat_role("ccpo") sam = UserFactory.from_atat_role("ccpo")
PortfolioFactory.create() PortfolioFactory.create()
@ -260,16 +289,18 @@ def test_get_for_update_information(portfolio, portfolio_owner):
assert portfolio == owner_ws assert portfolio == owner_ws
admin = UserFactory.create() admin = UserFactory.create()
Portfolios._create_portfolio_role( perm_sets = get_all_portfolio_permission_sets()
admin, portfolio, "admin", status=PortfolioRoleStatus.ACTIVE PortfolioRoleFactory.create(
user=admin, portfolio=portfolio, permission_sets=perm_sets
) )
admin_ws = Portfolios.get_for_update_information(admin, portfolio.id) admin_ws = Portfolios.get_for_update_information(admin, portfolio.id)
assert portfolio == admin_ws assert portfolio == admin_ws
ccpo = UserFactory.from_atat_role("ccpo") # TODO: implement ccpo roles
assert Portfolios.get_for_update_information(ccpo, portfolio.id) # ccpo = UserFactory.from_atat_role("ccpo")
# assert Portfolios.get_for_update_information(ccpo, portfolio.id)
developer = UserFactory.from_atat_role("developer") developer = UserFactory.create()
with pytest.raises(UnauthorizedError): with pytest.raises(UnauthorizedError):
Portfolios.get_for_update_information(developer, portfolio.id) Portfolios.get_for_update_information(developer, portfolio.id)

View File

@ -1,18 +0,0 @@
import pytest
from atst.domain.roles import Roles
from atst.domain.exceptions import NotFoundError
def test_get_all_roles():
roles = Roles.get_all()
assert roles
def test_get_existing_role():
role = Roles.get("developer")
assert role.name == "developer"
def test_get_nonexistent_role():
with pytest.raises(NotFoundError):
Roles.get("nonexistent")

View File

@ -2,6 +2,8 @@ import pytest
from atst.domain.task_orders import TaskOrders, TaskOrderError, DD254s from atst.domain.task_orders import TaskOrders, TaskOrderError, DD254s
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
from atst.domain.permission_sets import PermissionSets
from atst.domain.portfolio_roles import PortfolioRoles
from atst.models.attachment import Attachment from atst.models.attachment import Attachment
from tests.factories import ( from tests.factories import (
@ -90,7 +92,7 @@ def test_add_officer_who_is_already_portfolio_member():
assert task_order.contracting_officer == owner assert task_order.contracting_officer == owner
member = task_order.portfolio.members[0] member = task_order.portfolio.members[0]
assert member.user == owner and member.role_name == "owner" assert member.user == owner
def test_task_order_access(): def test_task_order_access():
@ -111,19 +113,26 @@ def test_task_order_access():
portfolio = PortfolioFactory.create(owner=creator) portfolio = PortfolioFactory.create(owner=creator)
task_order = TaskOrderFactory.create(creator=creator, portfolio=portfolio) task_order = TaskOrderFactory.create(creator=creator, portfolio=portfolio)
PortfolioRoleFactory.create(user=member, portfolio=task_order.portfolio) PortfolioRoleFactory.create(
user=member,
portfolio=task_order.portfolio,
permission_sets=[
PermissionSets.get(prms)
for prms in PortfolioRoles.DEFAULT_PORTFOLIO_PERMISSION_SETS
],
)
TaskOrders.add_officer( TaskOrders.add_officer(
creator, task_order, "contracting_officer", officer.to_dictionary() creator, task_order, "contracting_officer", officer.to_dictionary()
) )
check_access([creator, officer], [member, rando], "get", [task_order.id]) check_access([creator, officer, member], [rando], "get", [task_order.id])
check_access([creator], [officer, member, rando], "create", [portfolio]) check_access([creator, officer], [member, rando], "create", [portfolio])
check_access([creator, officer], [member, rando], "update", [task_order]) check_access([creator, officer], [member, rando], "update", [task_order])
check_access( check_access(
[creator], [creator, officer],
[officer, member, rando], [member, rando],
"add_officer", "add_officer",
[task_order, "contracting_officer", rando.to_dictionary()], [task_order, "contracting_officer", UserFactory.dictionary()],
) )

View File

@ -8,14 +8,14 @@ DOD_ID = "my_dod_id"
def test_create_user(): def test_create_user():
user = Users.create(DOD_ID, "developer") user = Users.create(DOD_ID, "default")
assert user.atat_role.name == "developer" assert user.atat_role.name == "default"
def test_create_user_with_existing_email(): def test_create_user_with_existing_email():
Users.create(DOD_ID, "developer", email="thisusersemail@usersRus.com") Users.create(DOD_ID, "default", email="thisusersemail@usersRus.com")
with pytest.raises(AlreadyExistsError): with pytest.raises(AlreadyExistsError):
Users.create(DOD_ID, "admin", email="thisusersemail@usersRus.com") Users.create(DOD_ID, "ccpo", email="thisusersemail@usersRus.com")
def test_create_user_with_nonexistent_role(): def test_create_user_with_nonexistent_role():
@ -24,61 +24,61 @@ def test_create_user_with_nonexistent_role():
def test_get_or_create_nonexistent_user(): def test_get_or_create_nonexistent_user():
user = Users.get_or_create_by_dod_id(DOD_ID, atat_role_name="developer") user = Users.get_or_create_by_dod_id(DOD_ID, atat_role_name="default")
assert user.dod_id == DOD_ID assert user.dod_id == DOD_ID
def test_get_or_create_existing_user(): def test_get_or_create_existing_user():
Users.get_or_create_by_dod_id(DOD_ID, atat_role_name="developer") Users.get_or_create_by_dod_id(DOD_ID, atat_role_name="default")
user = Users.get_or_create_by_dod_id(DOD_ID, atat_role_name="developer") user = Users.get_or_create_by_dod_id(DOD_ID, atat_role_name="default")
assert user assert user
def test_get_user(): def test_get_user():
new_user = Users.create(DOD_ID, "developer") new_user = Users.create(DOD_ID, "default")
user = Users.get(new_user.id) user = Users.get(new_user.id)
assert user.id == new_user.id assert user.id == new_user.id
def test_get_nonexistent_user(): def test_get_nonexistent_user():
Users.create(DOD_ID, "developer") Users.create(DOD_ID, "default")
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
Users.get(uuid4()) Users.get(uuid4())
def test_get_user_by_dod_id(): def test_get_user_by_dod_id():
new_user = Users.create(DOD_ID, "developer") new_user = Users.create(DOD_ID, "default")
user = Users.get_by_dod_id(DOD_ID) user = Users.get_by_dod_id(DOD_ID)
assert user == new_user assert user == new_user
def test_update_role(): def test_update_role():
new_user = Users.create(DOD_ID, "developer") new_user = Users.create(DOD_ID, "default")
updated_user = Users.update_role(new_user.id, "ccpo") updated_user = Users.update_role(new_user.id, "ccpo")
assert updated_user.atat_role.name == "ccpo" assert updated_user.atat_role.name == "ccpo"
def test_update_role_with_nonexistent_user(): def test_update_role_with_nonexistent_user():
Users.create(DOD_ID, "developer") Users.create(DOD_ID, "default")
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
Users.update_role(uuid4(), "ccpo") Users.update_role(uuid4(), "ccpo")
def test_update_existing_user_with_nonexistent_role(): def test_update_existing_user_with_nonexistent_role():
new_user = Users.create(DOD_ID, "developer") new_user = Users.create(DOD_ID, "default")
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
Users.update_role(new_user.id, "nonexistent") Users.update_role(new_user.id, "nonexistent")
def test_update_user(): def test_update_user():
new_user = Users.create(DOD_ID, "developer") new_user = Users.create(DOD_ID, "default")
updated_user = Users.update(new_user, {"first_name": "Jabba"}) updated_user = Users.update(new_user, {"first_name": "Jabba"})
assert updated_user.first_name == "Jabba" assert updated_user.first_name == "Jabba"
def test_update_user_with_dod_id(): def test_update_user_with_dod_id():
new_user = Users.create(DOD_ID, "developer") new_user = Users.create(DOD_ID, "default")
with pytest.raises(UnauthorizedError) as excinfo: with pytest.raises(UnauthorizedError) as excinfo:
Users.update(new_user, {"dod_id": "1234567890"}) Users.update(new_user, {"dod_id": "1234567890"})

View File

@ -12,14 +12,15 @@ from atst.models.environment import Environment
from atst.models.application import Application from atst.models.application import Application
from atst.models.task_order import TaskOrder from atst.models.task_order import TaskOrder
from atst.models.user import User from atst.models.user import User
from atst.models.role import Role from atst.models.permission_set import PermissionSet
from atst.models.portfolio import Portfolio from atst.models.portfolio import Portfolio
from atst.domain.roles import Roles, PORTFOLIO_ROLES from atst.domain.permission_sets import PermissionSets, PORTFOLIO_PERMISSION_SETS
from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
from atst.models.environment_role import EnvironmentRole from atst.models.environment_role import EnvironmentRole
from atst.models.invitation import Invitation, Status as InvitationStatus from atst.models.invitation import Invitation, Status as InvitationStatus
from atst.models.dd_254 import DD254 from atst.models.dd_254 import DD254
from atst.domain.invitations import Invitations from atst.domain.invitations import Invitations
from atst.domain.portfolio_roles import PortfolioRoles
def random_choice(choices): def random_choice(choices):
@ -63,9 +64,15 @@ def _random_date(year_min, year_max, operation):
) )
def random_portfolio_role(): def base_portfolio_permission_sets():
choice = random.choice(PORTFOLIO_ROLES) return [
return Roles.get(choice["name"]) PermissionSets.get(prms)
for prms in PortfolioRoles.DEFAULT_PORTFOLIO_PERMISSION_SETS
]
def get_all_portfolio_permission_sets():
return PermissionSets.get_many(PortfolioRoles.PORTFOLIO_PERMISSION_SETS)
class Base(factory.alchemy.SQLAlchemyModelFactory): class Base(factory.alchemy.SQLAlchemyModelFactory):
@ -82,7 +89,7 @@ class UserFactory(Base):
email = factory.Faker("email") email = factory.Faker("email")
first_name = factory.Faker("first_name") first_name = factory.Faker("first_name")
last_name = factory.Faker("last_name") last_name = factory.Faker("last_name")
atat_role = factory.LazyFunction(lambda: Roles.get("default")) atat_role = factory.LazyFunction(lambda: PermissionSets.get("default"))
dod_id = factory.LazyFunction(random_dod_id) dod_id = factory.LazyFunction(random_dod_id)
phone_number = factory.LazyFunction(random_phone_number) phone_number = factory.LazyFunction(random_phone_number)
service_branch = factory.LazyFunction(random_service_branch) service_branch = factory.LazyFunction(random_service_branch)
@ -95,7 +102,7 @@ class UserFactory(Base):
@classmethod @classmethod
def from_atat_role(cls, atat_role_name, **kwargs): def from_atat_role(cls, atat_role_name, **kwargs):
role = Roles.get(atat_role_name) role = PermissionSets.get(atat_role_name)
return cls.create(atat_role=role, **kwargs) return cls.create(atat_role=role, **kwargs)
@ -121,19 +128,29 @@ class PortfolioFactory(Base):
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
portfolio=portfolio, portfolio=portfolio,
role=Roles.get("owner"),
user=owner, user=owner,
status=PortfolioRoleStatus.ACTIVE, status=PortfolioRoleStatus.ACTIVE,
permission_sets=get_all_portfolio_permission_sets(),
) )
for member in members: for member in members:
user = member.get("user", UserFactory.create()) user = member.get("user", UserFactory.create())
role_name = member["role_name"] role_name = member["role_name"]
perms_set = None
if member.get("permissions_sets"):
perms_set = [
PermissionSets.get(perm_set)
for perm_set in member.get("permission_sets")
]
else:
perms_set = []
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
portfolio=portfolio, portfolio=portfolio,
role=Roles.get(role_name),
user=user, user=user,
status=PortfolioRoleStatus.ACTIVE, status=PortfolioRoleStatus.ACTIVE,
permission_sets=perms_set,
) )
portfolio.applications = applications portfolio.applications = applications
@ -186,9 +203,9 @@ class PortfolioRoleFactory(Base):
model = PortfolioRole model = PortfolioRole
portfolio = factory.SubFactory(PortfolioFactory) portfolio = factory.SubFactory(PortfolioFactory)
role = factory.LazyFunction(random_portfolio_role)
user = factory.SubFactory(UserFactory) user = factory.SubFactory(UserFactory)
status = PortfolioRoleStatus.PENDING status = PortfolioRoleStatus.PENDING
permission_sets = factory.LazyFunction(base_portfolio_permission_sets)
class EnvironmentRoleFactory(Base): class EnvironmentRoleFactory(Base):

View File

@ -5,7 +5,7 @@ from tests.factories import PortfolioFactory, UserFactory
def test_add_user_to_environment(): def test_add_user_to_environment():
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") developer = UserFactory.create()
portfolio = PortfolioFactory.create(owner=owner) portfolio = PortfolioFactory.create(owner=owner)
application = Applications.create( application = Applications.create(

View File

@ -1,10 +1,11 @@
import pytest
import datetime import datetime
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.permission_sets import PermissionSets
from atst.models.portfolio_role import Status from atst.models.portfolio_role import Status
from atst.models.role import Role
from atst.models.invitation import Status as InvitationStatus from atst.models.invitation import Status as InvitationStatus
from atst.models.audit_event import AuditEvent from atst.models.audit_event import AuditEvent
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
@ -20,12 +21,12 @@ from tests.factories import (
from atst.domain.portfolio_roles import PortfolioRoles from atst.domain.portfolio_roles import PortfolioRoles
def test_has_no_ws_role_history(session): def test_has_no_portfolio_role_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=owner) portfolio = PortfolioFactory.create(owner=owner)
portfolio_role = PortfolioRoles.add(user, portfolio.id, "developer") portfolio_role = PortfolioRoles.add(user, portfolio.id)
create_event = ( create_event = (
session.query(AuditEvent) session.query(AuditEvent)
.filter( .filter(
@ -37,7 +38,8 @@ def test_has_no_ws_role_history(session):
assert not create_event.changed_state assert not create_event.changed_state
def test_has_ws_role_history(session): @pytest.mark.skip(reason="need to update audit log permission set handling")
def test_has_portfolio_role_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = UserFactory.create() user = UserFactory.create()
@ -46,9 +48,7 @@ def test_has_ws_role_history(session):
# in order to get the history, we don't want the PortfolioRoleFactory # in order to get the history, we don't want the PortfolioRoleFactory
# to commit after create() # to commit after create()
PortfolioRoleFactory._meta.sqlalchemy_session_persistence = "flush" PortfolioRoleFactory._meta.sqlalchemy_session_persistence = "flush"
portfolio_role = PortfolioRoleFactory.create( portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=user)
portfolio=portfolio, user=user, role=role
)
PortfolioRoles.update_role(portfolio_role, "admin") PortfolioRoles.update_role(portfolio_role, "admin")
changed_events = ( changed_events = (
session.query(AuditEvent) session.query(AuditEvent)
@ -62,7 +62,7 @@ def test_has_ws_role_history(session):
assert changed_events[0].changed_state["role"][1] == "admin" assert changed_events[0].changed_state["role"][1] == "admin"
def test_has_ws_status_history(session): def test_has_portfolio_status_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = UserFactory.create() user = UserFactory.create()
@ -137,7 +137,7 @@ def test_event_details():
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=owner) portfolio = PortfolioFactory.create(owner=owner)
portfolio_role = PortfolioRoles.add(user, portfolio.id, "developer") portfolio_role = PortfolioRoles.add(user, portfolio.id)
assert portfolio_role.event_details["updated_user_name"] == user.displayname assert portfolio_role.event_details["updated_user_name"] == user.displayname
assert portfolio_role.event_details["updated_user_id"] == str(user.id) assert portfolio_role.event_details["updated_user_id"] == str(user.id)
@ -184,27 +184,16 @@ def test_has_environment_roles():
assert portfolio_role.has_environment_roles assert portfolio_role.has_environment_roles
def test_role_displayname():
owner = UserFactory.create()
developer_data = {
"dod_id": "1234567890",
"first_name": "Test",
"last_name": "User",
"email": "test.user@mail.com",
"portfolio_role": "developer",
}
portfolio = PortfolioFactory.create(owner=owner)
portfolio_role = Portfolios.create_member(owner, portfolio, developer_data)
assert portfolio_role.role_displayname == "Developer"
def test_status_when_member_is_active(): def test_status_when_member_is_active():
portfolio_role = PortfolioRoleFactory.create(status=Status.ACTIVE) portfolio_role = PortfolioRoleFactory.create(status=Status.ACTIVE)
assert portfolio_role.display_status == "Active" assert portfolio_role.display_status == "Active"
def test_status_when_member_is_disabled():
portfolio_role = PortfolioRoleFactory.create(status=Status.DISABLED)
assert portfolio_role.display_status == "Disabled"
def test_status_when_invitation_has_been_rejected_for_expirations(): def test_status_when_invitation_has_been_rejected_for_expirations():
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
@ -229,6 +218,18 @@ def test_status_when_invitation_has_been_rejected_for_wrong_user():
assert portfolio_role.display_status == "Error on invite" assert portfolio_role.display_status == "Error on invite"
def test_status_when_invitation_has_been_revoked():
portfolio = PortfolioFactory.create()
user = UserFactory.create()
portfolio_role = PortfolioRoleFactory.create(
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
)
invitation = InvitationFactory.create(
portfolio_role=portfolio_role, status=InvitationStatus.REVOKED
)
assert portfolio_role.display_status == "Invite revoked"
def test_status_when_invitation_is_expired(): def test_status_when_invitation_is_expired():
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
@ -298,3 +299,11 @@ def test_can_list_all_environments():
) )
assert len(portfolio.all_environments) == 9 assert len(portfolio.all_environments) == 9
def test_can_list_all_permissions():
role_one = PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING)
role_two = PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_REPORTS)
port_role = PortfolioRoleFactory.create(permission_sets=[role_one, role_two])
expected_perms = role_one.permissions + role_two.permissions
assert expected_perms == expected_perms

View File

@ -12,7 +12,7 @@ from tests.factories import (
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.domain.roles import Roles from atst.domain.permission_sets import PermissionSets
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
@ -29,7 +29,7 @@ def test_user_without_permission_has_no_budget_report_link(client, user_session)
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
Portfolios._create_portfolio_role( Portfolios._create_portfolio_role(
user, portfolio, "developer", status=PortfolioRoleStatus.ACTIVE user, portfolio, status=PortfolioRoleStatus.ACTIVE
) )
user_session(user) user_session(user)
response = client.get("/portfolios/{}/applications".format(portfolio.id)) response = client.get("/portfolios/{}/applications".format(portfolio.id))
@ -45,10 +45,7 @@ def test_user_with_permission_has_activity_log_link(client, user_session):
ccpo = UserFactory.from_atat_role("ccpo") ccpo = UserFactory.from_atat_role("ccpo")
admin = UserFactory.create() admin = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
portfolio=portfolio, portfolio=portfolio, user=admin, status=PortfolioRoleStatus.ACTIVE
user=admin,
role=Roles.get("admin"),
status=PortfolioRoleStatus.ACTIVE,
) )
user_session(portfolio.owner) user_session(portfolio.owner)
@ -103,7 +100,7 @@ def test_user_with_permission_has_add_application_link(client, user_session):
def test_user_without_permission_has_no_add_application_link(client, user_session): def test_user_without_permission_has_no_add_application_link(client, user_session):
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
Portfolios._create_portfolio_role(user, portfolio, "developer") Portfolios._create_portfolio_role(user, portfolio)
user_session(user) user_session(user)
response = client.get("/portfolios/{}/applications".format(portfolio.id)) response = client.get("/portfolios/{}/applications".format(portfolio.id))
assert ( assert (

View File

@ -12,6 +12,7 @@ from atst.domain.portfolios import Portfolios
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from atst.models.invitation import Status as InvitationStatus from atst.models.invitation import Status as InvitationStatus
from atst.domain.users import Users from atst.domain.users import Users
from atst.domain.permission_sets import PermissionSets
def test_existing_member_accepts_valid_invite(client, user_session): def test_existing_member_accepts_valid_invite(client, user_session):
@ -45,11 +46,18 @@ def test_new_member_accepts_valid_invite(monkeypatch, client, user_session):
user_info = UserFactory.dictionary() user_info = UserFactory.dictionary()
user_session(portfolio.owner) user_session(portfolio.owner)
client.post( response = client.post(
url_for("portfolios.create_member", portfolio_id=portfolio.id), url_for("portfolios.create_member", portfolio_id=portfolio.id),
data={"portfolio_role": "developer", **user_info}, data={
"perms_app_mgmt": PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
"perms_funding": PermissionSets.VIEW_PORTFOLIO_FUNDING,
"perms_reporting": PermissionSets.VIEW_PORTFOLIO_REPORTS,
"perms_portfolio_mgmt": PermissionSets.VIEW_PORTFOLIO_ADMIN,
**user_info,
},
) )
assert response.status_code == 302
user = Users.get_by_dod_id(user_info["dod_id"]) user = Users.get_by_dod_id(user_info["dod_id"])
token = user.invitations[0].token token = user.invitations[0].token
@ -94,7 +102,7 @@ def test_user_who_has_not_accepted_portfolio_invite_cannot_view(client, user_ses
user_session(portfolio.owner) user_session(portfolio.owner)
response = client.post( response = client.post(
url_for("portfolios.create_member", portfolio_id=portfolio.id), url_for("portfolios.create_member", portfolio_id=portfolio.id),
data={"portfolio_role": "developer", **user.to_dictionary()}, data=user.to_dictionary(),
) )
# user tries to view portfolio before accepting invitation # user tries to view portfolio before accepting invitation

View File

@ -12,35 +12,48 @@ from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.permission_sets import PermissionSets
from atst.queue import queue from atst.queue import queue
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from atst.models.invitation import Status as InvitationStatus from atst.models.invitation import Status as InvitationStatus
_DEFAULT_PERMS_FORM_DATA = {
"perms_app_mgmt": PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
"perms_funding": PermissionSets.VIEW_PORTFOLIO_FUNDING,
"perms_reporting": PermissionSets.VIEW_PORTFOLIO_REPORTS,
"perms_portfolio_mgmt": PermissionSets.VIEW_PORTFOLIO_ADMIN,
}
def create_portfolio_and_invite_user( def create_portfolio_and_invite_user(
ws_role="developer", ws_role="developer",
ws_status=PortfolioRoleStatus.PENDING, ws_status=PortfolioRoleStatus.PENDING,
invite_status=InvitationStatus.PENDING, invite_status=InvitationStatus.PENDING,
): ):
portfolio = PortfolioFactory.create() owner = UserFactory.create()
portfolio = PortfolioFactory.create(owner=owner)
if ws_role != "owner": if ws_role != "owner":
user = UserFactory.create() user = UserFactory.create()
member = PortfolioRoleFactory.create( member = PortfolioRoleFactory.create(
user=user, portfolio=portfolio, status=ws_status user=user, portfolio=portfolio, status=ws_status
) )
InvitationFactory.create( InvitationFactory.create(
user=portfolio.owner, inviter=portfolio.owner,
user=user,
portfolio_role=member, portfolio_role=member,
email=member.user.email, email=member.user.email,
status=invite_status, status=invite_status,
) )
return portfolio return (portfolio, member)
else:
return (portfolio, portfolio.members[0])
def test_user_with_permission_has_add_member_link(client, user_session): def test_user_with_permission_has_add_member_link(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
response = client.get("/portfolios/{}/members".format(portfolio.id)) response = client.get("/portfolios/{}/members".format(portfolio.id))
assert response.status_code == 200
assert ( assert (
'href="/portfolios/{}/members/new"'.format(portfolio.id).encode() 'href="/portfolios/{}/members/new"'.format(portfolio.id).encode()
in response.data in response.data
@ -50,7 +63,7 @@ def test_user_with_permission_has_add_member_link(client, user_session):
def test_user_without_permission_has_no_add_member_link(client, user_session): def test_user_without_permission_has_no_add_member_link(client, user_session):
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
Portfolios._create_portfolio_role(user, portfolio, "developer") Portfolios._create_portfolio_role(user, portfolio)
user_session(user) user_session(user)
response = client.get("/portfolios/{}/members".format(portfolio.id)) response = client.get("/portfolios/{}/members".format(portfolio.id))
assert ( assert (
@ -62,8 +75,8 @@ def test_user_without_permission_has_no_add_member_link(client, user_session):
def test_permissions_for_view_member(client, user_session): def test_permissions_for_view_member(client, user_session):
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
Portfolios._create_portfolio_role(user, portfolio, "developer") Portfolios._create_portfolio_role(user, portfolio)
member = PortfolioRoles.add(user, portfolio.id, "developer") member = PortfolioRoles.add(user, portfolio.id)
user_session(user) user_session(user)
response = client.get( response = client.get(
url_for("portfolios.view_member", portfolio_id=portfolio.id, member_id=user.id) url_for("portfolios.view_member", portfolio_id=portfolio.id, member_id=user.id)
@ -85,6 +98,7 @@ def test_create_member(client, user_session):
"last_name": "Zuckerman", "last_name": "Zuckerman",
"email": "some_pig@zuckermans.com", "email": "some_pig@zuckermans.com",
"portfolio_role": "developer", "portfolio_role": "developer",
**_DEFAULT_PERMS_FORM_DATA,
}, },
follow_redirects=True, follow_redirects=True,
) )
@ -94,13 +108,16 @@ def test_create_member(client, user_session):
assert user.has_portfolios assert user.has_portfolios
assert user.invitations assert user.invitations
assert len(queue.get_queue()) == queue_length + 1 assert len(queue.get_queue()) == queue_length + 1
portfolio_role = user.portfolio_roles[0]
assert len(portfolio_role.permission_sets) == 5
@pytest.mark.skip(reason="permission set display not implemented")
def test_view_member_shows_role(client, user_session): def test_view_member_shows_role(client, user_session):
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
Portfolios._create_portfolio_role(user, portfolio, "developer") Portfolios._create_portfolio_role(user, portfolio)
member = PortfolioRoles.add(user, portfolio.id, "developer") member = PortfolioRoles.add(user, portfolio.id)
user_session(portfolio.owner) user_session(portfolio.owner)
response = client.get( response = client.get(
url_for("portfolios.view_member", portfolio_id=portfolio.id, member_id=user.id) url_for("portfolios.view_member", portfolio_id=portfolio.id, member_id=user.id)
@ -112,25 +129,29 @@ def test_view_member_shows_role(client, user_session):
def test_update_member_portfolio_role(client, user_session): def test_update_member_portfolio_role(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
member = PortfolioRoles.add(user, portfolio.id, "developer") member = PortfolioRoles.add(user, portfolio.id)
user_session(portfolio.owner) user_session(portfolio.owner)
response = client.post( response = client.post(
url_for( url_for(
"portfolios.update_member", portfolio_id=portfolio.id, member_id=user.id "portfolios.update_member", portfolio_id=portfolio.id, member_id=user.id
), ),
data={"portfolio_role": "security_auditor"}, data={
**_DEFAULT_PERMS_FORM_DATA,
"perms_funding": PermissionSets.EDIT_PORTFOLIO_FUNDING,
},
follow_redirects=True, follow_redirects=True,
) )
assert response.status_code == 200 assert response.status_code == 200
assert b"role updated successfully" in response.data edit_funding = PermissionSets.get(PermissionSets.EDIT_PORTFOLIO_FUNDING)
assert member.role_name == "security_auditor" assert edit_funding in member.permission_sets
def test_update_member_portfolio_role_with_no_data(client, user_session): def test_update_member_portfolio_role_with_no_data(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
member = PortfolioRoles.add(user, portfolio.id, "developer") member = PortfolioRoles.add(user, portfolio.id)
user_session(portfolio.owner) user_session(portfolio.owner)
original_perms_len = len(member.permission_sets)
response = client.post( response = client.post(
url_for( url_for(
"portfolios.update_member", portfolio_id=portfolio.id, member_id=user.id "portfolios.update_member", portfolio_id=portfolio.id, member_id=user.id
@ -139,13 +160,13 @@ def test_update_member_portfolio_role_with_no_data(client, user_session):
follow_redirects=True, follow_redirects=True,
) )
assert response.status_code == 200 assert response.status_code == 200
assert member.role_name == "developer" assert len(member.permission_sets) == original_perms_len
def test_update_member_environment_role(client, user_session): def test_update_member_environment_role(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
member = PortfolioRoles.add(user, portfolio.id, "developer") member = PortfolioRoles.add(user, portfolio.id)
application = Applications.create( application = Applications.create(
portfolio.owner, portfolio.owner,
portfolio, portfolio,
@ -163,9 +184,9 @@ def test_update_member_environment_role(client, user_session):
"portfolios.update_member", portfolio_id=portfolio.id, member_id=user.id "portfolios.update_member", portfolio_id=portfolio.id, member_id=user.id
), ),
data={ data={
"portfolio_role": "developer",
"env_" + str(env1_id): "security_auditor", "env_" + str(env1_id): "security_auditor",
"env_" + str(env2_id): "devops", "env_" + str(env2_id): "devops",
**_DEFAULT_PERMS_FORM_DATA,
}, },
follow_redirects=True, follow_redirects=True,
) )
@ -179,7 +200,7 @@ def test_update_member_environment_role(client, user_session):
def test_update_member_environment_role_with_no_data(client, user_session): def test_update_member_environment_role_with_no_data(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
member = PortfolioRoles.add(user, portfolio.id, "developer") member = PortfolioRoles.add(user, portfolio.id)
application = Applications.create( application = Applications.create(
portfolio.owner, portfolio.owner,
portfolio, portfolio,
@ -242,11 +263,10 @@ def test_does_not_show_any_buttons_if_owner(client, user_session):
def test_only_shows_revoke_access_button_if_active(client, user_session): def test_only_shows_revoke_access_button_if_active(client, user_session):
portfolio = create_portfolio_and_invite_user( portfolio, member = create_portfolio_and_invite_user(
ws_status=PortfolioRoleStatus.ACTIVE, invite_status=InvitationStatus.ACCEPTED ws_status=PortfolioRoleStatus.ACTIVE, invite_status=InvitationStatus.ACCEPTED
) )
user_session(portfolio.owner) user_session(portfolio.owner)
member = portfolio.members[1]
response = client.get( response = client.get(
url_for( url_for(
"portfolios.view_member", "portfolios.view_member",
@ -254,17 +274,18 @@ def test_only_shows_revoke_access_button_if_active(client, user_session):
member_id=member.user.id, member_id=member.user.id,
) )
) )
assert response.status_code == 200
assert "Remove Portfolio Access" in response.data.decode() assert "Remove Portfolio Access" in response.data.decode()
assert "Revoke Invitation" not in response.data.decode() assert "Revoke Invitation" not in response.data.decode()
assert "Resend Invitation" not in response.data.decode() assert "Resend Invitation" not in response.data.decode()
def test_only_shows_revoke_invite_button_if_pending(client, user_session): def test_only_shows_revoke_invite_button_if_pending(client, user_session):
portfolio = create_portfolio_and_invite_user( portfolio, member = create_portfolio_and_invite_user(
ws_status=PortfolioRoleStatus.PENDING, invite_status=InvitationStatus.PENDING ws_status=PortfolioRoleStatus.PENDING, invite_status=InvitationStatus.PENDING
) )
user_session(portfolio.owner) user_session(portfolio.owner)
member = portfolio.members[1] # member = next((memb for memb in portfolio.members if memb != portfolio.owner), None)
response = client.get( response = client.get(
url_for( url_for(
"portfolios.view_member", "portfolios.view_member",
@ -278,12 +299,11 @@ def test_only_shows_revoke_invite_button_if_pending(client, user_session):
def test_only_shows_resend_button_if_expired(client, user_session): def test_only_shows_resend_button_if_expired(client, user_session):
portfolio = create_portfolio_and_invite_user( portfolio, member = create_portfolio_and_invite_user(
ws_status=PortfolioRoleStatus.PENDING, ws_status=PortfolioRoleStatus.PENDING,
invite_status=InvitationStatus.REJECTED_EXPIRED, invite_status=InvitationStatus.REJECTED_EXPIRED,
) )
user_session(portfolio.owner) user_session(portfolio.owner)
member = portfolio.members[1]
response = client.get( response = client.get(
url_for( url_for(
"portfolios.view_member", "portfolios.view_member",
@ -297,11 +317,10 @@ def test_only_shows_resend_button_if_expired(client, user_session):
def test_only_shows_resend_button_if_revoked(client, user_session): def test_only_shows_resend_button_if_revoked(client, user_session):
portfolio = create_portfolio_and_invite_user( portfolio, member = create_portfolio_and_invite_user(
ws_status=PortfolioRoleStatus.PENDING, invite_status=InvitationStatus.REVOKED ws_status=PortfolioRoleStatus.PENDING, invite_status=InvitationStatus.REVOKED
) )
user_session(portfolio.owner) user_session(portfolio.owner)
member = portfolio.members[1]
response = client.get( response = client.get(
url_for( url_for(
"portfolios.view_member", "portfolios.view_member",

View File

@ -2,7 +2,7 @@ from flask import url_for
import pytest import pytest
from datetime import timedelta, date from datetime import timedelta, date
from atst.domain.roles import Roles from atst.domain.permission_sets import PermissionSets
from atst.domain.task_orders import TaskOrders from atst.domain.task_orders import TaskOrders
from atst.models.portfolio_role import Status as PortfolioStatus from atst.models.portfolio_role import Status as PortfolioStatus
from atst.models.invitation import Status as InvitationStatus from atst.models.invitation import Status as InvitationStatus
@ -230,10 +230,13 @@ class TestTaskOrderInvitations:
def test_ko_can_view_task_order(client, user_session, portfolio, user): def test_ko_can_view_task_order(client, user_session, portfolio, user):
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("owner"),
portfolio=portfolio, portfolio=portfolio,
user=user, user=user,
status=PortfolioStatus.ACTIVE, status=PortfolioStatus.ACTIVE,
permission_sets=[
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO),
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING),
],
) )
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=user) task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=user)
user_session(user) user_session(user)
@ -294,16 +297,22 @@ def test_ko_can_view_ko_review_page(client, user_session):
cor = UserFactory.create() cor = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio, portfolio=portfolio,
user=ko, user=ko,
status=PortfolioStatus.ACTIVE, status=PortfolioStatus.ACTIVE,
permission_sets=[
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO),
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING),
],
) )
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio, portfolio=portfolio,
user=cor, user=cor,
status=PortfolioStatus.ACTIVE, status=PortfolioStatus.ACTIVE,
permission_sets=[
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO),
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING),
],
) )
task_order = TaskOrderFactory.create( task_order = TaskOrderFactory.create(
portfolio=portfolio, portfolio=portfolio,
@ -365,10 +374,13 @@ def test_mo_redirected_to_build_page(client, user_session, portfolio):
def test_cor_redirected_to_build_page(client, user_session, portfolio): def test_cor_redirected_to_build_page(client, user_session, portfolio):
cor = UserFactory.create() cor = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio, portfolio=portfolio,
user=cor, user=cor,
status=PortfolioStatus.ACTIVE, status=PortfolioStatus.ACTIVE,
permission_sets=[
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO),
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING),
],
) )
task_order = TaskOrderFactory.create( task_order = TaskOrderFactory.create(
portfolio=portfolio, contracting_officer_representative=cor portfolio=portfolio, contracting_officer_representative=cor
@ -384,10 +396,13 @@ def test_submit_completed_ko_review_page_as_cor(
client, user_session, pdf_upload, portfolio, user client, user_session, pdf_upload, portfolio, user
): ):
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio, portfolio=portfolio,
user=user, user=user,
status=PortfolioStatus.ACTIVE, status=PortfolioStatus.ACTIVE,
permission_sets=[
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO),
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING),
],
) )
task_order = TaskOrderFactory.create( task_order = TaskOrderFactory.create(
@ -429,10 +444,13 @@ def test_submit_completed_ko_review_page_as_ko(
ko = UserFactory.create() ko = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio, portfolio=portfolio,
user=ko, user=ko,
status=PortfolioStatus.ACTIVE, status=PortfolioStatus.ACTIVE,
permission_sets=[
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO),
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING),
],
) )
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko) task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
@ -470,10 +488,13 @@ def test_submit_completed_ko_review_page_as_ko(
def test_so_review_page(app, client, user_session, portfolio): def test_so_review_page(app, client, user_session, portfolio):
so = UserFactory.create() so = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio, portfolio=portfolio,
user=so, user=so,
status=PortfolioStatus.ACTIVE, status=PortfolioStatus.ACTIVE,
permission_sets=[
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO),
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING),
],
) )
task_order = TaskOrderFactory.create(portfolio=portfolio, security_officer=so) task_order = TaskOrderFactory.create(portfolio=portfolio, security_officer=so)
@ -508,10 +529,13 @@ def test_so_review_page(app, client, user_session, portfolio):
def test_submit_so_review(app, client, user_session, portfolio): def test_submit_so_review(app, client, user_session, portfolio):
so = UserFactory.create() so = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio, portfolio=portfolio,
user=so, user=so,
status=PortfolioStatus.ACTIVE, status=PortfolioStatus.ACTIVE,
permission_sets=[
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO),
PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING),
],
) )
task_order = TaskOrderFactory.create(portfolio=portfolio, security_officer=so) task_order = TaskOrderFactory.create(portfolio=portfolio, security_officer=so)
dd_254_data = DD254Factory.dictionary() dd_254_data = DD254Factory.dictionary()
@ -548,10 +572,7 @@ def test_resend_invite_when_invalid_invite_officer(
) )
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("owner"), portfolio=portfolio, user=user, status=PortfolioStatus.ACTIVE
portfolio=portfolio,
user=user,
status=PortfolioStatus.ACTIVE,
) )
user_session(user) user_session(user)
@ -580,10 +601,7 @@ def test_resend_invite_when_officer_type_missing(
) )
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("owner"), portfolio=portfolio, user=user, status=PortfolioStatus.ACTIVE
portfolio=portfolio,
user=user,
status=PortfolioStatus.ACTIVE,
) )
user_session(user) user_session(user)
@ -610,10 +628,7 @@ def test_resend_invite_when_ko(app, client, user_session, portfolio, user):
) )
portfolio_role = PortfolioRoleFactory.create( portfolio_role = PortfolioRoleFactory.create(
role=Roles.get("owner"), portfolio=portfolio, user=user, status=PortfolioStatus.ACTIVE
portfolio=portfolio,
user=user,
status=PortfolioStatus.ACTIVE,
) )
original_invitation = Invitations.create( original_invitation = Invitations.create(
@ -654,10 +669,7 @@ def test_resend_invite_when_not_pending(app, client, user_session, portfolio, us
) )
portfolio_role = PortfolioRoleFactory.create( portfolio_role = PortfolioRoleFactory.create(
role=Roles.get("owner"), portfolio=portfolio, user=user, status=PortfolioStatus.ACTIVE
portfolio=portfolio,
user=user,
status=PortfolioStatus.ACTIVE,
) )
original_invitation = InvitationFactory.create( original_invitation = InvitationFactory.create(

View File

@ -28,9 +28,6 @@ def test_invite_officers_to_task_order(client, user_session, queue):
# owner and three officers are portfolio members # owner and three officers are portfolio members
assert len(portfolio.members) == 4 assert len(portfolio.members) == 4
roles = [member.role.name for member in portfolio.members]
# officers exist in roles
assert roles.count("officer") == 3
# email invitations are enqueued # email invitations are enqueued
assert len(queue.get_queue()) == 3 assert len(queue.get_queue()) == 3
# task order has relationship to user for each officer role # task order has relationship to user for each officer role

View File

@ -340,3 +340,15 @@ def test_review_task_order_form(client, user_session, task_order):
) )
assert response.status_code == 200 assert response.status_code == 200
def test_update_task_order_clears_unnecessary_other_responses():
user = UserFactory.create()
to_data = TaskOrderFactory.dictionary()
to_data["complexity"] = ["storage"]
to_data["complexity_other"] = "something else"
to_data["dev_team"] = ["civilians"]
to_data["dev_team_other"] = "something else"
workflow = UpdateTaskOrderWorkflow(user, to_data)
assert workflow.task_order_form_data["complexity_other"] is None
assert workflow.task_order_form_data["dev_team_other"] is None

View File

@ -33,7 +33,7 @@ def test_non_owner_user_with_one_portfolio_redirected_to_portfolio_applications(
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
Portfolios._create_portfolio_role( Portfolios._create_portfolio_role(
user, portfolio, "developer", status=PortfolioRoleStatus.ACTIVE user, portfolio, status=PortfolioRoleStatus.ACTIVE
) )
user_session(user) user_session(user)
@ -51,7 +51,7 @@ def test_non_owner_user_with_mulitple_portfolios_redirected_to_portfolios(
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
portfolios.append(portfolio) portfolios.append(portfolio)
role = Portfolios._create_portfolio_role( role = Portfolios._create_portfolio_role(
user, portfolio, "developer", status=PortfolioRoleStatus.ACTIVE user, portfolio, status=PortfolioRoleStatus.ACTIVE
) )
user_session(user) user_session(user)

View File

@ -4,7 +4,7 @@ import pytest
from flask import session, url_for from flask import session, url_for
from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS
from atst.domain.users import Users from atst.domain.users import Users
from atst.domain.roles import Roles from atst.domain.permission_sets import PermissionSets
from atst.domain.exceptions import NotFoundError from atst.domain.exceptions import NotFoundError
from atst.domain.authnid.crl import CRLInvalidException from atst.domain.authnid.crl import CRLInvalidException
from atst.domain.auth import UNPROTECTED_ROUTES from atst.domain.auth import UNPROTECTED_ROUTES
@ -49,7 +49,7 @@ def test_successful_login_redirect_ccpo(client, monkeypatch):
monkeypatch.setattr( monkeypatch.setattr(
"atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True "atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True
) )
role = Roles.get("ccpo") role = PermissionSets.get("ccpo")
monkeypatch.setattr( monkeypatch.setattr(
"atst.domain.authnid.AuthenticationContext.get_user", "atst.domain.authnid.AuthenticationContext.get_user",
lambda *args: UserFactory.create(atat_role=role), lambda *args: UserFactory.create(atat_role=role),

View File

@ -558,6 +558,12 @@ portfolios:
subheading: Team Management subheading: Team Management
admin: admin:
activity_log_title: Activity Log activity_log_title: Activity Log
members:
permissions:
app_mgmt: App Mgmt
funding: Funding
reporting: Reporting
portfolio_mgmt: Portfolio Mgmt
testing: testing:
example_string: Hello World example_string: Hello World
example_with_variables: 'Hello, {name}!' example_with_variables: 'Hello, {name}!'