implement multi user usage

pull/9/head
JensDiemer 2020-10-24 17:15:05 +02:00
rodzic d1bf6f25d6
commit 58effa8f2e
11 zmienionych plików z 239 dodań i 4 usunięć

Wyświetl plik

@ -44,6 +44,8 @@ There exists two kind of installation/usage:
* local virtualenv (without docker)
* docker-compose
see below
=== prepare
{{{
@ -147,6 +149,28 @@ Notes:
----
== Multi user usage
PyInventory supports multiple users. The idea:
* Every normal user sees only his own created database entries
* All users used the Django admin
Note: All created Tags are shared for all existing users!
So setup a normal user:
* Set "Staff status"
* Unset "Superuser status"
* Add user to "normal_user" group
* Don't add any additional permissions
e.g.:
{{https://raw.githubusercontent.com/jedie/jedie.github.io/master/screenshots/PyInventory/PyInventory normal user example.png|normal user example}}
== Backwards-incompatible changes
Nothing, yet ;)

Wyświetl plik

@ -79,6 +79,8 @@ There exists two kind of installation/usage:
* docker-compose
see below
prepare
=======
@ -191,6 +193,34 @@ Screenshots
----
----------------
Multi user usage
----------------
PyInventory supports multiple users. The idea:
* Every normal user sees only his own created database entries
* All users used the Django admin
Note: All created Tags are shared for all existing users!
So setup a normal user:
* Set "Staff status"
* Unset "Superuser status"
* Add user to "normal_user" group
* Don't add any additional permissions
e.g.:
|normal user example|
.. |normal user example| image:: https://raw.githubusercontent.com/jedie/jedie.github.io/master/screenshots/PyInventory/PyInventory normal user example.png
------------------------------
Backwards-incompatible changes
------------------------------
@ -246,4 +276,4 @@ donation
------------
``Note: this file is generated from README.creole 2020-10-24 16:39:24 with "python-creole"``
``Note: this file is generated from README.creole 2020-10-24 17:15:43 with "python-creole"``

Wyświetl plik

@ -2,6 +2,10 @@ from reversion_compare.admin import CompareVersionAdmin
class BaseUserAdmin(CompareVersionAdmin):
def get_changelist(self, request, **kwargs):
self.user = request.user
return super().get_changelist(request, **kwargs)
def save_model(self, request, obj, form, change):
if obj.user_id is None:
obj.user = request.user

Wyświetl plik

@ -1,11 +1,14 @@
import tagulous
from adminsortable2.admin import SortableInlineAdminMixin
from django.contrib import admin
from django.contrib.admin.views.main import ChangeList
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _
from import_export.admin import ImportExportMixin
from import_export.resources import ModelResource
from inventory.admin.base import BaseUserAdmin
from inventory.forms import ItemModelModelForm
from inventory.models import ItemLinkModel, ItemModel
@ -13,6 +16,15 @@ class ItemLinkModelInline(SortableInlineAdminMixin, admin.TabularInline):
model = ItemLinkModel
extra = 1
def get_queryset(self, request):
qs = super().get_queryset(request)
if not request.user.is_superuser:
# Display only own created entries
qs = qs.filter(user=request.user)
return qs
class ItemModelResource(ModelResource):
@ -20,8 +32,19 @@ class ItemModelResource(ModelResource):
model = ItemModel
class ItemModelChangeList(ChangeList):
def get_queryset(self, request):
"""
List always the base instances
"""
qs = super().get_queryset(request)
qs = qs.filter(parent__isnull=True)
return qs
@admin.register(ItemModel)
class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
form = ItemModelModelForm
date_hierarchy = 'create_dt'
list_display = (
'kind', 'producer',
@ -76,5 +99,18 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
readonly_fields = ('id', 'create_dt', 'update_dt', 'user')
inlines = (ItemLinkModelInline,)
def get_changelist(self, request, **kwargs):
self.user = request.user
return ItemModelChangeList
def get_queryset(self, request):
qs = super().get_queryset(request)
if not request.user.is_superuser:
# Display only own created entries
qs = qs.filter(user=request.user)
return qs
tagulous.admin.enhance(ItemModel, ItemModelAdmin)

Wyświetl plik

@ -3,6 +3,7 @@ from import_export.admin import ImportExportMixin
from import_export.resources import ModelResource
from inventory.admin.base import BaseUserAdmin
from inventory.forms import LocationModelModelForm
from inventory.models import LocationModel
@ -14,4 +15,13 @@ class LocationModelResource(ModelResource):
@admin.register(LocationModel)
class LocationModelAdmin(ImportExportMixin, BaseUserAdmin):
pass
form = LocationModelModelForm
def get_queryset(self, request):
qs = super().get_queryset(request)
if not request.user.is_superuser:
# Display only own created entries
qs = qs.filter(user=request.user)
return qs

Wyświetl plik

@ -1,6 +1,8 @@
from pathlib import Path
from django.core.checks import Error, register
from django.core.checks import Error, Warning, register
from inventory.permissions import get_or_create_normal_user_group, setup_normal_user_permissions
@register()
@ -15,3 +17,25 @@ def inventory_checks(app_configs, **kwargs):
)
)
return errors
@register()
def inventory_user_groups(app_configs, **kwargs):
"""
Setup PyInventory user groups
"""
warnings = []
normal_user_group, created = get_or_create_normal_user_group()
if created:
warnings.append(
Warning(f'User group {normal_user_group} created')
)
updated = setup_normal_user_permissions(normal_user_group)
if updated:
warnings.append(
Warning(f'Update permissions for {normal_user_group}')
)
return warnings

35
inventory/forms.py 100644
Wyświetl plik

@ -0,0 +1,35 @@
from django import forms
from django.core.exceptions import FieldDoesNotExist
from inventory.request_dict import get_request_dict
class BaseUserOnlyModelForm(forms.ModelForm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Filter all related fields that has a "user" attribute for the current user
# e.g.:
# The user should only select his own "location" and "items"
user = get_request_dict()['user'] # get current user via threading.local()
for formfield in self.fields.values():
if not hasattr(formfield, 'queryset'):
continue
queryset = formfield.queryset
opts = queryset.model._meta
try:
opts.get_field('user')
except FieldDoesNotExist:
continue
formfield.queryset = queryset.filter(user=user)
class ItemModelModelForm(BaseUserOnlyModelForm):
pass
class LocationModelModelForm(BaseUserOnlyModelForm):
pass

Wyświetl plik

@ -0,0 +1,20 @@
from inventory.request_dict import clear_request_dict, get_request_dict
class RequestDictMiddleware:
"""
Make the "current user" information avaiable everywhere via threading.local()
Access e.g.:
user = get_request_dict()['user']
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
get_request_dict().update(user=request.user)
response = self.get_response(request)
clear_request_dict()
return response

Wyświetl plik

@ -0,0 +1,34 @@
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from inventory.models import ItemLinkModel, ItemModel, LocationModel
NORMAL_USER_GROUP_NAME = 'normal user'
def get_permissions(*models):
content_types = []
for model in models:
content_types.append(ContentType.objects.get_for_model(model))
return Permission.objects.filter(content_type__in=content_types)
def get_or_create_normal_user_group():
return Group.objects.get_or_create(name=NORMAL_USER_GROUP_NAME)
def setup_normal_user_permissions(normal_user_group):
"""
Setup PyInventory "normal user" permissions
"""
assert normal_user_group.name == NORMAL_USER_GROUP_NAME
permissions = get_permissions(ItemModel, ItemLinkModel, LocationModel)
existing_permissions = normal_user_group.permissions.all()
if set(permissions) != set(existing_permissions):
normal_user_group.permissions.set(permissions)
return True
return False

Wyświetl plik

@ -0,0 +1,16 @@
import threading
__request_dict = threading.local()
def get_request_dict():
try:
return __request_dict.context
except AttributeError:
__request_dict.context = {}
return __request_dict.context
def clear_request_dict():
__request_dict.context = {}

Wyświetl plik

@ -58,10 +58,12 @@ MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'inventory.middlewares.RequestDictMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django_tools.middlewares.ThreadLocal.ThreadLocalMiddleware',
]
TEMPLATES = [