From 29be6a3725680286b47bb2d048bff5809ef1eb8e Mon Sep 17 00:00:00 2001 From: Tomasz Jakub Rup Date: Sat, 6 May 2017 17:43:34 +0200 Subject: [PATCH] first working version --- .gitignore | 2 + README.md | 2 - README.rst | 60 +++++++++++++++ authrole/__init__.py | 0 authrole/admin.py | 14 ++++ authrole/auth/__init__.py | 0 authrole/auth/backends.py | 43 +++++++++++ authrole/locale/pl/LC_MESSAGES/django.po | 35 +++++++++ authrole/migrations/0001_initial.py | 29 +++++++ authrole/migrations/__init__.py | 0 authrole/models.py | 23 ++++++ manage.py | 8 ++ tests/__init__.py | 0 tests/fixtures/role.yaml | 97 ++++++++++++++++++++++++ tests/migrations/0001_initial.py | 28 +++++++ tests/migrations/__init__.py | 0 tests/models.py | 6 ++ tests/settings.py | 30 ++++++++ tests/tests.py | 32 ++++++++ 19 files changed, 407 insertions(+), 2 deletions(-) delete mode 100644 README.md create mode 100644 README.rst create mode 100644 authrole/__init__.py create mode 100644 authrole/admin.py create mode 100644 authrole/auth/__init__.py create mode 100644 authrole/auth/backends.py create mode 100644 authrole/locale/pl/LC_MESSAGES/django.po create mode 100644 authrole/migrations/0001_initial.py create mode 100644 authrole/migrations/__init__.py create mode 100644 authrole/models.py create mode 100755 manage.py create mode 100644 tests/__init__.py create mode 100644 tests/fixtures/role.yaml create mode 100644 tests/migrations/0001_initial.py create mode 100644 tests/migrations/__init__.py create mode 100644 tests/models.py create mode 100644 tests/settings.py create mode 100644 tests/tests.py diff --git a/.gitignore b/.gitignore index 72364f9..f86deb5 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,5 @@ ENV/ # Rope project settings .ropeproject + +test.sqlite \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 4bb625a..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# django_auth_role -Add roles to django-auth diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..89fd871 --- /dev/null +++ b/README.rst @@ -0,0 +1,60 @@ +================ +django-auth-role +================ + +Add roles to django-auth + +Installation +============ + +.. sourcecode:: sh + + pip install django-auth-role + +Quick start +=========== + +Add ``authrole`` to `INSTALLED_APPS`. ``django.contrib.auth`` and ``django.contrib.contenttypes`` are also required. + +.. sourcecode:: python + + INSTALLED_APPS = [ + ... + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'authrole', + ] + +Extend ``auth.User``. + +.. sourcecode:: python + + from django.db import models + + class MyUser(models.Model): + role = models.ForeignKey('authrole.Role', related_name='myusers') + user = models.OneToOneField('auth.User', related_name='user') + +Create tables. + +.. sourcecode:: sh + + ./manage.py migrate + +Extend Your own authentication backend. + +.. sourcecode:: python + + from authrole.auth.backends import BaseAuthRoleBackend + + class MyBackend(BaseAuthRoleBackend): + def fetch_permission(self, user_obj): + return Permission.objects.filter(group__roles__myusers__user=user_obj) + +And add it to `AUTHENTICATION_BACKENDS`. + +.. sourcecode:: python + + AUTHENTICATION_BACKENDS = ( + 'app.MyBackend', + ) diff --git a/authrole/__init__.py b/authrole/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authrole/admin.py b/authrole/admin.py new file mode 100644 index 0000000..e4521dc --- /dev/null +++ b/authrole/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin + +from .models import Role + + +# @admin.register(Role) +class RoleAdmin(admin.ModelAdmin): + search_fields = ['name'] + list_display = ('name',) + list_display_links = ('name',) + list_filter = ('name',) + filter_horizontal = ('groups',) + +admin.site.register(Role, RoleAdmin) diff --git a/authrole/auth/__init__.py b/authrole/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authrole/auth/backends.py b/authrole/auth/backends.py new file mode 100644 index 0000000..5f64f10 --- /dev/null +++ b/authrole/auth/backends.py @@ -0,0 +1,43 @@ +from __future__ import unicode_literals + +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.models import Permission + + +class BaseAuthRoleBackend(ModelBackend): + """ + Authenticates against authrole.models.Role. + """ + def fetch_permission(self, user_obj): + raise NotImplemented() + + def get_role_permissions(self, user_obj, obj=None): + """ + Returns a set of permission strings that this user has through his/her + role. + """ + if user_obj.is_anonymous() or obj is not None: + return set() + if not hasattr(user_obj, '_role_perm_cache'): + if user_obj.is_superuser: + perms = Permission.objects.all() + else: + perms = self.fetch_permission(user_obj) + perms = perms.values_list('content_type__app_label', 'codename') \ + .order_by() + user_obj._role_perm_cache = set(['%s.%s' % (ct, name) + for ct, name in perms]) + return user_obj._role_perm_cache + + def get_all_permissions(self, user_obj, obj=None): + if user_obj.is_anonymous() or obj is not None: + return set() + user_obj._perm_cache = super(BaseAuthRoleBackend, self) \ + .get_all_permissions(user_obj, obj) + user_obj._perm_cache.update(self.get_role_permissions(user_obj)) + return user_obj._perm_cache + + +class ElcarAuthRoleBackend(BaseAuthRoleBackend): + def fetch_permission(self, user_obj): + return Permission.objects.filter(group__roles__elcaruser__auth_user=user_obj) diff --git a/authrole/locale/pl/LC_MESSAGES/django.po b/authrole/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 0000000..766b768 --- /dev/null +++ b/authrole/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,35 @@ +# django-auth-role polish translation. +# Copyright (C) 2017 Tomasz Jakub Rup +# This file is distributed under the same license as the django-auth-role package. +# Tomasz Jakub Rup , 2017. +msgid "" +msgstr "" +"Project-Id-Version: django-auth-role\n" +"Report-Msgid-Bugs-To: https://github.com/tomi77/django-auth-role/issues\n" +"POT-Creation-Date: 2017-05-03 15:06+0200\n" +"PO-Revision-Date: 2017-05-03 15:07+0200\n" +"Last-Translator: Tomasz Jakub Rup \n" +"Language-Team: \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" +"%100<12 || n%100>=14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" +"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" + +#: models.py:11 +msgid "Role name" +msgstr "Nazwa roli" + +#: models.py:13 +msgid "Groups of permissions" +msgstr "Grupy uprawnieĊ„" + +#: models.py:16 +msgid "Role" +msgstr "Rola" + +#: models.py:17 +msgid "Roles" +msgstr "Role" diff --git a/authrole/migrations/0001_initial.py b/authrole/migrations/0001_initial.py new file mode 100644 index 0000000..5387147 --- /dev/null +++ b/authrole/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-03 08:19 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, unique=True, verbose_name='Role name')), + ('groups', models.ManyToManyField(related_name='roles', to='auth.Group', verbose_name='Groups of permissions')), + ], + options={ + 'verbose_name': 'Role', + 'verbose_name_plural': 'Roles', + }, + ), + ] diff --git a/authrole/migrations/__init__.py b/authrole/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authrole/models.py b/authrole/models.py new file mode 100644 index 0000000..1144f55 --- /dev/null +++ b/authrole/models.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals + +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ + + +@python_2_unicode_compatible +class Role(models.Model): + name = models.CharField(max_length=128, unique=True, + verbose_name=_('Role name')) + groups = models.ManyToManyField('auth.Group', related_name='roles', + verbose_name=_('Groups of permissions')) + + class Meta(object): + verbose_name = _('Role') + verbose_name_plural = _('Roles') + + def __repr__(self): + return 'authrole.models.Role[pk=%d]' % self.pk + + def __str__(self): + return self.name diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..d960e32 --- /dev/null +++ b/manage.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +import os + +os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' +from django.core import management + +if __name__ == "__main__": + management.execute_from_command_line() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/role.yaml b/tests/fixtures/role.yaml new file mode 100644 index 0000000..095df6a --- /dev/null +++ b/tests/fixtures/role.yaml @@ -0,0 +1,97 @@ +- pk: 1 + model: 'contenttypes.ContentType' + fields: + app_label: 'app' + model: 'model1' +- pk: 2 + model: 'contenttypes.ContentType' + fields: + app_label: 'app' + model: 'model2' + +- pk: 1 + model: 'auth.Permission' + fields: + name: 'can add app.model1' + content_type: 1 + codename: 'can_add_model1' +- pk: 2 + model: 'auth.Permission' + fields: + name: 'can update app.model1' + content_type: 1 + codename: 'can_update_model1' +- pk: 3 + model: 'auth.Permission' + fields: + name: 'can delete app.model1' + content_type: 1 + codename: 'can_delete_model1' +- pk: 4 + model: 'auth.Permission' + fields: + name: 'can add app.model2' + content_type: 2 + codename: 'can_add_model2' +- pk: 5 + model: 'auth.Permission' + fields: + name: 'can update app.model2' + content_type: 2 + codename: 'can_update_model2' +- pk: 6 + model: 'auth.Permission' + fields: + name: 'can delete app.model2' + content_type: 2 + codename: 'can_delete_model2' + +- pk: 1 + model: 'auth.Group' + fields: + name: 'all model1 permissions' + permissions: [1, 2, 3] +- pk: 2 + model: 'auth.Group' + fields: + name: 'all model2 permissions' + permissions: [4, 5, 6] + +- pk: 1 + model: 'authrole.Role' + fields: + name: 'empty group' + groups: [] +- pk: 2 + model: 'authrole.Role' + fields: + name: 'all groups' + groups: [1, 2] + +- pk: 1 + model: 'auth.User' + fields: + username: 'user1' + password: 'pbkdf2_sha256$10000$vkRy7QauoLLj$ry+3xm3YX+YrSXbri8s3EcXDIrx5ceM+xQjtpLdw2oE=' + is_active: true + is_superuser: false + is_staff: false +- pk: 2 + model: 'auth.User' + fields: + username: 'user2' + password: 'pbkdf2_sha256$10000$vkRy7QauoLLj$ry+3xm3YX+YrSXbri8s3EcXDIrx5ceM+xQjtpLdw2oE=' + is_active: true + is_superuser: false + is_staff: false + +- pk: 1 + model: 'tests.MyUser' + fields: + user: 1 + role: 1 +- pk: 2 + model: 'tests.MyUser' + fields: + user: 2 + role: 2 diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py new file mode 100644 index 0000000..b12bec4 --- /dev/null +++ b/tests/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-03 08:19 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('authrole', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='MyUser', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='myusers', to='authrole.Role')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/tests/migrations/__init__.py b/tests/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models.py b/tests/models.py new file mode 100644 index 0000000..ce1e066 --- /dev/null +++ b/tests/models.py @@ -0,0 +1,6 @@ +from django.db import models + + +class MyUser(models.Model): + role = models.ForeignKey('authrole.Role', related_name='myusers') + user = models.OneToOneField('auth.User', related_name='user') diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..5f6bfa3 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,30 @@ +import django + + +SECRET_KEY = 'qaz123' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'test.sqlite' + } +} + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'authrole', + 'tests', +] + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + }, +] + +MIDDLEWARE_CLASSES = [] + +if django.VERSION[:2] <= (1, 6): + INSTALLED_APPS += ['south'] diff --git a/tests/tests.py b/tests/tests.py new file mode 100644 index 0000000..2192952 --- /dev/null +++ b/tests/tests.py @@ -0,0 +1,32 @@ +from django.contrib.auth.models import Permission +from django.test.testcases import TestCase + +from authrole.auth.backends import BaseAuthRoleBackend + + +class MyBackend(BaseAuthRoleBackend): + def fetch_permission(self, user_obj): + return Permission.objects.filter(group__roles__myusers__user=user_obj) + +backend = MyBackend() + + +class BackendTestCase(TestCase): + fixtures = ['role'] + + def test_1(self): + user = backend.authenticate(None, 'user1', 'test') + permissions = backend.get_all_permissions(user) + self.assertSetEqual(permissions, set()) + + def test_2(self): + user = backend.authenticate(None, 'user2', 'test') + permissions = backend.get_all_permissions(user) + self.assertSetEqual(permissions, { + 'app.can_add_model1', + 'app.can_update_model1', + 'app.can_delete_model1', + 'app.can_add_model2', + 'app.can_update_model2', + 'app.can_delete_model2', + })