├── example_project ├── __init__.py ├── posts │ ├── __init__.py │ ├── templates │ │ └── posts │ │ │ └── post_detail.html │ ├── urls.py │ ├── views.py │ ├── admin.py │ └── models.py ├── requirements.txt ├── templates │ ├── base_1col.html │ ├── 403.html │ ├── 404.html │ ├── home.html │ ├── 500.html │ └── base.html ├── context_processors.py ├── manage.py ├── urls.py └── settings.py ├── guardian ├── conf │ ├── __init__.py │ └── settings.py ├── migrations │ ├── __init__.py │ ├── 0005_auto__chg_field_groupobjectpermission_object_pk__chg_field_userobjectp.py │ ├── 0003_update_objectpermission_object_pk.py │ ├── 0002_auto__add_field_groupobjectpermission_object_pk__add_field_userobjectp.py │ ├── 0001_initial.py │ └── 0004_auto__del_field_groupobjectpermission_object_id__del_unique_groupobjec.py ├── templatetags │ ├── __init__.py │ └── guardian_tags.py ├── tests │ ├── templates │ │ ├── 404.html │ │ ├── 500.html │ │ └── dummy403.html │ ├── urls.py │ ├── __init__.py │ ├── conf_test.py │ ├── forms_test.py │ ├── custompkmodel_test.py │ ├── utils_test.py │ ├── orphans_test.py │ ├── tags_test.py │ ├── core_test.py │ └── decorators_test.py ├── management │ ├── commands │ │ ├── __init__.py │ │ └── clean_orphan_obj_perms.py │ └── __init__.py ├── __init__.py ├── exceptions.py ├── templates │ └── admin │ │ └── guardian │ │ ├── model │ │ ├── field.html │ │ ├── change_form.html │ │ ├── obj_perms_manage_user.html │ │ ├── obj_perms_manage_group.html │ │ └── obj_perms_manage.html │ │ └── contrib │ │ └── grappelli │ │ ├── field.html │ │ ├── obj_perms_manage_group.html │ │ ├── obj_perms_manage_user.html │ │ └── obj_perms_manage.html ├── testsettings.py ├── fixtures │ └── tests.json ├── backends.py ├── models.py ├── utils.py ├── managers.py ├── core.py ├── decorators.py └── forms.py ├── docs ├── license.rst ├── develop │ ├── changes.rst │ ├── index.rst │ ├── example_project.rst │ └── testing.rst ├── theme │ └── ADCtheme │ │ ├── theme.conf │ │ ├── static │ │ ├── scrn1.png │ │ ├── scrn2.png │ │ ├── documentation.png │ │ ├── header_sm_mid.png │ │ ├── triangle_left.png │ │ ├── triangle_open.png │ │ ├── title_background.png │ │ ├── triangle_closed.png │ │ ├── searchfield_repeat.png │ │ ├── breadcrumb_background.png │ │ ├── searchfield_leftcap.png │ │ ├── searchfield_rightcap.png │ │ ├── mobile.css │ │ └── toc.js │ │ ├── LICENSE │ │ └── layout.html ├── userguide │ ├── index.rst │ ├── remove.rst │ ├── caveats.rst │ ├── admin-integration.rst │ ├── assign.rst │ └── check.rst ├── api │ ├── guardian.core.rst │ ├── guardian.management.commands.rst │ ├── guardian.backends.rst │ ├── guardian.admin.rst │ ├── guardian.templatetags.guardian_tags.rst │ ├── index.rst │ ├── guardian.utils.rst │ ├── guardian.decorators.rst │ ├── guardian.managers.rst │ ├── guardian.models.rst │ ├── guardian.forms.rst │ └── guardian.shortcuts.rst ├── index.rst ├── installation.rst ├── exts.py ├── overview.rst ├── configuration.rst ├── Makefile ├── make.bat └── conf.py ├── setup.cfg ├── .gitignore ├── run_test_and_report.sh ├── AUTHORS ├── MANIFEST.in ├── LICENSE ├── setup.py ├── tests.py ├── README.rst └── CHANGES /example_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /guardian/conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example_project/posts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /guardian/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /guardian/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /guardian/tests/templates/404.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /guardian/tests/templates/500.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /guardian/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /guardian/tests/templates/dummy403.html: -------------------------------------------------------------------------------- 1 | foobar403 2 | -------------------------------------------------------------------------------- /example_project/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.2.4 2 | wsgiref==0.1.2 3 | -------------------------------------------------------------------------------- /example_project/templates/base_1col.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | License 4 | ======= 5 | 6 | .. literalinclude:: ../LICENSE 7 | 8 | -------------------------------------------------------------------------------- /docs/develop/changes.rst: -------------------------------------------------------------------------------- 1 | .. _changes: 2 | 3 | Changelog 4 | --------- 5 | 6 | .. include:: ../../CHANGES 7 | 8 | -------------------------------------------------------------------------------- /example_project/posts/templates/posts/post_detail.html: -------------------------------------------------------------------------------- 1 |

{{ object.title }}

2 |

{{ object.content }}

3 | -------------------------------------------------------------------------------- /docs/theme/ADCtheme/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = adctheme.css 4 | pygments_style = friendly 5 | 6 | -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/scrn1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/scrn1.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/scrn2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/scrn2.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/documentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/documentation.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/header_sm_mid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/header_sm_mid.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/triangle_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/triangle_left.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/triangle_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/triangle_open.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/title_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/title_background.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/triangle_closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/triangle_closed.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/searchfield_repeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/searchfield_repeat.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/breadcrumb_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/breadcrumb_background.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/searchfield_leftcap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/searchfield_leftcap.png -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/searchfield_rightcap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/django-guardian/master/docs/theme/ADCtheme/static/searchfield_rightcap.png -------------------------------------------------------------------------------- /docs/develop/index.rst: -------------------------------------------------------------------------------- 1 | .. _develop: 2 | 3 | Development 4 | =========== 5 | 6 | .. toctree:: 7 | 8 | example_project 9 | testing 10 | changes 11 | 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/build/html 8 | 9 | [bdist_rpm] 10 | requires = Django >= 1.2 11 | -------------------------------------------------------------------------------- /example_project/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.contrib.flatpages.models import FlatPage 2 | 3 | def flats(request): 4 | return { 5 | 'flats': FlatPage.objects.all().values('title', 'url'), 6 | } 7 | 8 | -------------------------------------------------------------------------------- /docs/userguide/index.rst: -------------------------------------------------------------------------------- 1 | .. _guide: 2 | 3 | User Guide 4 | ========== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | assign 10 | check 11 | remove 12 | admin-integration 13 | caveats 14 | 15 | -------------------------------------------------------------------------------- /example_project/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base_1col.html" %} 2 | 3 | {% block title %}403 - Permission denied{% endblock %} 4 | 5 | {% block content %} 6 |

403

7 |

Permission Denied

8 | {% endblock %} 9 | 10 | -------------------------------------------------------------------------------- /guardian/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.contrib import admin 3 | 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | (r'^admin/', include(admin.site.urls)), 8 | ) 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.log 3 | *.egg 4 | *.egg-info 5 | *.swp 6 | *.bak 7 | *.db 8 | *.orig 9 | build 10 | _build 11 | dist 12 | .DS_Store 13 | .coverage 14 | .hgignore 15 | 16 | example_project/media 17 | example_project/conf/*.py 18 | 19 | -------------------------------------------------------------------------------- /example_project/posts/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | 4 | urlpatterns = patterns('posts.views', 5 | url(r'^(?P[-\w]+)/$', 6 | view='view_post', 7 | name='posts_post_detail'), 8 | ) 9 | 10 | -------------------------------------------------------------------------------- /example_project/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base_1col.html" %} 2 | 3 | {% block title %}404 - Page does not exists{% endblock %} 4 | 5 | {% block content %} 6 |

404

7 |

Page Does Not Exist

8 | {% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /run_test_and_report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running test suite with coverage report at the end" 4 | echo -e "( would require coverage python package to be installed )\n" 5 | 6 | coverage run setup.py test 7 | coverage report -m guardian/*.py 8 | 9 | -------------------------------------------------------------------------------- /docs/api/guardian.core.rst: -------------------------------------------------------------------------------- 1 | .. _api-core: 2 | 3 | Core 4 | ==== 5 | 6 | .. automodule:: guardian.core 7 | 8 | 9 | ObjectPermissionChecker 10 | ----------------------- 11 | 12 | .. autoclass:: guardian.core.ObjectPermissionChecker 13 | :members: 14 | 15 | -------------------------------------------------------------------------------- /docs/api/guardian.management.commands.rst: -------------------------------------------------------------------------------- 1 | .. _api-management-commands: 2 | 3 | Management commands 4 | =================== 5 | 6 | .. command:: clean_orphan_obj_perms 7 | 8 | .. autoclass:: guardian.management.commands.clean_orphan_obj_perms.Command 9 | 10 | -------------------------------------------------------------------------------- /docs/api/guardian.backends.rst: -------------------------------------------------------------------------------- 1 | .. _api-backends: 2 | 3 | Backends 4 | ======== 5 | 6 | .. automodule:: guardian.backends 7 | 8 | 9 | ObjectPermissionBackend 10 | ----------------------- 11 | 12 | .. autoclass:: guardian.backends.ObjectPermissionBackend 13 | :members: 14 | 15 | -------------------------------------------------------------------------------- /docs/api/guardian.admin.rst: -------------------------------------------------------------------------------- 1 | .. _api-admin: 2 | 3 | Admin 4 | ===== 5 | 6 | .. automodule:: guardian.admin 7 | 8 | 9 | .. admin:: GuardedModelAdmin 10 | 11 | GuardedModelAdmin 12 | ----------------- 13 | 14 | .. autoclass:: guardian.admin.GuardedModelAdmin 15 | :members: 16 | 17 | -------------------------------------------------------------------------------- /docs/api/guardian.templatetags.guardian_tags.rst: -------------------------------------------------------------------------------- 1 | .. _api-template-tags: 2 | 3 | Template tags 4 | ============= 5 | 6 | .. automodule:: guardian.templatetags.guardian_tags 7 | 8 | 9 | get_obj_perms 10 | ------------- 11 | 12 | .. autofunction:: guardian.templatetags.guardian_tags.get_obj_perms 13 | 14 | -------------------------------------------------------------------------------- /guardian/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from admin_test import * 2 | from conf_test import * 3 | from core_test import * 4 | from custompkmodel_test import * 5 | from decorators_test import * 6 | from forms_test import * 7 | from orphans_test import * 8 | from other_test import * 9 | from utils_test import * 10 | from shortcuts_test import * 11 | from tags_test import * 12 | 13 | -------------------------------------------------------------------------------- /guardian/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of per object permissions for Django 1.2. 3 | """ 4 | VERSION = (1, 0, 4, 'dev') 5 | 6 | __version__ = '.'.join((str(each) for each in VERSION[:4])) 7 | 8 | def get_version(): 9 | """ 10 | Returns shorter version (digit parts only) as string. 11 | """ 12 | return '.'.join((str(each) for each in VERSION[:4])) 13 | 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors ordered by first contribution 2 | 3 | - Lukasz Balcerzak 4 | - Cesar Canassa 5 | - Vincent Driessen 6 | - John Hensley 7 | - Ramanan Sivaranjan 8 | - Woosuk Suh 9 | - Bojan Mihelac 10 | - Rafael Ponieman 11 | 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES 2 | include LICENSE 3 | include README.rst 4 | include MANIFEST.in 5 | include tests.py 6 | include run_test_and_report.sh 7 | recursive-include guardian *.py 8 | recursive-include guardian/fixtures *.json 9 | recursive-include guardian/templates *.html 10 | recursive-include guardian/tests/templates *.html 11 | recursive-include docs * 12 | prune example_project 13 | prune docs/build 14 | -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/mobile.css: -------------------------------------------------------------------------------- 1 | /* 2 | * CSS adjustments (overrides) for mobile browsers that cannot handle 3 | * fix-positioned div's very well. 4 | * This makes long pages scrollable on mobile browsers. 5 | */ 6 | 7 | #breadcrumbs { 8 | display: none !important; 9 | } 10 | 11 | .document { 12 | bottom: inherit !important; 13 | } 14 | 15 | #sphinxsidebar { 16 | bottom: inherit !important; 17 | } 18 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API Reference 4 | ============= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | guardian.admin 10 | guardian.backends 11 | guardian.core 12 | guardian.decorators 13 | guardian.forms 14 | guardian.management.commands 15 | guardian.managers 16 | guardian.models 17 | guardian.shortcuts 18 | guardian.utils 19 | 20 | guardian.templatetags.guardian_tags 21 | 22 | -------------------------------------------------------------------------------- /example_project/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base_1col.html" %} 2 | 3 | {% block col-single-title %}Home{% endblock %} 4 | 5 | {% block col-single-content %} 6 |
7 |

Pages

8 | 13 |
14 | {% endblock %} 15 | 16 | -------------------------------------------------------------------------------- /example_project/posts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render_to_response, get_object_or_404 2 | from guardian.decorators import permission_required_or_403 3 | 4 | from posts.models import Post 5 | 6 | 7 | @permission_required_or_403('posts.view_post', (Post, 'slug', 'slug')) 8 | def view_post(request, slug, **kwargs): 9 | post = get_object_or_404(Post, slug=slug) 10 | return render_to_response('posts/post_detail.html', {'object': post}) 11 | 12 | -------------------------------------------------------------------------------- /docs/api/guardian.utils.rst: -------------------------------------------------------------------------------- 1 | .. _api-utils: 2 | 3 | .. currentmodule:: guardian.utils 4 | 5 | Utilities 6 | ========= 7 | 8 | .. automodule:: guardian.utils 9 | 10 | 11 | get_anonymous_user 12 | ------------------ 13 | 14 | .. autofunction:: get_anonymous_user 15 | 16 | get_identity 17 | ------------ 18 | 19 | .. autofunction:: get_identity 20 | 21 | clean_orphan_obj_perms 22 | ---------------------- 23 | 24 | .. autofunction:: clean_orphan_obj_perms 25 | 26 | -------------------------------------------------------------------------------- /guardian/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exceptions used by django-guardian. All internal and guardian-specific errors 3 | should extend GuardianError class. 4 | """ 5 | 6 | class GuardianError(Exception): 7 | pass 8 | 9 | class NotUserNorGroup(GuardianError): 10 | pass 11 | 12 | class ObjectNotPersisted(GuardianError): 13 | pass 14 | 15 | class WrongAppError(GuardianError): 16 | pass 17 | 18 | class MixedContentTypeError(GuardianError): 19 | pass 20 | 21 | -------------------------------------------------------------------------------- /guardian/templates/admin/guardian/model/field.html: -------------------------------------------------------------------------------- 1 |
2 | {% if field.errors %} 3 |
    4 | {% for error in field.errors %} 5 |
  • {{ error }}
  • 6 | {% endfor %} 7 |
8 | {% endif %} 9 |
10 | {{ field.label_tag }} 11 | {{ field }} 12 | {% if field.help_text %}

{{ field.help_text }}

{% endif %} 13 |
14 |
15 | -------------------------------------------------------------------------------- /example_project/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from example_project.posts.models import Post 4 | 5 | from guardian.admin import GuardedModelAdmin 6 | 7 | 8 | class PostAdmin(GuardedModelAdmin): 9 | prepopulated_fields = {"slug": ("title",)} 10 | list_display = ('title', 'slug', 'created_at') 11 | search_fields = ('title', 'content') 12 | ordering = ('-created_at',) 13 | date_hierarchy = 'created_at' 14 | 15 | admin.site.register(Post, PostAdmin) 16 | 17 | -------------------------------------------------------------------------------- /docs/api/guardian.decorators.rst: -------------------------------------------------------------------------------- 1 | .. _api-decorators: 2 | 3 | Decorators 4 | ========== 5 | 6 | .. automodule:: guardian.decorators 7 | 8 | 9 | .. _api-decorators-permission_required: 10 | 11 | permission_required 12 | ------------------- 13 | 14 | .. autofunction:: guardian.decorators.permission_required 15 | 16 | 17 | .. _api-decorators-permission_required_or_403: 18 | 19 | permission_required_or_403 20 | -------------------------- 21 | 22 | .. autofunction:: guardian.decorators.permission_required_or_403 23 | 24 | -------------------------------------------------------------------------------- /example_project/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page unavailable 5 | 6 | 7 | 8 |

Page unavailable

9 | 10 |

Sorry, but the requested page is unavailable due to a 11 | server hiccup.

12 | 13 |

Our engineers have been notified, so check back later.

14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/api/guardian.managers.rst: -------------------------------------------------------------------------------- 1 | .. _api-managers: 2 | 3 | Managers 4 | ======== 5 | 6 | .. automodule:: guardian.managers 7 | 8 | 9 | .. manager:: UserObjectPermissionManager 10 | 11 | UserObjectPermissionManager 12 | --------------------------- 13 | 14 | .. autoclass:: guardian.managers.UserObjectPermissionManager 15 | :members: 16 | 17 | 18 | .. manager:: GroupObjectPermissionManager 19 | 20 | GroupObjectPermissionManager 21 | ---------------------------- 22 | 23 | .. autoclass:: guardian.managers.GroupObjectPermissionManager 24 | :members: 25 | -------------------------------------------------------------------------------- /guardian/tests/conf_test.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from django.core.exceptions import ImproperlyConfigured 3 | from django.test import TestCase 4 | from guardian.conf import settings as guardian_settings 5 | 6 | 7 | class TestConfiguration(TestCase): 8 | 9 | def test_check_configuration(self): 10 | 11 | with mock.patch('guardian.conf.settings.RENDER_403', True): 12 | with mock.patch('guardian.conf.settings.RAISE_403', True): 13 | self.assertRaises(ImproperlyConfigured, 14 | guardian_settings.check_configuration) 15 | 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | 3 | ===================================================== 4 | django-guardian - per object permissions for Django 5 | ===================================================== 6 | 7 | :Date: |today| 8 | 9 | **Documentation**: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | overview 15 | installation 16 | configuration 17 | userguide/index 18 | api/index 19 | develop/index 20 | license 21 | 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | 30 | -------------------------------------------------------------------------------- /docs/theme/ADCtheme/static/toc.js: -------------------------------------------------------------------------------- 1 | var TOC = { 2 | load: function () { 3 | $('#toc_button').click(TOC.toggle); 4 | }, 5 | 6 | toggle: function () { 7 | if ($('#sphinxsidebar').toggle().is(':hidden')) { 8 | $('div.document').css('left', "0px"); 9 | $('toc_button').removeClass("open"); 10 | } else { 11 | $('div.document').css('left', "230px"); 12 | $('#toc_button').addClass("open"); 13 | } 14 | return $('#sphinxsidebar'); 15 | } 16 | }; 17 | 18 | $(document).ready(function () { 19 | TOC.load(); 20 | }); -------------------------------------------------------------------------------- /guardian/templates/admin/guardian/model/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block object-tools %} 5 | {% if change %}{% if not is_popup %} 6 | 11 | {% endif %}{% endif %} 12 | {% endblock %} 13 | 14 | -------------------------------------------------------------------------------- /guardian/templates/admin/guardian/contrib/grappelli/field.html: -------------------------------------------------------------------------------- 1 |
2 | {% if field.errors %} 3 |
    4 | {% for error in field.errors %} 5 |
  • {{ error }}
  • 6 | {% endfor %} 7 |
8 | {% endif %} 9 |
10 | 11 |
12 |
13 | {{ field }} 14 | {% if field.help_text %}

{{ field.help_text }}

{% endif %} 15 |
16 |
17 | 18 | -------------------------------------------------------------------------------- /example_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | def main(): 11 | execute_manager(settings) 12 | 13 | if __name__ == "__main__": 14 | main() 15 | 16 | -------------------------------------------------------------------------------- /docs/api/guardian.models.rst: -------------------------------------------------------------------------------- 1 | .. _api-models: 2 | 3 | Models 4 | ====== 5 | 6 | .. automodule:: guardian.models 7 | 8 | 9 | .. model:: BaseObjectPermission 10 | 11 | BaseObjectPermission 12 | -------------------- 13 | 14 | .. autoclass:: guardian.models.BaseObjectPermission 15 | :members: 16 | 17 | 18 | .. model:: UserObjectPermission 19 | 20 | UserObjectPermission 21 | -------------------- 22 | 23 | .. autoclass:: guardian.models.UserObjectPermission 24 | :members: 25 | 26 | 27 | .. model:: GroupObjectPermission 28 | 29 | GroupObjectPermission 30 | --------------------- 31 | 32 | .. autoclass:: guardian.models.GroupObjectPermission 33 | :members: 34 | 35 | -------------------------------------------------------------------------------- /example_project/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Post(models.Model): 5 | title = models.CharField('title', max_length=64) 6 | slug = models.SlugField(max_length=64) 7 | content = models.TextField('content') 8 | created_at = models.DateTimeField(auto_now_add=True, db_index=True) 9 | 10 | class Meta: 11 | permissions = ( 12 | ('view_post', 'Can view post'), 13 | ) 14 | get_latest_by = 'created_at' 15 | 16 | def __unicode__(self): 17 | return self.title 18 | 19 | @models.permalink 20 | def get_absolute_url(self): 21 | return ('posts_post_detail', (), {'slug': self.slug}) 22 | 23 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | This application requires Django_ 1.2 or higher and it is only prerequisite 7 | before ``django-guardian`` may be used. 8 | 9 | In order to install ``django-guardian`` simply use ``pip``:: 10 | 11 | pip install django-guardian 12 | 13 | or ``easy_install``:: 14 | 15 | easy_install django-guardian 16 | 17 | This would be enough to run ``django-guardian``. However, in order to run tests 18 | or boundled example application, there are some other requirements. See more 19 | details about the topics: 20 | 21 | - :ref:`Testing ` 22 | - :ref:`Example project ` 23 | 24 | .. _django: http://www.djangoproject.com/ 25 | 26 | -------------------------------------------------------------------------------- /guardian/management/__init__.py: -------------------------------------------------------------------------------- 1 | from django.db.models import signals 2 | from django.contrib.auth.models import User 3 | from guardian import models as guardian_app 4 | from guardian.conf import settings as guardian_settings 5 | 6 | def create_anonymous_user(sender, **kwargs): 7 | """ 8 | Creates anonymous User instance with id from settings. 9 | """ 10 | try: 11 | User.objects.get(pk=guardian_settings.ANONYMOUS_USER_ID) 12 | except User.DoesNotExist: 13 | User.objects.create(pk=guardian_settings.ANONYMOUS_USER_ID, 14 | username='AnonymousUser') 15 | 16 | signals.post_syncdb.connect(create_anonymous_user, sender=guardian_app, 17 | dispatch_uid="guardian.management.create_anonymous_user") 18 | 19 | -------------------------------------------------------------------------------- /docs/develop/example_project.rst: -------------------------------------------------------------------------------- 1 | .. _example-project: 2 | 3 | Example project 4 | =============== 5 | 6 | Example project should be boundled with archive and be available at 7 | ``example_project``. Before you can run it, some requirements have to be met. 8 | Those are easily installed using following command at example project's 9 | directory:: 10 | 11 | $ pip install -r requirements.txt 12 | 13 | And last thing before we can run example project is to create sqlite database:: 14 | 15 | $ python manage.py syncdb 16 | 17 | Finally we can run dev server:: 18 | 19 | $ python manage.py runserver 20 | 21 | Project is really basic and shows almost nothing but eventually it should 22 | expose some ``django-guardian`` functionality. 23 | 24 | -------------------------------------------------------------------------------- /docs/api/guardian.forms.rst: -------------------------------------------------------------------------------- 1 | .. _api-forms: 2 | 3 | Forms 4 | ===== 5 | 6 | .. automodule:: guardian.forms 7 | 8 | 9 | .. form:: UserObjectPermissionsForm 10 | 11 | UserObjectPermissionsForm 12 | ------------------------- 13 | 14 | .. autoclass:: guardian.forms.UserObjectPermissionsForm 15 | :members: 16 | :show-inheritance: 17 | 18 | 19 | .. form:: GroupObjectPermissionsForm 20 | 21 | GroupObjectPermissionsForm 22 | -------------------------- 23 | 24 | .. autoclass:: guardian.forms.GroupObjectPermissionsForm 25 | :members: 26 | :show-inheritance: 27 | 28 | 29 | .. form:: BaseObjectPermissionsForm 30 | 31 | BaseObjectPermissionsForm 32 | ------------------------- 33 | 34 | .. autoclass:: guardian.forms.BaseObjectPermissionsForm 35 | :members: 36 | 37 | -------------------------------------------------------------------------------- /docs/userguide/remove.rst: -------------------------------------------------------------------------------- 1 | .. _remove: 2 | 3 | Remove object permissions 4 | ========================= 5 | 6 | Removing object permissions is as easy as assigning them. Just instead of 7 | :func:`guardian.shortcuts.assign` we would use 8 | :func:`guardian.shortcuts.remove_perm` function (it accepts same arguments). 9 | 10 | Example 11 | ------- 12 | 13 | Let's get back to our fellow Joe:: 14 | 15 | >>> site = Site.object.get_current() 16 | >>> joe.has_perm('change_site', site) 17 | True 18 | 19 | Now, simply remove this permission:: 20 | 21 | >>> from guardian.shortcuts import remove_perm 22 | >>> remove_perm('change_site', joe, site) 23 | >>> joe = User.objects.get(username='joe') 24 | >>> joe.has_perm('change_site', site) 25 | False 26 | 27 | -------------------------------------------------------------------------------- /guardian/management/commands/clean_orphan_obj_perms.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import NoArgsCommand 2 | 3 | from guardian.utils import clean_orphan_obj_perms 4 | 5 | 6 | class Command(NoArgsCommand): 7 | """ 8 | clean_orphan_obj_perms command is a tiny wrapper around 9 | :func:`guardian.utils.clean_orphan_obj_perms`. 10 | 11 | Usage:: 12 | 13 | $ python manage.py clean_orphan_obj_perms 14 | Removed 11 object permission entries with no targets 15 | 16 | """ 17 | help = "Removes object permissions with not existing targets" 18 | 19 | def handle_noargs(self, **options): 20 | removed = clean_orphan_obj_perms() 21 | if options['verbosity'] > 0: 22 | print "Removed %d object permission entries with no targets" %\ 23 | removed 24 | 25 | -------------------------------------------------------------------------------- /guardian/conf/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | ANONYMOUS_USER_ID = getattr(settings, 'ANONYMOUS_USER_ID', None) 5 | if ANONYMOUS_USER_ID is None: 6 | raise ImproperlyConfigured("In order to use django-guardian's " 7 | "ObjectPermissionBackend authorization backend you have to configure " 8 | "ANONYMOUS_USER_ID at your settings module") 9 | 10 | RENDER_403 = getattr(settings, 'GUARDIAN_RENDER_403', False) 11 | TEMPLATE_403 = getattr(settings, 'GUARDIAN_TEMPLATE_403', '403.html') 12 | RAISE_403 = getattr(settings, 'GUARDIAN_RAISE_403', False) 13 | 14 | def check_configuration(): 15 | if RENDER_403 and RAISE_403: 16 | raise ImproperlyConfigured("Cannot use both GUARDIAN_RENDER_403 AND " 17 | "GUARDIAN_RAISE_403 - only one of this config may be True") 18 | 19 | check_configuration() 20 | 21 | -------------------------------------------------------------------------------- /guardian/testsettings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DEBUG = False 4 | 5 | ANONYMOUS_USER_ID = -1 6 | 7 | INSTALLED_APPS = ( 8 | 'django.contrib.auth', 9 | 'django.contrib.contenttypes', 10 | 'django.contrib.sessions', 11 | 'django.contrib.sites', 12 | 'django.contrib.admin', 13 | 'django.contrib.messages', 14 | 'guardian', 15 | ) 16 | 17 | AUTHENTICATION_BACKENDS = ( 18 | 'django.contrib.auth.backends.ModelBackend', 19 | 'guardian.backends.ObjectPermissionBackend', 20 | ) 21 | 22 | TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner' 23 | 24 | DATABASES = { 25 | 'default': { 26 | 'ENGINE': 'django.db.backends.sqlite3', 27 | 'NAME': ':memory:', 28 | 'TEST_NAME': ':memory:', 29 | }, 30 | } 31 | 32 | ROOT_URLCONF = 'guardian.tests.urls' 33 | SITE_ID = 1 34 | 35 | TEMPLATE_DIRS = ( 36 | os.path.join(os.path.dirname(__file__), 'tests', 'templates'), 37 | ) 38 | print TEMPLATE_DIRS 39 | 40 | -------------------------------------------------------------------------------- /example_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.conf import settings 3 | from django.contrib import admin 4 | 5 | from django.contrib.flatpages.models import FlatPage 6 | 7 | admin.autodiscover() 8 | 9 | urlpatterns = patterns('', 10 | url(r'^$', 'django.views.generic.list_detail.object_list', 11 | kwargs={'queryset': FlatPage.objects.all(), 12 | 'template_name': 'home.html', 13 | 'template_object_name': 'flatpage'}, 14 | name='home'), 15 | (r'^admin/', include(admin.site.urls)), 16 | (r'^posts/', include('example_project.posts.urls')), 17 | ) 18 | 19 | if settings.DEBUG: 20 | urlpatterns += patterns('', 21 | (r'^%s/(?P.*)$' % settings.STATIC_URL.strip('/'), 22 | 'django.views.static.serve', 23 | {'document_root': settings.STATIC_ROOT}), 24 | ) 25 | 26 | if 'grappelli' in settings.INSTALLED_APPS: 27 | urlpatterns += patterns('', 28 | (r'^grappelli/', include('grappelli.urls')), 29 | ) 30 | 31 | -------------------------------------------------------------------------------- /guardian/tests/forms_test.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.test import TestCase 4 | from guardian.forms import BaseObjectPermissionsForm 5 | 6 | 7 | class BaseObjectPermissionsFormTests(TestCase): 8 | 9 | def setUp(self): 10 | self.user = User.objects.create_user('joe', 'joe@example.com', 'joe') 11 | self.obj = ContentType.objects.create(name='foo', model='bar', 12 | app_label='fake-for-guardian-tests') 13 | 14 | def test_not_implemented(self): 15 | 16 | class MyUserObjectPermissionsForm(BaseObjectPermissionsForm): 17 | 18 | def __init__(formself, user, *args, **kwargs): 19 | self.user = user 20 | super(MyUserObjectPermissionsForm, formself).__init__(*args, 21 | **kwargs) 22 | 23 | form = MyUserObjectPermissionsForm(self.user, self.obj, {}) 24 | self.assertRaises(NotImplementedError, form.save_obj_perms) 25 | 26 | field_name = form.get_obj_perms_field_name() 27 | self.assertTrue(form.is_valid()) 28 | self.assertEqual(len(form.cleaned_data[field_name]), 0) 29 | 30 | -------------------------------------------------------------------------------- /docs/exts.py: -------------------------------------------------------------------------------- 1 | 2 | def setup(app): 3 | app.add_crossref_type( 4 | directivename = "admin", 5 | rolename = "admin", 6 | indextemplate = "pair: %s; admin", 7 | ) 8 | app.add_crossref_type( 9 | directivename = "command", 10 | rolename = "command", 11 | indextemplate = "pair: %s; command", 12 | ) 13 | app.add_crossref_type( 14 | directivename = "form", 15 | rolename = "form", 16 | indextemplate = "pair: %s; form", 17 | ) 18 | app.add_crossref_type( 19 | directivename = "manager", 20 | rolename = "manager", 21 | indextemplate = "pair: %s; manager", 22 | ) 23 | app.add_crossref_type( 24 | directivename = "model", 25 | rolename = "model", 26 | indextemplate = "pair: %s; model", 27 | ) 28 | app.add_crossref_type( 29 | directivename = "setting", 30 | rolename = "setting", 31 | indextemplate = "pair: %s; setting", 32 | ) 33 | app.add_crossref_type( 34 | directivename = "shortcut", 35 | rolename = "shortcut", 36 | indextemplate = "pair: %s; shortcut", 37 | ) 38 | 39 | -------------------------------------------------------------------------------- /guardian/tests/custompkmodel_test.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.test import TestCase 4 | from guardian.shortcuts import assign, remove_perm 5 | 6 | class CustomPKModelTest(TestCase): 7 | """ 8 | Tests agains custom model with primary key other than *standard* 9 | ``id`` integer field. 10 | """ 11 | 12 | def setUp(self): 13 | self.user = User.objects.create(username='joe') 14 | self.ctype = ContentType.objects.create(name='foo', model='bar', 15 | app_label='fake-for-guardian-tests') 16 | 17 | def test_assign(self): 18 | assign('contenttypes.change_contenttype', self.user, self.ctype) 19 | self.assertTrue(self.user.has_perm('contenttypes.change_contenttype', 20 | self.ctype)) 21 | 22 | def test_remove_perm(self): 23 | assign('contenttypes.change_contenttype', self.user, self.ctype) 24 | self.assertTrue(self.user.has_perm('contenttypes.change_contenttype', 25 | self.ctype)) 26 | remove_perm('contenttypes.change_contenttype', self.user, self.ctype) 27 | self.assertFalse(self.user.has_perm('contenttypes.change_contenttype', 28 | self.ctype)) 29 | 30 | -------------------------------------------------------------------------------- /guardian/tests/utils_test.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User, Group, AnonymousUser 3 | 4 | from guardian.tests.core_test import ObjectPermissionTestCase 5 | from guardian.utils import get_anonymous_user, get_identity 6 | from guardian.exceptions import NotUserNorGroup 7 | 8 | class GetAnonymousUserTest(TestCase): 9 | 10 | def test(self): 11 | anon = get_anonymous_user() 12 | self.assertTrue(isinstance(anon, User)) 13 | 14 | class GetIdentityTest(ObjectPermissionTestCase): 15 | 16 | def test_user(self): 17 | user, group = get_identity(self.user) 18 | self.assertTrue(isinstance(user, User)) 19 | self.assertEqual(group, None) 20 | 21 | def test_anonymous_user(self): 22 | anon = AnonymousUser() 23 | user, group = get_identity(anon) 24 | self.assertTrue(isinstance(user, User)) 25 | self.assertEqual(group, None) 26 | 27 | def test_group(self): 28 | user, group = get_identity(self.group) 29 | self.assertTrue(isinstance(group, Group)) 30 | self.assertEqual(user, None) 31 | 32 | def test_not_user_nor_group(self): 33 | self.assertRaises(NotUserNorGroup, get_identity, 1) 34 | self.assertRaises(NotUserNorGroup, get_identity, "User") 35 | self.assertRaises(NotUserNorGroup, get_identity, User) 36 | 37 | -------------------------------------------------------------------------------- /docs/api/guardian.shortcuts.rst: -------------------------------------------------------------------------------- 1 | .. _api-shortcuts: 2 | 3 | Shortcuts 4 | ========= 5 | 6 | .. automodule:: guardian.shortcuts 7 | 8 | 9 | .. _api-shortcuts-assign: 10 | 11 | assign 12 | ------ 13 | 14 | .. autofunction:: guardian.shortcuts.assign 15 | 16 | .. _api-shortcuts-remove_perm: 17 | 18 | remove_perm 19 | ----------- 20 | 21 | .. autofunction:: guardian.shortcuts.remove_perm 22 | 23 | .. _api-shortcuts-get_perms: 24 | 25 | get_perms 26 | --------- 27 | 28 | .. autofunction:: guardian.shortcuts.get_perms 29 | 30 | 31 | .. _api-shortcuts-get_perms_for_model: 32 | 33 | get_perms_for_model 34 | ------------------- 35 | 36 | .. autofunction:: guardian.shortcuts.get_perms_for_model 37 | 38 | 39 | .. _api-shortcuts-get_users_with_perms: 40 | 41 | get_users_with_perms 42 | -------------------- 43 | 44 | .. autofunction:: guardian.shortcuts.get_users_with_perms 45 | 46 | 47 | .. _api-shortcuts-get_groups_with_perms: 48 | 49 | get_groups_with_perms 50 | --------------------- 51 | 52 | .. autofunction:: guardian.shortcuts.get_groups_with_perms 53 | 54 | 55 | .. _api-shortcuts-get_objects_for_user: 56 | 57 | .. shortcut:: get_objects_for_user 58 | 59 | get_objects_for_user 60 | -------------------- 61 | 62 | .. autofunction:: guardian.shortcuts.get_objects_for_user 63 | 64 | get_objects_for_group 65 | --------------------- 66 | 67 | .. autofunction:: guardian.shortcuts.get_objects_for_group 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2011 Lukasz Balcerzak 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | .. _overview: 2 | 3 | Overview 4 | ======== 5 | 6 | ``django-guardian`` is an implementation of object permissions for Django_ 7 | providing extra *authentication backend*. 8 | 9 | Features 10 | -------- 11 | 12 | - Object permissions for Django_ 13 | - AnonymousUser support 14 | - High level API 15 | - Heavely tested 16 | - Django's admin integration 17 | - Decorators 18 | 19 | Incoming 20 | -------- 21 | 22 | - Admin templates for grappelli_ 23 | 24 | Source and issue tracker 25 | ------------------------ 26 | 27 | Sources are available at `issue-tracker`_. You may also file a bug there. 28 | 29 | Alternate projects 30 | ------------------ 31 | 32 | Django_ 1.2 still has *only* foundation for object permissions [1]_ and 33 | ``django-guardian`` make use of new facilities and it is based on them. There 34 | are some other pluggable applications which does *NOT* require latest 1.2 35 | version of Django_. For instance, there are great `django-authority`_ or 36 | `django-permissions`_ available out there. 37 | 38 | .. _django: http://www.djangoproject.com/ 39 | .. _django-authority: http://bitbucket.org/jezdez/django-authority/ 40 | .. _django-permissions: http://bitbucket.org/diefenbach/django-permissions/ 41 | .. _issue-tracker: http://github.com/lukaszb/django-guardian 42 | .. _grappelli: http://code.google.com/p/django-grappelli/ 43 | 44 | .. [1] See http://docs.djangoproject.com/en/1.2/topics/auth/#handling-object-permissions 45 | for more detail. 46 | 47 | -------------------------------------------------------------------------------- /guardian/fixtures/tests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": -1, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": false, 11 | "is_staff": false, 12 | "last_login": "2010-05-28 04:02:27", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "", 16 | "email": "", 17 | "date_joined": "2010-05-28 04:02:27" 18 | } 19 | }, 20 | { 21 | "pk": 1, 22 | "model": "auth.user", 23 | "fields": { 24 | "username": "jack", 25 | "first_name": "", 26 | "last_name": "", 27 | "is_active": true, 28 | "is_superuser": false, 29 | "is_staff": false, 30 | "last_login": "2010-05-28 04:02:34", 31 | "groups": [2], 32 | "user_permissions": [], 33 | "password": "", 34 | "email": "", 35 | "date_joined": "2010-05-28 04:02:34" 36 | } 37 | }, 38 | { 39 | "pk": 1, 40 | "model": "auth.group", 41 | "fields": { 42 | "name": "admins", 43 | "permissions": [] 44 | } 45 | }, 46 | { 47 | "pk": 2, 48 | "model": "auth.group", 49 | "fields": { 50 | "name": "jackGroup", 51 | "permissions": [] 52 | } 53 | } 54 | ] 55 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, find_packages 4 | 5 | guardian = __import__('guardian') 6 | readme_file = os.path.join(os.path.dirname(__file__), 'README.rst') 7 | try: 8 | long_description = open(readme_file).read() 9 | except IOError, err: 10 | sys.stderr.write("[ERROR] Cannot find file specified as " 11 | "``long_description`` (%s)\n" % readme_file) 12 | sys.exit(1) 13 | 14 | setup( 15 | name = 'django-guardian', 16 | version = guardian.get_version(), 17 | url = 'http://github.com/lukaszb/django-guardian', 18 | author = 'Lukasz Balcerzak', 19 | author_email = 'lukaszbalcerzak@gmail.com', 20 | download_url='http://github.com/lukaszb/django-guardian/downloads', 21 | description = guardian.__doc__, 22 | long_description = long_description, 23 | zip_safe = False, 24 | packages = find_packages(), 25 | include_package_data = True, 26 | scripts = [], 27 | requires = [], 28 | license = 'BSD', 29 | install_requires = [ 30 | 'Django>=1.2', 31 | ], 32 | classifiers = ['Development Status :: 5 - Production/Stable', 33 | 'Environment :: Web Environment', 34 | 'Framework :: Django', 35 | 'Intended Audience :: Developers', 36 | 'License :: OSI Approved :: BSD License', 37 | 'Operating System :: OS Independent', 38 | 'Programming Language :: Python', 39 | 'Topic :: Security', 40 | ], 41 | test_suite='tests.main', 42 | ) 43 | 44 | -------------------------------------------------------------------------------- /docs/theme/ADCtheme/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Corey Oordt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of Corey Oordt nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /guardian/templates/admin/guardian/model/obj_perms_manage_user.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block extrahead %}{{ block.super }} 5 | {{ form.media }} 6 | {% endblock %} 7 | 8 | {% block breadcrumbs %}{% if not is_popup %} 9 | 17 | {% endif %}{% endblock %} 18 | 19 | 20 | {% block content %} 21 |
22 |
23 | {% csrf_token %} 24 |
25 |

{% trans "Object permissions" %}

26 |
27 | {{ object }} 28 |
29 |
30 | {{ user_obj }} 31 |
32 |
33 | {{ form }} 34 |
35 |
36 |
37 | 38 |
39 |
40 |
41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /guardian/templates/admin/guardian/model/obj_perms_manage_group.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block extrahead %}{{ block.super }} 5 | {{ form.media }} 6 | {% endblock %} 7 | 8 | {% block breadcrumbs %}{% if not is_popup %} 9 | 17 | {% endif %}{% endblock %} 18 | 19 | 20 | {% block content %} 21 |
22 |
23 | {% csrf_token %} 24 |
25 |

{% trans "Object permissions" %}

26 |
27 | {{ object }} 28 |
29 |
30 | {{ group_obj }} 31 |
32 | {% for field in form %} 33 | {% include "admin/guardian/model/field.html" %} 34 | {% endfor %} 35 |
36 |
37 | 38 |
39 |
40 |
41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests runner for ``django-guardian`` based on boundled example project. 3 | Tests are independent from this example application but setuptools need 4 | instructions how to interpret ``test`` command when we run:: 5 | 6 | python setup.py test 7 | 8 | """ 9 | import os 10 | import sys 11 | 12 | os.environ["DJANGO_SETTINGS_MODULE"] = 'guardian.testsettings' 13 | from guardian import testsettings as settings 14 | 15 | settings.DJALOG_LEVEL = 40 16 | settings.INSTALLED_APPS = ( 17 | 'django.contrib.auth', 18 | 'django.contrib.sessions', 19 | 'django.contrib.contenttypes', 20 | 'django.contrib.admin', 21 | 'django.contrib.sites', 22 | 'guardian', 23 | ) 24 | 25 | def run_tests(settings): 26 | from django.test.utils import get_runner 27 | from django.utils.termcolors import colorize 28 | db_conf = settings.DATABASES['default'] 29 | output = [] 30 | msg = "Starting tests for db backend: %s" % db_conf['ENGINE'] 31 | embracer = '=' * len(msg) 32 | output.append(msg) 33 | for key, value in db_conf.iteritems(): 34 | if key == 'PASSWORD': 35 | value = '****************' 36 | line = ' %s: "%s"' % (key, value) 37 | output.append(line) 38 | embracer = colorize('=' * len(max(output, key=lambda s: len(s))), 39 | fg='green', opts=['bold']) 40 | output = [colorize(line, fg='blue') for line in output] 41 | output.insert(0, embracer) 42 | output.append(embracer) 43 | print '\n'.join(output) 44 | 45 | TestRunner = get_runner(settings) 46 | test_runner = TestRunner(interactive=False) 47 | failures = test_runner.run_tests(['auth', 'guardian']) 48 | return failures 49 | 50 | def main(): 51 | failures = run_tests(settings) 52 | sys.exit(failures) 53 | 54 | if __name__ == '__main__': 55 | main() 56 | 57 | -------------------------------------------------------------------------------- /example_project/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | {% block title %}django-guardian - example project{% endblock %} 6 | 7 | {% block head-css %} 8 | 9 | {% endblock %} 10 | {% block head-js %}{% endblock %} 11 | {% block extra-head %}{% endblock %} 12 | {% endblock %} 13 | 14 | 15 | 16 | 17 | {% block body %} 18 |
19 | {# Message Box #} 20 | {# For new message framework, available since Django 1.2 #} 21 | {% block messages-block %} 22 | {% if messages %} 23 |
    24 | {% for message in messages %} 25 |
  • {{ message }}
  • 26 | {% endfor %} 27 |
28 | {% endif %} 29 | {% endblock messages-block %} 30 | 31 | {% block content %} 32 | {% endblock content %} 33 | 34 |
35 | {# Footer #} 36 | 45 | {% endblock body %} 46 | 47 | {% block extra-body %}{% endblock %} 48 | {% block end-body %}{% endblock %} 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/userguide/caveats.rst: -------------------------------------------------------------------------------- 1 | .. _caveats: 2 | 3 | Caveats 4 | ======= 5 | 6 | Orphaned object permissions 7 | --------------------------- 8 | 9 | Permissions, including so called *per object permissions*, are sometimes tricky 10 | to manage. One case is how we can manage permissions that are no longer used. 11 | Normally, there should be no problems, however with some particular setup it is 12 | possible to reuse primary keys of database models which were used in the past 13 | once. We will not answer how bad such situation can be - instead we will try to 14 | cover how we can deal with this. 15 | 16 | Let's imagine our table has primary key to the filesystem path. We have a record 17 | with pk equal to ``/home/www/joe.config``. User *jane* has read access to 18 | joe's configuration and we store that information in database by creating 19 | guardian's object permissions. Now, *joe* user removes account from our site and 20 | another user creates account with *joe* as username. The problem is that if we 21 | haven't removed object permissions explicitly in the process of first *joe* 22 | account removal, *jane* still has read permissions for *joe's* configuration 23 | file - but this is another user. 24 | 25 | There is no easy way to deal with orphaned permissions as they are not foreign 26 | keyed with objects directly. Even if they would, there are some database engines 27 | - or *ON DELETE* rules - which restricts removal of related objects. 28 | 29 | .. important:: 30 | 31 | It is **extremely** important to remove :model:`UserObjectPermission` and 32 | :model:`GroupObjectPermission` as we delete objects for which permissions 33 | are defined. 34 | 35 | Guardian comes with utility function which tries to help to remove orphaned 36 | object permissions. Remember - those are only helpers. Applications should 37 | remove those object permissions explicitly. 38 | 39 | .. seealso:: 40 | 41 | - :func:`guardian.utils.clean_orphan_obj_perms` 42 | - :command:`clean_orphan_obj_perms` 43 | 44 | -------------------------------------------------------------------------------- /guardian/templates/admin/guardian/contrib/grappelli/obj_perms_manage_group.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block extrahead %}{{ block.super }} 5 | {{ form.media }} 6 | {% endblock %} 7 | 8 | {% block breadcrumbs %}{% if not is_popup %} 9 | 17 | {% endif %}{% endblock %} 18 | 19 | 20 | {% block content %} 21 |
22 |
23 | {% csrf_token %} 24 |
25 |

{% trans "Object permissions" %}

26 |
27 |
28 | 29 |
30 |
31 |

{{ object }}

32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |

{{ group_obj }}

40 |
41 |
42 | {% for field in form %} 43 | {% include "admin/guardian/contrib/grappelli/field.html" %} 44 | {% endfor %} 45 |
46 | 51 |
52 |
53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /guardian/templates/admin/guardian/contrib/grappelli/obj_perms_manage_user.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block extrahead %}{{ block.super }} 5 | {{ form.media }} 6 | {% endblock %} 7 | 8 | {% block breadcrumbs %}{% if not is_popup %} 9 | 17 | {% endif %}{% endblock %} 18 | 19 | 20 | {% block content %} 21 |
22 |
23 | {% csrf_token %} 24 |
25 |

{% trans "Object permissions" %}

26 |
27 |
28 | 29 |
30 |
31 |

{{ object }}

32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |

{{ user_obj }}

40 |
41 |
42 | {% for field in form %} 43 | {% include "admin/guardian/contrib/grappelli/field.html" %} 44 | {% endfor %} 45 |
46 | 51 |
52 |
53 | {% endblock %} 54 | 55 | -------------------------------------------------------------------------------- /guardian/backends.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | 4 | from guardian.conf import settings 5 | from guardian.exceptions import WrongAppError 6 | from guardian.core import ObjectPermissionChecker 7 | 8 | class ObjectPermissionBackend(object): 9 | supports_object_permissions = True 10 | supports_anonymous_user = True 11 | supports_inactive_user = True 12 | 13 | def authenticate(self, username, password): 14 | return None 15 | 16 | def has_perm(self, user_obj, perm, obj=None): 17 | """ 18 | Returns ``True`` if given ``user_obj`` has ``perm`` for ``obj``. If no 19 | ``obj`` is given, ``False`` is returned. 20 | 21 | .. note:: 22 | 23 | Remember, that if user is not *active*, all checks would return 24 | ``False``. 25 | 26 | Main difference between Django's ``ModelBackend`` is that we can pass 27 | ``obj`` instance here and ``perm`` doesn't have to contain 28 | ``app_label`` as it can be retrieved from given ``obj``. 29 | 30 | **Inactive user support** 31 | 32 | If user is authenticated but inactive at the same time, all checks 33 | always returns ``False``. 34 | """ 35 | # Backend checks only object permissions 36 | if obj is None: 37 | return False 38 | 39 | # Backend checks only permissions for Django models 40 | if not isinstance(obj, models.Model): 41 | return False 42 | 43 | # This is how we support anonymous users - simply try to retrieve User 44 | # instance and perform checks for that predefined user 45 | if not user_obj.is_authenticated(): 46 | user_obj = User.objects.get(pk=settings.ANONYMOUS_USER_ID) 47 | 48 | # Do not check any further if user is not active 49 | if user_obj.is_active is not True: 50 | return False 51 | 52 | if len(perm.split('.')) > 1: 53 | app_label, perm = perm.split('.') 54 | if app_label != obj._meta.app_label: 55 | raise WrongAppError("Passed perm has app label of '%s' and " 56 | "given obj has '%s'" % (app_label, obj._meta.app_label)) 57 | 58 | check = ObjectPermissionChecker(user_obj) 59 | return check.has_perm(perm, obj) 60 | 61 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | .. _configuration: 2 | 3 | Configuration 4 | ============= 5 | 6 | After :ref:`installation ` we can prepare our project for object 7 | permissions handling. In a settings module we need to add guardian to 8 | ``INSTALLED_APPS``:: 9 | 10 | INSTALLED_APPS = ( 11 | # ... 12 | 'guardian', 13 | ) 14 | 15 | and hook guardian's authentication backend:: 16 | 17 | AUTHENTICATION_BACKENDS = ( 18 | 'django.contrib.auth.backends.ModelBackend', # this is default 19 | 'guardian.backends.ObjectPermissionBackend', 20 | ) 21 | 22 | As ``django-guardian`` supports anonymous user's object permissions we also 23 | need to add following to our settings module:: 24 | 25 | ANONYMOUS_USER_ID = -1 26 | 27 | .. note:: 28 | Once project is configured to work with ``django-guardian``, calling 29 | ``syncdb`` management command would create ``User`` instance for 30 | anonymous user support (with name of ``AnonymousUser``). 31 | 32 | We can change id to whatever we like. Project should be now ready to use object 33 | permissions. 34 | 35 | 36 | Optional settings 37 | ================= 38 | 39 | In addition to requried ``ANONYMOUS_USER_ID`` setting, guardian has following, 40 | optional configuration variables: 41 | 42 | 43 | .. setting:: GUARDIAN_RENDER_403 44 | 45 | GUARDIAN_RENDER_403 46 | ------------------- 47 | 48 | .. versionadded:: 1.0.4 49 | 50 | If set to ``True``, guardian would try to render 403 response rather than 51 | return contentless ``django.http.HttpResponseForbidden``. Would use template 52 | pointed by :setting:`GUARDIAN_TEMPLATE_403` to do that. Default is ``False``. 53 | 54 | .. warning:: 55 | 56 | Remember that you cannot use both :setting:`GUARDIAN_RENDER_403` **AND** 57 | :setting:`GUARDIAN_RAISE_403` - if both are set to ``True``, 58 | ``django.core.exceptions.ImproperlyConfigured`` would be raised. 59 | 60 | 61 | .. setting:: GUARDIAN_TEMPLATE_403 62 | 63 | GUARDIAN_TEMPLATE_403 64 | --------------------- 65 | 66 | .. versionadded:: 1.0.4 67 | 68 | Tells parts of guardian what template to use for responses with status code 69 | ``403`` (i.e. :ref:`api-decorators-permission_required`). Defaults to 70 | ``403.html``. 71 | 72 | .. warning:: 73 | 74 | Remember that you cannot use both :setting:`GUARDIAN_RENDER_403` **AND** 75 | :setting:`GUARDIAN_RAISE_403` - if both are set to ``True``, 76 | ``django.core.exceptions.ImproperlyConfigured`` would be raised. 77 | 78 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | django-guardian 3 | =============== 4 | 5 | ``django-guardian`` is implementation of per object permissions [1]_ as 6 | authorization backend which is supported since Django_ 1.2. It won't 7 | work with older Django_ releases. 8 | 9 | Installation 10 | ------------ 11 | 12 | To install ``django-guardian`` simply run:: 13 | 14 | pip install django-guardian 15 | 16 | Configuration 17 | ------------- 18 | 19 | We need to hook ``django-guardian`` into our project. 20 | 21 | 1. Put ``guardian`` into your ``INSTALLED_APPS`` at settings module:: 22 | 23 | INSTALLED_APPS = ( 24 | ... 25 | 'guardian', 26 | ) 27 | 28 | 2. Add extra authorization backend:: 29 | 30 | AUTHENTICATION_BACKENDS = ( 31 | 'django.contrib.auth.backends.ModelBackend', # default 32 | 'guardian.backends.ObjectPermissionBackend', 33 | ) 34 | 35 | Usage 36 | ----- 37 | 38 | After installation and project hooks we can finally use object permissions 39 | with Django_. 40 | 41 | Lets start really quickly:: 42 | 43 | >>> jack = User.objects.create_user('jack', 'jack@example.com', 'topsecretagentjack') 44 | >>> admins = Group.objects.create(name='admins') 45 | >>> jack.has_perm('change_group', admins) 46 | False 47 | >>> UserObjectPermission.objects.assign('change_group', user=jack, obj=admins) 48 | 49 | >>> jack.has_perm('change_group', admins) 50 | True 51 | 52 | Of course our agent jack here would not be able to *change_group* globally:: 53 | 54 | >>> jack.has_perm('change_group') 55 | False 56 | 57 | Admin integration 58 | ----------------- 59 | 60 | Replace ``admin.ModelAdmin`` with ``GuardedModelAdmin`` for those models 61 | which should have object permissions support within admin panel. 62 | 63 | For example:: 64 | 65 | from django.contrib import admin 66 | from myapp.models import Author 67 | from guardian.admin import GuardedModelAdmin 68 | 69 | # Old way: 70 | #class AuthorAdmin(admin.ModelAdmin): 71 | # pass 72 | 73 | # With object permissions support 74 | class AuthorAdmin(GuardedModelAdmin): 75 | pass 76 | 77 | admin.site.register(Author, AuthorAdmin) 78 | 79 | Documentation 80 | ------------- 81 | 82 | There is an online documentation available at 83 | http://packages.python.org/django-guardian/. 84 | 85 | 86 | .. [1] Great paper about this feature is available at 87 | http://djangoadvent.com/1.2/object-permissions/. 88 | 89 | .. _Django: http://www.djangoproject.org/ 90 | 91 | -------------------------------------------------------------------------------- /guardian/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.exceptions import ValidationError 3 | from django.contrib.auth.models import User, Group, Permission 4 | from django.contrib.contenttypes.models import ContentType 5 | from django.contrib.contenttypes import generic 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | from guardian.managers import UserObjectPermissionManager 9 | from guardian.managers import GroupObjectPermissionManager 10 | from guardian.utils import get_anonymous_user 11 | 12 | class BaseObjectPermission(models.Model): 13 | """ 14 | Abstract ObjectPermission class. 15 | """ 16 | permission = models.ForeignKey(Permission) 17 | 18 | content_type = models.ForeignKey(ContentType) 19 | object_pk = models.CharField(_('object ID'), max_length=255) 20 | content_object = generic.GenericForeignKey(fk_field='object_pk') 21 | 22 | class Meta: 23 | abstract = True 24 | 25 | def __unicode__(self): 26 | return u'%s | %s | %s' % ( 27 | unicode(self.content_object), 28 | unicode(getattr(self, 'user', False) or self.group), 29 | unicode(self.permission.codename)) 30 | 31 | def save(self, *args, **kwargs): 32 | if self.content_type != self.permission.content_type: 33 | raise ValidationError("Cannot persist permission not designed for " 34 | "this class (permission's type is %s and object's type is %s)" 35 | % (self.permission.content_type, self.content_type)) 36 | return super(BaseObjectPermission, self).save(*args, **kwargs) 37 | 38 | class UserObjectPermission(BaseObjectPermission): 39 | """ 40 | **Manager**: :manager:`UserObjectPermissionManager` 41 | """ 42 | user = models.ForeignKey(User) 43 | 44 | objects = UserObjectPermissionManager() 45 | 46 | class Meta: 47 | unique_together = ['user', 'permission', 'content_type', 'object_pk'] 48 | 49 | class GroupObjectPermission(BaseObjectPermission): 50 | """ 51 | **Manager**: :manager:`GroupObjectPermissionManager` 52 | """ 53 | group = models.ForeignKey(Group) 54 | 55 | objects = GroupObjectPermissionManager() 56 | 57 | class Meta: 58 | unique_together = ['group', 'permission', 'content_type', 'object_pk'] 59 | 60 | 61 | # Prototype User and Group methods 62 | setattr(User, 'get_anonymous', staticmethod(lambda: get_anonymous_user())) 63 | setattr(User, 'add_obj_perm', 64 | lambda self, perm, obj: UserObjectPermission.objects.assign(perm, self, obj)) 65 | setattr(User, 'del_obj_perm', 66 | lambda self, perm, obj: UserObjectPermission.objects.remove_perm(perm, self, obj)) 67 | 68 | setattr(Group, 'add_obj_perm', 69 | lambda self, perm, obj: GroupObjectPermission.objects.assign(perm, self, obj)) 70 | setattr(Group, 'del_obj_perm', 71 | lambda self, perm, obj: GroupObjectPermission.objects.remove_perm(perm, self, obj)) 72 | 73 | -------------------------------------------------------------------------------- /guardian/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-guardian helper functions. 3 | 4 | Functions defined within this module should be considered as django-guardian's 5 | internal functionality. They are **not** guaranteed to be stable - which means 6 | they actual input parameters/output type may change in future releases. 7 | """ 8 | import logging 9 | from django.contrib.auth.models import User, AnonymousUser, Group 10 | from guardian.exceptions import NotUserNorGroup 11 | from guardian.conf.settings import ANONYMOUS_USER_ID 12 | from itertools import chain 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def get_anonymous_user(): 18 | """ 19 | Returns ``User`` instance (not ``AnonymousUser``) depending on 20 | ``ANONYMOUS_USER_ID`` configuration. 21 | """ 22 | return User.objects.get(id=ANONYMOUS_USER_ID) 23 | 24 | def get_identity(identity): 25 | """ 26 | Returns (user_obj, None) or (None, group_obj) tuple depending on what is 27 | given. Also accepts AnonymousUser instance but would return ``User`` 28 | instead - it is convenient and needed for authorization backend to support 29 | anonymous users. 30 | 31 | :param identity: either ``User`` or ``Group`` instance 32 | 33 | :raises ``NotUserNorGroup``: if cannot return proper identity instance 34 | 35 | **Examples**:: 36 | 37 | >>> user = User.objects.create(username='joe') 38 | >>> get_identity(user) 39 | (, None) 40 | 41 | >>> group = Group.objects.create(name='users') 42 | >>> get_identity(group) 43 | (None, ) 44 | 45 | >>> anon = AnonymousUser() 46 | >>> get_identity(anon) 47 | (, None) 48 | 49 | >>> get_identity("not instance") 50 | ... 51 | NotUserNorGroup: User/AnonymousUser or Group instance is required (got ) 52 | 53 | """ 54 | if isinstance(identity, AnonymousUser): 55 | identity = get_anonymous_user() 56 | 57 | if isinstance(identity, User): 58 | return identity, None 59 | elif isinstance(identity, Group): 60 | return None, identity 61 | 62 | raise NotUserNorGroup("User/AnonymousUser or Group instance is required " 63 | "(got %s)" % identity) 64 | 65 | def clean_orphan_obj_perms(): 66 | """ 67 | Seeks and removes all object permissions entries pointing at non-existing 68 | targets. 69 | 70 | Returns number of removed objects. 71 | """ 72 | from guardian.models import UserObjectPermission 73 | from guardian.models import GroupObjectPermission 74 | 75 | 76 | deleted = 0 77 | # TODO: optimise 78 | for perm in chain(UserObjectPermission.objects.all(), 79 | GroupObjectPermission.objects.all()): 80 | if perm.content_object is None: 81 | logger.debug("Removing %s (pk=%d)" % (perm, perm.pk)) 82 | perm.delete() 83 | deleted += 1 84 | logger.info("Total removed orphan object permissions instances: %d" % 85 | deleted) 86 | return deleted 87 | 88 | -------------------------------------------------------------------------------- /example_project/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import django 4 | 5 | from django.conf import global_settings 6 | 7 | abspath = lambda *p: os.path.abspath(os.path.join(*p)) 8 | 9 | DEBUG = True 10 | TEMPLATE_DEBUG = DEBUG 11 | 12 | PROJECT_ROOT = abspath(os.path.dirname(__file__)) 13 | GUARDIAN_MODULE_PATH = abspath(PROJECT_ROOT, '..') 14 | sys.path.insert(0, GUARDIAN_MODULE_PATH) 15 | sys.path.insert(0, PROJECT_ROOT) 16 | 17 | 18 | DATABASES = { 19 | 'default': { 20 | 'ENGINE': 'django.db.backends.sqlite3', 21 | 'NAME': abspath(PROJECT_ROOT, '.hidden.db'), 22 | 'TEST_NAME': ':memory:', 23 | }, 24 | } 25 | 26 | INSTALLED_APPS = ( 27 | 'django.contrib.auth', 28 | 'django.contrib.contenttypes', 29 | 'django.contrib.sessions', 30 | 'django.contrib.sites', 31 | 'django.contrib.admin', 32 | 'django.contrib.messages', 33 | 'django.contrib.staticfiles', 34 | 35 | 'guardian', 36 | 'guardian.tests', 37 | #'south', 38 | #'django_coverage', 39 | 'posts', 40 | ) 41 | if 'GRAPPELLI' in os.environ: 42 | try: 43 | __import__('grappelli') 44 | INSTALLED_APPS = ('grappelli',) + INSTALLED_APPS 45 | except ImportError: 46 | print "django-grappelli not installed" 47 | 48 | MIDDLEWARE_CLASSES = ( 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.transaction.TransactionMiddleware', 55 | ) 56 | 57 | STATIC_ROOT = abspath(PROJECT_ROOT, '..', 'public', 'static') 58 | STATIC_URL = '/static/' 59 | MEDIA_ROOT = abspath(PROJECT_ROOT, 'media') 60 | MEDIA_URL = '/media/' 61 | ADMIN_MEDIA_PREFIX = STATIC_URL + 'grappelli/' 62 | 63 | ROOT_URLCONF = 'example_project.urls' 64 | 65 | TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + ( 66 | 'django.core.context_processors.request', 67 | ) 68 | TEMPLATE_LOADERS = ( 69 | 'django.template.loaders.filesystem.load_template_source', 70 | 'django.template.loaders.app_directories.load_template_source', 71 | 'django.template.loaders.eggs.load_template_source', 72 | ) 73 | 74 | TEMPLATE_DIRS = ( 75 | os.path.join(os.path.dirname(__file__), 'templates'), 76 | ) 77 | 78 | SITE_ID = 1 79 | 80 | USE_I18N = True 81 | USE_L10N = True 82 | 83 | LOGIN_REDIRECT_URL = '/' 84 | 85 | TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner' 86 | 87 | AUTHENTICATION_BACKENDS = ( 88 | 'django.contrib.auth.backends.ModelBackend', 89 | 'guardian.backends.ObjectPermissionBackend', 90 | ) 91 | 92 | ANONYMOUS_USER_ID = -1 93 | 94 | # Neede as some models (located at guardian/tests/models.py) 95 | # are not migrated for tests 96 | SOUTH_TESTS_MIGRATE = False 97 | 98 | try: 99 | from conf.localsettings import * 100 | except ImportError: 101 | pass 102 | 103 | -------------------------------------------------------------------------------- /guardian/templatetags/guardian_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | ``django-guardian`` template tags. To use in a template just put the following 3 | *load* tag inside a template:: 4 | 5 | {% load guardian_tags %} 6 | 7 | """ 8 | from django import template 9 | from django.contrib.auth.models import User, Group, AnonymousUser 10 | 11 | from guardian.exceptions import NotUserNorGroup 12 | from guardian.core import ObjectPermissionChecker 13 | 14 | register = template.Library() 15 | 16 | class ObjectPermissionsNode(template.Node): 17 | def __init__(self, for_whom, obj, context_var): 18 | self.for_whom = template.Variable(for_whom) 19 | self.obj = template.Variable(obj) 20 | self.context_var = context_var 21 | 22 | def render(self, context): 23 | for_whom = self.for_whom.resolve(context) 24 | if isinstance(for_whom, User): 25 | self.user = for_whom 26 | self.group = None 27 | elif isinstance(for_whom, AnonymousUser): 28 | self.user = User.get_anonymous() 29 | self.group = None 30 | elif isinstance(for_whom, Group): 31 | self.user = None 32 | self.group = for_whom 33 | else: 34 | raise NotUserNorGroup("User or Group instance required (got %s)" 35 | % for_whom.__class__) 36 | obj = self.obj.resolve(context) 37 | 38 | check = ObjectPermissionChecker(for_whom) 39 | perms = check.get_perms(obj) 40 | 41 | context[self.context_var] = perms 42 | return '' 43 | 44 | @register.tag 45 | def get_obj_perms(parser, token): 46 | """ 47 | Returns a list of permissions (as ``codename`` strings) for a given 48 | ``user``/``group`` and ``obj`` (Model instance). 49 | 50 | Parses ``get_obj_perms`` tag which should be in format:: 51 | 52 | {% get_obj_perms user/group for obj as "context_var" %} 53 | 54 | Example of usage (assuming ``flatpage`` and ``perm`` objects are 55 | available from *context*):: 56 | 57 | {% get_obj_perms request.user for flatpage as "flatpage_perms" %} 58 | 59 | {% if "delete_flatpage" in flatpage_perms %} 60 | Remove page 61 | {% endif %} 62 | 63 | .. note:: 64 | Please remember that superusers would always get full list of permissions 65 | for a given object. 66 | 67 | """ 68 | bits = token.split_contents() 69 | format = '{% get_obj_perms user/group for obj as "context_var" %}' 70 | if len(bits) != 6 or bits[2] != 'for' or bits[4] != 'as': 71 | raise template.TemplateSyntaxError("get_obj_perms tag should be in " 72 | "format: %s" % format) 73 | 74 | for_whom = bits[1] 75 | obj = bits[3] 76 | context_var = bits[5] 77 | if context_var[0] != context_var[-1] or context_var[0] not in ('"', "'"): 78 | raise template.TemplateSyntaxError("get_obj_perms tag's context_var " 79 | "argument should be in quotes") 80 | context_var = context_var[1:-1] 81 | return ObjectPermissionsNode(for_whom, obj, context_var) 82 | 83 | -------------------------------------------------------------------------------- /docs/userguide/admin-integration.rst: -------------------------------------------------------------------------------- 1 | .. _admin-integration: 2 | 3 | Admin integration 4 | ================= 5 | 6 | Django comes with excellent and widely used *Admin* application. Basically, 7 | it provides content management for Django applications. User with access to 8 | admin panel can manage users, groups, permissions and other data provided by 9 | system. 10 | 11 | ``django-guardian`` comes with simple object permissions management integration 12 | for Django's admin application. 13 | 14 | Usage 15 | ----- 16 | 17 | It is very easy to use admin integration. Simply use :admin:`GuardedModelAdmin` 18 | instead of standard ``django.contrib.admin.ModelAdmin`` class for registering 19 | models within the admin. In example, look at following model: 20 | 21 | .. code-block:: python 22 | 23 | from django.db import models 24 | 25 | 26 | class Post(models.Model): 27 | title = models.CharField('title', max_length=64) 28 | slug = models.SlugField(max_length=64) 29 | content = models.TextField('content') 30 | created_at = models.DateTimeField(auto_now_add=True, db_index=True) 31 | 32 | class Meta: 33 | permissions = ( 34 | ('view_post', 'Can view post'), 35 | ) 36 | get_latest_by = 'created_at' 37 | 38 | def __unicode__(self): 39 | return self.title 40 | 41 | @models.permalink 42 | def get_absolute_url(self): 43 | return {'post_slug': self.slug} 44 | 45 | We want to register ``Post`` model within admin application. Normally, we would 46 | do this as follows within ``admin.py`` file of our application: 47 | 48 | .. code-block:: python 49 | 50 | from django.contrib import admin 51 | 52 | from example_project.posts.models import Post 53 | 54 | 55 | class PostAdmin(admin.ModelAdmin): 56 | prepopulated_fields = {"slug": ("title",)} 57 | list_display = ('title', 'slug', 'created_at') 58 | search_fields = ('title', 'content') 59 | ordering = ('-created_at',) 60 | date_hierarchy = 'created_at' 61 | 62 | admin.site.register(Post, PostAdmin) 63 | 64 | 65 | If we would like to add object permissions management for ``Post`` model we 66 | would need to change ``PostAdmin`` base class into ``GuardedModelAdmin``. 67 | Our code could look as follows: 68 | 69 | .. code-block:: python 70 | 71 | from django.contrib import admin 72 | 73 | from example_project.posts.models import Post 74 | 75 | from guardian.admin import GuardedModelAdmin 76 | 77 | 78 | class PostAdmin(GuardedModelAdmin): 79 | prepopulated_fields = {"slug": ("title",)} 80 | list_display = ('title', 'slug', 'created_at') 81 | search_fields = ('title', 'content') 82 | ordering = ('-created_at',) 83 | date_hierarchy = 'created_at' 84 | 85 | admin.site.register(Post, PostAdmin) 86 | 87 | And thats it. We can now navigate to **change** post page and just next to the 88 | *history* link we can click *Object permissions* button to manage row level 89 | permissions. 90 | 91 | .. note:: 92 | Example above is shipped with ``django-guardian`` package with the example 93 | project. 94 | 95 | -------------------------------------------------------------------------------- /docs/develop/testing.rst: -------------------------------------------------------------------------------- 1 | .. _testing: 2 | 3 | Testing 4 | ======= 5 | 6 | Introduction 7 | ------------ 8 | 9 | ``django-guardian`` is extending capabilities of Django's authorization 10 | facilities and as so, it changes it's security somehow. It is extremaly 11 | important to provide as simplest :ref:`api` as possible. 12 | 13 | According to OWASP_, `broken authentication 14 | `_ is one of most commonly 15 | security issue exposed in web applications. 16 | 17 | Having this on mind we tried to build small set of necessary functions and 18 | created a lot of testing scenarios. Neverteless, if anyone would found a bug in 19 | this application, please take a minute and file it at `issue-tracker`_. 20 | 21 | Running tests 22 | ------------- 23 | 24 | Tests are run by Django's buildin test runner. To call it simply run:: 25 | 26 | $ python setup.py test 27 | 28 | or inside a project with ``guardian`` set at ``INSTALLED_APPS``:: 29 | 30 | $ python manage.py test guardian 31 | 32 | Coverage support 33 | ---------------- 34 | 35 | Coverage_ is a tool for measuring code coverage of Python programs. It is great 36 | for tests and we use it as a backup - we try to cover 100% of the code used by 37 | ``django-guardian``. This of course does *NOT* mean that if all of the codebase 38 | is covered by tests we can be sure there is no bug (as specification of almost 39 | all applications requries some unique scenarios to be tested). On the other hand 40 | it definitely helps to track missing parts. 41 | 42 | To run tests with coverage_ support and show the report after we have provided 43 | simple bash script which can by called by running:: 44 | 45 | $ ./run_test_and_report.sh 46 | 47 | 48 | Result should be somehow similar to following:: 49 | 50 | (...) 51 | ................................................ 52 | ---------------------------------------------------------------------- 53 | Ran 48 tests in 2.516s 54 | 55 | OK 56 | Destroying test database 'default'... 57 | Name Stmts Exec Cover Missing 58 | ------------------------------------------------------------------- 59 | guardian/__init__ 4 4 100% 60 | guardian/backends 20 20 100% 61 | guardian/conf/__init__ 1 1 100% 62 | guardian/core 29 29 100% 63 | guardian/exceptions 8 8 100% 64 | guardian/management/__init__ 10 10 100% 65 | guardian/managers 40 40 100% 66 | guardian/models 36 36 100% 67 | guardian/shortcuts 30 30 100% 68 | guardian/templatetags/__init__ 1 1 100% 69 | guardian/templatetags/guardian_tags 39 39 100% 70 | guardian/utils 13 13 100% 71 | ------------------------------------------------------------------- 72 | TOTAL 231 231 100% 73 | 74 | .. _owasp: http://www.owasp.org/ 75 | .. _issue-tracker: http://github.com/lukaszb/django-guardian 76 | .. _coverage: http://nedbatchelder.com/code/coverage/ 77 | 78 | -------------------------------------------------------------------------------- /docs/userguide/assign.rst: -------------------------------------------------------------------------------- 1 | .. _assign: 2 | 3 | Assign object permissions 4 | ========================= 5 | 6 | Assigning object permissions should be very simple once permissions are created 7 | for models. 8 | 9 | Prepare permissions 10 | ------------------- 11 | 12 | Let's assume we have following model: 13 | 14 | .. code-block:: python 15 | 16 | class Task(models.Model): 17 | summary = models.CharField(max_length=32) 18 | content = models.TextField() 19 | reported_by = models.ForeignKey(User) 20 | created_at = models.DateTimeField(auto_now_add=True) 21 | 22 | ... and we want to be able to set custom permission *view_task*. We let know 23 | Django to do so by adding ``permissions`` tuple to ``Meta`` class and our final 24 | model could look like: 25 | 26 | .. code-block:: python 27 | 28 | class Task(models.Model): 29 | summary = models.CharField(max_length=32) 30 | content = models.TextField() 31 | reported_by = models.ForeignKey(User) 32 | created_at = models.DateTimeField(auto_now_add=True) 33 | 34 | class Meta: 35 | permissions = ( 36 | ('view_task', 'View task'), 37 | ) 38 | 39 | After we call ``syncdb`` management command our *view_task* permission would be 40 | added to default set of permissions. 41 | 42 | .. note:: 43 | By default, Django adds 3 permissions for each registered model: 44 | 45 | - *add_modelname* 46 | - *change_modelname* 47 | - *delete_modelname* 48 | 49 | (where *modelname* is a simplified name of our model's class). See 50 | http://docs.djangoproject.com/en/1.2/topics/auth/#default-permissions for 51 | more detail. 52 | 53 | There is nothing new here since creation of permissions is 54 | `handled by django `_. 55 | Now we can move to :ref:`assigning object permissions `. 56 | 57 | .. _assign-obj-perms: 58 | 59 | Assign object permissions 60 | ------------------------- 61 | 62 | We can assign permissions for any user/group and object pairs using same, 63 | convenient function: :func:`guardian.shortcuts.assign`. 64 | 65 | For user 66 | ~~~~~~~~ 67 | 68 | Continuing our example we now can allow Joe user to view some task: 69 | 70 | .. code-block:: python 71 | 72 | >>> boss = User.objects.create(username='Big Boss') 73 | >>> joe = User.objects.create(username='joe') 74 | >>> task = Task.objects.create(summary='Some job', content='', reported_by=boss) 75 | >>> joe.has_perm('view_task', task) 76 | False 77 | 78 | Well, not so fast Joe, let us create an object permission finally: 79 | 80 | .. code-block:: python 81 | 82 | >>> from guardian.shortcuts import assign 83 | >>> assign('view_task', joe, task) 84 | >>> joe.has_perm('view_task', task) 85 | True 86 | 87 | 88 | For group 89 | ~~~~~~~~~ 90 | 91 | This case doesn't really differ from user permissions assignment. The only 92 | difference is we have to pass ``Group`` instance rather than ``User``. 93 | 94 | .. code-block:: python 95 | 96 | >>> group = Group.objects.create(name='employees') 97 | >>> assign('change_task', group, task) 98 | >>> joe.has_perm('change_task', task) 99 | False 100 | >>> # Well, joe is not yet within an *employees* group 101 | >>> joe.groups.add(group) 102 | >>> joe.has_perm('change_task', task) 103 | True 104 | 105 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " pdf to make PDF document" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 27 | @echo " changes to make an overview of all changed/added/deprecated items" 28 | @echo " linkcheck to check all external links for integrity" 29 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 30 | 31 | clean: 32 | -rm -rf $(BUILDDIR)/* 33 | 34 | html: 35 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 36 | @echo 37 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 38 | 39 | dirhtml: 40 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 43 | 44 | pickle: 45 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 46 | @echo 47 | @echo "Build finished; now you can process the pickle files." 48 | 49 | json: 50 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 51 | @echo 52 | @echo "Build finished; now you can process the JSON files." 53 | 54 | htmlhelp: 55 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 56 | @echo 57 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 58 | ".hhp project file in $(BUILDDIR)/htmlhelp." 59 | 60 | qthelp: 61 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 62 | @echo 63 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 64 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 65 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-guardian.qhcp" 66 | @echo "To view the help file:" 67 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-guardian.qhc" 68 | 69 | latex: 70 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 71 | @echo 72 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 73 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 74 | "run these through (pdf)latex." 75 | 76 | changes: 77 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 78 | @echo 79 | @echo "The overview file is in $(BUILDDIR)/changes." 80 | 81 | linkcheck: 82 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 83 | @echo 84 | @echo "Link check complete; look for any errors in the above output " \ 85 | "or in $(BUILDDIR)/linkcheck/output.txt." 86 | 87 | doctest: 88 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 89 | @echo "Testing of doctests in the sources finished, look at the " \ 90 | "results in $(BUILDDIR)/doctest/output.txt." 91 | 92 | pdf: 93 | $(SPHINXBUILD) -b pdf $(ALLSPHINXOPTS) $(BUILDDIR)/pdf 94 | @echo 95 | @echo "Build finished. The PDF files are in $(BUILDDIR)/pdf." 96 | 97 | -------------------------------------------------------------------------------- /guardian/tests/orphans_test.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import models as auth_app 2 | from django.contrib.auth.management import create_permissions 3 | from django.contrib.auth.models import User, Group 4 | from django.contrib.contenttypes.models import ContentType 5 | from django.core.management import call_command 6 | from django.test import TestCase 7 | 8 | from guardian.utils import clean_orphan_obj_perms 9 | from guardian.shortcuts import assign 10 | 11 | 12 | class OrphanedObjectPermissionsTest(TestCase): 13 | 14 | def setUp(self): 15 | # Create objects for which we would assing obj perms 16 | self.target_user1 = User.objects.create(username='user1') 17 | self.target_group1 = Group.objects.create(name='group1') 18 | self.target_obj1 = ContentType.objects.create(name='ct1', model='foo', 19 | app_label='fake-for-guardian-tests') 20 | self.target_obj2 = ContentType.objects.create(name='ct2', model='bar', 21 | app_label='fake-for-guardian-tests') 22 | # Required if MySQL backend is used :/ 23 | create_permissions(auth_app, [], 1) 24 | 25 | 26 | self.user = User.objects.create(username='user') 27 | self.group = Group.objects.create(name='group') 28 | 29 | def test_clean_perms(self): 30 | 31 | # assign obj perms 32 | target_perms = { 33 | self.target_user1: ["change_user"], 34 | self.target_group1: ["delete_group"], 35 | self.target_obj1: ["change_contenttype", "delete_contenttype"], 36 | self.target_obj2: ["change_contenttype"], 37 | } 38 | obj_perms_count = sum([len(val) for key, val in target_perms.items()]) 39 | for target, perms in target_perms.items(): 40 | target.__old_pk = target.pk # Store pkeys 41 | for perm in perms: 42 | assign(perm, self.user, target) 43 | 44 | # Remove targets 45 | for target, perms in target_perms.items(): 46 | target.delete() 47 | 48 | # Clean orphans 49 | removed = clean_orphan_obj_perms() 50 | self.assertEqual(removed, obj_perms_count) 51 | 52 | # Recreate targets and check if user has no permissions 53 | for target, perms in target_perms.items(): 54 | target.pk = target.__old_pk 55 | target.save() 56 | for perm in perms: 57 | self.assertFalse(self.user.has_perm(perm, target)) 58 | 59 | def test_clean_perms_command(self): 60 | """ 61 | Same test as the one above but rather function directly, we call 62 | management command instead. 63 | """ 64 | 65 | # assign obj perms 66 | target_perms = { 67 | self.target_user1: ["change_user"], 68 | self.target_group1: ["delete_group"], 69 | self.target_obj1: ["change_contenttype", "delete_contenttype"], 70 | self.target_obj2: ["change_contenttype"], 71 | } 72 | for target, perms in target_perms.items(): 73 | target.__old_pk = target.pk # Store pkeys 74 | for perm in perms: 75 | assign(perm, self.user, target) 76 | 77 | # Remove targets 78 | for target, perms in target_perms.items(): 79 | target.delete() 80 | 81 | # Clean orphans 82 | call_command("clean_orphan_obj_perms", verbosity=0) 83 | 84 | # Recreate targets and check if user has no permissions 85 | for target, perms in target_perms.items(): 86 | target.pk = target.__old_pk 87 | target.save() 88 | for perm in perms: 89 | self.assertFalse(self.user.has_perm(perm, target)) 90 | 91 | -------------------------------------------------------------------------------- /guardian/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import Permission 3 | from django.contrib.contenttypes.models import ContentType 4 | 5 | from guardian.exceptions import ObjectNotPersisted 6 | 7 | class UserObjectPermissionManager(models.Manager): 8 | 9 | def assign(self, perm, user, obj): 10 | """ 11 | Assigns permission with given ``perm`` for an instance ``obj`` and 12 | ``user``. 13 | """ 14 | if getattr(obj, 'pk', None) is None: 15 | raise ObjectNotPersisted("Object %s needs to be persisted first" 16 | % obj) 17 | ctype = ContentType.objects.get_for_model(obj) 18 | permission = Permission.objects.get( 19 | content_type=ctype, codename=perm) 20 | 21 | obj_perm, created = self.get_or_create( 22 | content_type = ctype, 23 | permission = permission, 24 | object_pk = obj.pk, 25 | user = user) 26 | return obj_perm 27 | 28 | def remove_perm(self, perm, user, obj): 29 | """ 30 | Removes permission ``perm`` for an instance ``obj`` and given ``user``. 31 | """ 32 | if getattr(obj, 'pk', None) is None: 33 | raise ObjectNotPersisted("Object %s needs to be persisted first" 34 | % obj) 35 | self.filter( 36 | permission__codename=perm, 37 | user=user, 38 | object_pk=obj.pk, 39 | content_type=ContentType.objects.get_for_model(obj))\ 40 | .delete() 41 | 42 | def get_for_object(self, user, obj): 43 | if getattr(obj, 'pk', None) is None: 44 | raise ObjectNotPersisted("Object %s needs to be persisted first" 45 | % obj) 46 | ctype = ContentType.objects.get_for_model(obj) 47 | perms = self.filter( 48 | content_type = ctype, 49 | user = user, 50 | ) 51 | return perms 52 | 53 | class GroupObjectPermissionManager(models.Manager): 54 | 55 | def assign(self, perm, group, obj): 56 | """ 57 | Assigns permission with given ``perm`` for an instance ``obj`` and 58 | ``group``. 59 | """ 60 | if getattr(obj, 'pk', None) is None: 61 | raise ObjectNotPersisted("Object %s needs to be persisted first" 62 | % obj) 63 | ctype = ContentType.objects.get_for_model(obj) 64 | permission = Permission.objects.get( 65 | content_type=ctype, codename=perm) 66 | 67 | obj_perm, created = self.get_or_create( 68 | content_type = ctype, 69 | permission = permission, 70 | object_pk = obj.pk, 71 | group = group) 72 | return obj_perm 73 | 74 | def remove_perm(self, perm, group, obj): 75 | """ 76 | Removes permission ``perm`` for an instance ``obj`` and given ``group``. 77 | """ 78 | if getattr(obj, 'pk', None) is None: 79 | raise ObjectNotPersisted("Object %s needs to be persisted first" 80 | % obj) 81 | self.filter( 82 | permission__codename=perm, 83 | group=group, 84 | object_pk=obj.pk, 85 | content_type=ContentType.objects.get_for_model(obj))\ 86 | .delete() 87 | 88 | def get_for_object(self, group, obj): 89 | if getattr(obj, 'pk', None) is None: 90 | raise ObjectNotPersisted("Object %s needs to be persisted first" 91 | % obj) 92 | ctype = ContentType.objects.get_for_model(obj) 93 | perms = self.filter( 94 | content_type = ctype, 95 | group = group, 96 | ) 97 | return perms 98 | 99 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | set SPHINXBUILD=sphinx-build 6 | set BUILDDIR=build 7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 8 | if NOT "%PAPER%" == "" ( 9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 10 | ) 11 | 12 | if "%1" == "" goto help 13 | 14 | if "%1" == "help" ( 15 | :help 16 | echo.Please use `make ^` where ^ is one of 17 | echo. html to make standalone HTML files 18 | echo. pdf to make PDF document 19 | echo. dirhtml to make HTML files named index.html in directories 20 | echo. pickle to make pickle files 21 | echo. json to make JSON files 22 | echo. htmlhelp to make HTML files and a HTML help project 23 | echo. qthelp to make HTML files and a qthelp project 24 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 25 | echo. changes to make an overview over all changed/added/deprecated items 26 | echo. linkcheck to check all external links for integrity 27 | echo. doctest to run all doctests embedded in the documentation if enabled 28 | goto end 29 | ) 30 | 31 | if "%1" == "clean" ( 32 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 33 | del /q /s %BUILDDIR%\* 34 | goto end 35 | ) 36 | 37 | if "%1" == "html" ( 38 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 39 | echo. 40 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 41 | goto end 42 | ) 43 | 44 | if "%1" == "dirhtml" ( 45 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 48 | goto end 49 | ) 50 | 51 | if "%1" == "pickle" ( 52 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 53 | echo. 54 | echo.Build finished; now you can process the pickle files. 55 | goto end 56 | ) 57 | 58 | if "%1" == "json" ( 59 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 60 | echo. 61 | echo.Build finished; now you can process the JSON files. 62 | goto end 63 | ) 64 | 65 | if "%1" == "htmlhelp" ( 66 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 67 | echo. 68 | echo.Build finished; now you can run HTML Help Workshop with the ^ 69 | .hhp project file in %BUILDDIR%/htmlhelp. 70 | goto end 71 | ) 72 | 73 | if "%1" == "qthelp" ( 74 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 75 | echo. 76 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 77 | .qhcp project file in %BUILDDIR%/qthelp, like this: 78 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-guardian.qhcp 79 | echo.To view the help file: 80 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-guardian.ghc 81 | goto end 82 | ) 83 | 84 | if "%1" == "latex" ( 85 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 86 | echo. 87 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 88 | goto end 89 | ) 90 | 91 | if "%1" == "changes" ( 92 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 93 | echo. 94 | echo.The overview file is in %BUILDDIR%/changes. 95 | goto end 96 | ) 97 | 98 | if "%1" == "linkcheck" ( 99 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 100 | echo. 101 | echo.Link check complete; look for any errors in the above output ^ 102 | or in %BUILDDIR%/linkcheck/output.txt. 103 | goto end 104 | ) 105 | 106 | if "%1" == "doctest" ( 107 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 108 | echo. 109 | echo.Testing of doctests in the sources finished, look at the ^ 110 | results in %BUILDDIR%/doctest/output.txt. 111 | goto end 112 | ) 113 | 114 | if "%1" == "pdf" ( 115 | %SPHINXBUILD% -b pdf %ALLSPHINXOPTS% %BUILDDIR%/pdf 116 | echo. 117 | echo.Build finished. The PDF files are in %BUILDDIR%/pdf. 118 | goto end 119 | ) 120 | 121 | :end 122 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Release 1.0.3 (Jul 25, 2011) 2 | ================================== 3 | 4 | * added ``get_objects_for_group`` shortcut (thanks to Rafael Ponieman) 5 | * added ``user_can_access_owned_objects_only`` flag to ``GuardedModelAdmin`` 6 | * updated and fixed issues with example app (thanks to Bojan Mihelac) 7 | * minor typo fixed at documentation 8 | * included ADC theme for documentation 9 | 10 | Release 1.0.2 (Apr 12, 2011) 11 | ================================== 12 | 13 | * ``get_users_with_perms`` now accepts ``with_group_users`` flag 14 | * Fixed ``group_id`` issue at admin templates 15 | * Small fix for documentation building process 16 | * It's 2011 (updated dates within this file) 17 | 18 | 19 | Release 1.0.1 (Mar 25, 2011) 20 | ================================== 21 | 22 | * ``get_users_with_perms`` now accepts ``with_superusers`` flag 23 | * Small fix for documentation building process 24 | 25 | 26 | Release 1.0.0 (Jan 27, 2011) 27 | ================================== 28 | 29 | * A final v1.0 release! 30 | 31 | 32 | Release 1.0.0.beta2 (Jan 14, 2011) 33 | ================================== 34 | 35 | * Added ``get_objects_for_user`` shortcut function 36 | * Added few tests 37 | * Fixed issues related with ``django.contrib.auth`` tests 38 | * Removed example project from source distribution 39 | 40 | 41 | Release 1.0.0.beta1 (Jan 11, 2011) 42 | ================================== 43 | 44 | * Simplified example project 45 | * Fixed issues related with test suite 46 | * Added ability to clear orphaned object permissions 47 | * Added ``clean_orphan_obj_perms`` management command 48 | * Documentation cleanup 49 | * Added grappelli_ admin templates 50 | 51 | 52 | Release 1.0.0.alpha2 (Dec 2, 2010) 53 | ================================== 54 | 55 | * Added possibility to operate with global permissions for assign and 56 | ``remove_perm`` shortcut functions 57 | * Added possibility to generate PDF documentation 58 | * Fixed some tests 59 | 60 | 61 | Release 1.0.0.alpha1 (Nov 23, 2010) 62 | =================================== 63 | 64 | * Fixed admin templates not included in ``MANIFEST.in`` 65 | * Fixed admin integration codes 66 | 67 | 68 | Release 1.0.0.pre (Nov 23, 2010) 69 | ================================ 70 | 71 | * Added admin integration 72 | * Added reusable forms for object permissions management 73 | 74 | 75 | Release 0.2.3 (Nov 17, 2010) 76 | ============================ 77 | 78 | * Added ``guardian.shortcuts.get_users_with_perms`` function 79 | * Added ``AUTHORS`` file 80 | 81 | 82 | Release 0.2.2 (Oct 19, 2010) 83 | ============================ 84 | 85 | * Fixed migrations order (thanks to Daniel Rech) 86 | 87 | 88 | Release 0.2.1 (Oct 3, 2010) 89 | =========================== 90 | 91 | * Fixed migration (it wasn't actually updating object_pk field) 92 | 93 | 94 | Release 0.2.0 (Oct 3, 2010) 95 | =========================== 96 | 97 | Fixes 98 | ~~~~~ 99 | 100 | * #4: guardian now supports models with not-integer primary keys and 101 | they don't need to be called "id". 102 | 103 | .. important:: 104 | For 0.1.X users: it is required to *migrate* guardian in your projects. 105 | Add ``south`` to ``INSTALLED_APPS`` and run:: 106 | 107 | python manage.py syncdb 108 | python manage.py migrate guardian 0001 --fake 109 | python manage.py migrate guardian 110 | 111 | Improvements 112 | ~~~~~~~~~~~~ 113 | 114 | * Added South_ migrations support 115 | 116 | 117 | Release 0.1.1 (Sep 27, 2010) 118 | ============================ 119 | 120 | Improvements 121 | ~~~~~~~~~~~~ 122 | 123 | * Added view decorators: ``permission_required`` and 124 | ``permission_required_403`` 125 | 126 | 127 | Release 0.1.0 (Jun 6, 2010) 128 | =========================== 129 | 130 | * Initial public release 131 | 132 | 133 | .. _south: http://south.aeracode.org/ 134 | .. _grappelli: https://github.com/sehmaschine/django-grappelli 135 | 136 | -------------------------------------------------------------------------------- /guardian/core.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | 3 | from django.contrib.auth.models import Permission 4 | from django.contrib.contenttypes.models import ContentType 5 | from django.db.models import Q, F 6 | 7 | from guardian.utils import get_identity 8 | 9 | class ObjectPermissionChecker(object): 10 | """ 11 | Generic object permissions checker class being the heart of 12 | ``django-guardian``. 13 | 14 | .. note:: 15 | Once checked for single object, permissions are stored and we don't hit 16 | database again if another check is called for this object. This is great 17 | for templates, views or other request based checks (assuming we don't 18 | have hundreds of permissions on a single object as we fetch all 19 | permissions for checked object). 20 | 21 | On the other hand, if we call ``has_perm`` for perm1/object1, then we 22 | change permission state and call ``has_perm`` again for same 23 | perm1/object1 on same instance of ObjectPermissionChecker we won't see a 24 | difference as permissions are already fetched and stored within cache 25 | dictionary. 26 | """ 27 | def __init__(self, user_or_group=None): 28 | """ 29 | :param user_or_group: should be an ``User``, ``AnonymousUser`` or 30 | ``Group`` instance 31 | """ 32 | self.user, self.group = get_identity(user_or_group) 33 | self._obj_perms_cache = {} 34 | 35 | def has_perm(self, perm, obj): 36 | """ 37 | Checks if user/group has given permission for object. 38 | 39 | :param perm: permission as string, may or may not contain app_label 40 | prefix (if not prefixed, we grab app_label from ``obj``) 41 | :param obj: Django model instance for which permission should be checked 42 | 43 | """ 44 | perm = perm.split('.')[-1] 45 | if self.user and not self.user.is_active: 46 | return False 47 | elif self.user and self.user.is_superuser: 48 | return True 49 | return perm in self.get_perms(obj) 50 | 51 | def get_perms(self, obj): 52 | """ 53 | Returns list of ``codename``'s of all permissions for given ``obj``. 54 | 55 | :param obj: Django model instance for which permission should be checked 56 | 57 | """ 58 | ctype = ContentType.objects.get_for_model(obj) 59 | key = self.get_local_cache_key(obj) 60 | if not key in self._obj_perms_cache: 61 | if self.user and not self.user.is_active: 62 | return [] 63 | elif self.user and self.user.is_superuser: 64 | perms = list(chain(*Permission.objects 65 | .filter(content_type=ctype) 66 | .values_list("codename"))) 67 | elif self.user: 68 | perms = list(set(chain(*Permission.objects 69 | .filter(content_type=ctype) 70 | .filter( 71 | Q(userobjectpermission__content_type=F('content_type'), 72 | userobjectpermission__user=self.user, 73 | userobjectpermission__object_pk=obj.pk) | 74 | Q(groupobjectpermission__content_type=F('content_type'), 75 | groupobjectpermission__group__user=self.user, 76 | groupobjectpermission__object_pk=obj.pk)) 77 | .values_list("codename")))) 78 | else: 79 | perms = list(set(chain(*Permission.objects 80 | .filter(content_type=ctype) 81 | .filter( 82 | groupobjectpermission__content_type=F('content_type'), 83 | groupobjectpermission__group=self.group, 84 | groupobjectpermission__object_pk=obj.pk) 85 | .values_list("codename")))) 86 | self._obj_perms_cache[key] = perms 87 | return self._obj_perms_cache[key] 88 | 89 | def get_local_cache_key(self, obj): 90 | """ 91 | Returns cache key for ``_obj_perms_cache`` dict. 92 | """ 93 | ctype = ContentType.objects.get_for_model(obj) 94 | return (ctype.id, obj.pk) 95 | 96 | -------------------------------------------------------------------------------- /guardian/migrations/0005_auto__chg_field_groupobjectpermission_object_pk__chg_field_userobjectp.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | pass 12 | 13 | 14 | def backwards(self, orm): 15 | 16 | pass 17 | 18 | models = { 19 | 'auth.group': { 20 | 'Meta': {'object_name': 'Group'}, 21 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 22 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 23 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 24 | }, 25 | 'auth.permission': { 26 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 27 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 28 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 29 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 30 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 31 | }, 32 | 'auth.user': { 33 | 'Meta': {'object_name': 'User'}, 34 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 35 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 36 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 37 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 38 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 39 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 40 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 41 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 42 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 43 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 44 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 45 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 46 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 47 | }, 48 | 'contenttypes.contenttype': { 49 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 50 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 51 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 52 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 53 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 54 | }, 55 | 'guardian.groupobjectpermission': { 56 | 'Meta': {'unique_together': "(['group', 'permission', 'content_type', 'object_pk'],)", 'object_name': 'GroupObjectPermission'}, 57 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 58 | 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}), 59 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 60 | 'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 61 | 'permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Permission']"}) 62 | }, 63 | 'guardian.userobjectpermission': { 64 | 'Meta': {'unique_together': "(['user', 'permission', 'content_type', 'object_pk'],)", 'object_name': 'UserObjectPermission'}, 65 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 66 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 68 | 'permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Permission']"}), 69 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 70 | } 71 | } 72 | 73 | complete_apps = ['guardian'] 74 | -------------------------------------------------------------------------------- /guardian/tests/tags_test.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test import TestCase 3 | from django.template import Template, Context, TemplateSyntaxError 4 | from django.contrib.auth.models import User, Group, AnonymousUser 5 | from django.contrib.contenttypes.models import ContentType 6 | 7 | from guardian.exceptions import NotUserNorGroup 8 | from guardian.models import UserObjectPermission, GroupObjectPermission 9 | 10 | def render(template, context): 11 | """ 12 | Returns rendered ``template`` with ``context``, which are given as string 13 | and dict respectively. 14 | """ 15 | t = Template(template) 16 | return t.render(Context(context)) 17 | 18 | class GetObjPermsTagTest(TestCase): 19 | 20 | def setUp(self): 21 | self.ctype = ContentType.objects.create(name='foo', model='bar', 22 | app_label='fake-for-guardian-tests') 23 | self.group = Group.objects.create(name='jackGroup') 24 | self.user = User.objects.create(username='jack') 25 | self.user.groups.add(self.group) 26 | 27 | def test_wrong_formats(self): 28 | wrong_formats = ( 29 | '{% get_obj_perms user for contenttype as obj_perms %}', # no quotes 30 | '{% get_obj_perms user for contenttype as \'obj_perms" %}', # wrong quotes 31 | '{% get_obj_perms user for contenttype as \'obj_perms" %}', # wrong quotes 32 | '{% get_obj_perms user for contenttype as obj_perms" %}', # wrong quotes 33 | '{% get_obj_perms user for contenttype as obj_perms\' %}', # wrong quotes 34 | '{% get_obj_perms user for contenttype as %}', # no context_var 35 | '{% get_obj_perms for contenttype as "obj_perms" %}', # no user/group 36 | '{% get_obj_perms user contenttype as "obj_perms" %}', # no "for" bit 37 | '{% get_obj_perms user for contenttype "obj_perms" %}', # no "as" bit 38 | '{% get_obj_perms user for as "obj_perms" %}', # no object 39 | ) 40 | 41 | context = {'user': User.get_anonymous(), 'contenttype': self.ctype} 42 | for wrong in wrong_formats: 43 | fullwrong = '{% load guardian_tags %}' + wrong 44 | try: 45 | render(fullwrong, context) 46 | self.fail("Used wrong get_obj_perms tag format: \n\n\t%s\n\n " 47 | "but TemplateSyntaxError have not been raised" % wrong) 48 | except TemplateSyntaxError: 49 | pass 50 | 51 | def test_anonymous_user(self): 52 | template = ''.join(( 53 | '{% load guardian_tags %}', 54 | '{% get_obj_perms user for contenttype as "obj_perms" %}{{ perms }}', 55 | )) 56 | context = {'user': AnonymousUser(), 'contenttype': self.ctype} 57 | anon_output = render(template, context) 58 | context = {'user': User.get_anonymous(), 'contenttype': self.ctype} 59 | real_anon_user_output = render(template, context) 60 | self.assertEqual(anon_output, real_anon_user_output) 61 | 62 | def test_wrong_user_or_group(self): 63 | template = ''.join(( 64 | '{% load guardian_tags %}', 65 | '{% get_obj_perms some_obj for contenttype as "obj_perms" %}', 66 | )) 67 | context = {'some_obj': ContentType(), 'contenttype': self.ctype} 68 | # This test would raise TemplateSyntaxError instead of NotUserNorGroup 69 | # if TEMPLATE_DEBUG is set to True during tests 70 | tmp = settings.TEMPLATE_DEBUG 71 | settings.TEMPLATE_DEBUG = False 72 | self.assertRaises(NotUserNorGroup, render, template, context) 73 | settings.TEMPLATE_DEBUG = tmp 74 | 75 | def test_superuser(self): 76 | user = User.objects.create(username='superuser', is_superuser=True) 77 | template = ''.join(( 78 | '{% load guardian_tags %}', 79 | '{% get_obj_perms user for contenttype as "obj_perms" %}', 80 | '{{ obj_perms|join:" " }}', 81 | )) 82 | context = {'user': user, 'contenttype': self.ctype} 83 | output = render(template, context) 84 | 85 | for perm in ('add_contenttype', 'change_contenttype', 'delete_contenttype'): 86 | self.assertTrue(perm in output) 87 | 88 | def test_user(self): 89 | UserObjectPermission.objects.assign("change_contenttype", self.user, 90 | self.ctype) 91 | GroupObjectPermission.objects.assign("delete_contenttype", self.group, 92 | self.ctype) 93 | 94 | template = ''.join(( 95 | '{% load guardian_tags %}', 96 | '{% get_obj_perms user for contenttype as "obj_perms" %}', 97 | '{{ obj_perms|join:" " }}', 98 | )) 99 | context = {'user': self.user, 'contenttype': self.ctype} 100 | output = render(template, context) 101 | 102 | self.assertEqual( 103 | set(output.split(' ')), 104 | set('change_contenttype delete_contenttype'.split(' '))) 105 | 106 | def test_group(self): 107 | GroupObjectPermission.objects.assign("delete_contenttype", self.group, 108 | self.ctype) 109 | 110 | template = ''.join(( 111 | '{% load guardian_tags %}', 112 | '{% get_obj_perms group for contenttype as "obj_perms" %}', 113 | '{{ obj_perms|join:" " }}', 114 | )) 115 | context = {'group': self.group, 'contenttype': self.ctype} 116 | output = render(template, context) 117 | 118 | self.assertEqual(output, 'delete_contenttype') 119 | 120 | -------------------------------------------------------------------------------- /guardian/templates/admin/guardian/model/obj_perms_manage.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | {% load adminmedia %} 4 | 5 | {% block extrahead %}{{ block.super }} 6 | 8 | {% endblock %} 9 | 10 | {% block breadcrumbs %}{% if not is_popup %} 11 | 18 | {% endif %}{% endblock %} 19 | 20 | {% block content %} 21 |
22 | 23 |
24 | {% csrf_token %} 25 | {% if user_form.errors %} 26 |
27 |

{% trans "Please correct the errors below." %}

28 |
29 | {% endif %} 30 |
31 |

{% trans "Users" %}

32 | {% for error in user_form.errors %} 33 |

{{ error }}

34 | {% endfor %} 35 |
36 | 37 | 38 | 39 | 40 | 41 | {% for perm in model_perms %} 42 | 43 | {% endfor %} 44 | 45 | 46 | 47 | 48 | {% for user, user_perms in users_perms.items %} 49 | 50 | 51 | {% for perm in model_perms %} 52 | 59 | {% endfor %} 60 | 63 | 64 | {% endfor %} 65 | 66 |
{% trans "User permissions" %}
{% trans "User" %}{{ perm.name }}{% trans "Action" %}
{{ user }} 53 | {% if perm.codename in user_perms %} 54 | 55 | {% else %} 56 | 57 | {% endif %} 58 | 61 | {% trans "Edit" %} 62 |
67 |
68 | {% for field in user_form %} 69 | {% include "admin/guardian/model/field.html" %} 70 | {% endfor %} 71 |
72 | 73 |
74 |
75 |
76 | 77 |
78 | {% csrf_token %} 79 | {% if group_form.errors %} 80 |
81 |

{% trans "Please correct the errors below." %}

82 |
83 | {% endif %} 84 |
85 |

{% trans "Groups" %}

86 | {% for error in group_form.errors %} 87 |

{{ error }}

88 | {% endfor %} 89 |
90 | 91 | 92 | 93 | 94 | 95 | {% for perm in model_perms %} 96 | 97 | {% endfor %} 98 | 99 | 100 | 101 | 102 | {% for group, group_perms in groups_perms.items %} 103 | 104 | 105 | {% for perm in model_perms %} 106 | 113 | {% endfor %} 114 | 117 | 118 | {% endfor %} 119 | 120 |
{% trans "Group permissions" %}
{% trans "Group" %}{{ perm.name }}{% trans "Action" %}
{{ group }} 107 | {% if perm.codename in group_perms %} 108 | 109 | {% else %} 110 | 111 | {% endif %} 112 | 115 | {% trans "Edit" %} 116 |
121 |
122 | {% for field in group_form %} 123 | {% include "admin/guardian/model/field.html" %} 124 | {% endfor %} 125 |
126 | 127 |
128 | 129 | 130 |
131 | {% endblock %} 132 | 133 | -------------------------------------------------------------------------------- /guardian/migrations/0003_update_objectpermission_object_pk.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import DataMigration 5 | from django.db import models 6 | 7 | class Migration(DataMigration): 8 | 9 | def forwards(self, orm): 10 | """ 11 | Updates ``object_pk`` fields on both ``UserObjectPermission`` and 12 | ``GroupObjectPermission`` from ``object_id`` values. 13 | """ 14 | for Model in [orm.UserObjectPermission, orm.GroupObjectPermission]: 15 | for obj in Model.objects.all(): 16 | obj.object_pk = str(obj.object_id) 17 | obj.save() 18 | 19 | def backwards(self, orm): 20 | """ 21 | Updates ``object_id`` fields on both ``UserObjectPermission`` and 22 | ``GroupObjectPermission`` from ``object_pk`` values. 23 | """ 24 | for Model in [orm.UserObjectPermission, orm.GroupObjectPermission]: 25 | for obj in Model.objects.all(): 26 | obj.object_id = int(obj.object_pk) 27 | obj.save() 28 | 29 | models = { 30 | 'auth.group': { 31 | 'Meta': {'object_name': 'Group'}, 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 34 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 35 | }, 36 | 'auth.permission': { 37 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 38 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 39 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 40 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 41 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 42 | }, 43 | 'auth.user': { 44 | 'Meta': {'object_name': 'User'}, 45 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 47 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 48 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 51 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 52 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 53 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 54 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 55 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 56 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 57 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 58 | }, 59 | 'contenttypes.contenttype': { 60 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 61 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 62 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 63 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 64 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 65 | }, 66 | 'guardian.groupobjectpermission': { 67 | 'Meta': {'unique_together': "(['group', 'permission', 'content_type', 'object_id'],)", 'object_name': 'GroupObjectPermission'}, 68 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 69 | 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}), 70 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 71 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 72 | 'object_pk': ('django.db.models.fields.TextField', [], {'default': "''"}), 73 | 'permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Permission']"}) 74 | }, 75 | 'guardian.userobjectpermission': { 76 | 'Meta': {'unique_together': "(['user', 'permission', 'content_type', 'object_id'],)", 'object_name': 'UserObjectPermission'}, 77 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 78 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 79 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 80 | 'object_pk': ('django.db.models.fields.TextField', [], {'default': "''"}), 81 | 'permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Permission']"}), 82 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 83 | } 84 | } 85 | 86 | complete_apps = ['guardian'] 87 | -------------------------------------------------------------------------------- /guardian/migrations/0002_auto__add_field_groupobjectpermission_object_pk__add_field_userobjectp.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'GroupObjectPermission.object_pk' 12 | db.add_column('guardian_groupobjectpermission', 'object_pk', self.gf('django.db.models.fields.TextField')(default=''), keep_default=False) 13 | 14 | # Adding field 'UserObjectPermission.object_pk' 15 | db.add_column('guardian_userobjectpermission', 'object_pk', self.gf('django.db.models.fields.TextField')(default=''), keep_default=False) 16 | 17 | 18 | def backwards(self, orm): 19 | 20 | # Deleting field 'GroupObjectPermission.object_pk' 21 | db.delete_column('guardian_groupobjectpermission', 'object_pk') 22 | 23 | # Deleting field 'UserObjectPermission.object_pk' 24 | db.delete_column('guardian_userobjectpermission', 'object_pk') 25 | 26 | 27 | models = { 28 | 'auth.group': { 29 | 'Meta': {'object_name': 'Group'}, 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 32 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 33 | }, 34 | 'auth.permission': { 35 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 36 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 37 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 38 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 39 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 40 | }, 41 | 'auth.user': { 42 | 'Meta': {'object_name': 'User'}, 43 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 44 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 45 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 46 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 47 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 48 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 49 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 50 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 51 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 52 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 53 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 54 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 55 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 56 | }, 57 | 'contenttypes.contenttype': { 58 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 59 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 60 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 61 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 62 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 63 | }, 64 | 'guardian.groupobjectpermission': { 65 | 'Meta': {'unique_together': "(['group', 'permission', 'content_type', 'object_id'],)", 'object_name': 'GroupObjectPermission'}, 66 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 67 | 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}), 68 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 69 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 70 | 'object_pk': ('django.db.models.fields.TextField', [], {'default': "''"}), 71 | 'permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Permission']"}) 72 | }, 73 | 'guardian.userobjectpermission': { 74 | 'Meta': {'unique_together': "(['user', 'permission', 'content_type', 'object_id'],)", 'object_name': 'UserObjectPermission'}, 75 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 76 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 77 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 78 | 'object_pk': ('django.db.models.fields.TextField', [], {'default': "''"}), 79 | 'permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Permission']"}), 80 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 81 | } 82 | } 83 | 84 | complete_apps = ['guardian'] 85 | -------------------------------------------------------------------------------- /docs/theme/ADCtheme/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {%- block doctype -%} 3 | 5 | {%- endblock %} 6 | {%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %} 7 | {%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %} 8 | {%- block linktags %} 9 | {%- if hasdoc('about') %} 10 | 11 | {%- endif %} 12 | {%- if hasdoc('genindex') %} 13 | 14 | {%- endif %} 15 | {%- if hasdoc('search') %} 16 | 17 | {%- endif %} 18 | {%- if hasdoc('copyright') %} 19 | 20 | {%- endif %} 21 | 22 | {%- if parents %} 23 | 24 | {%- endif %} 25 | {%- if next %} 26 | 27 | {%- endif %} 28 | {%- if prev %} 29 | 30 | {%- endif %} 31 | 32 | {%- endblock %} 33 | {%- block extrahead %} {% endblock %} 34 | {%- block header %}{% endblock %} 35 | {%- block relbar1 %} 36 |
37 |

{{docstitle}}

38 |
39 | 50 | {% endblock %} 51 | 52 | {%- block sidebar1 %} 53 | {%- if not embedded %}{% if not theme_nosidebar|tobool %} 54 |
55 |
56 | {%- block sidebarlogo %} 57 | {%- if logo %} 58 | 61 | {%- endif %} 62 | {%- endblock %} 63 | {%- block sidebartoc %} 64 | 65 | {{ toctree() }} 66 | {%- endblock %} 67 | {%- block sidebarrel %} 68 | {%- endblock %} 69 | {%- block sidebarsourcelink %} 70 | {%- if show_source and has_source and sourcename %} 71 |

{{ _('This Page') }}

72 | 76 | {%- endif %} 77 | {%- endblock %} 78 | {%- if customsidebar %} 79 | {% include customsidebar %} 80 | {%- endif %} 81 | {%- block sidebarsearch %} 82 | {%- if pagename != "search" %} 83 | 99 | 100 | {%- endif %} 101 | {%- endblock %} 102 |
103 |
104 | {%- endif %}{% endif %} 105 | 106 | {% endblock %} 107 | {%- block document %} 108 |
109 | {%- if not embedded %}{% if not theme_nosidebar|tobool %} 110 |
111 | {%- endif %}{% endif %} 112 |
113 | {% block body %} {% endblock %} 114 |
115 | {%- if not embedded %}{% if not theme_nosidebar|tobool %} 116 |
117 | {%- endif %}{% endif %} 118 |
119 | 133 | {%- endblock %} 134 | {%- block sidebar2 %}{% endblock %} 135 | {%- block relbar2 %}{% endblock %} 136 | {%- block footer %} 137 | 144 | 145 | {%- endblock %} 146 | -------------------------------------------------------------------------------- /guardian/templates/admin/guardian/contrib/grappelli/obj_perms_manage.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | {% load adminmedia %} 4 | 5 | {% block extrahead %}{{ block.super }} 6 | 13 | {% endblock %} 14 | 15 | {% block breadcrumbs %}{% if not is_popup %} 16 | 23 | {% endif %}{% endblock %} 24 | 25 | {% block content %} 26 |
27 | 28 |
29 | {% csrf_token %} 30 | {% if user_form.errors %} 31 |
32 |

{% trans "Please correct the errors below." %}

33 |
34 | {% endif %} 35 |
36 |

{% trans "Users" %}

37 | {% for error in user_form.errors %} 38 |

{{ error }}

39 | {% endfor %} 40 |
41 | 42 | 43 | 44 | 45 | 46 | {% for perm in model_perms %} 47 | 48 | {% endfor %} 49 | 50 | 51 | 52 | 53 | {% for user, user_perms in users_perms.items %} 54 | 55 | 56 | {% for perm in model_perms %} 57 | 64 | {% endfor %} 65 | 68 | 69 | {% endfor %} 70 | 71 |
{% trans "User permissions" %}
{% trans "User" %}{{ perm.name }}{% trans "Action" %}
{{ user }} 58 | {% if perm.codename in user_perms %} 59 | 60 | {% else %} 61 | 62 | {% endif %} 63 | 66 | {% trans "Edit" %} 67 |
72 |
73 |
74 | {% for field in user_form %} 75 | {% include "admin/guardian/contrib/grappelli/field.html" %} 76 | {% endfor %} 77 |
    78 |
  • 79 | 80 |
  • 81 |
82 |
83 |
84 |
85 | 86 |
87 | 88 |
89 | {% csrf_token %} 90 | {% if group_form.errors %} 91 |
92 |

{% trans "Please correct the errors below." %}

93 |
94 | {% endif %} 95 |
96 |

{% trans "Groups" %}

97 | {% for error in group_form.errors %} 98 |

{{ error }}

99 | {% endfor %} 100 |
101 | 102 | 103 | 104 | 105 | 106 | {% for perm in model_perms %} 107 | 108 | {% endfor %} 109 | 110 | 111 | 112 | 113 | {% for group, group_perms in groups_perms.items %} 114 | 115 | 116 | {% for perm in model_perms %} 117 | 124 | {% endfor %} 125 | 128 | 129 | {% endfor %} 130 | 131 |
{% trans "Group permissions" %}
{% trans "Group" %}{{ perm.name }}{% trans "Action" %}
{{ group }} 118 | {% if perm.codename in group_perms %} 119 | 120 | {% else %} 121 | 122 | {% endif %} 123 | 126 | {% trans "Edit" %} 127 |
132 |
133 |
134 | {% for field in group_form %} 135 | {% include "admin/guardian/contrib/grappelli/field.html" %} 136 | {% endfor %} 137 |
    138 |
  • 139 | 140 |
  • 141 |
142 |
143 | 144 | 145 |
146 | {% endblock %} 147 | 148 | 149 | -------------------------------------------------------------------------------- /guardian/decorators.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import REDIRECT_FIELD_NAME 3 | from django.core.exceptions import PermissionDenied 4 | from django.http import HttpResponseForbidden, HttpResponseRedirect 5 | from django.utils.functional import wraps 6 | from django.utils.http import urlquote 7 | from django.db.models import Model, get_model 8 | from django.db.models.base import ModelBase 9 | from django.db.models.query import QuerySet 10 | from django.shortcuts import get_object_or_404, render_to_response 11 | from django.template import RequestContext, TemplateDoesNotExist 12 | from guardian.conf import settings as guardian_settings 13 | from guardian.exceptions import GuardianError 14 | 15 | 16 | def permission_required(perm, lookup_variables=None, **kwargs): 17 | """ 18 | Decorator for views that checks whether a user has a particular permission 19 | enabled. 20 | 21 | Optionally, instances for which check should be made may be passed as an 22 | second argument or as a tuple parameters same as those passed to 23 | ``get_object_or_404`` but must be provided as pairs of strings. 24 | 25 | :param login_url: if denied, user would be redirected to location set by 26 | this parameter. Defaults to ``django.conf.settings.LOGIN_URL``. 27 | :param redirect_field_name: name of the parameter passed if redirected. 28 | Defaults to ``django.contrib.auth.REDIRECT_FIELD_NAME``. 29 | :param return_403: if set to ``True`` then instead of redirecting to the 30 | login page, response with status code 403 is returned ( 31 | ``django.http.HttpResponseForbidden`` instance or rendered template - 32 | see :setting:`GUARDIAN_RENDER_403`). Defaults to ``False``. 33 | 34 | Examples:: 35 | 36 | @permission_required('auth.change_user', return_403=True) 37 | def my_view(request): 38 | return HttpResponse('Hello') 39 | 40 | @permission_required('auth.change_user', (User, 'username', 'username')) 41 | def my_view(request, username): 42 | user = get_object_or_404(User, username=username) 43 | return user.get_absolute_url() 44 | 45 | @permission_required('auth.change_user', 46 | (User, 'username', 'username', 'groups__name', 'group_name')) 47 | def my_view(request, username, group_name): 48 | user = get_object_or_404(User, username=username, 49 | group__name=group_name) 50 | return user.get_absolute_url() 51 | 52 | """ 53 | login_url = kwargs.pop('login_url', settings.LOGIN_URL) 54 | redirect_field_name = kwargs.pop('redirect_field_name', REDIRECT_FIELD_NAME) 55 | return_403 = kwargs.pop('return_403', False) 56 | 57 | # Check if perm is given as string in order not to decorate 58 | # view function itself which makes debugging harder 59 | if not isinstance(perm, basestring): 60 | raise GuardianError("First argument must be in format: " 61 | "'app_label.codename or a callable which return similar string'") 62 | 63 | def decorator(view_func): 64 | def _wrapped_view(request, *args, **kwargs): 65 | # if more than one parameter is passed to the decorator we try to 66 | # fetch object for which check would be made 67 | obj = None 68 | if lookup_variables: 69 | model, lookups = lookup_variables[0], lookup_variables[1:] 70 | # Parse model 71 | if isinstance(model, basestring): 72 | splitted = model.split('.') 73 | if len(splitted) != 2: 74 | raise GuardianError("If model should be looked up from " 75 | "string it needs format: 'app_label.ModelClass'") 76 | model = get_model(*splitted) 77 | elif type(model) in (Model, ModelBase, QuerySet): 78 | pass 79 | else: 80 | raise GuardianError("First lookup argument must always be " 81 | "a model, string pointing at app/model or queryset. " 82 | "Given: %s (type: %s)" % (model, type(model))) 83 | # Parse lookups 84 | if len(lookups) % 2 != 0: 85 | raise GuardianError("Lookup variables must be provided " 86 | "as pairs of lookup_string and view_arg") 87 | lookup_dict = {} 88 | for lookup, view_arg in zip(lookups[::2], lookups[1::2]): 89 | if view_arg not in kwargs: 90 | raise GuardianError("Argument %s was not passed " 91 | "into view function" % view_arg) 92 | lookup_dict[lookup] = kwargs[view_arg] 93 | obj = get_object_or_404(model, **lookup_dict) 94 | 95 | 96 | # Handles both original and with object provided permission check 97 | # as ``obj`` defaults to None 98 | if not request.user.has_perm(perm, obj): 99 | if return_403: 100 | if guardian_settings.RENDER_403: 101 | try: 102 | response = render_to_response( 103 | guardian_settings.TEMPLATE_403, {}, 104 | RequestContext(request)) 105 | response.status_code = 403 106 | return response 107 | except TemplateDoesNotExist, e: 108 | if settings.DEBUG: 109 | raise e 110 | elif guardian_settings.RAISE_403: 111 | raise PermissionDenied 112 | return HttpResponseForbidden() 113 | else: 114 | path = urlquote(request.get_full_path()) 115 | tup = login_url, redirect_field_name, path 116 | return HttpResponseRedirect("%s?%s=%s" % tup) 117 | return view_func(request, *args, **kwargs) 118 | return wraps(view_func)(_wrapped_view) 119 | return decorator 120 | 121 | def permission_required_or_403(perm, *args, **kwargs): 122 | """ 123 | Simple wrapper for permission_required decorator. 124 | 125 | Standard Django's permission_required decorator redirects user to login page 126 | in case permission check failed. This decorator may be used to return 127 | HttpResponseForbidden (status 403) instead of redirection. 128 | 129 | The only difference between ``permission_required`` decorator is that this 130 | one always set ``return_403`` parameter to ``True``. 131 | """ 132 | kwargs['return_403'] = True 133 | return permission_required(perm, *args, **kwargs) 134 | 135 | -------------------------------------------------------------------------------- /docs/userguide/check.rst: -------------------------------------------------------------------------------- 1 | .. _check: 2 | 3 | Check object permissions 4 | ======================== 5 | 6 | Once we have :ref:`assigned some permissions ` we can get into detail 7 | about verifying permissions of user or group. 8 | 9 | Standard way 10 | ------------ 11 | 12 | Normally to check if Joe is permitted to change ``Site`` objects we do this by 13 | calling ``has_perm`` method on an ``User`` instance:: 14 | 15 | >>> joe.has_perm('sites.change_site') 16 | False 17 | 18 | And for a specific ``Site`` instance we do the same but we pass ``site`` as 19 | additional argument:: 20 | 21 | >>> site = Site.objects.get_current() 22 | >>> joe.has_perm('sites.change_site', site) 23 | False 24 | 25 | Lets assign permission and check again:: 26 | 27 | >>> from guardian.shortcuts import assign 28 | >>> assign('sites.change_site', joe, site) 29 | 30 | >>> joe = User.objects.get(username='joe') 31 | >>> joe.has_perm('sites.change_site', site) 32 | True 33 | 34 | This uses backend we have specified at settings module (see 35 | :ref:`configuration`). More on a backend itself can be found at 36 | :class:`Backend's API `. 37 | 38 | Inside views 39 | ------------ 40 | 41 | Besides of standard ``has_perm`` method ``django-guardian`` provides some useful 42 | helpers for object permission checks. 43 | 44 | get_perms 45 | ~~~~~~~~~ 46 | 47 | To check permissions we can use quick-and-dirty shortcut:: 48 | 49 | >>> from guardian.shortcuts import get_perms 50 | >>> 51 | >>> joe = User.objects.get(username='joe') 52 | >>> site = Site.objects.get_current() 53 | >>> 54 | >>> 'change_site' in get_perms(joe, site) 55 | True 56 | 57 | 58 | It is probably better to use standard ``has_perm`` method. But for ``Group`` 59 | instances it is not as easy and ``get_perms`` could be handy here as it accepts 60 | both ``User`` and ``Group`` instances. And if we need to do some more work we 61 | can use lower level ``ObjectPermissionChecker`` class which is described in next 62 | section. 63 | 64 | get_objects_for_user 65 | ~~~~~~~~~~~~~~~~~~~~ 66 | 67 | Sometimes there is a need to extract list of objects based on particular user, 68 | type of the object and provided permissions. For instance, lets say there is a 69 | ``Project`` model at ``projects`` application with custom ``view_project`` 70 | permission. We want to show our users projects they can actually *view*. This 71 | could be easily achieved using :shortcut:`get_objects_for_user`: 72 | 73 | .. code-block:: python 74 | 75 | from django.shortcuts import render_to_response 76 | from django.template import RequestContext 77 | from projects.models import Project 78 | from guardian.shortcuts import get_objects_for_user 79 | 80 | def user_dashboard(request, template_name='projects/dashboard.html'): 81 | projects = get_objects_for_user(request.user, 'projects.view_project') 82 | return render_to_response(template_name, {'projects': projects}, 83 | RequestContext(request)) 84 | 85 | It is also possible to provide list of permissions rather than single string, 86 | own queryset (as ``klass`` argument) or control if result should be computed 87 | with (default) or without user's groups permissions. 88 | 89 | .. seealso:: 90 | Documentation for :shortcut:`get_objects_for_user` 91 | 92 | 93 | ObjectPermissionChecker 94 | ~~~~~~~~~~~~~~~~~~~~~~~ 95 | 96 | At the ``core`` module of ``django-guardian`` there is a 97 | :class:`guardian.core.ObjectPermissionChecker` which checks permission of 98 | user/group for specific object. It caches results so it may be used at part of 99 | codes where we check permissions more than once. 100 | 101 | Let's see it in action:: 102 | 103 | >>> joe = User.objects.get(username='joe') 104 | >>> site = Site.objects.get_current() 105 | >>> from guardian.core import ObjectPermissionChecker 106 | >>> checker = ObjectPermissionChecker(joe) # we can pass user or group 107 | >>> checker.has_perm('change_site', site) 108 | True 109 | >>> checker.has_perm('add_site', site) # no additional query made 110 | False 111 | >>> checker.get_perms(site) 112 | [u'change_site'] 113 | 114 | Using decorators 115 | ~~~~~~~~~~~~~~~~ 116 | 117 | Standard ``permission_required`` decorator doesn't allow to check for object 118 | permissions. ``django-guardian`` is shipped with two decorators which may be 119 | helpful for simple object permission checks but remember that those decorators 120 | hits database before decorated view is called - this means that if there is 121 | similar lookup made within a view then most probably one (or more, depending 122 | on lookups) extra database query would occur. 123 | 124 | Let's assume we pass ``'group_name'`` argument to our view function which 125 | returns form to edit the group. Moreover, we want to return 403 code if check 126 | fails. This can be simply achieved using ``permission_required_or_403`` 127 | decorator:: 128 | 129 | >>> joe = User.objects.get(username='joe') 130 | >>> foobars = Group.objects.create(name='foobars') 131 | >>> 132 | >>> from guardian.decorators import permission_required_or_403 133 | >>> from django.http import HttpResponse 134 | >>> 135 | >>> @permission_required_or_403('auth.change_group', 136 | >>> (Group, 'name', 'group_name')) 137 | >>> def edit_group(request, group_name): 138 | >>> return HttpResponse('some form') 139 | >>> 140 | >>> from django.http import HttpRequest 141 | >>> request = HttpRequest() 142 | >>> request.user = joe 143 | >>> edit_group(request, group_name='foobars') 144 | 145 | >>> 146 | >>> joe.groups.add(foobars) 147 | >>> edit_group(request, group_name='foobars') 148 | 149 | >>> 150 | >>> from guardian.shortcuts import assign 151 | >>> assign('auth.change_group', joe, foobars) 152 | 153 | >>> 154 | >>> edit_group(request, group_name='foobars') 155 | 156 | >>> # Note that we now get normal HttpResponse, not forbidden 157 | 158 | More on decorators can be read at corresponding :ref:`API page `. 159 | 160 | .. note:: 161 | Overall idea of decorators' lookups was taken from `django-authority`_ and 162 | all credits go to it's creator, Jannis Leidel. 163 | 164 | Inside templates 165 | ---------------- 166 | 167 | ``django-guardian`` comes with special template tag 168 | :func:`guardian.templatetags.guardian_tags.get_obj_perms` which can store object 169 | permissions for a given user/group and instance pair. In order to use it we need 170 | to put following inside a template:: 171 | 172 | {% load guardian_tags %} 173 | 174 | get_obj_perms 175 | ~~~~~~~~~~~~~ 176 | 177 | .. autofunction:: guardian.templatetags.guardian_tags.get_obj_perms 178 | :noindex: 179 | 180 | .. _django-authority: http://bitbucket.org/jezdez/django-authority/ 181 | 182 | -------------------------------------------------------------------------------- /guardian/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import ugettext as _ 3 | 4 | from guardian.shortcuts import assign 5 | from guardian.shortcuts import remove_perm 6 | from guardian.shortcuts import get_perms 7 | from guardian.shortcuts import get_perms_for_model 8 | 9 | 10 | class BaseObjectPermissionsForm(forms.Form): 11 | """ 12 | Base form for object permissions management. Needs to be extended for usage 13 | with users and/or groups. 14 | """ 15 | 16 | def __init__(self, obj, *args, **kwargs): 17 | """ 18 | :param obj: Any instance which form would use to manage object 19 | permissions" 20 | """ 21 | self.obj = obj 22 | super(BaseObjectPermissionsForm, self).__init__(*args, **kwargs) 23 | field_name = self.get_obj_perms_field_name() 24 | self.fields[field_name] = self.get_obj_perms_field() 25 | 26 | def get_obj_perms_field(self): 27 | """ 28 | Returns field instance for object permissions management. May be 29 | replaced entirely. 30 | """ 31 | field_class = self.get_obj_perms_field_class() 32 | field = field_class( 33 | label=self.get_obj_perms_field_label(), 34 | choices=self.get_obj_perms_field_choices(), 35 | initial=self.get_obj_perms_field_initial(), 36 | widget=self.get_obj_perms_field_widget(), 37 | required=self.are_obj_perms_required(), 38 | ) 39 | return field 40 | 41 | def get_obj_perms_field_name(self): 42 | """ 43 | Returns name of the object permissions management field. Default: 44 | ``permission``. 45 | """ 46 | return 'permissions' 47 | 48 | def get_obj_perms_field_label(self): 49 | """ 50 | Returns label of the object permissions management field. Defualt: 51 | ``_("Permissions")`` (marked to be translated). 52 | """ 53 | return _("Permissions") 54 | 55 | def get_obj_perms_field_choices(self): 56 | """ 57 | Returns choices for object permissions management field. Default: 58 | list of tuples ``(codename, name)`` for each ``Permission`` instance 59 | for the managed object. 60 | """ 61 | choices = [(p.codename, p.name) for p in get_perms_for_model(self.obj)] 62 | return choices 63 | 64 | def get_obj_perms_field_initial(self): 65 | """ 66 | Returns initial object permissions management field choices. Default: 67 | ``[]`` (empty list). 68 | """ 69 | return [] 70 | 71 | def get_obj_perms_field_class(self): 72 | """ 73 | Returns object permissions management field's base class. Default: 74 | ``django.forms.MultipleChoiceField``. 75 | """ 76 | return forms.MultipleChoiceField 77 | 78 | def get_obj_perms_field_widget(self): 79 | """ 80 | Returns object permissions management field's widget base class. 81 | Default: ``django.forms.SelectMultiple``. 82 | """ 83 | return forms.SelectMultiple 84 | 85 | def are_obj_perms_required(self): 86 | """ 87 | Indicates if at least one object permission should be required. Default: 88 | ``False``. 89 | """ 90 | return False 91 | 92 | def save_obj_perms(self): 93 | """ 94 | Must be implemented in concrete form class. This method should store 95 | selected object permissions. 96 | """ 97 | raise NotImplementedError 98 | 99 | 100 | class UserObjectPermissionsForm(BaseObjectPermissionsForm): 101 | """ 102 | Object level permissions management form for usage with ``User`` instances. 103 | 104 | Example usage:: 105 | 106 | from django.contrib.auth.models import User 107 | from django.shortcuts import get_object_or_404 108 | from myapp.models import Post 109 | from guardian.forms import UserObjectPermissionsForm 110 | 111 | def my_view(request, post_slug, user_id): 112 | user = get_object_or_404(User, id=user_id) 113 | post = get_object_or_404(Post, slug=post_slug) 114 | form = UserObjectPermissionsForm(user, post, request.POST or None) 115 | if request.method == 'POST' and form.is_valid(): 116 | form.save_obj_perms() 117 | ... 118 | 119 | """ 120 | 121 | def __init__(self, user, *args, **kwargs): 122 | self.user = user 123 | super(UserObjectPermissionsForm, self).__init__(*args, **kwargs) 124 | 125 | def get_obj_perms_field_initial(self): 126 | perms = get_perms(self.user, self.obj) 127 | return perms 128 | 129 | def save_obj_perms(self): 130 | """ 131 | Saves selected object permissions by creating new ones and removing 132 | those which were not selected but already exists. 133 | 134 | Should be called *after* form is validated. 135 | """ 136 | perms = self.cleaned_data[self.get_obj_perms_field_name()] 137 | model_perms = [c[0] for c in self.get_obj_perms_field_choices()] 138 | 139 | to_remove = set(model_perms) - set(perms) 140 | for perm in to_remove: 141 | remove_perm(perm, self.user, self.obj) 142 | 143 | for perm in perms: 144 | assign(perm, self.user, self.obj) 145 | 146 | 147 | class GroupObjectPermissionsForm(BaseObjectPermissionsForm): 148 | """ 149 | Object level permissions management form for usage with ``Group`` instances. 150 | 151 | Example usage:: 152 | 153 | from django.contrib.auth.models import Group 154 | from django.shortcuts import get_object_or_404 155 | from myapp.models import Post 156 | from guardian.forms import GroupObjectPermissionsForm 157 | 158 | def my_view(request, post_slug, group_id): 159 | group = get_object_or_404(Group, id=group_id) 160 | post = get_object_or_404(Post, slug=post_slug) 161 | form = GroupObjectPermissionsForm(group, post, request.POST or None) 162 | if request.method == 'POST' and form.is_valid(): 163 | form.save_obj_perms() 164 | ... 165 | 166 | """ 167 | 168 | def __init__(self, group, *args, **kwargs): 169 | self.group = group 170 | super(GroupObjectPermissionsForm, self).__init__(*args, **kwargs) 171 | 172 | def get_obj_perms_field_initial(self): 173 | perms = get_perms(self.group, self.obj) 174 | return perms 175 | 176 | def save_obj_perms(self): 177 | """ 178 | Saves selected object permissions by creating new ones and removing 179 | those which were not selected but already exists. 180 | 181 | Should be called *after* form is validated. 182 | """ 183 | perms = self.cleaned_data[self.get_obj_perms_field_name()] 184 | model_perms = [c[0] for c in self.get_obj_perms_field_choices()] 185 | 186 | to_remove = set(model_perms) - set(perms) 187 | for perm in to_remove: 188 | remove_perm(perm, self.group, self.obj) 189 | 190 | for perm in perms: 191 | assign(perm, self.group, self.obj) 192 | 193 | -------------------------------------------------------------------------------- /guardian/tests/core_test.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | 3 | from django.conf import settings 4 | from django.contrib.auth import models as auth_app 5 | from django.contrib.auth.management import create_permissions 6 | from django.contrib.auth.models import User, Group, Permission, AnonymousUser 7 | from django.contrib.contenttypes.models import ContentType 8 | from django.test import TestCase 9 | 10 | from guardian.core import ObjectPermissionChecker 11 | from guardian.exceptions import NotUserNorGroup 12 | from guardian.models import UserObjectPermission, GroupObjectPermission 13 | from guardian.shortcuts import assign 14 | 15 | class ObjectPermissionTestCase(TestCase): 16 | 17 | def setUp(self): 18 | self.group, created = Group.objects.get_or_create(name='jackGroup') 19 | self.user, created = User.objects.get_or_create(username='jack') 20 | self.user.groups.add(self.group) 21 | self.ctype = ContentType.objects.create(name='foo', model='bar', 22 | app_label='fake-for-guardian-tests') 23 | self.anonymous_user, created = User.objects.get_or_create( 24 | id=settings.ANONYMOUS_USER_ID, 25 | username='AnonymousUser') 26 | 27 | 28 | class ObjectPermissionCheckerTest(ObjectPermissionTestCase): 29 | 30 | def setUp(self): 31 | super(ObjectPermissionCheckerTest, self).setUp() 32 | # Required if MySQL backend is used :/ 33 | create_permissions(auth_app, [], 1) 34 | 35 | def test_cache_for_queries_count(self): 36 | settings.DEBUG = True 37 | try: 38 | from django.db import connection 39 | 40 | ContentType.objects.clear_cache() 41 | checker = ObjectPermissionChecker(self.user) 42 | 43 | # has_perm on Checker should spawn only one query plus one extra for 44 | # fetching the content type first time we check for specific model 45 | query_count = len(connection.queries) 46 | res = checker.has_perm("change_group", self.group) 47 | self.assertEqual(len(connection.queries), query_count + 2) 48 | 49 | # Checking again shouldn't spawn any queries 50 | query_count = len(connection.queries) 51 | res_new = checker.has_perm("change_group", self.group) 52 | self.assertEqual(res, res_new) 53 | self.assertEqual(len(connection.queries), query_count) 54 | 55 | # Checking for other permission but for Group object again 56 | # shouldn't spawn any query too 57 | query_count = len(connection.queries) 58 | checker.has_perm("delete_group", self.group) 59 | self.assertEqual(len(connection.queries), query_count) 60 | 61 | # Checking for same model but other instance should spawn 1 query 62 | new_group = Group.objects.create(name='new-group') 63 | query_count = len(connection.queries) 64 | checker.has_perm("change_group", new_group) 65 | self.assertEqual(len(connection.queries), query_count + 1) 66 | 67 | # Checking for permission for other model should spawn 2 queries 68 | # (again: content type and actual permissions for the object) 69 | query_count = len(connection.queries) 70 | checker.has_perm("change_user", self.user) 71 | self.assertEqual(len(connection.queries), query_count + 2) 72 | 73 | finally: 74 | settings.DEBUG = False 75 | 76 | def test_init(self): 77 | self.assertRaises(NotUserNorGroup, ObjectPermissionChecker, 78 | user_or_group=ContentType()) 79 | self.assertRaises(NotUserNorGroup, ObjectPermissionChecker) 80 | 81 | def test_anonymous_user(self): 82 | user = AnonymousUser() 83 | check = ObjectPermissionChecker(user) 84 | # assert anonymous user has no object permissions at all for obj 85 | self.assertTrue( [] == list(check.get_perms(self.ctype)) ) 86 | 87 | def test_superuser(self): 88 | user = User.objects.create(username='superuser', is_superuser=True) 89 | check = ObjectPermissionChecker(user) 90 | ctype = ContentType.objects.get_for_model(self.ctype) 91 | perms = sorted(chain(*Permission.objects 92 | .filter(content_type=ctype) 93 | .values_list('codename'))) 94 | self.assertEqual(perms, check.get_perms(self.ctype)) 95 | for perm in perms: 96 | self.assertTrue(check.has_perm(perm, self.ctype)) 97 | 98 | def test_not_active_superuser(self): 99 | user = User.objects.create(username='not_active_superuser', 100 | is_superuser=True, is_active=False) 101 | check = ObjectPermissionChecker(user) 102 | ctype = ContentType.objects.get_for_model(self.ctype) 103 | perms = sorted(chain(*Permission.objects 104 | .filter(content_type=ctype) 105 | .values_list('codename'))) 106 | self.assertEqual(check.get_perms(self.ctype), []) 107 | for perm in perms: 108 | self.assertFalse(check.has_perm(perm, self.ctype)) 109 | 110 | def test_not_active_user(self): 111 | user = User.objects.create(username='notactive') 112 | assign("change_contenttype", user, self.ctype) 113 | 114 | # new ObjectPermissionChecker is created for each User.has_perm call 115 | self.assertTrue(user.has_perm("change_contenttype", self.ctype)) 116 | user.is_active = False 117 | self.assertFalse(user.has_perm("change_contenttype", self.ctype)) 118 | 119 | # use on one checker only (as user's is_active attr should be checked 120 | # before try to use cache 121 | user = User.objects.create(username='notactive-cache') 122 | assign("change_contenttype", user, self.ctype) 123 | 124 | check = ObjectPermissionChecker(user) 125 | self.assertTrue(check.has_perm("change_contenttype", self.ctype)) 126 | user.is_active = False 127 | self.assertFalse(check.has_perm("change_contenttype", self.ctype)) 128 | 129 | def test_get_perms(self): 130 | group = Group.objects.create(name='group') 131 | obj1 = ContentType.objects.create(name='ct1', model='foo', 132 | app_label='guardian-tests') 133 | obj2 = ContentType.objects.create(name='ct2', model='bar', 134 | app_label='guardian-tests') 135 | 136 | assign_perms = { 137 | group: ('change_group', 'delete_group'), 138 | obj1: ('change_contenttype', 'delete_contenttype'), 139 | obj2: ('delete_contenttype',), 140 | } 141 | 142 | check = ObjectPermissionChecker(self.user) 143 | 144 | for obj, perms in assign_perms.items(): 145 | for perm in perms: 146 | UserObjectPermission.objects.assign(perm, self.user, obj) 147 | self.assertEqual(sorted(perms), sorted(check.get_perms(obj))) 148 | 149 | check = ObjectPermissionChecker(self.group) 150 | 151 | for obj, perms in assign_perms.items(): 152 | for perm in perms: 153 | GroupObjectPermission.objects.assign(perm, self.group, obj) 154 | self.assertEqual(sorted(perms), sorted(check.get_perms(obj))) 155 | 156 | -------------------------------------------------------------------------------- /guardian/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'UserObjectPermission' 12 | db.create_table('guardian_userobjectpermission', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('permission', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Permission'])), 15 | ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), 16 | ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 17 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), 18 | )) 19 | db.send_create_signal('guardian', ['UserObjectPermission']) 20 | 21 | # Adding unique constraint on 'UserObjectPermission', fields ['user', 'permission', 'content_type', 'object_id'] 22 | db.create_unique('guardian_userobjectpermission', ['user_id', 'permission_id', 'content_type_id', 'object_id']) 23 | 24 | # Adding model 'GroupObjectPermission' 25 | db.create_table('guardian_groupobjectpermission', ( 26 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 27 | ('permission', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Permission'])), 28 | ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), 29 | ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 30 | ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'])), 31 | )) 32 | db.send_create_signal('guardian', ['GroupObjectPermission']) 33 | 34 | # Adding unique constraint on 'GroupObjectPermission', fields ['group', 'permission', 'content_type', 'object_id'] 35 | db.create_unique('guardian_groupobjectpermission', ['group_id', 'permission_id', 'content_type_id', 'object_id']) 36 | 37 | 38 | def backwards(self, orm): 39 | 40 | # Removing unique constraint on 'GroupObjectPermission', fields ['group', 'permission', 'content_type', 'object_id'] 41 | db.delete_unique('guardian_groupobjectpermission', ['group_id', 'permission_id', 'content_type_id', 'object_id']) 42 | 43 | # Removing unique constraint on 'UserObjectPermission', fields ['user', 'permission', 'content_type', 'object_id'] 44 | db.delete_unique('guardian_userobjectpermission', ['user_id', 'permission_id', 'content_type_id', 'object_id']) 45 | 46 | # Deleting model 'UserObjectPermission' 47 | db.delete_table('guardian_userobjectpermission') 48 | 49 | # Deleting model 'GroupObjectPermission' 50 | db.delete_table('guardian_groupobjectpermission') 51 | 52 | 53 | models = { 54 | 'auth.group': { 55 | 'Meta': {'object_name': 'Group'}, 56 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 57 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 58 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 59 | }, 60 | 'auth.permission': { 61 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 62 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 63 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 64 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 65 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 66 | }, 67 | 'auth.user': { 68 | 'Meta': {'object_name': 'User'}, 69 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 70 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 71 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 72 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 75 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 76 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 77 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 78 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 79 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 80 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 81 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 82 | }, 83 | 'contenttypes.contenttype': { 84 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 85 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 86 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 87 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 88 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 89 | }, 90 | 'guardian.groupobjectpermission': { 91 | 'Meta': {'unique_together': "(['group', 'permission', 'content_type', 'object_id'],)", 'object_name': 'GroupObjectPermission'}, 92 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 93 | 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}), 94 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 95 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 96 | 'permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Permission']"}) 97 | }, 98 | 'guardian.userobjectpermission': { 99 | 'Meta': {'unique_together': "(['user', 'permission', 'content_type', 'object_id'],)", 'object_name': 'UserObjectPermission'}, 100 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 101 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 102 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 103 | 'permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Permission']"}), 104 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 105 | } 106 | } 107 | 108 | complete_apps = ['guardian'] 109 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-guardian documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Feb 18 23:18:28 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__)))) 21 | os.environ['DJANGO_SETTINGS_MODULE'] = 'guardian.testsettings' 22 | ANONYMOUS_USER_ID = -1 # Required by guardian 23 | guardian = __import__('guardian') 24 | 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'exts'] 30 | try: 31 | import rst2pdf 32 | if rst2pdf.version >= '0.16': 33 | extensions.append('rst2pdf.pdfbuilder') 34 | except ImportError: 35 | print "[NOTE] In order to build PDF you need rst2pdf with version >=0.16" 36 | 37 | 38 | autoclass_content = "both" 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['templates'] 42 | 43 | # The suffix of source filenames. 44 | source_suffix = '.rst' 45 | 46 | # The encoding of source files. 47 | #source_encoding = 'utf-8' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = u'django-guardian' 54 | copyright = u'2010, Lukasz Balcerzak' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = guardian.get_version() 62 | # The full version, including alpha/beta/rc tags. 63 | release = guardian.__version__ 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | #language = None 68 | 69 | # There are two options for replacing |today|: either, you set today to some 70 | # non-false value, then it is used: 71 | #today = '' 72 | # Else, today_fmt is used as the format for a strftime call. 73 | #today_fmt = '%B %d, %Y' 74 | 75 | # List of documents that shouldn't be included in the build. 76 | #unused_docs = [] 77 | 78 | # List of directories, relative to source directory, that shouldn't be searched 79 | # for source files. 80 | exclude_trees = ['build'] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | 103 | # -- Options for HTML output --------------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. Major themes that come with 106 | # Sphinx are currently 'default' and 'sphinxdoc'. 107 | #html_theme = 'default' 108 | # Theme URL: https://github.com/coordt/ADCtheme/ 109 | html_theme = 'ADCtheme' 110 | 111 | # Theme options are theme-specific and customize the look and feel of a theme 112 | # further. For a list of options available for each theme, see the 113 | # documentation. 114 | #html_theme_options = {} 115 | 116 | # Add any paths that contain custom themes here, relative to this directory. 117 | html_theme_path = ['theme'] 118 | 119 | # The name for this set of Sphinx documents. If None, it defaults to 120 | # " v documentation". 121 | #html_title = None 122 | 123 | # A shorter title for the navigation bar. Default is the same as html_title. 124 | #html_short_title = None 125 | 126 | # The name of an image file (relative to this directory) to place at the top 127 | # of the sidebar. 128 | #html_logo = None 129 | 130 | # The name of an image file (within the static path) to use as favicon of the 131 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 132 | # pixels large. 133 | #html_favicon = None 134 | 135 | # Add any paths that contain custom static files (such as style sheets) here, 136 | # relative to this directory. They are copied after the builtin static files, 137 | # so a file named "default.css" will overwrite the builtin "default.css". 138 | html_static_path = [] 139 | 140 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 141 | # using the given strftime format. 142 | #html_last_updated_fmt = '%b %d, %Y' 143 | 144 | # If true, SmartyPants will be used to convert quotes and dashes to 145 | # typographically correct entities. 146 | #html_use_smartypants = True 147 | 148 | # Custom sidebar templates, maps document names to template names. 149 | #html_sidebars = {} 150 | 151 | # Additional templates that should be rendered to pages, maps page names to 152 | # template names. 153 | #html_additional_pages = {} 154 | 155 | # If false, no module index is generated. 156 | #html_use_modindex = True 157 | 158 | # If false, no index is generated. 159 | #html_use_index = True 160 | 161 | # If true, the index is split into individual pages for each letter. 162 | #html_split_index = False 163 | 164 | # If true, links to the reST sources are added to the pages. 165 | #html_show_sourcelink = True 166 | 167 | # If true, an OpenSearch description file will be output, and all pages will 168 | # contain a tag referring to it. The value of this option must be the 169 | # base URL from which the finished HTML is served. 170 | #html_use_opensearch = '' 171 | 172 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 173 | #html_file_suffix = '' 174 | 175 | # Output file base name for HTML help builder. 176 | htmlhelp_basename = 'guardiandoc' 177 | 178 | 179 | # -- Options for LaTeX output -------------------------------------------------- 180 | 181 | # The paper size ('letter' or 'a4'). 182 | #latex_paper_size = 'letter' 183 | 184 | # The font size ('10pt', '11pt' or '12pt'). 185 | #latex_font_size = '10pt' 186 | 187 | # Grouping the document tree into LaTeX files. List of tuples 188 | # (source start file, target name, title, author, documentclass [howto/manual]). 189 | latex_documents = [ 190 | ('index', 'guardian.tex', u'guardian Documentation', 191 | u'Lukasz Balcerzak', 'manual'), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | #latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | #latex_use_parts = False 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #latex_preamble = '' 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_use_modindex = True 210 | 211 | pdf_documents = [ 212 | ('index', u'django-guardian', u'Documentation for django-guardian', 213 | u'Lukasz Balcerzak'), 214 | ] 215 | pdf_stylesheets = ['sphinx','kerning','a4'] 216 | pdf_break_level = 2 217 | pdf_inline_footnotes = True 218 | #pdf_extensions = ['vectorpdf', 'dotted_toc'] 219 | 220 | -------------------------------------------------------------------------------- /guardian/tests/decorators_test.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from django.test import TestCase 3 | from django.conf import settings 4 | from django.contrib.auth.models import User, Group, AnonymousUser 5 | from django.core.exceptions import PermissionDenied 6 | from django.http import HttpRequest 7 | from django.http import HttpResponse 8 | from django.http import HttpResponseForbidden 9 | from django.http import HttpResponseRedirect 10 | from django.shortcuts import get_object_or_404 11 | from django.template import TemplateDoesNotExist 12 | from guardian.decorators import permission_required, permission_required_or_403 13 | from guardian.exceptions import GuardianError 14 | from guardian.shortcuts import assign 15 | 16 | 17 | class PermissionRequiredTest(TestCase): 18 | 19 | fixtures = ['tests.json'] 20 | 21 | def setUp(self): 22 | self.anon = AnonymousUser() 23 | self.user = User.objects.get(username='jack') 24 | self.group = Group.objects.get(name='jackGroup') 25 | 26 | def _get_request(self, user=None): 27 | if user is None: 28 | user = AnonymousUser() 29 | request = HttpRequest() 30 | request.user = user 31 | return request 32 | 33 | def test_no_args(self): 34 | 35 | try: 36 | @permission_required 37 | def dummy_view(request): 38 | return HttpResponse('dummy_view') 39 | except GuardianError: 40 | pass 41 | else: 42 | self.fail("Trying to decorate using permission_required without " 43 | "permission as first argument should raise exception") 44 | 45 | def test_RENDER_403_is_false(self): 46 | request = self._get_request(self.anon) 47 | 48 | @permission_required_or_403('not_installed_app.change_user') 49 | def dummy_view(request): 50 | return HttpResponse('dummy_view') 51 | 52 | with mock.patch('guardian.conf.settings.RENDER_403', False): 53 | response = dummy_view(request) 54 | self.assertEqual(response.content, '') 55 | self.assertTrue(isinstance(response, HttpResponseForbidden)) 56 | 57 | @mock.patch('guardian.conf.settings.RENDER_403', True) 58 | def test_TEMPLATE_403_setting(self): 59 | request = self._get_request(self.anon) 60 | 61 | @permission_required_or_403('not_installed_app.change_user') 62 | def dummy_view(request): 63 | return HttpResponse('dummy_view') 64 | 65 | with mock.patch('guardian.conf.settings.TEMPLATE_403', 'dummy403.html'): 66 | response = dummy_view(request) 67 | self.assertEqual(response.content, 'foobar403\n') 68 | 69 | @mock.patch('guardian.conf.settings.RENDER_403', True) 70 | def test_403_response_is_empty_if_template_cannot_be_found(self): 71 | request = self._get_request(self.anon) 72 | 73 | @permission_required_or_403('not_installed_app.change_user') 74 | def dummy_view(request): 75 | return HttpResponse('dummy_view') 76 | with mock.patch('guardian.conf.settings.TEMPLATE_403', 77 | '_non-exisitng-403.html'): 78 | response = dummy_view(request) 79 | self.assertEqual(response.status_code, 403) 80 | self.assertEqual(response.content, '') 81 | 82 | @mock.patch('guardian.conf.settings.RENDER_403', True) 83 | def test_403_response_raises_error_if_debug_is_turned_on(self): 84 | org_DEBUG = settings.DEBUG 85 | settings.DEBUG = True 86 | request = self._get_request(self.anon) 87 | 88 | @permission_required_or_403('not_installed_app.change_user') 89 | def dummy_view(request): 90 | return HttpResponse('dummy_view') 91 | with mock.patch('guardian.conf.settings.TEMPLATE_403', 92 | '_non-exisitng-403.html'): 93 | self.assertRaises(TemplateDoesNotExist, dummy_view, request) 94 | settings.DEBUG = org_DEBUG 95 | 96 | @mock.patch('guardian.conf.settings.RENDER_403', False) 97 | @mock.patch('guardian.conf.settings.RAISE_403', True) 98 | def test_RAISE_403_setting_is_true(self): 99 | request = self._get_request(self.anon) 100 | 101 | @permission_required_or_403('not_installed_app.change_user') 102 | def dummy_view(request): 103 | return HttpResponse('dummy_view') 104 | 105 | with self.assertRaises(PermissionDenied): 106 | dummy_view(request) 107 | 108 | def test_anonymous_user_wrong_app(self): 109 | 110 | request = self._get_request(self.anon) 111 | 112 | @permission_required_or_403('not_installed_app.change_user') 113 | def dummy_view(request): 114 | return HttpResponse('dummy_view') 115 | self.assertEqual(dummy_view(request).status_code, 403) 116 | 117 | def test_anonymous_user_wrong_codename(self): 118 | 119 | request = self._get_request() 120 | 121 | @permission_required_or_403('auth.wrong_codename') 122 | def dummy_view(request): 123 | return HttpResponse('dummy_view') 124 | self.assertEqual(dummy_view(request).status_code, 403) 125 | 126 | def test_anonymous_user(self): 127 | 128 | request = self._get_request() 129 | 130 | @permission_required_or_403('auth.change_user') 131 | def dummy_view(request): 132 | return HttpResponse('dummy_view') 133 | self.assertEqual(dummy_view(request).status_code, 403) 134 | 135 | def test_wrong_lookup_variables_number(self): 136 | 137 | request = self._get_request() 138 | 139 | try: 140 | @permission_required_or_403('auth.change_user', (User, 'username')) 141 | def dummy_view(request, username): 142 | pass 143 | dummy_view(request, username='jack') 144 | except GuardianError: 145 | pass 146 | else: 147 | self.fail("If lookup variables are passed they must be tuple of: " 148 | "(ModelClass/app_label.ModelClass/queryset, " 149 | ")\n" 150 | "Otherwise GuardianError should be raised") 151 | 152 | def test_wrong_lookup_variables(self): 153 | 154 | request = self._get_request() 155 | 156 | args = ( 157 | (2010, 'username', 'username'), 158 | ('User', 'username', 'username'), 159 | (User, 'username', 'no_arg'), 160 | ) 161 | for tup in args: 162 | try: 163 | @permission_required_or_403('auth.change_user', tup) 164 | def show_user(request, username): 165 | user = get_object_or_404(User, username=username) 166 | return HttpResponse("It's %s here!" % user.username) 167 | show_user(request, 'jack') 168 | except GuardianError: 169 | pass 170 | else: 171 | self.fail("Wrong arguments given but GuardianError not raised") 172 | 173 | def test_model_lookup(self): 174 | 175 | request = self._get_request(self.user) 176 | 177 | perm = 'auth.change_user' 178 | joe, created = User.objects.get_or_create(username='joe') 179 | assign(perm, self.user, obj=joe) 180 | 181 | models = ( 182 | 'auth.User', 183 | User, 184 | User.objects.filter(is_active=True), 185 | ) 186 | for model in models: 187 | @permission_required_or_403(perm, (model, 'username', 'username')) 188 | def dummy_view(request, username): 189 | get_object_or_404(User, username=username) 190 | return HttpResponse('hello') 191 | response = dummy_view(request, username=joe.username) 192 | self.assertEqual(response.content, 'hello') 193 | 194 | def test_redirection(self): 195 | 196 | request = self._get_request(self.user) 197 | 198 | foo = User.objects.create(username='foo') 199 | foobar = Group.objects.create(name='foobar') 200 | foo.groups.add(foobar) 201 | 202 | @permission_required('auth.change_group', 203 | (User, 'groups__name', 'group_name'), 204 | login_url='/foobar/') 205 | def dummy_view(request, group_name): 206 | pass 207 | response = dummy_view(request, group_name='foobar') 208 | self.assertTrue(isinstance(response, HttpResponseRedirect)) 209 | self.assertTrue(response._headers['location'][1].startswith( 210 | '/foobar/')) 211 | 212 | -------------------------------------------------------------------------------- /guardian/migrations/0004_auto__del_field_groupobjectpermission_object_id__del_unique_groupobjec.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Changing field 'GroupObjectPermission.object_pk' 12 | db.alter_column('guardian_groupobjectpermission', 'object_pk', self.gf('django.db.models.fields.CharField')(max_length=255)) 13 | 14 | # Changing field 'UserObjectPermission.object_pk' 15 | db.alter_column('guardian_userobjectpermission', 'object_pk', self.gf('django.db.models.fields.CharField')(max_length=255)) 16 | 17 | # Removing unique constraint on 'UserObjectPermission', fields ['object_id', 'user', 'content_type', 'permission'] 18 | db.delete_unique('guardian_userobjectpermission', ['object_id', 'user_id', 'content_type_id', 'permission_id']) 19 | 20 | # Removing unique constraint on 'GroupObjectPermission', fields ['group', 'object_id', 'content_type', 'permission'] 21 | db.delete_unique('guardian_groupobjectpermission', ['group_id', 'object_id', 'content_type_id', 'permission_id']) 22 | 23 | # Deleting field 'GroupObjectPermission.object_id' 24 | db.delete_column('guardian_groupobjectpermission', 'object_id') 25 | 26 | # Adding unique constraint on 'GroupObjectPermission', fields ['object_pk', 'group', 'content_type', 'permission'] 27 | db.create_unique('guardian_groupobjectpermission', ['object_pk', 'group_id', 'content_type_id', 'permission_id']) 28 | 29 | # Deleting field 'UserObjectPermission.object_id' 30 | db.delete_column('guardian_userobjectpermission', 'object_id') 31 | 32 | # Adding unique constraint on 'UserObjectPermission', fields ['object_pk', 'user', 'content_type', 'permission'] 33 | db.create_unique('guardian_userobjectpermission', ['object_pk', 'user_id', 'content_type_id', 'permission_id']) 34 | 35 | 36 | def backwards(self, orm): 37 | 38 | # Changing field 'GroupObjectPermission.object_pk' 39 | db.alter_column('guardian_groupobjectpermission', 'object_pk', self.gf('django.db.models.fields.TextField')()) 40 | 41 | # Changing field 'UserObjectPermission.object_pk' 42 | db.alter_column('guardian_userobjectpermission', 'object_pk', self.gf('django.db.models.fields.TextField')()) 43 | 44 | # Removing unique constraint on 'UserObjectPermission', fields ['object_pk', 'user', 'content_type', 'permission'] 45 | db.delete_unique('guardian_userobjectpermission', ['object_pk', 'user_id', 'content_type_id', 'permission_id']) 46 | 47 | # Removing unique constraint on 'GroupObjectPermission', fields ['object_pk', 'group', 'content_type', 'permission'] 48 | db.delete_unique('guardian_groupobjectpermission', ['object_pk', 'group_id', 'content_type_id', 'permission_id']) 49 | 50 | # We cannot add back in field 'GroupObjectPermission.object_id' 51 | raise RuntimeError( 52 | "Cannot reverse this migration. 'GroupObjectPermission.object_id' and its values cannot be restored.") 53 | 54 | # Adding unique constraint on 'GroupObjectPermission', fields ['group', 'object_id', 'content_type', 'permission'] 55 | db.create_unique('guardian_groupobjectpermission', ['group_id', 'object_id', 'content_type_id', 'permission_id']) 56 | 57 | # We cannot add back in field 'UserObjectPermission.object_id' 58 | raise RuntimeError( 59 | "Cannot reverse this migration. 'UserObjectPermission.object_id' and its values cannot be restored.") 60 | 61 | # Adding unique constraint on 'UserObjectPermission', fields ['object_id', 'user', 'content_type', 'permission'] 62 | db.create_unique('guardian_userobjectpermission', ['object_id', 'user_id', 'content_type_id', 'permission_id']) 63 | 64 | 65 | models = { 66 | 'auth.group': { 67 | 'Meta': {'object_name': 'Group'}, 68 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 69 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 70 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 71 | }, 72 | 'auth.permission': { 73 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 74 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 75 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 76 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 77 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 78 | }, 79 | 'auth.user': { 80 | 'Meta': {'object_name': 'User'}, 81 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 82 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 83 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 84 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 85 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 86 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 87 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 88 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 89 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 90 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 91 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 92 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 93 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 94 | }, 95 | 'contenttypes.contenttype': { 96 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 97 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 98 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 99 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 100 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 101 | }, 102 | 'guardian.groupobjectpermission': { 103 | 'Meta': {'unique_together': "(['group', 'permission', 'content_type', 'object_pk'],)", 'object_name': 'GroupObjectPermission'}, 104 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 105 | 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}), 106 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 107 | 'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 108 | 'permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Permission']"}) 109 | }, 110 | 'guardian.userobjectpermission': { 111 | 'Meta': {'unique_together': "(['user', 'permission', 'content_type', 'object_pk'],)", 'object_name': 'UserObjectPermission'}, 112 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 113 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 114 | 'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 115 | 'permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Permission']"}), 116 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 117 | } 118 | } 119 | 120 | complete_apps = ['guardian'] 121 | --------------------------------------------------------------------------------