Merge pull request #962 from anoek/rbac

"Completion" of RBAC migrations; resource_field elimination
This commit is contained in:
Akita Noek 2016-02-15 13:36:31 -05:00
commit 380ba8a41d
12 changed files with 1990 additions and 64 deletions

View File

@ -91,9 +91,8 @@ class ImplicitResourceField(models.ForeignKey):
class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
"""Descriptor Implict Role Fields. Auto-creates the appropriate role entry on first access"""
def __init__(self, role_name, resource_field, permissions, parent_role, *args, **kwargs):
def __init__(self, role_name, permissions, parent_role, *args, **kwargs):
self.role_name = role_name
self.resource_field = resource_field
self.permissions = permissions
self.parent_role = parent_role
@ -143,10 +142,10 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
setattr(instance, self.field.name, role)
instance.save(update_fields=[self.field.name,])
if self.resource_field and self.permissions:
if self.permissions is not None:
permissions = RolePermission(
role=role,
resource=getattr(instance, self.resource_field)
resource=instance.resource
)
if 'all' in self.permissions and self.permissions['all']:
@ -170,9 +169,8 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
class ImplicitRoleField(models.ForeignKey):
"""Implicitly creates a role entry for a resource"""
def __init__(self, role_name=None, resource_field=None, permissions=None, parent_role=None, *args, **kwargs):
def __init__(self, role_name=None, permissions=None, parent_role=None, *args, **kwargs):
self.role_name = role_name
self.resource_field = resource_field
self.permissions = permissions
self.parent_role = parent_role
@ -187,7 +185,6 @@ class ImplicitRoleField(models.ForeignKey):
self.name,
ImplicitRoleDescriptor(
self.role_name,
self.resource_field,
self.permissions,
self.parent_role,
self
@ -211,7 +208,8 @@ class ImplicitRoleField(models.ForeignKey):
first_field_name = field_name.split('.')[0]
field = getattr(cls, first_field_name)
if type(field) is ReverseManyRelatedObjectsDescriptor:
if type(field) is ReverseManyRelatedObjectsDescriptor or \
type(field) is ManyRelatedObjectsDescriptor:
if found_m2m_field:
# This limitation is due to a lack of understanding on my part, the
# trouble being that I can't seem to get m2m_changed to call anything that
@ -227,14 +225,17 @@ class ImplicitRoleField(models.ForeignKey):
found_m2m_field = True
self.m2m_field_name = first_field_name
self.m2m_field_attr = field_name.split('.',1)[1]
m2m_changed.connect(self.m2m_update, field.through)
if type(field) is ManyRelatedObjectsDescriptor:
raise Exception('ManyRelatedObjectsDescriptor references are currently unsupported ' +
'(but the reverse is, so supporting this is probably easy to add)): %s.%s' %
(cls.__name__, first_field_name))
if type(field) is ReverseManyRelatedObjectsDescriptor:
m2m_changed.connect(self.m2m_update, field.through)
else:
m2m_changed.connect(self.m2m_update_related, field.related.through)
def m2m_update_related(self, **kwargs):
kwargs['reverse'] = not kwargs['reverse']
self.m2m_update(**kwargs)
def m2m_update(self, sender, instance, action, reverse, model, pk_set, **kwargs):
if action == 'post_add' or action == 'pre_remove':
if reverse:

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
from collections import defaultdict
import _old_access as old_access
def migrate_users(apps, schema_editor):
migrations = list()
@ -52,7 +53,7 @@ def migrate_inventory(apps, schema_editor):
for inventory in Inventory.objects.all():
teams, users = [], []
for perm in Permission.objects.filter(inventory=inventory):
for perm in Permission.objects.filter(inventory=inventory, active=True):
role = None
execrole = None
if perm.permission_type == 'admin':
@ -64,6 +65,10 @@ def migrate_inventory(apps, schema_editor):
elif perm.permission_type == 'write':
role = inventory.updater_role
pass
elif perm.permission_type == 'check':
pass
elif perm.permission_type == 'run':
pass
else:
raise Exception('Unhandled permission type for inventory: %s' % perm.permission_type)
if perm.run_ad_hoc_commands:
@ -108,7 +113,7 @@ def migrate_projects(apps, schema_editor):
Permission = apps.get_model('main', 'Permission')
for project in Project.objects.all():
if project.organization is None and project.created_by is not None:
if project.organizations.count() == 0 and project.created_by is not None:
project.admin_role.members.add(project.created_by)
migrations[project.name]['users'].add(project.created_by)
@ -116,19 +121,98 @@ def migrate_projects(apps, schema_editor):
team.member_role.children.add(project.member_role)
migrations[project.name]['teams'].add(team)
if project.organization is not None:
for user in project.organization.users.all():
project.member_role.members.add(user)
migrations[project.name]['users'].add(user)
if project.organizations.count() > 0:
for org in project.organizations.all():
for user in org.users.all():
project.member_role.members.add(user)
migrations[project.name]['users'].add(user)
for perm in Permission.objects.filter(project=project):
for perm in Permission.objects.filter(project=project, active=True):
# All perms at this level just imply a user or team can read
if perm.team:
team.member_role.children.add(project.member_role)
migrations[project.name]['teams'].add(team)
perm.team.member_role.children.add(project.member_role)
migrations[project.name]['teams'].add(perm.team)
if perm.user:
project.member_role.members.add(perm.user)
migrations[project.name]['users'].add(perm.user)
return migrations
def migrate_job_templates(apps, schema_editor):
'''
NOTE: This must be run after orgs, inventory, projects, credential, and
users have been migrated
'''
'''
I can see job templates when:
X I am a superuser.
- I can read the inventory, project and credential (which means I am an
org admin or member of a team with access to all of the above).
- I have permission explicitly granted to check/deploy with the inventory
and project.
#This does not mean I would be able to launch a job from the template or
#edit the template.
- access.py can_read for JobTemplate enforces that you can only
see it if you can launch it, so the above imply launch too
'''
'''
Tower administrators, organization administrators, and project
administrators, within a project under their purview, may create and modify
new job templates for that project.
When editing a job template, they may select among the inventory groups and
credentials in the organization for which they have usage permissions, or
they may leave either blank to be selected at runtime.
Additionally, they may specify one or more users/teams that have execution
permission for that job template, among the users/teams that are a member
of that project.
That execution permission is valid irrespective of any explicit permissions
the user has or has not been granted to the inventory group or credential
specified in the job template.
'''
migrations = defaultdict(lambda: defaultdict(set))
User = apps.get_model('auth', 'User')
JobTemplate = apps.get_model('main', 'JobTemplate')
Team = apps.get_model('main', 'Team')
Permission = apps.get_model('main', 'Permission')
for jt in JobTemplate.objects.all():
for team in Team.objects.all():
if Permission.objects.filter(
team=team,
inventory=jt.inventory,
project=jt.project,
active=True,
permission_type__in=['create', 'check', 'run'] if jt.job_type == 'check' else ['create', 'run']
):
team.member_role.children.add(jt.executor_role);
migrations[jt.name]['teams'].add(team)
for user in User.objects.all():
if jt.accessible_by(user, {'execute': True}):
# If the job template is already accessible by the user, because they
# are a sytem, organization, or project admin, then don't add an explicit
# role entry for them
continue
if old_access.check_user_access(user, jt.__class__, 'start', jt, False):
jt.executor_role.members.add(user)
migrations[jt.name]['users'].add(user)
return migrations

View File

@ -158,12 +158,10 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
owner_role = ImplicitRoleField(
role_name='Credential Owner',
parent_role='team.admin_role',
resource_field='resource',
permissions = {'all': True}
)
usage_role = ImplicitRoleField(
role_name='Credential User',
resource_field='resource',
parent_role= 'team.member_role',
permissions = {'use': True}
)

View File

@ -96,13 +96,11 @@ class Inventory(CommonModel, ResourceMixin):
admin_role = ImplicitRoleField(
role_name='Inventory Administrator',
parent_role='organization.admin_role',
resource_field='resource',
permissions = {'all': True}
)
auditor_role = ImplicitRoleField(
role_name='Inventory Auditor',
parent_role='organization.auditor_role',
resource_field='resource',
permissions = {'read': True}
)
updater_role = ImplicitRoleField(
@ -545,25 +543,21 @@ class Group(CommonModelNameNotUnique, ResourceMixin):
admin_role = ImplicitRoleField(
role_name='Inventory Group Administrator',
parent_role=['inventory.admin_role', 'parents.admin_role'],
resource_field='resource',
permissions = {'all': True}
)
auditor_role = ImplicitRoleField(
role_name='Inventory Group Auditor',
parent_role=['inventory.auditor_role', 'parents.auditor_role'],
resource_field='resource',
permissions = {'read': True}
)
updater_role = ImplicitRoleField(
role_name='Inventory Group Updater',
parent_role=['inventory.updater_role', 'parents.updater_role'],
resource_field='resource',
permissions = {'read': True, 'write': True, 'create': True, 'use': True},
)
executor_role = ImplicitRoleField(
role_name='Inventory Group Executor',
parent_role=['inventory.executor_role', 'parents.executor_role'],
resource_field='resource',
permissions = {'read':True, 'execute':True},
)

View File

@ -184,20 +184,16 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
admin_role = ImplicitRoleField(
role_name='Job Template Administrator',
parent_role='project.admin_role',
resource_field='resource',
permissions = {'all': True}
)
auditor_role = ImplicitRoleField(
role_name='Job Template Auditor',
parent_role='project.auditor_role',
resource_field='resource',
permissions = {'read': True}
)
executor_role = ImplicitRoleField(
role_name='Job Template Executor',
parent_role='project.auditor_role',
resource_field='resource',
permissions = {'execute': True}
permissions = {'read': True, 'execute': True}
)
@classmethod

View File

@ -43,10 +43,6 @@ class Organization(CommonModel, ResourceMixin):
blank=True,
related_name='admin_of_organizations',
)
# TODO: This field is deprecated. In 3.0 all projects will have exactly one
# organization parent, the foreign key field representing that has been
# moved to the Project model.
projects = models.ManyToManyField(
'Project',
blank=True,
@ -55,18 +51,15 @@ class Organization(CommonModel, ResourceMixin):
admin_role = ImplicitRoleField(
role_name='Organization Administrator',
parent_role='singleton:System Administrator',
resource_field='resource',
permissions = {'all': True}
)
auditor_role = ImplicitRoleField(
role_name='Organization Auditor',
parent_role='singleton:System Auditor',
resource_field='resource',
permissions = {'read': True}
)
member_role = ImplicitRoleField(
role_name='Organization Member',
resource_field='resource',
permissions = {'read': True}
)
@ -114,19 +107,16 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
admin_role = ImplicitRoleField(
role_name='Team Administrator',
parent_role='organization.admin_role',
resource_field='resource',
permissions = {'all': True}
)
auditor_role = ImplicitRoleField(
role_name='Team Auditor',
parent_role='organization.auditor_role',
resource_field='resource',
permissions = {'read': True}
)
member_role = ImplicitRoleField(
role_name='Team Member',
parent_role='admin_role',
resource_field='resource',
permissions = {'read':True},
)

View File

@ -196,14 +196,6 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
app_label = 'main'
ordering = ('id',)
organization = models.ForeignKey(
'Organization',
blank=False,
null=True,
on_delete=models.SET_NULL,
related_name='project_list', # TODO: this should eventually be refactored
# back to 'projects' - anoek 2016-01-28
)
scm_delete_on_next_update = models.BooleanField(
default=False,
editable=False,
@ -217,25 +209,21 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
)
admin_role = ImplicitRoleField(
role_name='Project Administrator',
parent_role='organization.admin_role',
resource_field='resource',
parent_role='organizations.admin_role',
permissions = {'all': True}
)
auditor_role = ImplicitRoleField(
role_name='Project Auditor',
parent_role='organization.auditor_role',
resource_field='resource',
parent_role='organizations.auditor_role',
permissions = {'read': True}
)
member_role = ImplicitRoleField(
role_name='Project Member',
resource_field='resource',
permissions = {'read': True}
)
scm_update_role = ImplicitRoleField(
role_name='Project Updater',
parent_role='admin_role',
resource_field='resource',
permissions = {'scm_update': True}
)

View File

@ -6,6 +6,7 @@ from awx.main.models.inventory import (
Group,
)
from awx.main.models.projects import Project
from awx.main.models.jobs import JobTemplate
from awx.main.models.organization import (
Organization,
Team,
@ -23,13 +24,37 @@ def user():
return user
return u
@pytest.fixture
def check_jobtemplate(project, inventory, credential):
return \
JobTemplate.objects.create(
job_type='check',
project=project,
inventory=inventory,
credential=credential,
name='check-job-template'
)
@pytest.fixture
def deploy_jobtemplate(project, inventory, credential):
return \
JobTemplate.objects.create(
job_type='run',
project=project,
inventory=inventory,
credential=credential,
name='deploy-job-template'
)
@pytest.fixture
def team(organization):
return Team.objects.create(organization=organization, name='test-team')
@pytest.fixture
def project(organization):
return Project.objects.create(name="test-project", organization=organization, description="test-project-desc")
prj = Project.objects.create(name="test-project", description="test-project-desc")
prj.organizations.add(organization)
return prj
@pytest.fixture
def user_project(user):

View File

@ -0,0 +1,133 @@
import pytest
from awx.main.migrations import _rbac as rbac
from awx.main.models import Permission
from django.apps import apps
@pytest.mark.django_db
def test_job_template_migration_check(deploy_jobtemplate, check_jobtemplate, user):
admin = user('admin', is_superuser=True)
joe = user('joe')
check_jobtemplate.project.organizations.all()[0].users.add(joe)
Permission(user=joe, inventory=check_jobtemplate.inventory, permission_type='read').save()
Permission(user=joe, inventory=check_jobtemplate.inventory,
project=check_jobtemplate.project, permission_type='check').save()
rbac.migrate_users(apps, None)
rbac.migrate_organization(apps, None)
rbac.migrate_projects(apps, None)
rbac.migrate_inventory(apps, None)
assert check_jobtemplate.project.accessible_by(joe, {'read': True})
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
migrations = rbac.migrate_job_templates(apps, None)
assert len(migrations[check_jobtemplate.name]['users']) == 1
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False
@pytest.mark.django_db
def test_job_template_migration_deploy(deploy_jobtemplate, check_jobtemplate, user):
admin = user('admin', is_superuser=True)
joe = user('joe')
deploy_jobtemplate.project.organizations.all()[0].users.add(joe)
Permission(user=joe, inventory=deploy_jobtemplate.inventory, permission_type='read').save()
Permission(user=joe, inventory=deploy_jobtemplate.inventory,
project=deploy_jobtemplate.project, permission_type='run').save()
rbac.migrate_users(apps, None)
rbac.migrate_organization(apps, None)
rbac.migrate_projects(apps, None)
rbac.migrate_inventory(apps, None)
assert deploy_jobtemplate.project.accessible_by(joe, {'read': True})
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False
migrations = rbac.migrate_job_templates(apps, None)
assert len(migrations[deploy_jobtemplate.name]['users']) == 1
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
@pytest.mark.django_db
def test_job_template_team_migration_check(deploy_jobtemplate, check_jobtemplate, organization, team, user):
admin = user('admin', is_superuser=True)
joe = user('joe')
team.users.add(joe)
team.organization = organization
team.save()
check_jobtemplate.project.organizations.all()[0].users.add(joe)
Permission(team=team, inventory=check_jobtemplate.inventory, permission_type='read').save()
Permission(team=team, inventory=check_jobtemplate.inventory,
project=check_jobtemplate.project, permission_type='check').save()
rbac.migrate_users(apps, None)
rbac.migrate_team(apps, None)
rbac.migrate_organization(apps, None)
rbac.migrate_projects(apps, None)
rbac.migrate_inventory(apps, None)
assert check_jobtemplate.project.accessible_by(joe, {'read': True})
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
migrations = rbac.migrate_job_templates(apps, None)
assert len(migrations[check_jobtemplate.name]['users']) == 0
assert len(migrations[check_jobtemplate.name]['teams']) == 1
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False
@pytest.mark.django_db
def test_job_template_team_deploy_migration(deploy_jobtemplate, check_jobtemplate, organization, team, user):
admin = user('admin', is_superuser=True)
joe = user('joe')
team.users.add(joe)
team.organization = organization
team.save()
deploy_jobtemplate.project.organizations.all()[0].users.add(joe)
Permission(team=team, inventory=deploy_jobtemplate.inventory, permission_type='read').save()
Permission(team=team, inventory=deploy_jobtemplate.inventory,
project=deploy_jobtemplate.project, permission_type='run').save()
rbac.migrate_users(apps, None)
rbac.migrate_team(apps, None)
rbac.migrate_organization(apps, None)
rbac.migrate_projects(apps, None)
rbac.migrate_inventory(apps, None)
assert deploy_jobtemplate.project.accessible_by(joe, {'read': True})
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False
migrations = rbac.migrate_job_templates(apps, None)
assert len(migrations[deploy_jobtemplate.name]['users']) == 0
assert len(migrations[deploy_jobtemplate.name]['teams']) == 1
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True

View File

@ -3,10 +3,16 @@ import pytest
from awx.main.migrations import _rbac as rbac
from awx.main.models import Permission
from django.apps import apps
from awx.main.migrations import _old_access as old_access
@pytest.mark.django_db
def test_project_user_project(user_project, project, user):
u = user('owner')
assert old_access.check_user_access(u, user_project.__class__, 'read', user_project)
assert old_access.check_user_access(u, project.__class__, 'read', project) is False
assert user_project.accessible_by(u, {'read': True}) is False
assert project.accessible_by(u, {'read': True}) is False
migrations = rbac.migrate_projects(apps, None)
@ -20,11 +26,14 @@ def test_project_accessible_by_sa(user, project):
u = user('systemadmin', is_superuser=True)
assert project.accessible_by(u, {'read': True}) is False
rbac.migrate_organization(apps, None)
su_migrations = rbac.migrate_users(apps, None)
migrations = rbac.migrate_projects(apps, None)
assert len(su_migrations) == 1
assert len(migrations[project.name]['users']) == 0
assert len(migrations[project.name]['teams']) == 0
print(project.admin_role.ancestors.all())
print(project.admin_role.ancestors.all())
assert project.accessible_by(u, {'read': True, 'write': True}) is True
@pytest.mark.django_db
@ -58,6 +67,7 @@ def test_project_team(user, team, project):
assert project.accessible_by(member, {'read': True}) is False
rbac.migrate_team(apps, None)
rbac.migrate_organization(apps, None)
migrations = rbac.migrate_projects(apps, None)
assert len(migrations[project.name]['users']) == 0
@ -66,13 +76,18 @@ def test_project_team(user, team, project):
assert project.accessible_by(nonmember, {'read': True}) is False
@pytest.mark.django_db
def test_project_explicit_permission(user, team, project):
u = user('user')
p = Permission(user=u, project=project, permission_type='check')
def test_project_explicit_permission(user, team, project, organization):
u = user('prjuser')
assert old_access.check_user_access(u, project.__class__, 'read', project) is False
organization.users.add(u)
p = Permission(user=u, project=project, permission_type='create', name='Perm name')
p.save()
assert project.accessible_by(u, {'read': True}) is False
rbac.migrate_organization(apps, None)
migrations = rbac.migrate_projects(apps, None)
assert len(migrations[project.name]['users']) == 1

View File

@ -103,7 +103,16 @@ The `singleton` static method is a helper method on the `Role` model that helps
`role_name` is the display name of the role. This is useful when generating reports or looking the results of queries.
`permissions` is a dictionary of set permissions that a user with this role will gain to your `Resource`. A permission defaults to `False` if not explicitly provided. Below is a list of available permissions. The special permission `all` is a shortcut for generating a dict with all of the explicit permissions listed below set to `True`.
`permissions` can be used when the model that contains the
`ImplicitRoleField` utilizs the `ResourceMixin`. When present, a
`RolePermission` entry will be automatically created to grant the specified
permissions on the resource to the role defined by the `ImplicitRoleField`.
This field should be specified as a dictionary of permissions you wish to
automatically grant. Below is a list of available permissions. The special
permission `all` is a shortcut for generating a dict with all of the explicit
permissions listed below set to `True`. Note that permissions default to
`False` if not explicitly provided.
```python
# Available Permissions