mirror of
https://github.com/ansible/awx.git
synced 2024-11-01 08:21:15 +03:00
Merge branch 'rbac' of github.com:ansible/ansible-tower into rbac
This commit is contained in:
commit
13dd27ac52
@ -57,20 +57,6 @@ access_registry = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def user_or_team(data):
|
|
||||||
try:
|
|
||||||
if 'user' in data:
|
|
||||||
pk = get_pk_from_dict(data, 'user')
|
|
||||||
return get_object_or_400(User, pk=pk), None
|
|
||||||
elif 'team' in data:
|
|
||||||
pk = get_pk_from_dict(data, 'team')
|
|
||||||
return None, get_object_or_400(Team, pk=pk)
|
|
||||||
else:
|
|
||||||
return None, None
|
|
||||||
except ParseError:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
|
||||||
def register_access(model_class, access_class):
|
def register_access(model_class, access_class):
|
||||||
access_classes = access_registry.setdefault(model_class, [])
|
access_classes = access_registry.setdefault(model_class, [])
|
||||||
access_classes.append(access_class)
|
access_classes.append(access_class)
|
||||||
@ -557,27 +543,27 @@ class CredentialAccess(BaseAccess):
|
|||||||
if self.user.is_superuser:
|
if self.user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
user, team = user_or_team(data)
|
if 'user' in data:
|
||||||
if user is None and team is None:
|
pk = get_pk_from_dict(data, 'user')
|
||||||
return False
|
user = get_object_or_400(User, pk=pk)
|
||||||
|
|
||||||
if user is not None:
|
|
||||||
return user.accessible_by(self.user, {'write': True})
|
return user.accessible_by(self.user, {'write': True})
|
||||||
if team is not None:
|
elif 'organization' in data:
|
||||||
return team.accessible_by(self.user, {'write':True})
|
pk = get_pk_from_dict(data, 'organization')
|
||||||
|
org = get_object_or_400(Organization, pk=pk)
|
||||||
|
return org.accessible_by(self.user, {'write': True})
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def can_change(self, obj, data):
|
def can_change(self, obj, data):
|
||||||
if self.user.is_superuser:
|
if self.user.is_superuser:
|
||||||
return True
|
return True
|
||||||
if not self.can_add(data):
|
|
||||||
return False
|
|
||||||
return obj.accessible_by(self.user, {'read':True, 'update': True, 'delete':True})
|
return obj.accessible_by(self.user, {'read':True, 'update': True, 'delete':True})
|
||||||
|
|
||||||
def can_delete(self, obj):
|
def can_delete(self, obj):
|
||||||
# Unassociated credentials may be marked deleted by anyone, though we
|
# Unassociated credentials may be marked deleted by anyone, though we
|
||||||
# shouldn't ever end up with those.
|
# shouldn't ever end up with those.
|
||||||
if obj.user is None and obj.team is None:
|
#if obj.user is None and obj.team is None:
|
||||||
return True
|
# return True
|
||||||
return self.can_change(obj, None)
|
return self.can_change(obj, None)
|
||||||
|
|
||||||
class TeamAccess(BaseAccess):
|
class TeamAccess(BaseAccess):
|
||||||
|
@ -14,8 +14,8 @@ class Migration(migrations.Migration):
|
|||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(rbac.migrate_users),
|
migrations.RunPython(rbac.migrate_users),
|
||||||
migrations.RunPython(rbac.migrate_organization),
|
migrations.RunPython(rbac.migrate_organization),
|
||||||
migrations.RunPython(rbac.migrate_credential),
|
|
||||||
migrations.RunPython(rbac.migrate_team),
|
migrations.RunPython(rbac.migrate_team),
|
||||||
migrations.RunPython(rbac.migrate_inventory),
|
migrations.RunPython(rbac.migrate_inventory),
|
||||||
migrations.RunPython(rbac.migrate_projects),
|
migrations.RunPython(rbac.migrate_projects),
|
||||||
|
migrations.RunPython(rbac.migrate_credential),
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from awx.main.utils import getattrd
|
||||||
import _old_access as old_access
|
import _old_access as old_access
|
||||||
|
|
||||||
def migrate_users(apps, schema_editor):
|
def migrate_users(apps, schema_editor):
|
||||||
@ -52,18 +54,104 @@ def migrate_team(apps, schema_editor):
|
|||||||
migrations[t.name].append(user)
|
migrations[t.name].append(user)
|
||||||
return migrations
|
return migrations
|
||||||
|
|
||||||
|
def attrfunc(attr_path):
|
||||||
|
'''attrfunc returns a function that will
|
||||||
|
attempt to use the attr_path to access the attribute
|
||||||
|
of an instance that is passed in to the returned function.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
get_org = attrfunc('inventory.organization')
|
||||||
|
org = get_org(JobTemplateInstance)
|
||||||
|
'''
|
||||||
|
def attr(inst):
|
||||||
|
return getattrd(inst, attr_path)
|
||||||
|
return attr
|
||||||
|
|
||||||
|
def _update_credential_parents(org, cred):
|
||||||
|
org.admin_role.children.add(cred.owner_role)
|
||||||
|
org.member_role.children.add(cred.usage_role)
|
||||||
|
cred.user, cred.team = None, None
|
||||||
|
cred.save()
|
||||||
|
|
||||||
|
def _discover_credentials(instances, cred, orgfunc):
|
||||||
|
'''_discover_credentials will find shared credentials across
|
||||||
|
organizations. If a shared credential is found, it will duplicate
|
||||||
|
the credential, ensure the proper role permissions are added to the new
|
||||||
|
credential, and update any references from the old to the newly created
|
||||||
|
credential.
|
||||||
|
|
||||||
|
instances is a list of all objects that were matched when filtered
|
||||||
|
with cred.
|
||||||
|
|
||||||
|
orgfunc is a function that when called with an instance from instances
|
||||||
|
will produce an Organization object.
|
||||||
|
'''
|
||||||
|
orgs = defaultdict(list)
|
||||||
|
for inst in instances:
|
||||||
|
orgs[orgfunc(inst)].append(inst)
|
||||||
|
|
||||||
|
if len(orgs) == 1:
|
||||||
|
_update_credential_parents(instances[0].inventory.organization, cred)
|
||||||
|
else:
|
||||||
|
for pos, org in enumerate(orgs):
|
||||||
|
if pos == 0:
|
||||||
|
_update_credential_parents(org, cred)
|
||||||
|
else:
|
||||||
|
# Create a new credential
|
||||||
|
cred.pk = None
|
||||||
|
cred.save()
|
||||||
|
|
||||||
|
# Unlink the old information from the new credential
|
||||||
|
cred.user, cred.team = None, None
|
||||||
|
cred.owner_role, cred.usage_role = None, None
|
||||||
|
cred.save()
|
||||||
|
|
||||||
|
for i in orgs[org]:
|
||||||
|
i.credential = cred
|
||||||
|
i.save()
|
||||||
|
_update_credential_parents(org, cred)
|
||||||
|
|
||||||
def migrate_credential(apps, schema_editor):
|
def migrate_credential(apps, schema_editor):
|
||||||
migrations = defaultdict(list)
|
Credential = apps.get_model('main', "Credential")
|
||||||
credential = apps.get_model('main', "Credential")
|
JobTemplate = apps.get_model('main', 'JobTemplate')
|
||||||
for cred in credential.objects.all():
|
Project = apps.get_model('main', 'Project')
|
||||||
if cred.user:
|
InventorySource = apps.get_model('main', 'InventorySource')
|
||||||
cred.owner_role.members.add(cred.user)
|
|
||||||
migrations[cred.name].append(cred.user)
|
migrated = []
|
||||||
elif cred.team:
|
for cred in Credential.objects.all():
|
||||||
cred.owner_role.parents.add(cred.team.admin_role)
|
migrated.append(cred)
|
||||||
cred.usage_role.parents.add(cred.team.member_role)
|
|
||||||
migrations[cred.name].append(cred.team)
|
results = (JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all() or
|
||||||
return migrations
|
InventorySource.objects.filter(credential=cred).all())
|
||||||
|
if results:
|
||||||
|
if len(results) == 1:
|
||||||
|
_update_credential_parents(results[0].inventory.organization, cred)
|
||||||
|
else:
|
||||||
|
_discover_credentials(results, cred, attrfunc('inventory.organization'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
projs = Project.objects.filter(credential=cred).all()
|
||||||
|
if projs:
|
||||||
|
if len(projs) == 1:
|
||||||
|
_update_credential_parents(projs[0].organization, cred)
|
||||||
|
else:
|
||||||
|
_discover_credentials(projs, cred, attrfunc('organization'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cred.team is not None:
|
||||||
|
cred.team.admin_role.children.add(cred.owner_role)
|
||||||
|
cred.team.member_role.children.add(cred.usage_role)
|
||||||
|
cred.user, cred.team = None, None
|
||||||
|
cred.save()
|
||||||
|
|
||||||
|
elif cred.user is not None:
|
||||||
|
cred.user.admin_role.children.add(cred.owner_role)
|
||||||
|
cred.user, cred.team = None, None
|
||||||
|
cred.save()
|
||||||
|
|
||||||
|
# no match found, log
|
||||||
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
def migrate_inventory(apps, schema_editor):
|
def migrate_inventory(apps, schema_editor):
|
||||||
migrations = defaultdict(dict)
|
migrations = defaultdict(dict)
|
||||||
|
@ -163,8 +163,6 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
role_name='Credential Owner',
|
role_name='Credential Owner',
|
||||||
role_description='Owner of the credential',
|
role_description='Owner of the credential',
|
||||||
parent_role=[
|
parent_role=[
|
||||||
'team.admin_role',
|
|
||||||
'user.admin_role',
|
|
||||||
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||||
],
|
],
|
||||||
permissions = {'all': True}
|
permissions = {'all': True}
|
||||||
@ -180,8 +178,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
usage_role = ImplicitRoleField(
|
usage_role = ImplicitRoleField(
|
||||||
role_name='Credential User',
|
role_name='Credential User',
|
||||||
role_description='May use this credential, but not read sensitive portions or modify it',
|
role_description='May use this credential, but not read sensitive portions or modify it',
|
||||||
parent_role= 'team.member_role',
|
permissions = {'use': True}
|
||||||
permissions = {'read': True, 'use': True}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -2,6 +2,8 @@ import pytest
|
|||||||
|
|
||||||
from awx.main.access import CredentialAccess
|
from awx.main.access import CredentialAccess
|
||||||
from awx.main.models.credential import Credential
|
from awx.main.models.credential import Credential
|
||||||
|
from awx.main.models.jobs import JobTemplate
|
||||||
|
from awx.main.models.inventory import InventorySource
|
||||||
from awx.main.migrations import _rbac as rbac
|
from awx.main.migrations import _rbac as rbac
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -49,9 +51,6 @@ def test_credential_migration_team_admin(credential, team, user, permissions):
|
|||||||
credential.team = team
|
credential.team = team
|
||||||
credential.save()
|
credential.save()
|
||||||
|
|
||||||
# No permissions pre-migration
|
|
||||||
team.admin_role.children.remove(credential.owner_role)
|
|
||||||
team.member_role.children.remove(credential.usage_role)
|
|
||||||
assert not credential.accessible_by(u, permissions['usage'])
|
assert not credential.accessible_by(u, permissions['usage'])
|
||||||
|
|
||||||
# Usage permissions post migration
|
# Usage permissions post migration
|
||||||
@ -76,17 +75,15 @@ def test_credential_access_admin(user, team, credential):
|
|||||||
access = CredentialAccess(u)
|
access = CredentialAccess(u)
|
||||||
|
|
||||||
assert access.can_add({'user': u.pk})
|
assert access.can_add({'user': u.pk})
|
||||||
assert access.can_add({'team': team.pk})
|
|
||||||
|
|
||||||
assert not access.can_change(credential, {'user': u.pk})
|
assert not access.can_change(credential, {'user': u.pk})
|
||||||
|
|
||||||
# unowned credential can be deleted
|
# unowned credential is superuser only
|
||||||
assert access.can_delete(credential)
|
assert not access.can_delete(credential)
|
||||||
|
|
||||||
# credential is now part of a team
|
# credential is now part of a team
|
||||||
# that is part of an organization
|
# that is part of an organization
|
||||||
# that I am an admin for
|
# that I am an admin for
|
||||||
credential.team = team
|
credential.owner_role.parents.add(team.admin_role)
|
||||||
credential.save()
|
credential.save()
|
||||||
credential.owner_role.rebuild_role_ancestor_list()
|
credential.owner_role.rebuild_role_ancestor_list()
|
||||||
|
|
||||||
@ -96,3 +93,114 @@ def test_credential_access_admin(user, team, credential):
|
|||||||
|
|
||||||
# should have can_change access as org-admin
|
# should have can_change access as org-admin
|
||||||
assert access.can_change(credential, {'user': u.pk})
|
assert access.can_change(credential, {'user': u.pk})
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cred_job_template(user, deploy_jobtemplate):
|
||||||
|
a = user('admin', False)
|
||||||
|
org = deploy_jobtemplate.project.organization
|
||||||
|
org.admin_role.members.add(a)
|
||||||
|
|
||||||
|
cred = deploy_jobtemplate.credential
|
||||||
|
cred.user = user('john', False)
|
||||||
|
cred.save()
|
||||||
|
|
||||||
|
access = CredentialAccess(a)
|
||||||
|
rbac.migrate_credential(apps, None)
|
||||||
|
assert access.can_change(cred, {'organization': org.pk})
|
||||||
|
|
||||||
|
org.admin_role.members.remove(a)
|
||||||
|
assert not access.can_change(cred, {'organization': org.pk})
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cred_multi_job_template_single_org(user, deploy_jobtemplate):
|
||||||
|
a = user('admin', False)
|
||||||
|
org = deploy_jobtemplate.project.organization
|
||||||
|
org.admin_role.members.add(a)
|
||||||
|
|
||||||
|
cred = deploy_jobtemplate.credential
|
||||||
|
cred.user = user('john', False)
|
||||||
|
cred.save()
|
||||||
|
|
||||||
|
access = CredentialAccess(a)
|
||||||
|
rbac.migrate_credential(apps, None)
|
||||||
|
assert access.can_change(cred, {'organization': org.pk})
|
||||||
|
|
||||||
|
org.admin_role.members.remove(a)
|
||||||
|
assert not access.can_change(cred, {'organization': org.pk})
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_single_cred_multi_job_template_multi_org(user, organizations, credential):
|
||||||
|
orgs = organizations(2)
|
||||||
|
jts = []
|
||||||
|
for org in orgs:
|
||||||
|
inv = org.inventories.create(name="inv-%d" % org.pk)
|
||||||
|
jt = JobTemplate.objects.create(
|
||||||
|
inventory=inv,
|
||||||
|
credential=credential,
|
||||||
|
name="test-jt-org-%d" % org.pk,
|
||||||
|
job_type='check',
|
||||||
|
)
|
||||||
|
jts.append(jt)
|
||||||
|
|
||||||
|
a = user('admin', False)
|
||||||
|
orgs[0].admin_role.members.add(a)
|
||||||
|
orgs[1].admin_role.members.add(a)
|
||||||
|
|
||||||
|
access = CredentialAccess(a)
|
||||||
|
rbac.migrate_credential(apps, None)
|
||||||
|
|
||||||
|
for jt in jts:
|
||||||
|
jt.refresh_from_db()
|
||||||
|
|
||||||
|
assert jts[0].credential != jts[1].credential
|
||||||
|
assert access.can_change(jts[0].credential, {'organization': org.pk})
|
||||||
|
assert access.can_change(jts[1].credential, {'organization': org.pk})
|
||||||
|
|
||||||
|
orgs[0].admin_role.members.remove(a)
|
||||||
|
assert not access.can_change(jts[0].credential, {'organization': org.pk})
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cred_inventory_source(user, inventory, credential):
|
||||||
|
u = user('member', False)
|
||||||
|
inventory.organization.member_role.members.add(u)
|
||||||
|
|
||||||
|
InventorySource.objects.create(
|
||||||
|
name="test-inv-src",
|
||||||
|
credential=credential,
|
||||||
|
inventory=inventory,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not credential.accessible_by(u, {'use':True})
|
||||||
|
|
||||||
|
rbac.migrate_credential(apps, None)
|
||||||
|
assert credential.accessible_by(u, {'use':True})
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cred_project(user, credential, project):
|
||||||
|
u = user('member', False)
|
||||||
|
project.organization.member_role.members.add(u)
|
||||||
|
project.credential = credential
|
||||||
|
project.save()
|
||||||
|
|
||||||
|
assert not credential.accessible_by(u, {'use':True})
|
||||||
|
|
||||||
|
rbac.migrate_credential(apps, None)
|
||||||
|
assert credential.accessible_by(u, {'use':True})
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cred_no_org(user, credential):
|
||||||
|
su = user('su', True)
|
||||||
|
access = CredentialAccess(su)
|
||||||
|
assert access.can_change(credential, {'user': su.pk})
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cred_team(user, team, credential):
|
||||||
|
u = user('a', False)
|
||||||
|
team.member_role.members.add(u)
|
||||||
|
credential.team = team
|
||||||
|
credential.save()
|
||||||
|
|
||||||
|
assert not credential.accessible_by(u, {'use':True})
|
||||||
|
|
||||||
|
rbac.migrate_credential(apps, None)
|
||||||
|
assert credential.accessible_by(u, {'use':True})
|
||||||
|
Loading…
Reference in New Issue
Block a user