Skip to content

Commit

Permalink
Merge pull request #20 from davidslusser/service_accounts
Browse files Browse the repository at this point in the history
Service accounts
  • Loading branch information
davidslusser committed Oct 14, 2020
2 parents 4018bbc + 16a78c3 commit fc0a11c
Show file tree
Hide file tree
Showing 20 changed files with 615 additions and 192 deletions.
30 changes: 29 additions & 1 deletion docs/source/features.rst
Expand Up @@ -110,4 +110,32 @@ user-defined preferences
------------------------
User preferences, for settings like theme, start page, recents count, etc. are available in the UserPreference model.
A view for displaying and editing these preferences , ``DetailUser``, is available at ``userextensions:detail_user``
which uses Twitter Bootsrap. On this page there are links to refresh the API token and edit available preferences.
which uses Twitter Bootstrap. On this page there are links to refresh the API token and edit available preferences.

.. image:: images/detail_user.png


service account management
--------------------------
Version 0.0.10 of django-userextensions introduces service account management and provides the ability to link a
service account to an existing group. By default one service account per group is allowed. Adding a service account
creates a new User (django.contrib.auth.models.User) and a new entry in the ServiceAccount
(userextensions.models.ServiceAccount) that links the created user and group. A DRF API token is created automatically.
The User username is created based on the group name and optional service account prefix and service account suffix.
These can be set in django settings with the following parameters: ``SRV_ACCOUNT_PREFIX`` and ``SRV_ACCOUNT_SUFFIX``
If neither of these parameters are set, the default name will be used: ``<group>_srv``

A view for displaying and editing these preferences , ``ManageServiceAccounts``, is available at
``userextensions:manage_service_accounts`` which uses Twitter Bootstrap. This page provides a list all current service
accounts the current user has rights to and all groups without a service account. This is based on existing groups the
user is a member of self-service action are also available.

Self-service actions on this page include:
- display service account API token
- refresh API token
- enable/disable service account
- delete service account
- list users in group
- create service account

.. image:: images/manage_service_accounts.png
Binary file added docs/source/images/detail_user.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/images/manage_service_accounts.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions docs/source/internals.rst
Expand Up @@ -19,22 +19,22 @@ Middleware
Signals
-------
.. automodule:: userextensions.signals
:members: add_user_preference, trim_recents
:members: add_user_preference, trim_recents, create_srv_account_token


Models
------
.. automodule:: userextensions.models
:members: Theme, UserPreference, UserRecent, UserFavorite
:members: Theme, UserPreference, UserRecent, UserFavorite, ServiceAccount


Action Views
------------
.. automodule:: userextensions.views.action
:members: RefreshApiToken, AddFavorite, DeleteFavorite, DeleteRecent, UserLoginRedirect, SetStartPage
:members: RefreshApiToken, RefreshSrvAcctApiToken, AddFavorite, DeleteFavorite, DeleteRecent, UserLoginRedirect, SetStartPage, CreateServiceAccount, DeleteServiceAccount, EnableServiceAccount, DisableServiceAccount


GUI Views
---------
.. automodule:: userextensions.views.gui
:members: ListRecents, ListFavorites, DetailUser
:members: ListRecents, ListFavorites, DetailUser, ManageServiceAccounts
1 change: 1 addition & 0 deletions docs/source/version_history.rst
Expand Up @@ -12,6 +12,7 @@ Pre-Release
:header: "Release", "Details"
:widths: 20, 100

"0.0.10", "added service account management"
"0.0.6", "added documentation via sphinx"
"0.0.5", "fixed base_tempalte in recents/favorites views"
"0.0.4", "added views and templates for user pages"
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
@@ -1,5 +1,5 @@
Django>=2.2.9,<3.0.0
django-braces==1.13.0
django-handyhelpers==0.0.25
django-handyhelpers==0.0.26
djangorestframework==3.11.0
Sphinx==1.8.5
2 changes: 1 addition & 1 deletion userextensions/__init__.py
Expand Up @@ -7,7 +7,7 @@
"""

__title__ = 'django-userextensions'
__version__ = '0.0.9'
__version__ = '0.0.10'
__author__ = 'David Slusser'
__email__ = 'dbslusser@gmail.com'
__license__ = 'GPL-3.0'
Expand Down
9 changes: 8 additions & 1 deletion userextensions/admin.py
@@ -1,7 +1,7 @@
from django.contrib import admin

# import models
from userextensions.models import (Theme, UserFavorite, UserPreference, UserRecent)
from userextensions.models import (Theme, UserFavorite, UserPreference, UserRecent, ServiceAccount)


class ThemeAdmin(admin.ModelAdmin):
Expand All @@ -26,8 +26,15 @@ class UserFavoriteAdmin(admin.ModelAdmin):
list_filter = ["user"]


class ServiceAccountAdmin(admin.ModelAdmin):
list_display = ("id", "user", "group", "description", "enabled")
search_fields = ["user__username", "group__name", "description"]
list_filter = ["enabled"]


# register models
admin.site.register(Theme, ThemeAdmin)
admin.site.register(UserPreference, UserPreferenceAdmin)
admin.site.register(UserFavorite, UserFavoriteAdmin)
admin.site.register(UserRecent, UserRecentAdmin)
admin.site.register(ServiceAccount, ServiceAccountAdmin)
14 changes: 13 additions & 1 deletion userextensions/forms.py
@@ -1,7 +1,7 @@
from django import forms

# import models
from userextensions.models import (UserPreference)
from userextensions.models import (UserPreference, ServiceAccount)


class UserPreferenceForm(forms.ModelForm):
Expand All @@ -15,3 +15,15 @@ class Meta:
'theme': forms.Select(attrs={'class': 'form-control'}),
'start_page': forms.TextInput(attrs={'class': 'form-control'}),
}


class ServiceAccountForm(forms.ModelForm):
""" Form class used to add/edit ServiceAccount objects """
class Meta:
model = ServiceAccount
exclude = ['created_at', 'updated_at', 'user']
widgets = {
'group': forms.Select(attrs={'class': 'form-control'}),
'description': forms.TextInput(attrs={'class': 'form-control'}),
'enabled': forms.CheckboxInput(attrs={'class': 'form-control'}),
}
79 changes: 67 additions & 12 deletions userextensions/models.py
@@ -1,16 +1,20 @@
"""
"""
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.db import models
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError

from handyhelpers.managers import HandyHelperModelManager
from rest_framework.authtoken.models import Token


class UserExtensionBaseModel(models.Model):
""" base model for UserExtension tables """
objects = HandyHelperModelManager()
created_at = models.DateTimeField(auto_now_add=True, help_text="date/time when this row was first created")
updated_at = models.DateTimeField(auto_now=True, help_text="date/time this row was last updated")
created_at = models.DateTimeField(auto_now_add=True, help_text='date/time when this row was first created')
updated_at = models.DateTimeField(auto_now=True, help_text='date/time this row was last updated')

class Meta:
abstract = True
Expand All @@ -19,9 +23,9 @@ class Meta:
class Theme(UserExtensionBaseModel):
""" This model tracks themes. It can be used to provide user preferred frontend styling options based on
defined css files. """
name = models.CharField(max_length=32, unique=True, help_text="name of theme")
name = models.CharField(max_length=32, unique=True, help_text='name of theme')
css_file = models.CharField(max_length=255, unique=True, blank=True, null=True,
help_text="path to css file for theme")
help_text='path to css file for theme')

def __str__(self):
return self.name
Expand All @@ -30,13 +34,13 @@ def __str__(self):
class UserPreference(UserExtensionBaseModel):
""" This table tracks user preferences. Fields include theme, recents_count, page_refresh_time, and start_page. """
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='preference')
theme = models.ForeignKey(Theme, blank=True, null=True, help_text="theme to use for web pages",
theme = models.ForeignKey(Theme, blank=True, null=True, help_text='theme to use for web pages',
on_delete=models.CASCADE)
recents_count = models.IntegerField(default=25, blank=True, null=True,
help_text="number of recents to keep a record of")
help_text='number of recents to keep a record of')
page_refresh_time = models.IntegerField(default=5, blank=True, null=True,
help_text="time, in minutes, to auto-refresh a page (where applicable")
start_page = models.CharField(max_length=255, blank=True, null=True, help_text="url to redirect to after login")
help_text='time, in minutes, to auto-refresh a page (where applicable')
start_page = models.CharField(max_length=255, blank=True, null=True, help_text='url to redirect to after login')

def __str__(self):
return self.user.username
Expand All @@ -45,7 +49,7 @@ def __str__(self):
class UserRecent(UserExtensionBaseModel):
""" This table stored recently visited urls. """
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='recent')
url = models.URLField(help_text="url endpoint")
url = models.URLField(help_text='url endpoint')

class Meta:
unique_together = (('url', 'user'), )
Expand All @@ -57,11 +61,62 @@ def __str__(self):
class UserFavorite(UserExtensionBaseModel):
""" This table stores user-defined favorites. """
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='favorite')
name = models.CharField(max_length=32, blank=True, null=True, help_text="name/label/reference for this favorite")
url = models.URLField(help_text="url endpoint")
name = models.CharField(max_length=32, blank=True, null=True, help_text='name/label/reference for this favorite')
url = models.URLField(help_text='url endpoint')

class Meta:
unique_together = (('url', 'user'), )

def __str__(self):
return self.url


class ServiceAccount(UserExtensionBaseModel):
""" This table stores service accounts and maps to a (service account) user and group """
user = models.OneToOneField(User, blank=True, null=True, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
enabled = models.BooleanField(default=True, help_text='enable/disable state of user')
description = models.CharField(max_length=254, blank=True, null=True, help_text='optional description')

class Meta:
unique_together = (('user', 'group'), )

def __str__(self):
return self.user.username

def clean(self):
""" clean/update/validate data before saving """
if not self.user:
# Create name based on prefix + group + suffix; prefix and/or suffix is required. The prefix & suffix values
# are provided by django settings variables. If neither is set, a default suffix ('_srv') will be used.
prefix = getattr(settings, 'SRV_ACCOUNT_PREFIX', '')
suffix = getattr(settings, 'SRV_ACCOUNT_SUFFIX', '')
if not prefix and not suffix:
suffix = '_srv'
username = prefix + self.group.name + suffix
else:
username = self.user.username

# before creating the service account, we need to get_or_create the user
self.user = User.objects.get_or_create(username=username)[0]
# add now add the new user to the matching group
self.user.groups.add(self.group)

# check if multiple service accounts per group are allowed. This is set in the django settings file with
# the ALLOW_MULTIPLE_SRV_ACCOUNTS variable. By default, only one service account per group is allowed. To
# enabled multiple service accounts per group, set SRV_ALLOW_MULTIPLE_ACCOUNTS=True
if not self.pk:
allow_multiple = getattr(settings, 'ALLOW_MULTIPLE_SRV_ACCOUNTS', False)
existing = ServiceAccount.objects.filter(group=self.group)
if existing and allow_multiple is False:
raise ValidationError({'group': 'Multiple service accounts per group is currently not allowed. To '
'enable multiple accounts per group, set '
'ALLOW_MULTIPLE_SRV_ACCOUNTS=True in the django settings. '})

def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)

def create_drf_token(self):
""" create a drf token for this service account """
Token.objects.get_or_create(user=self.user)
15 changes: 11 additions & 4 deletions userextensions/signals.py
Expand Up @@ -4,17 +4,17 @@

# import models
from django.contrib.auth.models import User
from userextensions.models import (UserPreference, UserRecent)
from userextensions.models import (UserPreference, UserRecent, ServiceAccount)


@receiver(post_save, sender=User, dispatch_uid="add_user_preference")
@receiver(post_save, sender=User, dispatch_uid='add_user_preference')
def add_user_preference(sender, instance, created, **kwargs):
""" This post-save signal adds a UserPreference object when a User is created """
if created:
UserPreference.objects.create(user=instance)


@receiver(post_save, sender=UserRecent, dispatch_uid="trim_recents")
@receiver(post_save, sender=UserRecent, dispatch_uid='trim_recents')
def trim_recents(sender, instance, created, **kwargs):
""" This post-save signal trims a users recents to only maintain the x most recent urls, where x is the
recents_count configured in the UserPreference table """
Expand All @@ -32,5 +32,12 @@ def trim_recents(sender, instance, created, **kwargs):
if UserRecent.objects.filter(user=instance.user).count() <= recents_count:
return
recent_id_list = UserRecent.objects.filter(user=instance.user
).order_by('-updated_at')[:recents_count].values_list("id", flat=True)
).order_by('-updated_at')[:recents_count].values_list('id', flat=True)
UserRecent.objects.filter(user=instance.user).exclude(pk__in=list(recent_id_list)).delete()


@receiver(post_save, sender=ServiceAccount, dispatch_uid='create_srv_account_token')
def create_srv_account_token(sender, instance, created, **kwargs):
""" This post-save signal creates a drf token when a new ServiceAccount is created """
if created:
instance.create_drf_token()
@@ -0,0 +1,12 @@
<br>
<div class="container-fluid">
<table class="table table-condensed table-bordered table-striped">
<tbody>
{% for row in queryset %}
<tr>
<td>{{ row }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

0 comments on commit fc0a11c

Please sign in to comment.