1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
|
%global _empty_manifest_terminate_build 0
Name: python-django-rules
Version: 0.2
Release: 1
Summary: Flexible per-object authorization backend for Django
License: BSD
URL: http://github.com/maraujop/django-rules
Source0: https://mirrors.nju.edu.cn/pypi/web/packages/78/f3/0a77aafe3dfdff29ec67ae445ab7fd9184eeeba6e34fe50488bbd5afac8c/django-rules-0.2.tar.gz
BuildArch: noarch
%description
h1(#abstract). django-rules
django-rules is a Django authorization backend that offers a unified, per-object authorization management. It is quite different from other authorization backends in the way it lets you flexibly manage per-object permissions.
In django-rules every rule adds an authorization constraint to a given model. The authorization constraint will check if the given user complies with the constraint (e.g. if the user has the right permissions to execute a certain action over an object, etc.). The authorization constraint can be a boolean attribute, property or method of the model, whichever you prefer for each rule :)
h2(#philosophy). Philosophy
django-rules strives to build a flexible and scalable authorization backend. Why is it better than other authorization backends out there?
* The backend is simple, concise and compact. Less lines of code mean less complexity, faster execution and (hopefully :) less errors and bugs.
* You can implement each authorization constraint as a boolean attribute, property or method of the model, whichever you prefer for each rule :) This way you will be able to re-implement how authorizations work at any time. It is dynamic and you know dynamic sounds way better than static :)
* You don't have to add extra permissions or groups to your users. You simply program the constraints however you like them to be and then you assign them to the rules. Done!
* You have fine granularity control over how the rules handle the authentication: one rule can be using an authorization constraint that uses LDAP while other rules call a web service (or anything you wish to hook in the authorization constraint).
* Other per-object authorization backends create a row in a table for every object, every user and every permission combination. Even with an average-size site, you will have scalability nightmares, no matter how much you can cache.
* Other authorization backends have to SELECT all permissions that a user has even if you only need to check one specific permission, making the memory footprint bigger.
* Other authorization backends don't have a way to set centralized permissions, which are a real necessity in most projects out there.
h2(#requirements). Requirements
django-rules requires a proper installation of Django 1.2 (at least).
h2(#installation). Installation
h3(#pypi). From Pypi
As simple as doing:
<pre>
pip install django-rules
</pre>
h3(#source). From source
To install django-rules from source:
<pre>
git clone https://github.com/maraujop/django-rules/
cd django-rules
python setup.py install
</pre>
h2(#configuration). Configuration
For django-rules to work, you have to hook it into your project:
* Add it to the list of <code>INSTALLED_APPS</code> in <code>settings.py</code>:
<pre>
INSTALLED_APPS = (
...
'django_rules',
)
</pre>
* Add the django-rules authorization backend to the list of <code>AUTHENTICATION_BACKENDS</code> in <code>settings.py</code>:
<pre>
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # Django's default auth backend
'django_rules.backends.ObjectPermissionBackend',
)
</pre>
* Run syncdb to update the database with the new django-rules models:
<pre>
python manage.py syncdb
</pre>
h2(#rules). Rules
A rule represents a functional authorization constraint that restricts the actions that a certain user can carry out on a certain object (an instance of a Model).
Every rule definition is composed of 6 parameters (3 compulsory and 3 optional):
* <code>app_name</code>: The name of the app to which the rule applies.
* <code>codename</code>: The name of the rule, _unique across all applications_. It should be a brief but distinctive name.
* <code>model</code>: The name of the model associated with the rule.
* <code>field_name</code> _(optional)_: The name of the boolean attribute, property or method of the model that implements the authorization constraint. If not set, it defaults to the value of the compulsory field <code>codename</code>.
* <code>view_param_pk</code> _(optional)_: The view parameter's name to use for getting the primary key of the model. It is used in the decorated views for getting the actual instance of the model, the object where authorizations will be checked on. If not set, it defaults to the name of the primary key field in the model. Note that if the name of the parameter of the view that holds the value of the object's primary key doesn't match the name of the primary key of the model, the new name must be specified in this parameter (we will talk about this special case in "the section on Decorators":#decorators).
* <code>description</code> _(optional)_: A brief (140 characters maximum) description explaining the expected behaviour of the authorization constraint. Although optional, it is considered a Good Practice ^TM^ and should always be used.
The rules should be created per-Django application. That is, under the root directory of the Django-application in which you want to create rules, you should have a <code>rules.py</code> containing only the declarations of those rules specific to that Django-application.
Once you have defined the rules in <code>rules.py</code>, you will want to activate them. For every rule that you want to activate you *must* add a registration point for it by calling <code>django_rules.utils.register</code> in <code>rules.py</code>.
Finally, once you have <code>rules.py</code> properly set up, you will want to sync the rules to the database. In your Django project you will have to run <code>sync_rules</code> command:
<pre>
python manage.py sync_rules
</pre>
This command will look for all your <code>rules.py</code> files under your <code>INSTALLED_APPS</code> and will sync the latest changes to the database, so you don't have to run <code>syncdb</code> or rebuild the full database at all.
h2(#examples). Examples:
h3(#ex1). Example 1: Creating a simple, compact rule for the Item model in the 'shipping' Django-application
Let's image that, within the <code>shipping</code> Django-application, I have the following <code>models.py</code>:
<pre>
class Item(models.Model):
supplier = models.ForeignKey(User)
description = models.CharField(max_length = 50)
</pre>
Then, imagine that the business logic in our application has a functional authorization constraint for every item such as "An item can only be shipped by its supplier". Now, to comply with the functional authorization constraint we only have to create a simple rule.
First, let's start by adding an authorization constraint to the Item model. Remember that we can use a method, a boolean attribute or a boolean-returning property. This time we will be using a method:
<pre>
class Item(models.Model):
supplier = models.ForeignKey(User)
description = models.CharField(max_length = 50)
def can_ship(self, user_obj):
"""
Checks if the given user_obj is the supplier of the item
"""
return self.supplier == user_obj
</pre>
Then, to associate the authorization constraint with a rule, we have to set up the rule in the application's <code>rules.py</code> file:
<pre>
from django_rules import utils
rules_list = [
{'codename':'can_ship', 'model':'Item'},
]
# NOTE:
# Although the above rule definition follows the minimal style, it is
# Good Practice ^TM^ to always add the optional 'description' field
# to give a brief explanation about the expected behaviour of the rule.
# For the rules to be active, we *must* register them:
for rule in rules_list:
utils.register(app_name='shipping', **rule)
</pre>
Finally, do not forget to sync the rules to make sure that all the new definitions, changes, etc. are synced to the database.
<pre>
python manage.py sync_rules
</pre>
h3(#ex2). Example 2: Creating a rule that doesn't follow the naming conventions
Imagine that we would like to name our authorization constraints however we want. For example, let's change the previous Item model:
<pre>
class Item(models.Model):
supplier = models.ForeignKey(User)
description = models.CharField(max_length = 50)
def isSameSupplier(self, user_obj): #--> change in the name convention of the authorization constraint
"""
Checks if the given user_obj is the supplier of the item
"""
return self.supplier == user_obj
</pre>
Then, we would have to set up a more verbose rule in the application's <code>rules.py</code> file by using its additional optional fields. This time, only <code>field_name</code> would be needed but it is also Good Practice ^TM^ to give a brief <code>description</code>:
<pre>
from django_rules import utils
rules_list = [
{'codename':'can_ship', 'model':'Item', 'field_name':'isSameSupplier',
'description':'Checks if the given user is the supplier of the item'},
]
# For the rules to be active, we *must* register them:
for rule in rules_list:
utils.register(app_name='shipping', **rule)
</pre>
Again, do not forget to sync the rules to make sure that all the new definitions, changes, etc. are applied to the database.
<pre>
python manage.py sync_rules
</pre>
h3. Using your rules
Once you have set up a rule that implements a functional authorization constraint, you can (and should :) use it in your application. It is really simple! In every place you want to enforce an authorization constraint on a user, you will simply make the following call:
<pre>
user_obj.has_perm(codename, model_obj)
</pre>
Following the previous "Example 1":#ex1, let's imagine that the application is already running with data in the database (at least one supplier and one item, both with ids equal to 1). Remember that we have already implemented, defined, registered and synced the following rule:
<pre>
{'codename':'can_ship', 'model':'Item'}
</pre>
Then, if we wanted to check whether a supplier can ship an item, we would only have to enforce the rule by doing:
<pre>
supplier = Supplier.objects.get(pk = 1)
item = Item.objects.get(pk = 1)
if supplier.has_perm('can_ship', item):
print 'Yay! The supplier can ship the item! :)'
</pre>
Easy, right? :)
h4. Details about the internal magic of django-rules
Please note that what follows is a detailed explanation of how all the inner magic in django-rules flows. If you don't really care, please move along. You really don't need these details to be able to write rules and use django-rules effectively. However, if you are curious and want to know more, please pay great attention to the details below.
Here is how all the pieces of the puzzle come together:
* When you call <code>user_obj.has_perm(codename, model_obj)</code> (in the previous example, <code>supplier.has_perm('can_ship', item)</code>), Django handles the control over to the django-rules backend.
* The django-rules backend will then try to match the <code>codename</code> with a rule. Note that we are requesting a rule with a <code>codename</code> of <code>'can_ship'</code> and a <code>model_obj</code> like the Model of the item object. Because in "Example 1":#ex1 we have defined the rule <code>{'codename':'can_ship', 'model':'Item'}</code>, there will be a match.
* Then, django-rules will check whether <code>field_name</code> is an attribute, a property or a method, and will act accordingly. If <code>field_name</code> is a method, the django-rules backend will check if it requires just one user parameter or no parameter at all. Depending on the parameter requirements, it will execute <code>model_obj.field_name()</code> or <code>model_obj.field_name(user_obj)</code>. In our "Example 1":#ex1 we require a user parameter so it will execute <code>item.can_ship(supplier)</code>.
* Finally, if the authorization constraint implemented in <code>field_name</code> is True or returns True, the constraint is considered fulfilled. Otherwise, you will not be authorized.
h3. Details of using model methods in rules
As we have seen, django-rules will check whether <code>field_name</code> is an attribute, a property or a method, and will act accordingly. You can create rules based on model's attributes and properties, which are more than fine for very simple cases but most of the time you will be setting <code>field_name</code> to a method in the model.
It is important to note that this method is limited to having just one parameter (a user object) or no parameters at all. It cannot receive multiple arguments or an argument that is not an instance of User. Although this might seem like a limitation, we could not think of a use case where the rest of the information needed coudn't be retrieved from the user or the model object. If you get into a situation where this is limiting you, please get in touch and explain your problem so that we can think of how to get around it! :)
Finally, you need to be aware of something very important: a method assigned to a rule (let's call them _rule methods_ from now on) *should never call any other method*. That is, they should be self-contained. This is to avoid a potential infinite recursion. Imagine a situation where the rule method calls another method that has _the same_ authorization constraint of the previous rule method. Boom! You just created an infinite loop. Run for your life! :)
You may be thinking that you can control this but, trust me, it will get very difficult to maintain and scale. Things will not always be that simple, maybe you will end up calling a method that is later modified and ends up calling a helper function that triggers the same authorization loop. Yeah, I know. Indirection is a bitch :) Or, in other words "Great power comes with great responsibility". So beware of the infinite loop ;)
h2(#decorators). Decorators
If you like Python as much as I do, you will love decorators. Django has a <code>permission_required</code> decorator, so it felt natural that django-rules implemented an <code>object_permission_required</code> decorator.
Imagine that for our "Example 1":#ex1 we have the following code in <code>views.py</code>:
<pre>
def ship_item(request, id):
item = Item.objects.get(pk = id)
if request.user.has_perms('can_ship', item):
return HttpResponse('success')
return HttpResponse('error')
</pre>
We could easily decorate the view to make the method much more compact and easy to read:
<pre>
from django_rules import object_permission_required
@object_permission_required('can_ship')
def ship_item(request, id):
return HttpResponse('Item successfully shipper! :)')
</pre>
The magic of the decorator is very cool indeed. First, it matches the rule and gets the type of model from it. Then, it gets the <code>id</code> parameter from the view's kwargs and instantiates a Model object with <code>item = model.objects.get(pk = id)</code>. Finally, it can call the <code>request.user.has_perm('can_ship', item)</code> for you and redirect to a fail page if the constraint is not fulfilled.
Note how we have maintained the name of the model's primary key in the parameters of the view. If the parameter has a name that doesn't match the name of the primary key in the model, remember that we will have to add another optional parameter to the rule. From "the section on Rules:":#rules
* <code>view_param_pk</code> _(optional)_: The view parameter's name to use for getting the primary key of the model. It is used in the decorated views for getting the actual instance of the model. If not set, it defaults to the name of the primary key field in the model. Note that if the name of the parameter of the view that holds the value of the object's primary key doesn't match the name of the primary key of the model, the new name must be specified in this parameter.
For example, if we modify the parameter of the view:
<pre>
from django_rules import object_permission_required
@object_permission_required('can_ship')
def ship_item(request, my_item_code): #--> change in the naming of the parameter in the view
return HttpResponse('success')
</pre>
We would have to specify the <code>view_param_pk</code> in the rule definition:
<pre>
rules_list = [
{'codename': 'can_ship', 'model': 'Item', 'view_param_pk': 'my_item_code',
'description': 'Checks if the given user is the supplier of the item'},
]
</pre>
The <code>object_permission_required</code> decorator can receive 4 arguments:
<pre>
@object_permission_required('can_ship', return_403=True)
@object_permission_required('can_ship', redirect_url='/more/foo/bar/')
@object_permission_required('can_ship', redirect_field_name='myFooField')
@object_permission_required('can_ship', login_url='/foo/bar/')
</pre>
By default:
* <code>return_403</code> is set to False.
* <code>redirect_url</code> is set to an empty string.
* <code>redirect_field_name</code> is set to django.contrib.auth's REDIRECT_FIELD_NAME.
* <code>login_url</code> is set to <code>settings.LOGIN_URL</code>.
Thus, if the authorization constraint is not fulfilled, the decorator will default to a redirect to the login page in Django-style :)
Also, note that a couple of the parameters have a specificity. Namely:
* if <code>return_403</code> is set to True it will override the rest of the parameters and the decorator will return a HttpResponseForbidden.
* if <code>redirect_url</code> is set to a URL, it will override that of <code>login_url</code>.
Finally, it is important to note a tricky detail regarding the use of the decorator to guard the access of those methods that are not directly exposed as views mapped to external URLs. When a view method is an entry point through URLs (that is, if your view method is mapped directly to one of the <code>urls.py</code> entries), Django parses the URL and passes the parameters to the view as <code>kwargs</code>. Thus, if you want to use the <code>object_permission_required</code> decorator over an internal method (a method that is called inside one of those external views or somewhere else in your code) you must use <code>kwargs</code> when passing the parameters.
Let's see an example:
<pre>
def item_shipper(request, id):
internal_code = 'XXX-' + my_item_code
return _ship_item(request, id=internal_code) # instead of doing return _ship_item(request, internal_code)
@object_permission_required('can_ship')
def _ship_item(request, id)
return HttpResponse('success')
</pre>
h2(#centralizedpermissions). Centralized Permissions
django-rules has a central authorization dispatcher that is aimed towards a very common need in real life projects: the special, privileged groups such as administrators, user-support staff, etc., that have permissions to override certain aspects of the authorization constraints in the application. For such cases, django-rules has a way to let you bypass its authorization system for whatever reasons you have.
To set up centralized permissions, you will need to set in your project settings the variable <code>CENTRAL_AUTHORIZATIONS</code> pointing to a module. Within that module, you will have to define a a boolean-returning function named <code>central_authorizations</code> accepting exactly two parameters:
* <code>user_obj</code>: the user object.
* <code>codename</code>: the codename of the rule we will be overriden. It is very useful to refine the permissions of a special user "a la ACL".
Note that, although the naming of the parameters doesn't really matter, the order does. The first parameter will receive a user object, and the second parameter, the codename of the rule.
This <code>central_authorizations()</code> function will be called *before* any other rule, so you can override all of them here.
For example, in <code>settings.py</code> you will add:
<pre>
CENTRAL_AUTHORIZATIONS = 'myProjectFoo.utils'
</pre>
And then, within myProjectFoo, in <code>utils.py</code>, you will implement the <code>central_authorizations()</code> function with the overrides for the special users.
Imagine you want to give some special access to user support staff that will be able to access some private fields in the profile (for example, email and age) that generally are hidden to regular users of the application. They are user support, so they should not be able to override certain things in the application. Yet, you also want your über-admins (generally the developers) to be able to access anything within the application so that they can code and test quickly while developing.
In such case, you can write the following <code>central_authorizations()</code> function:
<pre>
def central_authorizations(user_obj, codename):
"""
This function will be called *before* any other rule,
so you can override all of the permissions here.
"""
isAuthorized = False
if user_obj.get_profile().isUberAdmin():
isAuthorized = True
elif user_obj.get_profile().isUserSupport() and codename in ['can_see_full_profile', 'can_delete_item']:
isAuthorized = True
return isAuthorized
</pre>
As you can imagine, everything that is checked in <code>central_authorizations</code> is global to the *whole* project.
h2. Status and testing
django-rules is meant to be a security application. Thus, it has been thoroughly tested. It comes with a battery of tests that tries to cover all of the available funcionality. However, if you come across a bug or an irregular situation, feel free to report it through the "Github bug tracker":https://github.com/maraujop/django-rules/issues.
Finally, the application comes geared with many different exceptions that will make sure rules are created properly. They are also aimed to keep the security of your application away from negligence. Manage the exceptions wisely and you will be a happy and secure coder, as security is kept away from possible neglicence. You should manage them carefuly.
h3. Testing django-rules
To run tests, get into tests directory and execute:
<pre>
./runtests.py
</pre>
It should always say OK. If not, there is a broken test that I hope you will be reporting soon :)
h2. Need more examples?
I have done my best trying to explain the concept behind django-rules but, if you would rather look at more code examples, I am sure you will find the "code in the tests":https://github.com/maraujop/django-rules/blob/master/django_rules/tests/test_core.py quite useful :)
h2. More Documentation
In case you want to know where all this "per-object authentication backend in Django" came to exist, you should at least read the following links:
* A great article about "per-object permission backends in Django":http://djangoadvent.com/1.2/object-permissions/ by Florian Apolloner
* Also, check the explanation of the changes introduced when fixing the "django ticket #11010":http://code.djangoproject.com/ticket/11010
Finally, my most sincere appreciation goes to everybody that contributes to the wonderful Django development framework and also to the rest of developers and committers that build django-rules with their help. Respect! :)
%package -n python3-django-rules
Summary: Flexible per-object authorization backend for Django
Provides: python-django-rules
BuildRequires: python3-devel
BuildRequires: python3-setuptools
BuildRequires: python3-pip
%description -n python3-django-rules
h1(#abstract). django-rules
django-rules is a Django authorization backend that offers a unified, per-object authorization management. It is quite different from other authorization backends in the way it lets you flexibly manage per-object permissions.
In django-rules every rule adds an authorization constraint to a given model. The authorization constraint will check if the given user complies with the constraint (e.g. if the user has the right permissions to execute a certain action over an object, etc.). The authorization constraint can be a boolean attribute, property or method of the model, whichever you prefer for each rule :)
h2(#philosophy). Philosophy
django-rules strives to build a flexible and scalable authorization backend. Why is it better than other authorization backends out there?
* The backend is simple, concise and compact. Less lines of code mean less complexity, faster execution and (hopefully :) less errors and bugs.
* You can implement each authorization constraint as a boolean attribute, property or method of the model, whichever you prefer for each rule :) This way you will be able to re-implement how authorizations work at any time. It is dynamic and you know dynamic sounds way better than static :)
* You don't have to add extra permissions or groups to your users. You simply program the constraints however you like them to be and then you assign them to the rules. Done!
* You have fine granularity control over how the rules handle the authentication: one rule can be using an authorization constraint that uses LDAP while other rules call a web service (or anything you wish to hook in the authorization constraint).
* Other per-object authorization backends create a row in a table for every object, every user and every permission combination. Even with an average-size site, you will have scalability nightmares, no matter how much you can cache.
* Other authorization backends have to SELECT all permissions that a user has even if you only need to check one specific permission, making the memory footprint bigger.
* Other authorization backends don't have a way to set centralized permissions, which are a real necessity in most projects out there.
h2(#requirements). Requirements
django-rules requires a proper installation of Django 1.2 (at least).
h2(#installation). Installation
h3(#pypi). From Pypi
As simple as doing:
<pre>
pip install django-rules
</pre>
h3(#source). From source
To install django-rules from source:
<pre>
git clone https://github.com/maraujop/django-rules/
cd django-rules
python setup.py install
</pre>
h2(#configuration). Configuration
For django-rules to work, you have to hook it into your project:
* Add it to the list of <code>INSTALLED_APPS</code> in <code>settings.py</code>:
<pre>
INSTALLED_APPS = (
...
'django_rules',
)
</pre>
* Add the django-rules authorization backend to the list of <code>AUTHENTICATION_BACKENDS</code> in <code>settings.py</code>:
<pre>
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # Django's default auth backend
'django_rules.backends.ObjectPermissionBackend',
)
</pre>
* Run syncdb to update the database with the new django-rules models:
<pre>
python manage.py syncdb
</pre>
h2(#rules). Rules
A rule represents a functional authorization constraint that restricts the actions that a certain user can carry out on a certain object (an instance of a Model).
Every rule definition is composed of 6 parameters (3 compulsory and 3 optional):
* <code>app_name</code>: The name of the app to which the rule applies.
* <code>codename</code>: The name of the rule, _unique across all applications_. It should be a brief but distinctive name.
* <code>model</code>: The name of the model associated with the rule.
* <code>field_name</code> _(optional)_: The name of the boolean attribute, property or method of the model that implements the authorization constraint. If not set, it defaults to the value of the compulsory field <code>codename</code>.
* <code>view_param_pk</code> _(optional)_: The view parameter's name to use for getting the primary key of the model. It is used in the decorated views for getting the actual instance of the model, the object where authorizations will be checked on. If not set, it defaults to the name of the primary key field in the model. Note that if the name of the parameter of the view that holds the value of the object's primary key doesn't match the name of the primary key of the model, the new name must be specified in this parameter (we will talk about this special case in "the section on Decorators":#decorators).
* <code>description</code> _(optional)_: A brief (140 characters maximum) description explaining the expected behaviour of the authorization constraint. Although optional, it is considered a Good Practice ^TM^ and should always be used.
The rules should be created per-Django application. That is, under the root directory of the Django-application in which you want to create rules, you should have a <code>rules.py</code> containing only the declarations of those rules specific to that Django-application.
Once you have defined the rules in <code>rules.py</code>, you will want to activate them. For every rule that you want to activate you *must* add a registration point for it by calling <code>django_rules.utils.register</code> in <code>rules.py</code>.
Finally, once you have <code>rules.py</code> properly set up, you will want to sync the rules to the database. In your Django project you will have to run <code>sync_rules</code> command:
<pre>
python manage.py sync_rules
</pre>
This command will look for all your <code>rules.py</code> files under your <code>INSTALLED_APPS</code> and will sync the latest changes to the database, so you don't have to run <code>syncdb</code> or rebuild the full database at all.
h2(#examples). Examples:
h3(#ex1). Example 1: Creating a simple, compact rule for the Item model in the 'shipping' Django-application
Let's image that, within the <code>shipping</code> Django-application, I have the following <code>models.py</code>:
<pre>
class Item(models.Model):
supplier = models.ForeignKey(User)
description = models.CharField(max_length = 50)
</pre>
Then, imagine that the business logic in our application has a functional authorization constraint for every item such as "An item can only be shipped by its supplier". Now, to comply with the functional authorization constraint we only have to create a simple rule.
First, let's start by adding an authorization constraint to the Item model. Remember that we can use a method, a boolean attribute or a boolean-returning property. This time we will be using a method:
<pre>
class Item(models.Model):
supplier = models.ForeignKey(User)
description = models.CharField(max_length = 50)
def can_ship(self, user_obj):
"""
Checks if the given user_obj is the supplier of the item
"""
return self.supplier == user_obj
</pre>
Then, to associate the authorization constraint with a rule, we have to set up the rule in the application's <code>rules.py</code> file:
<pre>
from django_rules import utils
rules_list = [
{'codename':'can_ship', 'model':'Item'},
]
# NOTE:
# Although the above rule definition follows the minimal style, it is
# Good Practice ^TM^ to always add the optional 'description' field
# to give a brief explanation about the expected behaviour of the rule.
# For the rules to be active, we *must* register them:
for rule in rules_list:
utils.register(app_name='shipping', **rule)
</pre>
Finally, do not forget to sync the rules to make sure that all the new definitions, changes, etc. are synced to the database.
<pre>
python manage.py sync_rules
</pre>
h3(#ex2). Example 2: Creating a rule that doesn't follow the naming conventions
Imagine that we would like to name our authorization constraints however we want. For example, let's change the previous Item model:
<pre>
class Item(models.Model):
supplier = models.ForeignKey(User)
description = models.CharField(max_length = 50)
def isSameSupplier(self, user_obj): #--> change in the name convention of the authorization constraint
"""
Checks if the given user_obj is the supplier of the item
"""
return self.supplier == user_obj
</pre>
Then, we would have to set up a more verbose rule in the application's <code>rules.py</code> file by using its additional optional fields. This time, only <code>field_name</code> would be needed but it is also Good Practice ^TM^ to give a brief <code>description</code>:
<pre>
from django_rules import utils
rules_list = [
{'codename':'can_ship', 'model':'Item', 'field_name':'isSameSupplier',
'description':'Checks if the given user is the supplier of the item'},
]
# For the rules to be active, we *must* register them:
for rule in rules_list:
utils.register(app_name='shipping', **rule)
</pre>
Again, do not forget to sync the rules to make sure that all the new definitions, changes, etc. are applied to the database.
<pre>
python manage.py sync_rules
</pre>
h3. Using your rules
Once you have set up a rule that implements a functional authorization constraint, you can (and should :) use it in your application. It is really simple! In every place you want to enforce an authorization constraint on a user, you will simply make the following call:
<pre>
user_obj.has_perm(codename, model_obj)
</pre>
Following the previous "Example 1":#ex1, let's imagine that the application is already running with data in the database (at least one supplier and one item, both with ids equal to 1). Remember that we have already implemented, defined, registered and synced the following rule:
<pre>
{'codename':'can_ship', 'model':'Item'}
</pre>
Then, if we wanted to check whether a supplier can ship an item, we would only have to enforce the rule by doing:
<pre>
supplier = Supplier.objects.get(pk = 1)
item = Item.objects.get(pk = 1)
if supplier.has_perm('can_ship', item):
print 'Yay! The supplier can ship the item! :)'
</pre>
Easy, right? :)
h4. Details about the internal magic of django-rules
Please note that what follows is a detailed explanation of how all the inner magic in django-rules flows. If you don't really care, please move along. You really don't need these details to be able to write rules and use django-rules effectively. However, if you are curious and want to know more, please pay great attention to the details below.
Here is how all the pieces of the puzzle come together:
* When you call <code>user_obj.has_perm(codename, model_obj)</code> (in the previous example, <code>supplier.has_perm('can_ship', item)</code>), Django handles the control over to the django-rules backend.
* The django-rules backend will then try to match the <code>codename</code> with a rule. Note that we are requesting a rule with a <code>codename</code> of <code>'can_ship'</code> and a <code>model_obj</code> like the Model of the item object. Because in "Example 1":#ex1 we have defined the rule <code>{'codename':'can_ship', 'model':'Item'}</code>, there will be a match.
* Then, django-rules will check whether <code>field_name</code> is an attribute, a property or a method, and will act accordingly. If <code>field_name</code> is a method, the django-rules backend will check if it requires just one user parameter or no parameter at all. Depending on the parameter requirements, it will execute <code>model_obj.field_name()</code> or <code>model_obj.field_name(user_obj)</code>. In our "Example 1":#ex1 we require a user parameter so it will execute <code>item.can_ship(supplier)</code>.
* Finally, if the authorization constraint implemented in <code>field_name</code> is True or returns True, the constraint is considered fulfilled. Otherwise, you will not be authorized.
h3. Details of using model methods in rules
As we have seen, django-rules will check whether <code>field_name</code> is an attribute, a property or a method, and will act accordingly. You can create rules based on model's attributes and properties, which are more than fine for very simple cases but most of the time you will be setting <code>field_name</code> to a method in the model.
It is important to note that this method is limited to having just one parameter (a user object) or no parameters at all. It cannot receive multiple arguments or an argument that is not an instance of User. Although this might seem like a limitation, we could not think of a use case where the rest of the information needed coudn't be retrieved from the user or the model object. If you get into a situation where this is limiting you, please get in touch and explain your problem so that we can think of how to get around it! :)
Finally, you need to be aware of something very important: a method assigned to a rule (let's call them _rule methods_ from now on) *should never call any other method*. That is, they should be self-contained. This is to avoid a potential infinite recursion. Imagine a situation where the rule method calls another method that has _the same_ authorization constraint of the previous rule method. Boom! You just created an infinite loop. Run for your life! :)
You may be thinking that you can control this but, trust me, it will get very difficult to maintain and scale. Things will not always be that simple, maybe you will end up calling a method that is later modified and ends up calling a helper function that triggers the same authorization loop. Yeah, I know. Indirection is a bitch :) Or, in other words "Great power comes with great responsibility". So beware of the infinite loop ;)
h2(#decorators). Decorators
If you like Python as much as I do, you will love decorators. Django has a <code>permission_required</code> decorator, so it felt natural that django-rules implemented an <code>object_permission_required</code> decorator.
Imagine that for our "Example 1":#ex1 we have the following code in <code>views.py</code>:
<pre>
def ship_item(request, id):
item = Item.objects.get(pk = id)
if request.user.has_perms('can_ship', item):
return HttpResponse('success')
return HttpResponse('error')
</pre>
We could easily decorate the view to make the method much more compact and easy to read:
<pre>
from django_rules import object_permission_required
@object_permission_required('can_ship')
def ship_item(request, id):
return HttpResponse('Item successfully shipper! :)')
</pre>
The magic of the decorator is very cool indeed. First, it matches the rule and gets the type of model from it. Then, it gets the <code>id</code> parameter from the view's kwargs and instantiates a Model object with <code>item = model.objects.get(pk = id)</code>. Finally, it can call the <code>request.user.has_perm('can_ship', item)</code> for you and redirect to a fail page if the constraint is not fulfilled.
Note how we have maintained the name of the model's primary key in the parameters of the view. If the parameter has a name that doesn't match the name of the primary key in the model, remember that we will have to add another optional parameter to the rule. From "the section on Rules:":#rules
* <code>view_param_pk</code> _(optional)_: The view parameter's name to use for getting the primary key of the model. It is used in the decorated views for getting the actual instance of the model. If not set, it defaults to the name of the primary key field in the model. Note that if the name of the parameter of the view that holds the value of the object's primary key doesn't match the name of the primary key of the model, the new name must be specified in this parameter.
For example, if we modify the parameter of the view:
<pre>
from django_rules import object_permission_required
@object_permission_required('can_ship')
def ship_item(request, my_item_code): #--> change in the naming of the parameter in the view
return HttpResponse('success')
</pre>
We would have to specify the <code>view_param_pk</code> in the rule definition:
<pre>
rules_list = [
{'codename': 'can_ship', 'model': 'Item', 'view_param_pk': 'my_item_code',
'description': 'Checks if the given user is the supplier of the item'},
]
</pre>
The <code>object_permission_required</code> decorator can receive 4 arguments:
<pre>
@object_permission_required('can_ship', return_403=True)
@object_permission_required('can_ship', redirect_url='/more/foo/bar/')
@object_permission_required('can_ship', redirect_field_name='myFooField')
@object_permission_required('can_ship', login_url='/foo/bar/')
</pre>
By default:
* <code>return_403</code> is set to False.
* <code>redirect_url</code> is set to an empty string.
* <code>redirect_field_name</code> is set to django.contrib.auth's REDIRECT_FIELD_NAME.
* <code>login_url</code> is set to <code>settings.LOGIN_URL</code>.
Thus, if the authorization constraint is not fulfilled, the decorator will default to a redirect to the login page in Django-style :)
Also, note that a couple of the parameters have a specificity. Namely:
* if <code>return_403</code> is set to True it will override the rest of the parameters and the decorator will return a HttpResponseForbidden.
* if <code>redirect_url</code> is set to a URL, it will override that of <code>login_url</code>.
Finally, it is important to note a tricky detail regarding the use of the decorator to guard the access of those methods that are not directly exposed as views mapped to external URLs. When a view method is an entry point through URLs (that is, if your view method is mapped directly to one of the <code>urls.py</code> entries), Django parses the URL and passes the parameters to the view as <code>kwargs</code>. Thus, if you want to use the <code>object_permission_required</code> decorator over an internal method (a method that is called inside one of those external views or somewhere else in your code) you must use <code>kwargs</code> when passing the parameters.
Let's see an example:
<pre>
def item_shipper(request, id):
internal_code = 'XXX-' + my_item_code
return _ship_item(request, id=internal_code) # instead of doing return _ship_item(request, internal_code)
@object_permission_required('can_ship')
def _ship_item(request, id)
return HttpResponse('success')
</pre>
h2(#centralizedpermissions). Centralized Permissions
django-rules has a central authorization dispatcher that is aimed towards a very common need in real life projects: the special, privileged groups such as administrators, user-support staff, etc., that have permissions to override certain aspects of the authorization constraints in the application. For such cases, django-rules has a way to let you bypass its authorization system for whatever reasons you have.
To set up centralized permissions, you will need to set in your project settings the variable <code>CENTRAL_AUTHORIZATIONS</code> pointing to a module. Within that module, you will have to define a a boolean-returning function named <code>central_authorizations</code> accepting exactly two parameters:
* <code>user_obj</code>: the user object.
* <code>codename</code>: the codename of the rule we will be overriden. It is very useful to refine the permissions of a special user "a la ACL".
Note that, although the naming of the parameters doesn't really matter, the order does. The first parameter will receive a user object, and the second parameter, the codename of the rule.
This <code>central_authorizations()</code> function will be called *before* any other rule, so you can override all of them here.
For example, in <code>settings.py</code> you will add:
<pre>
CENTRAL_AUTHORIZATIONS = 'myProjectFoo.utils'
</pre>
And then, within myProjectFoo, in <code>utils.py</code>, you will implement the <code>central_authorizations()</code> function with the overrides for the special users.
Imagine you want to give some special access to user support staff that will be able to access some private fields in the profile (for example, email and age) that generally are hidden to regular users of the application. They are user support, so they should not be able to override certain things in the application. Yet, you also want your über-admins (generally the developers) to be able to access anything within the application so that they can code and test quickly while developing.
In such case, you can write the following <code>central_authorizations()</code> function:
<pre>
def central_authorizations(user_obj, codename):
"""
This function will be called *before* any other rule,
so you can override all of the permissions here.
"""
isAuthorized = False
if user_obj.get_profile().isUberAdmin():
isAuthorized = True
elif user_obj.get_profile().isUserSupport() and codename in ['can_see_full_profile', 'can_delete_item']:
isAuthorized = True
return isAuthorized
</pre>
As you can imagine, everything that is checked in <code>central_authorizations</code> is global to the *whole* project.
h2. Status and testing
django-rules is meant to be a security application. Thus, it has been thoroughly tested. It comes with a battery of tests that tries to cover all of the available funcionality. However, if you come across a bug or an irregular situation, feel free to report it through the "Github bug tracker":https://github.com/maraujop/django-rules/issues.
Finally, the application comes geared with many different exceptions that will make sure rules are created properly. They are also aimed to keep the security of your application away from negligence. Manage the exceptions wisely and you will be a happy and secure coder, as security is kept away from possible neglicence. You should manage them carefuly.
h3. Testing django-rules
To run tests, get into tests directory and execute:
<pre>
./runtests.py
</pre>
It should always say OK. If not, there is a broken test that I hope you will be reporting soon :)
h2. Need more examples?
I have done my best trying to explain the concept behind django-rules but, if you would rather look at more code examples, I am sure you will find the "code in the tests":https://github.com/maraujop/django-rules/blob/master/django_rules/tests/test_core.py quite useful :)
h2. More Documentation
In case you want to know where all this "per-object authentication backend in Django" came to exist, you should at least read the following links:
* A great article about "per-object permission backends in Django":http://djangoadvent.com/1.2/object-permissions/ by Florian Apolloner
* Also, check the explanation of the changes introduced when fixing the "django ticket #11010":http://code.djangoproject.com/ticket/11010
Finally, my most sincere appreciation goes to everybody that contributes to the wonderful Django development framework and also to the rest of developers and committers that build django-rules with their help. Respect! :)
%package help
Summary: Development documents and examples for django-rules
Provides: python3-django-rules-doc
%description help
h1(#abstract). django-rules
django-rules is a Django authorization backend that offers a unified, per-object authorization management. It is quite different from other authorization backends in the way it lets you flexibly manage per-object permissions.
In django-rules every rule adds an authorization constraint to a given model. The authorization constraint will check if the given user complies with the constraint (e.g. if the user has the right permissions to execute a certain action over an object, etc.). The authorization constraint can be a boolean attribute, property or method of the model, whichever you prefer for each rule :)
h2(#philosophy). Philosophy
django-rules strives to build a flexible and scalable authorization backend. Why is it better than other authorization backends out there?
* The backend is simple, concise and compact. Less lines of code mean less complexity, faster execution and (hopefully :) less errors and bugs.
* You can implement each authorization constraint as a boolean attribute, property or method of the model, whichever you prefer for each rule :) This way you will be able to re-implement how authorizations work at any time. It is dynamic and you know dynamic sounds way better than static :)
* You don't have to add extra permissions or groups to your users. You simply program the constraints however you like them to be and then you assign them to the rules. Done!
* You have fine granularity control over how the rules handle the authentication: one rule can be using an authorization constraint that uses LDAP while other rules call a web service (or anything you wish to hook in the authorization constraint).
* Other per-object authorization backends create a row in a table for every object, every user and every permission combination. Even with an average-size site, you will have scalability nightmares, no matter how much you can cache.
* Other authorization backends have to SELECT all permissions that a user has even if you only need to check one specific permission, making the memory footprint bigger.
* Other authorization backends don't have a way to set centralized permissions, which are a real necessity in most projects out there.
h2(#requirements). Requirements
django-rules requires a proper installation of Django 1.2 (at least).
h2(#installation). Installation
h3(#pypi). From Pypi
As simple as doing:
<pre>
pip install django-rules
</pre>
h3(#source). From source
To install django-rules from source:
<pre>
git clone https://github.com/maraujop/django-rules/
cd django-rules
python setup.py install
</pre>
h2(#configuration). Configuration
For django-rules to work, you have to hook it into your project:
* Add it to the list of <code>INSTALLED_APPS</code> in <code>settings.py</code>:
<pre>
INSTALLED_APPS = (
...
'django_rules',
)
</pre>
* Add the django-rules authorization backend to the list of <code>AUTHENTICATION_BACKENDS</code> in <code>settings.py</code>:
<pre>
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # Django's default auth backend
'django_rules.backends.ObjectPermissionBackend',
)
</pre>
* Run syncdb to update the database with the new django-rules models:
<pre>
python manage.py syncdb
</pre>
h2(#rules). Rules
A rule represents a functional authorization constraint that restricts the actions that a certain user can carry out on a certain object (an instance of a Model).
Every rule definition is composed of 6 parameters (3 compulsory and 3 optional):
* <code>app_name</code>: The name of the app to which the rule applies.
* <code>codename</code>: The name of the rule, _unique across all applications_. It should be a brief but distinctive name.
* <code>model</code>: The name of the model associated with the rule.
* <code>field_name</code> _(optional)_: The name of the boolean attribute, property or method of the model that implements the authorization constraint. If not set, it defaults to the value of the compulsory field <code>codename</code>.
* <code>view_param_pk</code> _(optional)_: The view parameter's name to use for getting the primary key of the model. It is used in the decorated views for getting the actual instance of the model, the object where authorizations will be checked on. If not set, it defaults to the name of the primary key field in the model. Note that if the name of the parameter of the view that holds the value of the object's primary key doesn't match the name of the primary key of the model, the new name must be specified in this parameter (we will talk about this special case in "the section on Decorators":#decorators).
* <code>description</code> _(optional)_: A brief (140 characters maximum) description explaining the expected behaviour of the authorization constraint. Although optional, it is considered a Good Practice ^TM^ and should always be used.
The rules should be created per-Django application. That is, under the root directory of the Django-application in which you want to create rules, you should have a <code>rules.py</code> containing only the declarations of those rules specific to that Django-application.
Once you have defined the rules in <code>rules.py</code>, you will want to activate them. For every rule that you want to activate you *must* add a registration point for it by calling <code>django_rules.utils.register</code> in <code>rules.py</code>.
Finally, once you have <code>rules.py</code> properly set up, you will want to sync the rules to the database. In your Django project you will have to run <code>sync_rules</code> command:
<pre>
python manage.py sync_rules
</pre>
This command will look for all your <code>rules.py</code> files under your <code>INSTALLED_APPS</code> and will sync the latest changes to the database, so you don't have to run <code>syncdb</code> or rebuild the full database at all.
h2(#examples). Examples:
h3(#ex1). Example 1: Creating a simple, compact rule for the Item model in the 'shipping' Django-application
Let's image that, within the <code>shipping</code> Django-application, I have the following <code>models.py</code>:
<pre>
class Item(models.Model):
supplier = models.ForeignKey(User)
description = models.CharField(max_length = 50)
</pre>
Then, imagine that the business logic in our application has a functional authorization constraint for every item such as "An item can only be shipped by its supplier". Now, to comply with the functional authorization constraint we only have to create a simple rule.
First, let's start by adding an authorization constraint to the Item model. Remember that we can use a method, a boolean attribute or a boolean-returning property. This time we will be using a method:
<pre>
class Item(models.Model):
supplier = models.ForeignKey(User)
description = models.CharField(max_length = 50)
def can_ship(self, user_obj):
"""
Checks if the given user_obj is the supplier of the item
"""
return self.supplier == user_obj
</pre>
Then, to associate the authorization constraint with a rule, we have to set up the rule in the application's <code>rules.py</code> file:
<pre>
from django_rules import utils
rules_list = [
{'codename':'can_ship', 'model':'Item'},
]
# NOTE:
# Although the above rule definition follows the minimal style, it is
# Good Practice ^TM^ to always add the optional 'description' field
# to give a brief explanation about the expected behaviour of the rule.
# For the rules to be active, we *must* register them:
for rule in rules_list:
utils.register(app_name='shipping', **rule)
</pre>
Finally, do not forget to sync the rules to make sure that all the new definitions, changes, etc. are synced to the database.
<pre>
python manage.py sync_rules
</pre>
h3(#ex2). Example 2: Creating a rule that doesn't follow the naming conventions
Imagine that we would like to name our authorization constraints however we want. For example, let's change the previous Item model:
<pre>
class Item(models.Model):
supplier = models.ForeignKey(User)
description = models.CharField(max_length = 50)
def isSameSupplier(self, user_obj): #--> change in the name convention of the authorization constraint
"""
Checks if the given user_obj is the supplier of the item
"""
return self.supplier == user_obj
</pre>
Then, we would have to set up a more verbose rule in the application's <code>rules.py</code> file by using its additional optional fields. This time, only <code>field_name</code> would be needed but it is also Good Practice ^TM^ to give a brief <code>description</code>:
<pre>
from django_rules import utils
rules_list = [
{'codename':'can_ship', 'model':'Item', 'field_name':'isSameSupplier',
'description':'Checks if the given user is the supplier of the item'},
]
# For the rules to be active, we *must* register them:
for rule in rules_list:
utils.register(app_name='shipping', **rule)
</pre>
Again, do not forget to sync the rules to make sure that all the new definitions, changes, etc. are applied to the database.
<pre>
python manage.py sync_rules
</pre>
h3. Using your rules
Once you have set up a rule that implements a functional authorization constraint, you can (and should :) use it in your application. It is really simple! In every place you want to enforce an authorization constraint on a user, you will simply make the following call:
<pre>
user_obj.has_perm(codename, model_obj)
</pre>
Following the previous "Example 1":#ex1, let's imagine that the application is already running with data in the database (at least one supplier and one item, both with ids equal to 1). Remember that we have already implemented, defined, registered and synced the following rule:
<pre>
{'codename':'can_ship', 'model':'Item'}
</pre>
Then, if we wanted to check whether a supplier can ship an item, we would only have to enforce the rule by doing:
<pre>
supplier = Supplier.objects.get(pk = 1)
item = Item.objects.get(pk = 1)
if supplier.has_perm('can_ship', item):
print 'Yay! The supplier can ship the item! :)'
</pre>
Easy, right? :)
h4. Details about the internal magic of django-rules
Please note that what follows is a detailed explanation of how all the inner magic in django-rules flows. If you don't really care, please move along. You really don't need these details to be able to write rules and use django-rules effectively. However, if you are curious and want to know more, please pay great attention to the details below.
Here is how all the pieces of the puzzle come together:
* When you call <code>user_obj.has_perm(codename, model_obj)</code> (in the previous example, <code>supplier.has_perm('can_ship', item)</code>), Django handles the control over to the django-rules backend.
* The django-rules backend will then try to match the <code>codename</code> with a rule. Note that we are requesting a rule with a <code>codename</code> of <code>'can_ship'</code> and a <code>model_obj</code> like the Model of the item object. Because in "Example 1":#ex1 we have defined the rule <code>{'codename':'can_ship', 'model':'Item'}</code>, there will be a match.
* Then, django-rules will check whether <code>field_name</code> is an attribute, a property or a method, and will act accordingly. If <code>field_name</code> is a method, the django-rules backend will check if it requires just one user parameter or no parameter at all. Depending on the parameter requirements, it will execute <code>model_obj.field_name()</code> or <code>model_obj.field_name(user_obj)</code>. In our "Example 1":#ex1 we require a user parameter so it will execute <code>item.can_ship(supplier)</code>.
* Finally, if the authorization constraint implemented in <code>field_name</code> is True or returns True, the constraint is considered fulfilled. Otherwise, you will not be authorized.
h3. Details of using model methods in rules
As we have seen, django-rules will check whether <code>field_name</code> is an attribute, a property or a method, and will act accordingly. You can create rules based on model's attributes and properties, which are more than fine for very simple cases but most of the time you will be setting <code>field_name</code> to a method in the model.
It is important to note that this method is limited to having just one parameter (a user object) or no parameters at all. It cannot receive multiple arguments or an argument that is not an instance of User. Although this might seem like a limitation, we could not think of a use case where the rest of the information needed coudn't be retrieved from the user or the model object. If you get into a situation where this is limiting you, please get in touch and explain your problem so that we can think of how to get around it! :)
Finally, you need to be aware of something very important: a method assigned to a rule (let's call them _rule methods_ from now on) *should never call any other method*. That is, they should be self-contained. This is to avoid a potential infinite recursion. Imagine a situation where the rule method calls another method that has _the same_ authorization constraint of the previous rule method. Boom! You just created an infinite loop. Run for your life! :)
You may be thinking that you can control this but, trust me, it will get very difficult to maintain and scale. Things will not always be that simple, maybe you will end up calling a method that is later modified and ends up calling a helper function that triggers the same authorization loop. Yeah, I know. Indirection is a bitch :) Or, in other words "Great power comes with great responsibility". So beware of the infinite loop ;)
h2(#decorators). Decorators
If you like Python as much as I do, you will love decorators. Django has a <code>permission_required</code> decorator, so it felt natural that django-rules implemented an <code>object_permission_required</code> decorator.
Imagine that for our "Example 1":#ex1 we have the following code in <code>views.py</code>:
<pre>
def ship_item(request, id):
item = Item.objects.get(pk = id)
if request.user.has_perms('can_ship', item):
return HttpResponse('success')
return HttpResponse('error')
</pre>
We could easily decorate the view to make the method much more compact and easy to read:
<pre>
from django_rules import object_permission_required
@object_permission_required('can_ship')
def ship_item(request, id):
return HttpResponse('Item successfully shipper! :)')
</pre>
The magic of the decorator is very cool indeed. First, it matches the rule and gets the type of model from it. Then, it gets the <code>id</code> parameter from the view's kwargs and instantiates a Model object with <code>item = model.objects.get(pk = id)</code>. Finally, it can call the <code>request.user.has_perm('can_ship', item)</code> for you and redirect to a fail page if the constraint is not fulfilled.
Note how we have maintained the name of the model's primary key in the parameters of the view. If the parameter has a name that doesn't match the name of the primary key in the model, remember that we will have to add another optional parameter to the rule. From "the section on Rules:":#rules
* <code>view_param_pk</code> _(optional)_: The view parameter's name to use for getting the primary key of the model. It is used in the decorated views for getting the actual instance of the model. If not set, it defaults to the name of the primary key field in the model. Note that if the name of the parameter of the view that holds the value of the object's primary key doesn't match the name of the primary key of the model, the new name must be specified in this parameter.
For example, if we modify the parameter of the view:
<pre>
from django_rules import object_permission_required
@object_permission_required('can_ship')
def ship_item(request, my_item_code): #--> change in the naming of the parameter in the view
return HttpResponse('success')
</pre>
We would have to specify the <code>view_param_pk</code> in the rule definition:
<pre>
rules_list = [
{'codename': 'can_ship', 'model': 'Item', 'view_param_pk': 'my_item_code',
'description': 'Checks if the given user is the supplier of the item'},
]
</pre>
The <code>object_permission_required</code> decorator can receive 4 arguments:
<pre>
@object_permission_required('can_ship', return_403=True)
@object_permission_required('can_ship', redirect_url='/more/foo/bar/')
@object_permission_required('can_ship', redirect_field_name='myFooField')
@object_permission_required('can_ship', login_url='/foo/bar/')
</pre>
By default:
* <code>return_403</code> is set to False.
* <code>redirect_url</code> is set to an empty string.
* <code>redirect_field_name</code> is set to django.contrib.auth's REDIRECT_FIELD_NAME.
* <code>login_url</code> is set to <code>settings.LOGIN_URL</code>.
Thus, if the authorization constraint is not fulfilled, the decorator will default to a redirect to the login page in Django-style :)
Also, note that a couple of the parameters have a specificity. Namely:
* if <code>return_403</code> is set to True it will override the rest of the parameters and the decorator will return a HttpResponseForbidden.
* if <code>redirect_url</code> is set to a URL, it will override that of <code>login_url</code>.
Finally, it is important to note a tricky detail regarding the use of the decorator to guard the access of those methods that are not directly exposed as views mapped to external URLs. When a view method is an entry point through URLs (that is, if your view method is mapped directly to one of the <code>urls.py</code> entries), Django parses the URL and passes the parameters to the view as <code>kwargs</code>. Thus, if you want to use the <code>object_permission_required</code> decorator over an internal method (a method that is called inside one of those external views or somewhere else in your code) you must use <code>kwargs</code> when passing the parameters.
Let's see an example:
<pre>
def item_shipper(request, id):
internal_code = 'XXX-' + my_item_code
return _ship_item(request, id=internal_code) # instead of doing return _ship_item(request, internal_code)
@object_permission_required('can_ship')
def _ship_item(request, id)
return HttpResponse('success')
</pre>
h2(#centralizedpermissions). Centralized Permissions
django-rules has a central authorization dispatcher that is aimed towards a very common need in real life projects: the special, privileged groups such as administrators, user-support staff, etc., that have permissions to override certain aspects of the authorization constraints in the application. For such cases, django-rules has a way to let you bypass its authorization system for whatever reasons you have.
To set up centralized permissions, you will need to set in your project settings the variable <code>CENTRAL_AUTHORIZATIONS</code> pointing to a module. Within that module, you will have to define a a boolean-returning function named <code>central_authorizations</code> accepting exactly two parameters:
* <code>user_obj</code>: the user object.
* <code>codename</code>: the codename of the rule we will be overriden. It is very useful to refine the permissions of a special user "a la ACL".
Note that, although the naming of the parameters doesn't really matter, the order does. The first parameter will receive a user object, and the second parameter, the codename of the rule.
This <code>central_authorizations()</code> function will be called *before* any other rule, so you can override all of them here.
For example, in <code>settings.py</code> you will add:
<pre>
CENTRAL_AUTHORIZATIONS = 'myProjectFoo.utils'
</pre>
And then, within myProjectFoo, in <code>utils.py</code>, you will implement the <code>central_authorizations()</code> function with the overrides for the special users.
Imagine you want to give some special access to user support staff that will be able to access some private fields in the profile (for example, email and age) that generally are hidden to regular users of the application. They are user support, so they should not be able to override certain things in the application. Yet, you also want your über-admins (generally the developers) to be able to access anything within the application so that they can code and test quickly while developing.
In such case, you can write the following <code>central_authorizations()</code> function:
<pre>
def central_authorizations(user_obj, codename):
"""
This function will be called *before* any other rule,
so you can override all of the permissions here.
"""
isAuthorized = False
if user_obj.get_profile().isUberAdmin():
isAuthorized = True
elif user_obj.get_profile().isUserSupport() and codename in ['can_see_full_profile', 'can_delete_item']:
isAuthorized = True
return isAuthorized
</pre>
As you can imagine, everything that is checked in <code>central_authorizations</code> is global to the *whole* project.
h2. Status and testing
django-rules is meant to be a security application. Thus, it has been thoroughly tested. It comes with a battery of tests that tries to cover all of the available funcionality. However, if you come across a bug or an irregular situation, feel free to report it through the "Github bug tracker":https://github.com/maraujop/django-rules/issues.
Finally, the application comes geared with many different exceptions that will make sure rules are created properly. They are also aimed to keep the security of your application away from negligence. Manage the exceptions wisely and you will be a happy and secure coder, as security is kept away from possible neglicence. You should manage them carefuly.
h3. Testing django-rules
To run tests, get into tests directory and execute:
<pre>
./runtests.py
</pre>
It should always say OK. If not, there is a broken test that I hope you will be reporting soon :)
h2. Need more examples?
I have done my best trying to explain the concept behind django-rules but, if you would rather look at more code examples, I am sure you will find the "code in the tests":https://github.com/maraujop/django-rules/blob/master/django_rules/tests/test_core.py quite useful :)
h2. More Documentation
In case you want to know where all this "per-object authentication backend in Django" came to exist, you should at least read the following links:
* A great article about "per-object permission backends in Django":http://djangoadvent.com/1.2/object-permissions/ by Florian Apolloner
* Also, check the explanation of the changes introduced when fixing the "django ticket #11010":http://code.djangoproject.com/ticket/11010
Finally, my most sincere appreciation goes to everybody that contributes to the wonderful Django development framework and also to the rest of developers and committers that build django-rules with their help. Respect! :)
%prep
%autosetup -n django-rules-0.2
%build
%py3_build
%install
%py3_install
install -d -m755 %{buildroot}/%{_pkgdocdir}
if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi
if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi
if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi
if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi
pushd %{buildroot}
if [ -d usr/lib ]; then
find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst
fi
if [ -d usr/lib64 ]; then
find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst
fi
if [ -d usr/bin ]; then
find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst
fi
if [ -d usr/sbin ]; then
find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst
fi
touch doclist.lst
if [ -d usr/share/man ]; then
find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst
fi
popd
mv %{buildroot}/filelist.lst .
mv %{buildroot}/doclist.lst .
%files -n python3-django-rules -f filelist.lst
%dir %{python3_sitelib}/*
%files help -f doclist.lst
%{_docdir}/*
%changelog
* Fri Apr 21 2023 Python_Bot <Python_Bot@openeuler.org> - 0.2-1
- Package Spec generated
|