Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Additional baseline features #639

Open
wants to merge 9 commits into
base: devel
Choose a base branch
from
Empty file.
21 changes: 21 additions & 0 deletions ansible_base/django_template/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.apps import AppConfig
from django.db.models import signals


def _initialize_data(sender, **kwargs):
from ansible_base.django_template.signals.preloaded_data import create_preload_data

create_preload_data(**kwargs)


class DjangoTemplateConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'ansible_base.django_template'
label = 'dab_django_template'
verbose_name = 'Django AAP Template'

def ready(self):
signals.post_migrate.connect(_initialize_data, sender=self, weak=False)

# Load the signals
import ansible_base.django_template.signals # noqa 401
12 changes: 12 additions & 0 deletions ansible_base/django_template/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# User must be imported first or else we end up with a circular import
from ansible_base.django_template.models.user import AbstractTemplateUser # noqa: 401 # isort: skip
from ansible_base.django_template.models.organization import AbstractTemplateOrganization # noqa: 401 # isort: skip
from ansible_base.django_template.models.team import AbstractTemplateTeam # noqa: 401 # isort: skip

from ansible_base.lib.utils.auth import get_organization_model, get_team_model
from ansible_base.rbac import permission_registry

if get_team_model(return_none_on_error=True) is not None:
permission_registry.register(get_team_model(), parent_field_name='organization')
if get_organization_model(return_none_on_error=True) is not None:
permission_registry.register(get_organization_model(), parent_field_name=None)
47 changes: 47 additions & 0 deletions ansible_base/django_template/models/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from django.contrib.auth import get_user_model
from django.db.models import Model

from ansible_base.lib.utils.response import get_relative_url
from ansible_base.rbac.models import ObjectRole, RoleDefinition


class UsersMembersMixin(Model):
class Meta:
abstract = True

admin_rd_name = None
member_rd_name = None

def related_fields(self, request):
ret = super().related_fields(request)
for key in ('users', 'admins'):
ret[key] = get_relative_url(f'{self._meta.model_name}-{key}-list', kwargs={'pk': self.id})
return ret

@property
def member_rd(self):
return RoleDefinition.objects.get(name=self.member_rd_name)

@property
def admin_rd(self):
return RoleDefinition.objects.get(name=self.admin_rd_name)

def add_member(self, user):
self.member_rd.give_permission(user, self)

def add_admin(self, user):
self.admin_rd.give_permission(user, self)

def remove_member(self, user):
self.member_rd.remove_permission(user, self)

def remove_admin(self, user):
self.admin_rd.remove_permission(user, self)

@property
def admins(self):
return get_user_model().objects.filter(has_roles__in=ObjectRole.objects.filter(object_id=self.pk, role_definition__name=self.admin_rd_name))

@property
def users(self):
return get_user_model().objects.filter(has_roles__in=ObjectRole.objects.filter(object_id=self.pk, role_definition__name=self.member_rd_name))
42 changes: 42 additions & 0 deletions ansible_base/django_template/models/organization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _

from ansible_base.activitystream.models import AuditableModel
from ansible_base.django_template.models.mixins import UsersMembersMixin
from ansible_base.lib.abstract_models.organization import AbstractOrganization
from ansible_base.lib.utils.auth import get_team_model
from ansible_base.rbac.managed import OrganizationAdmin, OrganizationMember
from ansible_base.rbac.models import ObjectRole
from ansible_base.resource_registry.fields import AnsibleResourceField


class AbstractTemplateOrganization(UsersMembersMixin, AbstractOrganization, AuditableModel):
class Meta:
abstract = True

admin_rd_name = OrganizationAdmin.name
member_rd_name = OrganizationMember.name

resource = AnsibleResourceField(primary_key_field="id")

managed = models.BooleanField(
editable=False,
blank=False,
default=False,
help_text=_("Indicates if this organization is managed by the system. It cannot be modified once created."),
)

def get_summary_fields(self):
# TODO: We should probably come up with a more codified and standard
# way to return this kind of info from models.
response = super().get_summary_fields()
response["related_field_counts"] = {}
if get_team_model(return_none_on_error=True) is not None:
response["related_field_counts"]["teams"] = self.teams.count()

response["related_field_counts"]["users"] = get_user_model().objects.filter(
has_roles__in=ObjectRole.objects.filter(object_id=self.pk, role_definition__name=self.member_rd_name)
).count()

return response
29 changes: 29 additions & 0 deletions ansible_base/django_template/models/team.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _

from ansible_base.activitystream.models import AuditableModel
from ansible_base.django_template.models.mixins import UsersMembersMixin
from ansible_base.lib.abstract_models import AbstractTeam
from ansible_base.rbac.managed import TeamAdmin, TeamMember
from ansible_base.resource_registry.fields import AnsibleResourceField


class AbstractTemplateTeam(UsersMembersMixin, AbstractTeam, AuditableModel):
class Meta(AbstractTeam.Meta):
abstract = True

admin_rd_name = TeamAdmin.name
member_rd_name = TeamMember.name

resource = AnsibleResourceField(primary_key_field="id")

ignore_relations = ['parents', 'teams']

# If we remove this in the future, you can also remove the ignore_relations
parents = models.ManyToManyField(
settings.ANSIBLE_BASE_TEAM_MODEL,
blank=True,
symmetrical=False,
help_text=_("The list of teams that are parents of this team"),
)
104 changes: 104 additions & 0 deletions ansible_base/django_template/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import logging

from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH, get_hashers_by_algorithm, identify_hasher, make_password
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext as _

from ansible_base.activitystream.models import AuditableModel
from ansible_base.lib.abstract_models.common import CommonModel
from ansible_base.lib.abstract_models.user import AbstractDABUser
from ansible_base.lib.managers.user import UserUnmanagedManager
from ansible_base.lib.utils.models import user_summary_fields
from ansible_base.resource_registry.fields import AnsibleResourceField

logger = logging.getLogger('ansible_base.django_template.models.user')


def password_is_hashed(password):
"""
Returns a boolean of whether password is hashed with loaded algorithms
"""
if password is None:
return False
try:
hasher = identify_hasher(password)
except ValueError:
# hasher can't be identified or is not loaded
return False
return hasher.algorithm in get_hashers_by_algorithm().keys()


def password_is_usable(password):
"""
Returns True if password is None or wasn't generated by django.contrib.auth.hashers.make_password(None)
"""
unusable_password_len = len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH

# what are the odds that a user password starts with unusable prefix and the same length :-?
return password is None or not (password.startswith(UNUSABLE_PASSWORD_PREFIX) and len(password) == unusable_password_len)


class AbstractTemplateUser(AbstractDABUser, CommonModel, AuditableModel):
class Meta(AbstractUser.Meta):
abstract = True

ignore_relations = [
'groups', # not using the auth app stuff, see Team model
'user_permissions', # not using auth app permissions
'logentry', # used for Django admin pages, not the API
'social_auth', # Social auth endpoint
'organizations_administered', # We are going to merge [teams|orgs] the user is an admin in with [teams|orgs] the user is a member of
'teams_administered',
]
activity_stream_excluded_field_names = ['last_login']

encrypted_fields = () # handed as special case by UserSerializer
PASSWORD_FIELDS = ["password"] # Mark password fields so ansible_base.lib.rest_filters can properly block attempts to filter over password

resource = AnsibleResourceField(primary_key_field="id")

managed = models.BooleanField(
editable=False,
blank=False,
default=False,
help_text=_("Indicates if this user is managed by the system. It cannot be modified once created."),
)

# By default, skip managed users (use all_objects for all users queryset)
objects = UserUnmanagedManager()

def __init__(self, *args, is_platform_auditor=False, **kwargs):
super().__init__(*args, **kwargs)
if is_platform_auditor:
self._is_platform_auditor = True
# Store the original value of the fields to check for field changes later
self._original_fields = self._get_fields()

def _get_fields(self):
"""
Return a dictionary of the model's instance fields and their current values.
"""
return {field.name: self.__dict__.get(field.name) for field in self._meta.fields}

def save(self, *args, **kwargs):
# If the password is empty string lets make it None, this will get turned into an unusable password by make_password
if self.password == '':
self.password = None

if password_is_usable(self.password) and not password_is_hashed(self.password):
self.password = make_password(self.password)

super().save(*args, **kwargs)

def summary_fields(self):
return user_summary_fields(self)

@property
def organizations(self):

from ansible_base.lib.utils.auth import get_organization_model
if get_organization_model(return_none_on_error=True) is None:
raise AttributeError("Property not available")
else:
return get_organization_model().access_qs(self, 'member')
35 changes: 35 additions & 0 deletions ansible_base/django_template/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from ansible_base.django_template import views
from ansible_base.django_template.views.api.v1.user import OrganizationRelatedUserViewSet, TeamRelatedUserViewSet
from ansible_base.lib.routers import AssociationResourceRouter
from ansible_base.lib.utils.auth import get_organization_model, get_team_model

router = AssociationResourceRouter()
router.register(
r'users',
views.UserViewSet,
related_views={},
)
if get_organization_model(return_none_on_error=True) is not None:
related_views = {
'users': (OrganizationRelatedUserViewSet, 'users'),
'admins': (OrganizationRelatedUserViewSet, 'admins'),
}
if get_team_model(return_none_on_error=True) is not None:
related_views['teams'] = (views.TeamViewSet, 'teams')
router.register(
r'organizations',
views.OrganizationViewSet,
related_views=related_views,
basename="organization",
)

if get_team_model(return_none_on_error=True) is not None:
router.register(
r'teams',
views.TeamViewSet,
related_views={
'users': (TeamRelatedUserViewSet, 'users'),
'admins': (TeamRelatedUserViewSet, 'admins'),
},
basename='team',
)
3 changes: 3 additions & 0 deletions ansible_base/django_template/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ansible_base.django_template.serializers.organization import OrganizationSerializer # noqa: 401
from ansible_base.django_template.serializers.team import TeamSerializer # noqa: 401
from ansible_base.django_template.serializers.user import UserSerializer # noqa: 401
11 changes: 11 additions & 0 deletions ansible_base/django_template/serializers/organization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from ansible_base.lib.serializers.common import NamedCommonModelSerializer
from ansible_base.lib.utils.auth import get_organization_model


class OrganizationSerializer(NamedCommonModelSerializer):
class Meta:
model = get_organization_model()
fields = NamedCommonModelSerializer.Meta.fields + [
'description',
'managed',
]
38 changes: 38 additions & 0 deletions ansible_base/django_template/serializers/team.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# from rest_framework import serializers
from ansible_base.lib.serializers.common import NamedCommonModelSerializer
from ansible_base.lib.utils.auth import get_organization_model, get_team_model
from ansible_base.rbac.api.related import RelatedAccessMixin


class TeamSerializer(RelatedAccessMixin, NamedCommonModelSerializer):
lookup_field = 'users'

class Meta:
model = get_team_model()
fields = NamedCommonModelSerializer.Meta.fields + [
'organization',
'description',
]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
request = self.context.get('request')
if request:
self.fields['organization'].queryset = get_organization_model().access_qs(request.user)

def get_extra_kwargs(self):
extra_kwargs = super().get_extra_kwargs()
request = self.context.get('request')
if request and request.user.is_superuser:
return extra_kwargs

view = self.context.get('view')
if view:
action = view.action

if action in ['create', 'update', 'partial_update']:
kwargs = extra_kwargs.get('organization')
kwargs['read_only'] = action in ['update', 'partial_update']
extra_kwargs['organization'] = kwargs

return extra_kwargs
Loading
Loading