From 5b50ebb8daeeadcf816388576a9da6135cdc05ec Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 29 Jan 2016 13:18:32 -0500 Subject: [PATCH] Added a ResourceMixin to be added to any model that is a "Resource" Also added initial permissions checking and accessible object methods to the mixin --- awx/main/models/credential.py | 11 +-- awx/main/models/inventory.py | 21 ++--- awx/main/models/jobs.py | 7 +- awx/main/models/mixins.py | 136 ++++++++++++++++++++++++++++++++ awx/main/models/organization.py | 9 +-- awx/main/models/projects.py | 6 +- 6 files changed, 155 insertions(+), 35 deletions(-) create mode 100644 awx/main/models/mixins.py diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 3f4e6bf27e..6e16851145 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -11,15 +11,16 @@ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.core.urlresolvers import reverse # AWX -from awx.main.fields import ImplicitResourceField, ImplicitRoleField +from awx.main.fields import ImplicitRoleField from awx.main.constants import CLOUD_PROVIDERS from awx.main.utils import decrypt_field from awx.main.models.base import * # noqa +from awx.main.models.mixins import ResourceMixin __all__ = ['Credential'] -class Credential(PasswordFieldsModel, CommonModelNameNotUnique): +class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): ''' A credential contains information about how to talk to a remote resource Usually this is a SSH key location, and possibly an unlock password. @@ -154,12 +155,6 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): default='', help_text=_('Vault password (or "ASK" to prompt the user).'), ) - resource = ImplicitResourceField( - parent_resource=[ - 'user.resource', - 'team.resource' - ] - ) owner_role = ImplicitRoleField( role_name='Credential Owner', parent_role=[ diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index a4c593e5d2..b75e55cfa5 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -18,11 +18,12 @@ from django.utils.timezone import now # AWX from awx.main.constants import CLOUD_PROVIDERS -from awx.main.fields import AutoOneToOneField, ImplicitResourceField, ImplicitRoleField +from awx.main.fields import AutoOneToOneField, ImplicitRoleField from awx.main.managers import HostManager from awx.main.models.base import * # noqa from awx.main.models.jobs import Job from awx.main.models.unified_jobs import * # noqa +from awx.main.models.mixins import ResourceMixin from awx.main.utils import ignore_inventory_computed_fields, _inventory_updates __all__ = ['Inventory', 'Host', 'Group', 'InventorySource', 'InventoryUpdate', 'CustomInventoryScript'] @@ -30,7 +31,7 @@ __all__ = ['Inventory', 'Host', 'Group', 'InventorySource', 'InventoryUpdate', ' logger = logging.getLogger('awx.main.models.inventory') -class Inventory(CommonModel): +class Inventory(CommonModel, ResourceMixin): ''' an inventory source contains lists and hosts. ''' @@ -92,9 +93,6 @@ class Inventory(CommonModel): editable=False, help_text=_('Number of external inventory sources in this inventory with failures.'), ) - resource = ImplicitResourceField( - parent_resource='organization.resource' - ) admin_role = ImplicitRoleField( role_name='Inventory Administrator', parent_role='organization.admin_role', @@ -468,7 +466,7 @@ class Host(CommonModelNameNotUnique): # Use .job_events.all() to get events affecting this host. -class Group(CommonModelNameNotUnique): +class Group(CommonModelNameNotUnique, ResourceMixin): ''' A group containing managed hosts. A group or host may belong to multiple groups. @@ -538,9 +536,6 @@ class Group(CommonModelNameNotUnique): editable=False, help_text=_('Inventory source(s) that created or modified this group.'), ) - resource = ImplicitResourceField( - parent_resource='inventory.resource' - ) admin_role = ImplicitRoleField( role_name='Inventory Group Administrator', parent_role='inventory.admin_role', @@ -1096,7 +1091,7 @@ class InventorySourceOptions(BaseModel): return ','.join(choices) -class InventorySource(UnifiedJobTemplate, InventorySourceOptions): +class InventorySource(UnifiedJobTemplate, InventorySourceOptions, ResourceMixin): class Meta: app_label = 'main' @@ -1123,12 +1118,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): update_cache_timeout = models.PositiveIntegerField( default=0, ) - resource = ImplicitResourceField( - parent_resource=[ - 'group.resource', - 'inventory.resource' - ] - ) admin_role = ImplicitRoleField( role_name='Inventory Group Administrator', parent_role=[ diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 019be64abd..9c3866b593 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -26,7 +26,9 @@ from awx.main.utils import decrypt_field, ignore_inventory_computed_fields from awx.main.utils import emit_websocket_notification from awx.main.redact import PlainTextCleaner from awx.main.conf import tower_settings -from awx.main.fields import ImplicitResourceField, ImplicitRoleField +from awx.main.fields import ImplicitRoleField +from awx.main.models.mixins import ResourceMixin + logger = logging.getLogger('awx.main.models.jobs') @@ -150,7 +152,7 @@ class JobOptions(BaseModel): else: return [] -class JobTemplate(UnifiedJobTemplate, JobOptions): +class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): ''' A job template is a reusable job definition for applying a project (with playbook) to an inventory source with a given credential. @@ -179,7 +181,6 @@ class JobTemplate(UnifiedJobTemplate, JobOptions): blank=True, default={}, ) - resource = ImplicitResourceField() admin_role = ImplicitRoleField( role_name='Job Template Administrator', parent_role='project.admin_role', diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py new file mode 100644 index 0000000000..32a5d31a57 --- /dev/null +++ b/awx/main/models/mixins.py @@ -0,0 +1,136 @@ +# Django +from django.db import models +from django.db import connection + +# AWX +from awx.main.models.rbac import RolePermission, Role, RoleHierarchy +from awx.main.fields import ImplicitResourceField + +__all__ = 'ResourceMixin' + +class ResourceMixin(models.Model): + + class Meta: + abstract = True + + resource = ImplicitResourceField() + + @classmethod + def accessible_objects(cls, user, permissions): + ''' + Use instead of `MyModel.objects` when you want to only consider + resources that a user has specific permissions for. For example: + + MyModel.accessible_objects(user, {'read': True}).filter(name__istartswith='bar'); + + NOTE: This should only be used for list type things. If you have a + specific resource you want to check permissions on, it is more + performant to resolve the resource in question then call + `myresource.get_permissions(user)`. + ''' + + perm_clause = '' + aggregates = '' + for perm in permissions: + if not perm_clause: + perm_clause = 'WHERE ' + else: + perm_clause += ' AND ' + perm_clause += '"%s" = %d' % (perm, int(permissions[perm])) + aggregates += ', MAX("%s") as "%s"' % (perm, perm) + + return cls.objects.extra( + where=[ + ''' + %(table_name)s.resource_id in ( + SELECT resource_id FROM ( + SELECT %(rbac_permission)s.resource_id %(aggregates)s + FROM %(rbac_role)s_members + LEFT JOIN %(rbac_role_hierachy)s + ON (%(rbac_role_hierachy)s.ancestor_id = %(rbac_role)s_members.role_id) + LEFT JOIN %(rbac_permission)s + ON (%(rbac_permission)s.role_id = %(rbac_role_hierachy)s.role_id) + WHERE %(rbac_role)s_members.user_id=%(user_id)d + GROUP BY %(rbac_permission)s.resource_id + ) summarized_permissions + %(perm_clause)s + ) + ''' + % + { + 'table_name': cls._meta.db_table, + 'aggregates': aggregates, + 'user_id': user.id, + 'perm_clause': perm_clause, + 'rbac_role': Role._meta.db_table, + 'rbac_permission': RolePermission._meta.db_table, + 'rbac_role_hierachy': RoleHierarchy._meta.db_table + } + ] + ) + + def get_permissions(self, user): + ''' + Returns a dict (or None) of the permissions a user has for a given + resource. + + Note: Each field in the dict is the `or` of all respective permissions + that have been granted to the roles that are applicable for the given + user. + + In example, if a user has been granted read access through a permission + on one role and write access through a permission on a separate role, + the returned dict will denote that the user has both read and write + access. + ''' + + + with connection.cursor() as cursor: + cursor.execute( + ''' + SELECT + MAX("create") as "create", + MAX("read") as "read", + MAX("write") as "write", + MAX("update") as "update", + MAX("delete") as "delete", + MAX("scm_update") as "scm_update", + MAX("execute") as "execute", + MAX("use") as "use" + + FROM %(rbac_permission)s + LEFT JOIN %(rbac_role_hierachy)s + ON (%(rbac_permission)s.role_id = %(rbac_role_hierachy)s.role_id) + LEFT JOIN %(rbac_role)s_members + ON ( + %(rbac_role)s_members.role_id = %(rbac_role_hierachy)s.ancestor_id + AND %(rbac_role)s_members.user_id = %(user_id)d + ) + + WHERE %(rbac_permission)s.resource_id=%(resource_id)s + GROUP BY %(rbac_role)s_members.user_id + ''' + % + { + 'user_id': user.id, + 'resource_id': self.resource.id, + 'rbac_role': Role._meta.db_table, + 'rbac_permission': RolePermission._meta.db_table, + 'rbac_role_hierachy': RoleHierarchy._meta.db_table + } + ) + row = cursor.fetchone() + if row: + return dict(zip([x.name for x in cursor.description], row)) + return None + + def accessible_by(self, user, permissions): + ''' + Returns true if the user has all of the specified permissions + ''' + + perms = self.get_permissions(user) + for k in permissions: + if k not in perms or perms[k] < permissions[k]: + return False + return True diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index fc74aabd58..1caa694dd5 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -16,14 +16,15 @@ from django.utils.timezone import now as tz_now from django.utils.translation import ugettext_lazy as _ # AWX -from awx.main.fields import AutoOneToOneField, ImplicitResourceField, ImplicitRoleField +from awx.main.fields import AutoOneToOneField, ImplicitRoleField from awx.main.models.base import * # noqa +from awx.main.models.mixins import ResourceMixin from awx.main.conf import tower_settings __all__ = ['Organization', 'Team', 'Permission', 'Profile', 'AuthToken'] -class Organization(CommonModel): +class Organization(CommonModel, ResourceMixin): ''' An organization is the basic unit of multi-tenancy divisions ''' @@ -51,7 +52,6 @@ class Organization(CommonModel): blank=True, related_name='organizations', ) - resource = ImplicitResourceField() admin_role = ImplicitRoleField( role_name='Organization Administrator', resource_field='resource', @@ -77,7 +77,7 @@ class Organization(CommonModel): super(Organization, self).mark_inactive(save=save) -class Team(CommonModelNameNotUnique): +class Team(CommonModelNameNotUnique, ResourceMixin): ''' A team is a group of users that work on common projects. ''' @@ -104,7 +104,6 @@ class Team(CommonModelNameNotUnique): blank=True, related_name='teams', ) - resource = ImplicitResourceField() admin_role = ImplicitRoleField( role_name='Team Administrator', parent_role='organization.admin_role', diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 172fd09d6f..cb3f143dbd 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -21,8 +21,9 @@ from awx.lib.compat import slugify from awx.main.models.base import * # noqa from awx.main.models.jobs import Job from awx.main.models.unified_jobs import * # noqa +from awx.main.models.mixins import ResourceMixin from awx.main.utils import update_scm_url -from awx.main.fields import ImplicitResourceField, ImplicitRoleField +from awx.main.fields import ImplicitRoleField __all__ = ['Project', 'ProjectUpdate'] @@ -186,7 +187,7 @@ class ProjectOptions(models.Model): return sorted(results, key=lambda x: smart_str(x).lower()) -class Project(UnifiedJobTemplate, ProjectOptions): +class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): ''' A project represents a playbook git repo that can access a set of inventories ''' @@ -214,7 +215,6 @@ class Project(UnifiedJobTemplate, ProjectOptions): default=0, blank=True, ) - resource = ImplicitResourceField() admin_role = ImplicitRoleField( role_name='Project Administrator', parent_role='organization.admin_role',