Project

General

Profile

Authzdesign » History » Version 16

Shuvam Misra, 05/12/2018 04:38 PM

1 16 Shuvam Misra
# Authorization module design
2 1 Shuvam Misra
3
The authorization module uses a set of rules in a rulebase to decide who can perform what action on what object. "Who" here is a human user, "what action" is an operation performed on a computer system like a voucher entry, a report viewing, or the adding of a new task in a task tracker. "What object" here means an information entity like a salary record, a voucher, a task list, *etc*.
4
5
{{>toc}}
6
7 16 Shuvam Misra
This module is not specific to any business application. It can be used anywhere. It can be implemented as a web service, with its own small set of web service calls, and therefore can be deployed as a standalone sub-module with very clean separation from the rest of your business application. It has its own database, and can be installed in its own Docker container too if needed.
8 2 Shuvam Misra
9 1 Shuvam Misra
## The basic entities
10
11
Any authorisation system deals with the following entities:
12
13
* **user**: the human user
14
* **data objects**: the item, or set of items, or class of items, on which operations are being performed
15
* **operations**: the operations being attempted
16
* **permissions**: rules which specify which combinations of the above three entities are permitted. All else are blocked.
17
18
Additional derived entities, defined for convenience, are
19
20
* a group of users, called **user groups**
21 15 Shuvam Misra
* a group of permissions, called **roles**
22 1 Shuvam Misra
23
This design assumes that there is a user master table, a group master table, and a groupusersmap table which maps users to groups of which they are members.
24
25
## The structure of an access rule
26
27
We need to understand the demands on the access rule.
28
29
In its simplest form, a rule will just have three parts:
30
31
* which user the rule applies to: *e.g.* "user `sanjeev`"
32
* which entity the rule applies to: *e.g.* "vouchers" (a class of objects, not just a single object)
33
* which operation is being permitted by the rule: *e.g.* "create"
34
35
This three-part rule says "User `sanjeev` can create vouchers".
36
37
For simple designs, this can even be combined to a two-part rule:
38
39
* the user (`sanjeev`)
40
* the privilege (`vouchercreate`)
41
42
where you combine the class of objects with the operation name and call this combined thing the privilege being given.
43
44
We need more sophistication than this for more complex business applications.
45
46 7 Shuvam Misra
* **Access to a specific object**: we may want to give User `sanjeev` access to modify a specific indent or purchase order, but not give him the right to do the same with all indents or purchase orders. In our language we are giving a user *ad hoc* access to a specific *object instance*, not a *class of objects*.
47 1 Shuvam Misra
* **Access to a sub-object**: we may want to give User `sanjeev` access to the Vendor Details section of a specific purchase order, not all sections. We may want to give User `galahad` access to Tax Computations section of the same purchase order. This requires our access rule to have the ability to refer to a sub-object.
48
* **Relationship based access**: we may want to give a specific access to a user if he has a specific relationship with the object instance, for instance the user who created a new Purchase Order has edit rights to it, but others in his department only have read-only access.
49 10 Shuvam Misra
50
Note that the complexity of a sub-object reference only comes when we need to give a user rights to a specific sub-object of a *specific object instance*. If we had to give the user rights to a sub-object of a whole class of objects, then we would just have re-defined our object definition to refer to a specific section of the object, not the whole object. For instance, if we needed to give `sanjeev` access to the Vendor Details section of *all Purchase Orders*, we would simply have re-defined our object to refer to "`povendorsection`" instead of the entire "`po`". And then we would have given `sanjeev` the access to all `povendorsection`. The real complexity comes when we want to give `sanjeev` the access to the Vendor Details section of *a specific Purchase Order*. In that case, we need to add a new attribute to the access rule.
51 1 Shuvam Misra
52
So, the final structure of an access rule can be as follows:
53
54
* **ID**: a mandatory, unique ID, the primary key for database access and cross-reference purposes.
55
56
* **usertype**: one char, with "`U`" for user and "`G`" for user-group. Mandatory, non-NULL
57
58
* **who**: string, will contain a username or a groupname. Mandatory non-NULL. A "`*`" here means that this rule applies to all users without exception.
59 10 Shuvam Misra
60 16 Shuvam Misra
* **resource**: string, will specify the resource on which the access control will apply. This may have a path notation to identify a module or sub-module in a hierarchy. This allows us to specify an access control at any level of a hierarchy. Possible values could be "`/ui/fa`" meaning the UI components of the Financial Accounting system, or "`/ws/fa`" meaning the web services of the FA system, or "`/ws/fa/vouchers`" to indicate web services of voucher management within the FA system, and so on. Mandatory, non-NULL. The string format shown in these examples are built into the design, but the string *content* is not. In other words, you don't need to start with `/ws/` or `/ui/` -- you can start with `/acct/` for the Accounts module or `/ho/` to define a sub-tree related to all Head Office operations. It's entirely up to you.
61 10 Shuvam Misra
62 1 Shuvam Misra
* **instance**: string, may contain the ID of a specific resource instance of the type or class indicated by **`resource`**. For instances, if the **`resource`** fields specified "`ws/fa/vouchers`" and the **`instance`** has the value "`20a00bce`", then this is the unique ID of a specific voucher on which the access control is being applied. Optional, may be empty.
63 10 Shuvam Misra
64 1 Shuvam Misra
* **part**: a value indicating a section or part of the object instance on which the access control is being applied. The object instance itself is identified in the **`instance`** attribute above. Examples could be "`vendordetails`", "`candidate[02]`" (for the second candidate in a list of candidates), and so on. Optional, may be NULL.
65 10 Shuvam Misra
66
* **relationship**: a value indicating the relationship the user must have with the object for this access to be applicable. This value should be from an enumerated set of possible values. Optional, may be NULL.
67
68 1 Shuvam Misra
* **action**: a value indicating the operation being permitted, from the enumerated set of all possible operations. For UI related operations, values could be `show`, `edit`, `delete`, *etc*. For web services, operations could be `get`, `update`, `create`, *etc*. There could be any number of possible operations, and a complete set can only be defined in the context of the application and the business operations it supports.
69
70
If the system permits it, the **`action`** attribute may be multi-valued, and can carry a list of operations which are permitted. If the system does not support multi-valued attributes, the entire rule will have to be repeated in the database table for each valiue of **`action`**.
71
72
## Supporting the idea of roles
73
74
A role is a collection of access rules. It's a convenience. If all Sales Executives need to be given a set of 23 access permissions, it's nice to be able to group them into something called a "role", and then assign permission to users based on such roles.
75
76
A Roles master can contain the following useful fields:
77
78
* **ID**: the mandatory unique ID column
79
* **descr**: a string description of the role, for human consumption
80
81
The access rules table's **`who`** attribute, which is already capable of holding usernames or user-group names, can now be extended to hold role IDs too, and the **usertype** attribute will then contain "`R`". This means that
82
83
* an ordinary user can be linked to an access rule,
84
* a user-group can be associated with the rule, or
85
* a role can be associated with it
86
87
In addition to this, a roles mapping table is needed, to map a role to either individual users or user-groups. This table will have just three columns:
88
89
* **role**: the ID of the role record in the Roles master
90
* **usertype**: a single-character field, holding "`U`" or "`G`"
91
* **who**: the username of a user, or the groupname of a group
92
93 16 Shuvam Misra
All columns here are mandatory non-NULL, and the uniqueness criterion will apply to the entire 3-tuple. One can always a fourth column, optional, to hold a human-readable description of the role. Plus, there are the administrative and audit columns...
94 1 Shuvam Misra
95
## Summary of tables
96
97
* **`accessrules`**
98
  * **`ID`**
99
  * **`usertype`**
100
  * **`who`**
101
  * **`resource`**
102
  * **`instance`**
103
  * **`part`**
104 12 Shuvam Misra
  * **`relationship`**
105 1 Shuvam Misra
  * **`action`**
106
107
108
* **`roles`**
109
  * **`ID`**
110
  * **`descr`**
111
112
* **`rolemembersmap`**
113
  * **`role`**
114
  * **`usertype`**
115
  * **`who`**
116
117
## Loading an access profile
118
119 16 Shuvam Misra
When a user logs in to the application, her access profile should be loaded from database into in-core storage or a fast cache so that it can be traversed rapidly at each subsequent request or operation to check whether the user is permitted to perform the operation.
120 1 Shuvam Misra
121
```
122
load_access_profile(user, rulestree)
123
    groupslist = load all user-group names from groups table
124
            where user is a member
125
    roleslist = load all roles from rolemembersmap where
126
            (usertype == 'U' and who == username) OR
127
            (usertype == 'G' and who exists in groupslist)
128
129
    ruleIDlist = load the IDs of all rules from accessrules where
130
            (usertype == 'U' and who == username) OR
131
            (usertype == 'G' and who exists in groupslist) OR
132
            (usertype == 'R' and who exists in roleslist) OR
133
            (who == '*')
134
135
    // we now have the IDs of all the rules which apply to this user
136
137
    // We now convert the set of lists into a tree data structure,
138
    // based on the paths in the "resource" attributes of the rules.
139
    // Any rule with "resource" == "ui" or "ws" or just a one-part
140
    // path will be associated with a Level 1 node just below the root.
141
    // Any rule with "resource" == "ui/fa" will be associated with
142
    // a Level 2 node, labelled "fa", below the "ui" node. And so on.
143
    //
144
    // Each node in the tree has a set of one or more access rules and
145
    // their associated attributes.
146
147
    rulestree = rules_list_to_tree(ruleIDlist)
148
end procedure
149
```
150
151
This process creates an in-memory tree data structure which can be traversed very efficiently whenever a specific access is attempted.
152
153 15 Shuvam Misra
If the tree data structure needs to be kept in a shared network-accessed cache, *e.g. memcached or Redis, then this is possible by using key-value pairs, where the tuple of `(username+resource+seqno)` becomes the key, and the rest of the access rule becomes the value. If there are half a dozen access rules for the same resource and username, there will now be six key-value pairs in Redis for this. The `seqno` will be used to separate the key-value pairs.
154 1 Shuvam Misra
155 15 Shuvam Misra
## The access-rules tree
156 1 Shuvam Misra
157 15 Shuvam Misra
Shown below is a sample of the `rulestree` generated in the previous section.
158
159
![](accessrules-tree.png)
160
161
The tag or label against each node is the value of the `resource` attribute of the access rules attached with that node. The root node is labelled "/".
162
163 16 Shuvam Misra
All access rules associated with the current logged-in user will be in this `rulestree`, attached with one or other node in the tree. All access rules with `resource == "/hr/payroll"` will be attached with the node shown here, labelled `"/hr/payroll"`. It is implicit, therefore, that the value of the `resource` attribute must always be in the form of a path, whose components are alphanumeric and the separator is the "/" character. This syntax must be followed for the tree to be created from the rules correctly.
164 15 Shuvam Misra
165
We look at three example access rules below. Unique ID and optional `NULL` attributes omitted from examples.
166
167
```
168
usertype: U, who: sanjeev, resource: /hr/payroll, action: create
169
```
170
171
```
172
usertype: G, who: hrteam, resource: /hr/payroll/tds, action: get
173
```
174 1 Shuvam Misra
175
```
176 15 Shuvam Misra
usertype: U, who: sanjeev, resource: /hr/payroll/tds, action: update
177
```
178
179
The second rule grants TDS (Tax Deducted At Source) data viewing rights to all members of the `hrteam` user-group. The first and third access rules are specifically for one user, `sanjeev`.
180
181
If we assume that the user-group `hrteam` includes the user `sanjeev`, then all three access rules will be present in the `rulestree` created when user `sanjeev` logs in. The first access rule will be attached to the node labelled "`/hr/payroll`", and the other two to the node labelled "`/hr/payroll/tds`".
182 16 Shuvam Misra
183
Our examples above have shown each rule with three attributes, to illustrate a simple sub-set of possibilities. The full-fledged design actually works with five attributes per rule: the "`relation`" and the "`part`" are also supposed to be there as optional attributes of a rule.
184 15 Shuvam Misra
185 1 Shuvam Misra
## Checking access
186
187
Whenever any access is attempted, a function "`is_allowed()`" is called, which traverses the "`rulestree`" created in the loading step, and returns a boolean "`TRUE`" or "`FALSE`". The "`is_allowed()`" function is called with:
188
189
* `user`: the username of the user who is attempting the operation
190
* `resource`: the resource type being operated upon. May in some cases just identify a module or a resource class.
191
* `instance`: the unique ID of the specific instance of type `resource`, *e.g.* Purchase Order ID or voucher ID. May be null for certain operations, for instance an operation which impacts all or many instances of the resource type
192
* `part`: a path string indicating the sub-part of the resource which is being operated upon. This may be NULL.
193 13 Shuvam Misra
* `relationship`: a value indicating a specific relationship between user and resource. May be NULL.
194 1 Shuvam Misra
* `operation`: an identifier indicating what is the operation being attempted.
195
196
197
```
198
boolean
199
is_allowed(user, resource, instance, part, relationship, operation)
200
    patharray[] = break resource into parts at "/"
201
    thisnode = rulestree    // initialise to the root node of tree
202
203
    for each pathstep in patharray[] do
204
        if any of the rules at thisnode matches all the other parameters of the call, then
205
            return TRUE
206
        else if thisnode.childnode corresponding to pathstep exists then
207
            thisnode = thisnode.childnode
208
        else
209
            return FALSE
210
        endif
211
    endfor
212
213
    return FALSE
214
end function
215
```
216
217
This function will be called by every web service call which requires authentication, and if the `rulestree` data structure is passed to the front-end Javascript code at login time, then it can be used by the front-end code too, to decide which UI segments to display or hide.
218 15 Shuvam Misra
219
The tree will be traversed from the root downwards, by matching the `resource` value passed as parameter to `is_allowed()` with the labels or tags of each node. At each node of the tree, any access rules which match the current call to `is_allowed()` at the current node will be acted upon first. If no matching access rule is found in the current node, then the traversal will move to the next node one level down in the tree. This will continue till a matching access rule is found, or the full path of the input `resource` parameter is complete and no match is found, or there are no further matching nodes in the tree as per the path indicated by the `resource` parameter.
220
221
We look once again at the earlier three example access rules below.
222
223
```
224
usertype: U, who: sanjeev, resource: /hr/payroll, action: create
225
```
226
227
```
228
usertype: G, who: hrteam, resource: /hr/payroll/tds, action: get
229
```
230
231
```
232
usertype: U, who: sanjeev, resource: /hr/payroll/tds, action: update
233
```
234
235
Once again, we assume that user `sanjeev` is a member of the user-group `hrteam`, therefore all three rules will be part of the `rulestree` of `sanjeev`.
236
237
Now some example calls to `is_allowed()` will be applied to this set of access rules.
238
239
**Example 1**
240
241
```
242
is_allowed(rahul, /hr/payroll/tds, NULL, NULL, NULL, get)
243
```
244
where user `rahul` is part of the `hrteam` user-group. In this case, the second access rule will result in `is_allowed()` 
245
returning `TRUE`.
246
247
**Example 2**
248
249
```
250
is_allowed(rahul, /hr/payroll/tds, 8a3a8509, NULL, NULL, get)
251
```
252
In this case too, `is_allowed()` will return `TRUE`, because a specific instance ID in the call will match a rule which is instance-agnostic (*i.e.* where the `instance` attribute is NULL, thus indicating that the rule applies irrespective of any instance-ID). The second access rule would therefore apply.
253
254
The same result would have been seen if the `relationship` parameter in `is_allowed()` had specified a value. This is because the access rule is relationship-agnostic, since its own `relationship` attribute is NULL.
255
256
**Example 3**
257
258
```
259
is_allowed(sanjeev, /hr/payroll/tds, NULL, NULL, NULL, create)
260
```
261
In this case, the traversal through the tree would have returned from the node labelled `/hr/payroll`, since that node would have the first access rule, and it would have given user `sanjeev` the `create` permission to `/hr/payroll` and all sub-nodes and lower-level resources below it.