Project

General

Profile

Authz2023 » History » Version 23

Shuvam Misra, 27/12/2023 02:06 PM

1 1 Shuvam Misra
# Authorization architecture, design and implementation
2
3
It's 2023. Trump may become President of God's Own Country next year. We have moved from bespoke authentication and authorization design and implementation to Keycloak and IDshield. With all this comes a new view about the architecture of authorization data, and its implementation.
4
5
## Authorisation information
6
7
This information specifies what a user can and cannot do. It has four dimensions:
8 12 Shuvam Misra
* **Raw capability**: this specifies if the user can perform a specific operation. In implementation, it maps on to a web service call (WSC). Can user X call WSC Y?
9
* **Visibility constraints**: this specifies whether the user can see all data in a specific table or for a specific call (*e.g.* all sales data) and if not, then which subsets can he see? This is equivalent to doing a `SELECT` on a table and using the visibility constraints for a `WHERE` clause. For instance, user X can see all sales data and user Y can see only North Zone data. In other words, the visibility constraint defines the scope of the access right.
10 1 Shuvam Misra
* **Attribute constraints**: this specifies whether the user can see/edit all the attributes (or columns, in DB parlance) of a class of entities, or can only see a subset. For instance, some privileged users can see the full employee list with all attributes, but most users are not allowed to see the salary data. So, there are restrictions on certain restricted attributes. We can then divide the list of attributes into a general-access set and a privileged set.
11
* **Value constraints**: this specifies whether a user's access is restricted to certain value limits of certain quantitative fields. For instance, a junior manager is permitted to approve an invoice with a total value less than a million dollars, whereas a vice president can approve invoices of up to ten million.
12
13
Combining all these, we can make a sample statement like this: Mister Joe Pesci can do voucher edits (he has the `voucheredit` **capability**) for vouchers of only retail sales (**visibility constraint** based on voucher type) whose value is less than $20,000 (**value constraint**). And while he does so, he is not permitted to change the date of the voucher (**attribute constraint**).
14
15
Expressing this in a tight notation, we can specify this tuple:
16
```
17 5 Shuvam Misra
joe.pesci:   (voucheredit, vouchertype=retailsales, val=(amt,le,20000), attr=!date)
18 1 Shuvam Misra
```
19
20 12 Shuvam Misra
We call these tuples **qualified capabilities**, which are the result of applying constraints to raw capabilities. Here, the `(amt, le, 20000)` indicates that the amount of the voucher needs to be less than or equal to (hence `le`) the limit given.
21 3 Shuvam Misra
22 12 Shuvam Misra
Mr Pesci may have multiple such qualified capabilities, for various combinations of these four elements.
23 1 Shuvam Misra
```
24 5 Shuvam Misra
joe.pesci:   (voucherview, vouchertype=ALL, val={}, attr={})
25
joe.pesci:   (voucheredit, vouchertype=retailsales, val=(amt,le,20000), attr=!date)
26
joe.pesci:   (vouchernew,  vouchertype=retailsales, val=(amt,le,20000), attr={})
27 1 Shuvam Misra
```
28
With this set of records, Mr Pesci can see all vouchers, create vouchers only in retail sales of value less than $20,000 and enter all details (no attribute constraints) but when editing these vouchers, he is not allowed to edit the date.
29
30 12 Shuvam Misra
We can collapse the semantics of the fourth term (attribute constraints) if we expand the set of values for the first term. So, instead of having a generic `voucheredit` raw capability, we define two raw capabilities: `vouchereditfull` and `vouchereditnodate`. With this refinement, we can express the previous set of qualified capabilities in the following way:
31 1 Shuvam Misra
```
32 5 Shuvam Misra
joe.pesci:   (voucherview,        vouchertype=ALL,         val={})
33 11 Shuvam Misra
joe.pesci:   (vouchereditnodate,  vouchertype=retailsales, val=(amt,le,20000))
34 5 Shuvam Misra
joe.pesci:   (vouchernewfull,     vouchertype=retailsales, val=(amt,le,20000))
35 1 Shuvam Misra
```
36
Thus, the fourth member of the tuple can be eliminated everywhere by applying this trick of defining a more fine-grained set of capabilities.
37 11 Shuvam Misra
38
We make one more pragmatic simplification: we assume that quantitative limits (*i.e.* `val=`) will always be upper caps. It is very unlikely that there will be a lower cap to authorisation constraints. In that case, we may assume that the operator will always be `le`, therefore `val=(amt, le, 20000)` now becomes simplified to `limit=(amt, 20000)`. Therefore we now have:
39
```
40 1 Shuvam Misra
joe.pesci:   (voucherview,        vouchertype=ALL,         limit={})
41
joe.pesci:   (vouchereditnodate,  vouchertype=retailsales, limit=(amt,20000))
42
joe.pesci:   (vouchernewfull,     vouchertype=retailsales, limit=(amt,20000))
43
```
44
45 12 Shuvam Misra
We can now move from the particular to the general. If we generalise the example of Mr Pesci's capabilities. We can now say that a qualified capability has
46
* one raw capability
47
* zero or more scope constraints
48
* zero or more upper-limit constraints
49
50
Switching to JSON, we get
51
``` json
52
"usercaps": {
53
    "user": "joe.pesci",
54
    "caplist": [{
55
        "cap": "voucherview",
56
        "scope": [
57
            {"vouchertype": "ALL"}
58
        ],
59
        "limit": []
60
    },{
61
        "cap": "vouchereditnodate",
62
        "scope": [
63
            {"vouchertype": "retailsales"}
64
        ],
65
        "limit": [
66
            {"amt": 20000}
67
        ]
68
    },{
69
        "cap": "vouchernewfull",
70
        "scope": [
71
            {"vouchertype": "retailsales"}
72
        ],
73
        "limit": [
74
            {"amt": 20000}
75
        ]
76
    }]
77
}
78
```
79
There will be one such block for each user in the system, and the `caplist` can have dozens or hundreds of elements in its array. In this representation, one qualified capability is represented by:
80
``` json
81
    {
82
        "cap": "vouchernewfull",
83
        "scope": [
84
            {"vouchertype": "retailsales"}
85
        ],
86
        "limit": [
87
            {"amt": 20000}
88
        ]
89
    }
90
```
91 13 Shuvam Misra
Both `scope` and `limit` are arrays, so it's possible to have additional entries in them. For example:
92
``` json
93
    {
94
        "cap": "vouchernewfull",
95
        "scope": [
96
            {"vouchertype": "retailsales"},
97
            {"region": "N"}
98
        ],
99
        "limit": [
100
            {"amt": 20000},
101
            {"voucherage": 30}
102
        ]
103
    }
104
```
105
This may mean that the user has the `vouchernewfull` raw capability, which allows the user to create new vouchers, but
106
* only for retail sales transactions
107
* only for the North region, not for any other part of the business
108
* only for voucher values less than or equal to 20,000 in whatever is the currency
109
* only for vouchers younger than or equal to 30 days, which means there is a cap on how far back-dated the new vouchers may be
110 4 Shuvam Misra
111 20 Shuvam Misra
## Using the authorisation information: `authz_check()`
112 4 Shuvam Misra
113 20 Shuvam Misra
A function called `authz_check()` will go through a user's `usercaps` data structure and decide whether there is any qualified capability in her `caplist` which matches the access being attempted.
114 1 Shuvam Misra
115 18 Shuvam Misra
Before the application code calls `authz_check()`, it needs to put together the list of attributes based on which permission will be granted. Let's call this information packet an `opreq` for operation request. The application code needs to submit the operation request to `authz_check()` and ask whether the request can be allowed. Basing our example on the tuples shown earlier, the code may go through the following questions and put together the `opreq` data structure:
116 4 Shuvam Misra
* who is the user? `joe.pesci`
117 15 Shuvam Misra
* what operation is being attempted? Voucher edit. Therefore the application code knows the `cap` the operation needs:
118 4 Shuvam Misra
  * Is the date too being updated? If yes, then `vouchereditfull`
119 16 Shuvam Misra
  * Else, `vouchereditnodate` or `vouchereditfull`
120 4 Shuvam Misra
* What's the type of the voucher being accessed? The application code sees the ID of the voucher being updated, pulls out the voucher record from the database and finds out its type. Let us say it turns out to be `bulksales`
121 15 Shuvam Misra
* What is the amount of the voucher being accessed? This is obtained from the web service request if the amount is being updated, or else from the voucher record in the database: `15520.50`
122 16 Shuvam Misra
123
It is not necessary that all details of the operation being attempted are contained in the request parameters of the WSC. Some authorisation determining parameters may be environmental, like
124 4 Shuvam Misra
* what is the country from which the access is being attempted (use geo-IP)
125 1 Shuvam Misra
* what is the time of day? (It's conceivable that access rules do not permit editing of records outside office hours, only viewing.)
126
127 18 Shuvam Misra
So, first, the application code creates the `opreq` structure:
128 1 Shuvam Misra
``` json
129 18 Shuvam Misra
"opreq": {
130 1 Shuvam Misra
    "user": "joe.pesci",
131
    "capneeded": ["vouchereditfull", "vouchereditnodate"]
132
    "scope": [
133
        {"vouchertype": "bulksales"},
134
    ],
135
    "limit": [
136 4 Shuvam Misra
        {"voucheramt": "15520.50"}
137 1 Shuvam Misra
    ]
138
}
139
```
140
This structure must be passed to `authz_check()`, which can then load the user's `caplist` from backing store and perform a matching operation.
141 18 Shuvam Misra
142 20 Shuvam Misra
The matching operation steps through the user's `caplist`, matching each qualified capability against `opreq`. For a qualified capability to match:
143
* one of the raw capabilities in `opreq` must match the `cap` of the qualified capability
144 18 Shuvam Misra
* all the `scope` terms in the capability must match the corresponding terms in `opreq`
145 19 Shuvam Misra
* all the `limit` terms in the capability must equal or exceed the figures given in the corresponding terms in `opreq`
146 20 Shuvam Misra
* if there is a term missing in the `scope` or `limit` of `opreq` which is present in the qualified capability, then it is deemed a match. So, for instance, if there is no `region` specified in `opreq` when Mister Pesci attempts to view vouchers, and the capability says `"region": "N"`, it is deemed that Mister Pesci can be permitted to perform the operation. (The `"region": "N"` becomes a constraint which will be returned by `authz_check()` and will be enforced by the application code, outside the authorisation checking function.)
147
* if there is a term missing in the `scope` or `limit` of the qualified capability which is present in the `opreq`, it is deemed a match.
148 18 Shuvam Misra
149
The matching operation will return the list of matching elements from the `caplist`. More than one entry may match; all matching entries will be returned in an array.
150
151 19 Shuvam Misra
The application code which called `authz_check()` will now scan the entries returned, and will apply any constraints indicated by them. For instance, two users may attempt a `voucherview` operation, but one user may have a qualified capability with `"scope": [{"region": "N"}]` and the other user may not have any `"region"` constraint. In that case, the business logic will receive the matching capabilities list from `authz_check()`, and in the first user's case, will see the `"region"` constraint, and will pull out only the matching subset of vouchers to show the user. In the second user's case, the business logic must query the database and pull out all vouchers which exist, since there is no visibility constraint. This is to be done *by the business logic*, not by the authorisation module.
152 18 Shuvam Misra
153 21 Shuvam Misra
## How `authz_check()` works
154 18 Shuvam Misra
155 21 Shuvam Misra
The function will take one parameter, `opreq`, and will return *(i)* a boolean to indicate whether the request is allowed, and *(ii)* an array of one or more qualified capabilities which match `opreq`. This array will be of length zero if the first response is `false`.
156 1 Shuvam Misra
157 21 Shuvam Misra
```
158
func authz_check(opreq)
159 23 Shuvam Misra
        returns boolean permitted
160
                caplist matchingcaps
161
162 21 Shuvam Misra
    thiscaplist=get this user's caplist from in-memory cache
163
    if caplist is not there in the cache or if it has expired then
164
        load from backing store
165
        insert into cache with an expiry time
166
    endif
167
168
    matchingcaps = []     // empty array of qualified capabilities
169 1 Shuvam Misra
    for each qualifiedcap in caplist do
170 23 Shuvam Misra
        if qualifiedcap.cap is absent in opreq.capneeded then
171
            // we must skip this capability, because the cap itself doesn't match
172
            break out of for-loop
173
        endif
174
175 1 Shuvam Misra
        entrymatches = true
176 23 Shuvam Misra
177
        //
178
        // check each entry in the scope arrays for mismatch
179
        //
180 21 Shuvam Misra
        for each scopeentry in qualifiedcap.scope do
181
            if the corresponding entry is present in opreq then
182
                if the two values do not match then
183
                    entrymatches = false
184
                    break out of for-loop
185
                endif
186
            endif
187
        endfor
188
189 1 Shuvam Misra
        if entrymatches == false then
190 23 Shuvam Misra
            // if the scope list itself threw up a mismatch, why
191
            // bother trying to match the limit list?
192 21 Shuvam Misra
            break out of for-loop
193 1 Shuvam Misra
        endfor
194 23 Shuvam Misra
195
        //
196
        // check each entry in the limit array for mismatch
197
        //
198 21 Shuvam Misra
        for each limitentry in qualifiedcap.limit do
199
            if the corresponding entry is present in opreq then
200
                if the value in opreq > the value in limitentry then
201
                    entrymatches = false
202
                    break out of for-loop
203
                endif
204
            endif
205 1 Shuvam Misra
        endfor
206 23 Shuvam Misra
207
        // at this point, we've matched all the scope and limit terms
208
        // of qualifiedcap and opreq. If they all match, we must add the
209
        // qualifiedcap to the array of matching caps to be returned.
210
        //
211 21 Shuvam Misra
        if entrymatches == true then
212
            append qualifiedcap to matchingcaps
213 1 Shuvam Misra
        endif
214 22 Shuvam Misra
    endfor    // for each qualifiedcap in caplist
215 21 Shuvam Misra
216
    if matchingcaps has one or more entries then
217
        return "true" and matchingcaps array
218
    else
219
        return "false" and matchingcaps empty array
220
    endif
221
end   // func authz_check()
222
```
223 8 Shuvam Misra
224
## Variety of constraint variables
225
226
The examples above have dealt with voucher operations including viewing, editing, and creation. For those operations, the list of constraint variables were
227
* voucher type
228
* voucher amount
229
230
For an entirely different operation, say viewing of MIS reports of sales data, the constraints may be
231
* which zone the user belongs to (he will see only his own region's data)
232 9 Shuvam Misra
* which department he belongs to (he will see only his product category's data, and dept maps to product category)
233 8 Shuvam Misra
* what his rank is (this decides whether he can see only the last month's data, or the last year's, or all historical data)
234
235
So, for voucher-related capabilities, one set of constraint variables are applied, and for sales report viewing, a totally different set of variables apply. These can be keyed to the capability:
236
* for `voucherview`, `vouchereditfull`, `vouchereditnondate`, `vouchernew`, the voucher type is a constraint variable
237
* for `salesreport`, the user's department ID is a constraint variable