mirror of
https://github.com/ansible/awx.git
synced 2026-01-09 15:02:07 -03:30
Develop ability to list permissions for existing roles Create a model registry for RBAC-tracked models Write the data migration logic for creating the preloaded role definitions Write migration to migrate old Role into ObjectRole model This loops over the old Role model, knowing it is unique on object and role_field Most of the logic is concerned with identifying the needed permissions, and then corresponding role definition As needed, object roles are created and users then teams are assigned Write re-computation of cache logic for teams and then for object role permissions Migrate new RBAC internals to ansible_base Migrate tests to ansible_base Implement solution for visible_roles Expose URLs for DAB RBAC
415 lines
13 KiB
Python
415 lines
13 KiB
Python
# Copyright (c) 2015 Ansible, Inc.
|
|
# All Rights Reserved.
|
|
|
|
# Django
|
|
from django.db import models
|
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils.timezone import now
|
|
|
|
# django-ansible-base
|
|
from ansible_base.lib.utils.models import get_type_for_model
|
|
|
|
# Django-CRUM
|
|
from crum import get_current_user
|
|
|
|
# AWX
|
|
from awx.main.utils import encrypt_field, parse_yaml_or_json
|
|
from awx.main.constants import CLOUD_PROVIDERS
|
|
|
|
__all__ = [
|
|
'VarsDictProperty',
|
|
'BaseModel',
|
|
'CreatedModifiedModel',
|
|
'PasswordFieldsModel',
|
|
'PrimordialModel',
|
|
'CommonModel',
|
|
'CommonModelNameNotUnique',
|
|
'NotificationFieldsModel',
|
|
'PERM_INVENTORY_DEPLOY',
|
|
'PERM_INVENTORY_SCAN',
|
|
'PERM_INVENTORY_CHECK',
|
|
'JOB_TYPE_CHOICES',
|
|
'AD_HOC_JOB_TYPE_CHOICES',
|
|
'PROJECT_UPDATE_JOB_TYPE_CHOICES',
|
|
'CLOUD_INVENTORY_SOURCES',
|
|
'VERBOSITY_CHOICES',
|
|
]
|
|
|
|
PERM_INVENTORY_DEPLOY = 'run'
|
|
PERM_INVENTORY_CHECK = 'check'
|
|
PERM_INVENTORY_SCAN = 'scan'
|
|
|
|
JOB_TYPE_CHOICES = [
|
|
(PERM_INVENTORY_DEPLOY, _('Run')),
|
|
(PERM_INVENTORY_CHECK, _('Check')),
|
|
(PERM_INVENTORY_SCAN, _('Scan')),
|
|
]
|
|
|
|
NEW_JOB_TYPE_CHOICES = [
|
|
(PERM_INVENTORY_DEPLOY, _('Run')),
|
|
(PERM_INVENTORY_CHECK, _('Check')),
|
|
]
|
|
|
|
AD_HOC_JOB_TYPE_CHOICES = [
|
|
(PERM_INVENTORY_DEPLOY, _('Run')),
|
|
(PERM_INVENTORY_CHECK, _('Check')),
|
|
]
|
|
|
|
PROJECT_UPDATE_JOB_TYPE_CHOICES = [
|
|
(PERM_INVENTORY_DEPLOY, _('Run')),
|
|
(PERM_INVENTORY_CHECK, _('Check')),
|
|
]
|
|
|
|
CLOUD_INVENTORY_SOURCES = list(CLOUD_PROVIDERS) + ['scm']
|
|
|
|
VERBOSITY_CHOICES = [
|
|
(0, '0 (Normal)'),
|
|
(1, '1 (Verbose)'),
|
|
(2, '2 (More Verbose)'),
|
|
(3, '3 (Debug)'),
|
|
(4, '4 (Connection Debug)'),
|
|
(5, '5 (WinRM Debug)'),
|
|
]
|
|
|
|
|
|
class VarsDictProperty(object):
|
|
"""
|
|
Retrieve a string of variables in YAML or JSON as a dictionary.
|
|
"""
|
|
|
|
def __init__(self, field='variables', key_value=False):
|
|
self.field = field
|
|
self.key_value = key_value
|
|
|
|
def __get__(self, obj, type=None):
|
|
if obj is None:
|
|
return self
|
|
v = getattr(obj, self.field)
|
|
if hasattr(v, 'items'):
|
|
return v
|
|
v = v.encode('utf-8')
|
|
return parse_yaml_or_json(v)
|
|
|
|
def __set__(self, obj, value):
|
|
raise AttributeError('readonly property')
|
|
|
|
|
|
class BaseModel(models.Model):
|
|
"""
|
|
Base model class with common methods for all models.
|
|
"""
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
def __str__(self):
|
|
if 'name' in self.__dict__:
|
|
return u'%s-%s' % (self.name, self.pk)
|
|
else:
|
|
return u'%s-%s' % (self._meta.verbose_name, self.pk)
|
|
|
|
def clean_fields(self, exclude=None):
|
|
"""
|
|
Override default clean_fields to support methods for cleaning
|
|
individual model fields.
|
|
"""
|
|
exclude = exclude or []
|
|
errors = {}
|
|
try:
|
|
super(BaseModel, self).clean_fields(exclude)
|
|
except ValidationError as e:
|
|
errors = e.update_error_dict(errors)
|
|
for f in self._meta.fields:
|
|
if f.name in exclude:
|
|
continue
|
|
if hasattr(self, 'clean_%s' % f.name):
|
|
try:
|
|
setattr(self, f.name, getattr(self, 'clean_%s' % f.name)())
|
|
except ValidationError as e:
|
|
errors[f.name] = e.messages
|
|
if errors:
|
|
raise ValidationError(errors)
|
|
|
|
def update_fields(self, **kwargs):
|
|
save = kwargs.pop('save', True)
|
|
update_fields = []
|
|
for field, value in kwargs.items():
|
|
if getattr(self, field) != value:
|
|
setattr(self, field, value)
|
|
update_fields.append(field)
|
|
if save and update_fields:
|
|
self.save(update_fields=update_fields)
|
|
return update_fields
|
|
|
|
def summary_fields(self):
|
|
"""
|
|
This exists for use by django-ansible-base,
|
|
which has standard patterns that differ from AWX, but we enable views from DAB
|
|
for those views to list summary_fields for AWX models, those models need to provide this
|
|
"""
|
|
from awx.api.serializers import SUMMARIZABLE_FK_FIELDS
|
|
|
|
model_name = get_type_for_model(self)
|
|
related_fields = SUMMARIZABLE_FK_FIELDS.get(model_name, {})
|
|
summary_data = {}
|
|
for field_name in related_fields:
|
|
fval = getattr(self, field_name, None)
|
|
if fval is not None:
|
|
summary_data[field_name] = fval
|
|
return summary_data
|
|
|
|
|
|
class CreatedModifiedModel(BaseModel):
|
|
"""
|
|
Common model with created/modified timestamp fields. Allows explicitly
|
|
specifying created/modified timestamps in certain cases (migrations, job
|
|
events), calculates automatically if not specified.
|
|
"""
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
created = models.DateTimeField(
|
|
default=None,
|
|
editable=False,
|
|
)
|
|
modified = models.DateTimeField(
|
|
default=None,
|
|
editable=False,
|
|
)
|
|
|
|
def save(self, *args, **kwargs):
|
|
update_fields = list(kwargs.get('update_fields', []))
|
|
# Manually perform auto_now_add and auto_now logic.
|
|
if not self.pk and not self.created:
|
|
self.created = now()
|
|
if 'created' not in update_fields:
|
|
update_fields.append('created')
|
|
if 'modified' not in update_fields or not self.modified:
|
|
self.modified = now()
|
|
update_fields.append('modified')
|
|
super(CreatedModifiedModel, self).save(*args, **kwargs)
|
|
|
|
|
|
class PasswordFieldsModel(BaseModel):
|
|
"""
|
|
Abstract base class for a model with password fields that should be stored
|
|
as encrypted values.
|
|
"""
|
|
|
|
PASSWORD_FIELDS = ()
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
def _password_field_allows_ask(self, field):
|
|
return False # Override in subclasses if needed.
|
|
|
|
def save(self, *args, **kwargs):
|
|
new_instance = not bool(self.pk)
|
|
# If update_fields has been specified, add our field names to it,
|
|
# if it hasn't been specified, then we're just doing a normal save.
|
|
update_fields = kwargs.get('update_fields', [])
|
|
# When first saving to the database, don't store any password field
|
|
# values, but instead save them until after the instance is created.
|
|
# Otherwise, store encrypted values to the database.
|
|
for field in self.PASSWORD_FIELDS:
|
|
if new_instance:
|
|
value = getattr(self, field, '')
|
|
setattr(self, '_saved_%s' % field, value)
|
|
setattr(self, field, '')
|
|
else:
|
|
ask = self._password_field_allows_ask(field)
|
|
self.encrypt_field(field, ask)
|
|
self.mark_field_for_save(update_fields, field)
|
|
super(PasswordFieldsModel, self).save(*args, **kwargs)
|
|
# After saving a new instance for the first time, set the password
|
|
# fields and save again.
|
|
if new_instance:
|
|
update_fields = []
|
|
for field in self.PASSWORD_FIELDS:
|
|
saved_value = getattr(self, '_saved_%s' % field, '')
|
|
setattr(self, field, saved_value)
|
|
self.mark_field_for_save(update_fields, field)
|
|
|
|
from awx.main.signals import disable_activity_stream
|
|
|
|
with disable_activity_stream():
|
|
# We've already got an activity stream record for the object
|
|
# creation, there's no need to have an extra one for the
|
|
# secondary save for secrets
|
|
self.save(update_fields=update_fields)
|
|
|
|
def encrypt_field(self, field, ask):
|
|
encrypted = encrypt_field(self, field, ask)
|
|
setattr(self, field, encrypted)
|
|
|
|
def mark_field_for_save(self, update_fields, field):
|
|
if field not in update_fields:
|
|
update_fields.append(field)
|
|
|
|
|
|
class HasEditsMixin(BaseModel):
|
|
"""Mixin which will keep the versions of field values from last edit
|
|
so we can tell if current model has unsaved changes.
|
|
"""
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
@classmethod
|
|
def _get_editable_fields(cls):
|
|
fds = set([])
|
|
for field in cls._meta.concrete_fields:
|
|
if hasattr(field, 'attname'):
|
|
if field.attname == 'id':
|
|
continue
|
|
elif field.attname.endswith('ptr_id'):
|
|
# polymorphic fields should always be non-editable, see:
|
|
# https://github.com/django-polymorphic/django-polymorphic/issues/349
|
|
continue
|
|
if getattr(field, 'editable', True):
|
|
fds.add(field.attname)
|
|
return fds
|
|
|
|
def _get_fields_snapshot(self, fields_set=None):
|
|
new_values = {}
|
|
if fields_set is None:
|
|
fields_set = self._get_editable_fields()
|
|
for attr, val in self.__dict__.items():
|
|
if attr in fields_set:
|
|
new_values[attr] = val
|
|
return new_values
|
|
|
|
def _values_have_edits(self, new_values):
|
|
return any(new_values.get(fd_name, None) != self._prior_values_store.get(fd_name, None) for fd_name in new_values.keys())
|
|
|
|
|
|
class PrimordialModel(HasEditsMixin, CreatedModifiedModel):
|
|
"""
|
|
Common model for all object types that have these standard fields
|
|
must use a subclass CommonModel or CommonModelNameNotUnique though
|
|
as this lacks a name field.
|
|
"""
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
description = models.TextField(
|
|
blank=True,
|
|
default='',
|
|
)
|
|
created_by = models.ForeignKey(
|
|
'auth.User',
|
|
related_name='%s(class)s_created+',
|
|
default=None,
|
|
null=True,
|
|
editable=False,
|
|
on_delete=models.SET_NULL,
|
|
)
|
|
modified_by = models.ForeignKey(
|
|
'auth.User',
|
|
related_name='%s(class)s_modified+',
|
|
default=None,
|
|
null=True,
|
|
editable=False,
|
|
on_delete=models.SET_NULL,
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
r = super(PrimordialModel, self).__init__(*args, **kwargs)
|
|
if self.pk:
|
|
self._prior_values_store = self._get_fields_snapshot()
|
|
else:
|
|
self._prior_values_store = {}
|
|
return r
|
|
|
|
def save(self, *args, **kwargs):
|
|
update_fields = kwargs.get('update_fields', [])
|
|
user = get_current_user()
|
|
if user and not user.id:
|
|
user = None
|
|
if (not self.pk) and (user is not None) and (not self.created_by):
|
|
self.created_by = user
|
|
if 'created_by' not in update_fields:
|
|
update_fields.append('created_by')
|
|
# Update modified_by if any editable fields have changed
|
|
new_values = self._get_fields_snapshot()
|
|
if (not self.pk and not self.modified_by) or self._values_have_edits(new_values):
|
|
if self.modified_by != user:
|
|
self.modified_by = user
|
|
if 'modified_by' not in update_fields:
|
|
update_fields.append('modified_by')
|
|
super(PrimordialModel, self).save(*args, **kwargs)
|
|
self._prior_values_store = new_values
|
|
|
|
def clean_description(self):
|
|
# Description should always be empty string, never null.
|
|
return self.description or ''
|
|
|
|
def validate_unique(self, exclude=None):
|
|
super(PrimordialModel, self).validate_unique(exclude=exclude)
|
|
model = type(self)
|
|
if not hasattr(model, 'SOFT_UNIQUE_TOGETHER'):
|
|
return
|
|
errors = []
|
|
for ut in model.SOFT_UNIQUE_TOGETHER:
|
|
kwargs = {}
|
|
for field_name in ut:
|
|
kwargs[field_name] = getattr(self, field_name, None)
|
|
try:
|
|
obj = model.objects.get(**kwargs)
|
|
except ObjectDoesNotExist:
|
|
continue
|
|
if not (self.pk and self.pk == obj.pk):
|
|
errors.append('%s with this (%s) combination already exists.' % (model.__name__, ', '.join(set(ut) - {'polymorphic_ctype'})))
|
|
if errors:
|
|
raise ValidationError(errors)
|
|
|
|
|
|
class CommonModel(PrimordialModel):
|
|
'''a base model where the name is unique'''
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
name = models.CharField(
|
|
max_length=512,
|
|
unique=True,
|
|
)
|
|
|
|
|
|
class CommonModelNameNotUnique(PrimordialModel):
|
|
'''a base model where the name is not unique'''
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
name = models.CharField(
|
|
max_length=512,
|
|
unique=False,
|
|
)
|
|
|
|
|
|
class NotificationFieldsModel(BaseModel):
|
|
class Meta:
|
|
abstract = True
|
|
|
|
notification_templates_error = models.ManyToManyField("NotificationTemplate", blank=True, related_name='%(class)s_notification_templates_for_errors')
|
|
|
|
notification_templates_success = models.ManyToManyField("NotificationTemplate", blank=True, related_name='%(class)s_notification_templates_for_success')
|
|
|
|
notification_templates_started = models.ManyToManyField("NotificationTemplate", blank=True, related_name='%(class)s_notification_templates_for_started')
|
|
|
|
|
|
def accepts_json(relation):
|
|
"""
|
|
Used to mark a model field as allowing JSON e.g,. JobTemplate.extra_vars
|
|
This is *mostly* used as a way to provide type hints for certain fields
|
|
so that HTTP OPTIONS reports the type data we need for the CLI to allow
|
|
JSON/YAML input.
|
|
"""
|
|
setattr(relation, '__accepts_json__', True)
|
|
return relation
|