Skip to content

Commit 2e17e61

Browse files
Create django_template app
1 parent 08b0633 commit 2e17e61

33 files changed

+1158
-21
lines changed

ansible_base/django_template/__init__.py

Whitespace-only changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django.apps import AppConfig
2+
from django.db.models import signals
3+
4+
5+
def _initialize_data(sender, **kwargs):
6+
from ansible_base.django_template.signals.preloaded_data import create_preload_data
7+
8+
create_preload_data(**kwargs)
9+
10+
11+
class DjangoTemplateConfig(AppConfig):
12+
default_auto_field = 'django.db.models.BigAutoField'
13+
name = 'ansible_base.django_template'
14+
label = 'dab_django_template'
15+
verbose_name = 'Django AAP Template'
16+
17+
def ready(self):
18+
signals.post_migrate.connect(_initialize_data, sender=self, weak=False)
19+
20+
# Load the signals
21+
import ansible_base.django_template.signals # noqa 401
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# User must be imported first or else we end up with a circular import
2+
from ansible_base.django_template.models.user import AbstractTemplateUser # noqa: 401 # isort: skip
3+
from ansible_base.django_template.models.organization import AbstractTemplateOrganization # noqa: 401 # isort: skip
4+
from ansible_base.django_template.models.team import AbstractTemplateTeam # noqa: 401 # isort: skip
5+
6+
from ansible_base.lib.utils.auth import get_organization_model, get_team_model
7+
from ansible_base.rbac import permission_registry
8+
9+
if get_team_model(return_none_on_error=True) is not None:
10+
permission_registry.register(get_team_model(), parent_field_name='organization')
11+
if get_organization_model(return_none_on_error=True) is not None:
12+
permission_registry.register(get_organization_model(), parent_field_name=None)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from django.contrib.auth import get_user_model
2+
from django.db.models import Model
3+
4+
from ansible_base.lib.utils.response import get_relative_url
5+
from ansible_base.rbac.models import ObjectRole, RoleDefinition
6+
7+
8+
class UsersMembersMixin(Model):
9+
class Meta:
10+
abstract = True
11+
12+
admin_rd_name = None
13+
member_rd_name = None
14+
15+
def related_fields(self, request):
16+
ret = super().related_fields(request)
17+
for key in ('users', 'admins'):
18+
ret[key] = get_relative_url(f'{self._meta.model_name}-{key}-list', kwargs={'pk': self.id})
19+
return ret
20+
21+
@property
22+
def member_rd(self):
23+
return RoleDefinition.objects.get(name=self.member_rd_name)
24+
25+
@property
26+
def admin_rd(self):
27+
return RoleDefinition.objects.get(name=self.admin_rd_name)
28+
29+
def add_member(self, user):
30+
self.member_rd.give_permission(user, self)
31+
32+
def add_admin(self, user):
33+
self.admin_rd.give_permission(user, self)
34+
35+
def remove_member(self, user):
36+
self.member_rd.remove_permission(user, self)
37+
38+
def remove_admin(self, user):
39+
self.admin_rd.remove_permission(user, self)
40+
41+
@property
42+
def admins(self):
43+
return get_user_model().objects.filter(has_roles__in=ObjectRole.objects.filter(object_id=self.pk, role_definition__name=self.admin_rd_name))
44+
45+
@property
46+
def users(self):
47+
return get_user_model().objects.filter(has_roles__in=ObjectRole.objects.filter(object_id=self.pk, role_definition__name=self.member_rd_name))
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from django.contrib.auth import get_user_model
2+
from django.db import models
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from ansible_base.activitystream.models import AuditableModel
6+
from ansible_base.django_template.models.mixins import UsersMembersMixin
7+
from ansible_base.lib.abstract_models.organization import AbstractOrganization
8+
from ansible_base.lib.utils.auth import get_team_model
9+
from ansible_base.rbac.managed import OrganizationAdmin, OrganizationMember
10+
from ansible_base.rbac.models import ObjectRole
11+
from ansible_base.resource_registry.fields import AnsibleResourceField
12+
13+
14+
class AbstractTemplateOrganization(UsersMembersMixin, AbstractOrganization, AuditableModel):
15+
class Meta:
16+
abstract = True
17+
18+
admin_rd_name = OrganizationAdmin.name
19+
member_rd_name = OrganizationMember.name
20+
21+
resource = AnsibleResourceField(primary_key_field="id")
22+
23+
managed = models.BooleanField(
24+
editable=False,
25+
blank=False,
26+
default=False,
27+
help_text=_("Indicates if this organization is managed by the system. It cannot be modified once created."),
28+
)
29+
30+
def get_summary_fields(self):
31+
# TODO: We should probably come up with a more codified and standard
32+
# way to return this kind of info from models.
33+
response = super().get_summary_fields()
34+
response["related_field_counts"] = {}
35+
if get_team_model(return_none_on_error=True) is not None:
36+
response["related_field_counts"]["teams"] = self.teams.count()
37+
38+
response["related_field_counts"]["users"] = get_user_model().objects.filter(
39+
has_roles__in=ObjectRole.objects.filter(object_id=self.pk, role_definition__name=self.member_rd_name)
40+
).count()
41+
42+
return response
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django.conf import settings
2+
from django.db import models
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from ansible_base.activitystream.models import AuditableModel
6+
from ansible_base.django_template.models.mixins import UsersMembersMixin
7+
from ansible_base.lib.abstract_models import AbstractTeam
8+
from ansible_base.rbac.managed import TeamAdmin, TeamMember
9+
from ansible_base.resource_registry.fields import AnsibleResourceField
10+
11+
12+
class AbstractTemplateTeam(UsersMembersMixin, AbstractTeam, AuditableModel):
13+
class Meta(AbstractTeam.Meta):
14+
abstract = True
15+
16+
admin_rd_name = TeamAdmin.name
17+
member_rd_name = TeamMember.name
18+
19+
resource = AnsibleResourceField(primary_key_field="id")
20+
21+
ignore_relations = ['parents', 'teams']
22+
23+
# If we remove this in the future, you can also remove the ignore_relations
24+
parents = models.ManyToManyField(
25+
settings.ANSIBLE_BASE_TEAM_MODEL,
26+
blank=True,
27+
symmetrical=False,
28+
help_text=_("The list of teams that are parents of this team"),
29+
)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import logging
2+
3+
from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH, get_hashers_by_algorithm, identify_hasher, make_password
4+
from django.contrib.auth.models import AbstractUser
5+
from django.db import models
6+
from django.utils.translation import gettext as _
7+
8+
from ansible_base.activitystream.models import AuditableModel
9+
from ansible_base.lib.abstract_models.common import CommonModel
10+
from ansible_base.lib.abstract_models.user import AbstractDABUser
11+
from ansible_base.lib.managers.user import UserUnmanagedManager
12+
from ansible_base.lib.utils.models import user_summary_fields
13+
from ansible_base.resource_registry.fields import AnsibleResourceField
14+
15+
logger = logging.getLogger('ansible_base.django_template.models.user')
16+
17+
18+
def password_is_hashed(password):
19+
"""
20+
Returns a boolean of whether password is hashed with loaded algorithms
21+
"""
22+
if password is None:
23+
return False
24+
try:
25+
hasher = identify_hasher(password)
26+
except ValueError:
27+
# hasher can't be identified or is not loaded
28+
return False
29+
return hasher.algorithm in get_hashers_by_algorithm().keys()
30+
31+
32+
def password_is_usable(password):
33+
"""
34+
Returns True if password is None or wasn't generated by django.contrib.auth.hashers.make_password(None)
35+
"""
36+
unusable_password_len = len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH
37+
38+
# what are the odds that a user password starts with unusable prefix and the same length :-?
39+
return password is None or not (password.startswith(UNUSABLE_PASSWORD_PREFIX) and len(password) == unusable_password_len)
40+
41+
42+
class AbstractTemplateUser(AbstractDABUser, CommonModel, AuditableModel):
43+
class Meta(AbstractUser.Meta):
44+
abstract = True
45+
46+
ignore_relations = [
47+
'groups', # not using the auth app stuff, see Team model
48+
'user_permissions', # not using auth app permissions
49+
'logentry', # used for Django admin pages, not the API
50+
'social_auth', # Social auth endpoint
51+
'organizations_administered', # We are going to merge [teams|orgs] the user is an admin in with [teams|orgs] the user is a member of
52+
'teams_administered',
53+
]
54+
activity_stream_excluded_field_names = ['last_login']
55+
56+
encrypted_fields = () # handed as special case by UserSerializer
57+
PASSWORD_FIELDS = ["password"] # Mark password fields so ansible_base.lib.rest_filters can properly block attempts to filter over password
58+
59+
resource = AnsibleResourceField(primary_key_field="id")
60+
61+
managed = models.BooleanField(
62+
editable=False,
63+
blank=False,
64+
default=False,
65+
help_text=_("Indicates if this user is managed by the system. It cannot be modified once created."),
66+
)
67+
68+
# By default, skip managed users (use all_objects for all users queryset)
69+
objects = UserUnmanagedManager()
70+
71+
def __init__(self, *args, is_platform_auditor=False, **kwargs):
72+
super().__init__(*args, **kwargs)
73+
if is_platform_auditor:
74+
self._is_platform_auditor = True
75+
# Store the original value of the fields to check for field changes later
76+
self._original_fields = self._get_fields()
77+
78+
def _get_fields(self):
79+
"""
80+
Return a dictionary of the model's instance fields and their current values.
81+
"""
82+
return {field.name: self.__dict__.get(field.name) for field in self._meta.fields}
83+
84+
def save(self, *args, **kwargs):
85+
# If the password is empty string lets make it None, this will get turned into an unusable password by make_password
86+
if self.password == '':
87+
self.password = None
88+
89+
if password_is_usable(self.password) and not password_is_hashed(self.password):
90+
self.password = make_password(self.password)
91+
92+
super().save(*args, **kwargs)
93+
94+
def summary_fields(self):
95+
return user_summary_fields(self)
96+
97+
@property
98+
def organizations(self):
99+
100+
from ansible_base.lib.utils.auth import get_organization_model
101+
if get_organization_model(return_none_on_error=True) is None:
102+
raise AttributeError("Property not available")
103+
else:
104+
return get_organization_model().access_qs(self, 'member')
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from ansible_base.django_template import views
2+
from ansible_base.django_template.views.api.v1.user import OrganizationRelatedUserViewSet, TeamRelatedUserViewSet
3+
from ansible_base.lib.routers import AssociationResourceRouter
4+
from ansible_base.lib.utils.auth import get_organization_model, get_team_model
5+
6+
router = AssociationResourceRouter()
7+
router.register(
8+
r'users',
9+
views.UserViewSet,
10+
related_views={},
11+
)
12+
if get_organization_model(return_none_on_error=True) is not None:
13+
related_views = {
14+
'users': (OrganizationRelatedUserViewSet, 'users'),
15+
'admins': (OrganizationRelatedUserViewSet, 'admins'),
16+
}
17+
if get_team_model(return_none_on_error=True) is not None:
18+
related_views['teams'] = (views.TeamViewSet, 'teams')
19+
router.register(
20+
r'organizations',
21+
views.OrganizationViewSet,
22+
related_views=related_views,
23+
basename="organization",
24+
)
25+
26+
if get_team_model(return_none_on_error=True) is not None:
27+
router.register(
28+
r'teams',
29+
views.TeamViewSet,
30+
related_views={
31+
'users': (TeamRelatedUserViewSet, 'users'),
32+
'admins': (TeamRelatedUserViewSet, 'admins'),
33+
},
34+
basename='team',
35+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ansible_base.django_template.serializers.organization import OrganizationSerializer # noqa: 401
2+
from ansible_base.django_template.serializers.team import TeamSerializer # noqa: 401
3+
from ansible_base.django_template.serializers.user import UserSerializer # noqa: 401
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from ansible_base.lib.serializers.common import NamedCommonModelSerializer
2+
from ansible_base.lib.utils.auth import get_organization_model
3+
4+
5+
class OrganizationSerializer(NamedCommonModelSerializer):
6+
class Meta:
7+
model = get_organization_model()
8+
fields = NamedCommonModelSerializer.Meta.fields + [
9+
'description',
10+
'managed',
11+
]

0 commit comments

Comments
 (0)