Authz2023 » History » Revision 21
« Previous |
Revision 21/25
(diff)
| Next »
Shuvam Misra, 27/12/2023 01:59 PM
Authorization architecture, design and implementation¶
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.
Authorisation information¶
This information specifies what a user can and cannot do. It has four dimensions:
- 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?
- 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 aWHERE
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. - 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.
- 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.
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).
Expressing this in a tight notation, we can specify this tuple:
joe.pesci: (voucheredit, vouchertype=retailsales, val=(amt,le,20000), attr=!date)
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.
Mr Pesci may have multiple such qualified capabilities, for various combinations of these four elements.
joe.pesci: (voucherview, vouchertype=ALL, val={}, attr={}) joe.pesci: (voucheredit, vouchertype=retailsales, val=(amt,le,20000), attr=!date) joe.pesci: (vouchernew, vouchertype=retailsales, val=(amt,le,20000), attr={})
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.
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:
joe.pesci: (voucherview, vouchertype=ALL, val={}) joe.pesci: (vouchereditnodate, vouchertype=retailsales, val=(amt,le,20000)) joe.pesci: (vouchernewfull, vouchertype=retailsales, val=(amt,le,20000))
Thus, the fourth member of the tuple can be eliminated everywhere by applying this trick of defining a more fine-grained set of capabilities.
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:
joe.pesci: (voucherview, vouchertype=ALL, limit={}) joe.pesci: (vouchereditnodate, vouchertype=retailsales, limit=(amt,20000)) joe.pesci: (vouchernewfull, vouchertype=retailsales, limit=(amt,20000))
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
- one raw capability
- zero or more scope constraints
- zero or more upper-limit constraints
Switching to JSON, we get
"usercaps": {
"user": "joe.pesci",
"caplist": [{
"cap": "voucherview",
"scope": [
{"vouchertype": "ALL"}
],
"limit": []
},{
"cap": "vouchereditnodate",
"scope": [
{"vouchertype": "retailsales"}
],
"limit": [
{"amt": 20000}
]
},{
"cap": "vouchernewfull",
"scope": [
{"vouchertype": "retailsales"}
],
"limit": [
{"amt": 20000}
]
}]
}
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:
{
"cap": "vouchernewfull",
"scope": [
{"vouchertype": "retailsales"}
],
"limit": [
{"amt": 20000}
]
}
Both scope
and limit
are arrays, so it's possible to have additional entries in them. For example:
{
"cap": "vouchernewfull",
"scope": [
{"vouchertype": "retailsales"},
{"region": "N"}
],
"limit": [
{"amt": 20000},
{"voucherage": 30}
]
}
This may mean that the user has the vouchernewfull
raw capability, which allows the user to create new vouchers, but
- only for retail sales transactions
- only for the North region, not for any other part of the business
- only for voucher values less than or equal to 20,000 in whatever is the currency
- 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
Using the authorisation information: authz_check()
¶
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.
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:
- who is the user?
joe.pesci
- what operation is being attempted? Voucher edit. Therefore the application code knows the
cap
the operation needs:- Is the date too being updated? If yes, then
vouchereditfull
- Else,
vouchereditnodate
orvouchereditfull
- Is the date too being updated? If yes, then
- 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
- 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
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
- what is the country from which the access is being attempted (use geo-IP)
- what is the time of day? (It's conceivable that access rules do not permit editing of records outside office hours, only viewing.)
So, first, the application code creates the opreq
structure:
"opreq": {
"user": "joe.pesci",
"capneeded": ["vouchereditfull", "vouchereditnodate"]
"scope": [
{"vouchertype": "bulksales"},
],
"limit": [
{"voucheramt": "15520.50"}
]
}
This structure must be passed to authz_check()
, which can then load the user's caplist
from backing store and perform a matching operation.
The matching operation steps through the user's caplist
, matching each qualified capability against opreq
. For a qualified capability to match:
- one of the raw capabilities in
opreq
must match thecap
of the qualified capability - all the
scope
terms in the capability must match the corresponding terms inopreq
- all the
limit
terms in the capability must equal or exceed the figures given in the corresponding terms inopreq
- if there is a term missing in the
scope
orlimit
ofopreq
which is present in the qualified capability, then it is deemed a match. So, for instance, if there is noregion
specified inopreq
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 byauthz_check()
and will be enforced by the application code, outside the authorisation checking function.) - if there is a term missing in the
scope
orlimit
of the qualified capability which is present in theopreq
, it is deemed a match.
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.
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.
How authz_check()
works¶
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
.
func authz_check(opreq) thiscaplist=get this user's caplist from in-memory cache if caplist is not there in the cache or if it has expired then load from backing store insert into cache with an expiry time endif matchingcaps = [] // empty array of qualified capabilities for each qualifiedcap in caplist do entrymatches = true for each scopeentry in qualifiedcap.scope do if the corresponding entry is present in opreq then if the two values do not match then entrymatches = false break out of for-loop endif endif endfor if entrymatches == false then break out of for-loop endfor for each limitentry in qualifiedcap.limit do if the corresponding entry is present in opreq then if the value in opreq > the value in limitentry then entrymatches = false break out of for-loop endif endif endfor if entrymatches == true then append qualifiedcap to matchingcaps endif endfor if matchingcaps has one or more entries then return "true" and matchingcaps array else return "false" and matchingcaps empty array endif end // func authz_check()
Variety of constraint variables¶
The examples above have dealt with voucher operations including viewing, editing, and creation. For those operations, the list of constraint variables were
- voucher type
- voucher amount
For an entirely different operation, say viewing of MIS reports of sales data, the constraints may be
- which zone the user belongs to (he will see only his own region's data)
- which department he belongs to (he will see only his product category's data, and dept maps to product category)
- 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)
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:
- for
voucherview
,vouchereditfull
,vouchereditnondate
,vouchernew
, the voucher type is a constraint variable - for
salesreport
, the user's department ID is a constraint variable
Updated by Shuvam Misra over 1 year ago · 21 revisions