RosiKyu commented on PR #12973:
URL: https://github.com/apache/cloudstack/pull/12973#issuecomment-5280168335

   @nvazquez, I have re-tested the PR on a 4.22 KVM environment, on a build 
that includes the latest fix commit, alongside a 4.22 build without the PR. 
Everything below is a measured before and after.
   
   **The performance fix works as expected.** 
   
   - One `createAccount` by a Domain Admin, counted from the MySQL query log, 
on both environments with `dynamic.apichecker.cache.period=0`:
   
   | | without the PR | with the PR |
   |---|---|---|
   | database queries for role permissions | **10939** | **5** |
   | total database queries | 11421 | 212 |
   | time for the API call to return | **10633 ms** | ~1300 ms |
   
   A single account creation took over 10 seconds on the build without the PR.
   
   - Turning the cache on drops it further still. Three `createAccount` calls 
in a row with `dynamic.apichecker.cache.period=300`:
   
   ```
   run 1 (first call after restart)   role permission queries = 2
   run 2                              role permission queries = 0
   run 3                              role permission queries = 0
   ```
   
   The first call reads the permissions once, and later calls reuse them from 
memory until the cache period runs out.
   
   **Escalation is still correctly blocked**, and the new `Escalated APIs: 
[...]` message is a real improvement because it names the exact API responsible:
   
   ```
   Error: (HTTP 531, error code 4365) User of Account Account 
[{"accountName":"restricted-da-user",...}] and domain Domain {...} cannot 
create an account with access to more privileges than they have. Escalated 
APIs: [createServiceOffering]
   ```
   
   **The improved message for the missing role case works.** A default Domain 
Admin creating a User account with the Root Admin role now gets:
   
   ```
   Error: (HTTP 531, error code 4365) User of account: test-domadmin cannot 
assign this role on the requested account: test-account
   ```
   
   instead of the previous `has role null or unknown`, with the original detail 
kept in the log. Much clearer.
   
   **No regressions found.** All 17 account and user APIs affected by this 
change were exercised as a Domain Admin against an account it owns: create, 
update, enable, disable and delete account; create, update, enable, disable and 
delete user; register and get user keys; move user; update resource limit and 
count. All pass. Valid account creations are not wrongly refused, including a 
restricted role creating an account that carries that same role.
   
   ## Two additional issues found
   
   ### 1. Revoking a permission does not take effect until the cache expires
   
   Only happens when `dynamic.apichecker.cache.period` is greater than 0. The 
default is 0, so default installations are not affected.
   
   After a permission is revoked from a role, account creation carries on using 
the remembered copy. A restricted Domain Admin can create an account that still 
has the revoked permission, for as long as the cache period lasts. The window 
is temporary but the account created in it is permanent, so this reopens #5781 
for that period.
   
   **Steps to reproduce**, with `cache.period=300` and the management server 
restarted so nothing is remembered yet:
   
   1. Grant `createServiceOffering` to the restricted user's role (a copy of 
Domain Admin with that one rule denied)
   2. Check in the database that the rule reads `ALLOW`
   3. As that user, create an account with the default Domain Admin role. This 
is allowed at this point, and it is the call that puts the permission into the 
cache
   4. Revoke `createServiceOffering` from the restricted user's role
   5. Check in the database that the rule now reads `DENY`
   6. Immediately create another account with the default Domain Admin role
   
   Step 6 should be refused with `Escalated APIs: [createServiceOffering]`. 
(**It succeeds instead.**)
   
   Steps 3 to 6 have to be done within the cache period, counted from step 3, 
because step 3 is what puts the permission into memory. If that time runs out 
in between, the database is read again correctly and the problem does not show.
   
   **Observed**, 49 seconds after step 3, with the database reading `DENY`:
   
   ```
   account_name    role            created
   test-acct-1     Domain Admin    09:37:27      <- step 3, allowed
   test-acct-2     Domain Admin    09:38:16      <- step 6, should have been 
refused
   
   restricted user's role permission in database: DENY
   test-acct-2 has createServiceOffering=ALLOW:   yes
   ```
   
   **The same sequence on a build without the PR refuses it immediately**, with 
the same cache setting:
   
   ```
   cache.period = 1800
   
   [10:46:56] grant createServiceOffering          database = ALLOW
   [10:46:57] create #1 (puts permission in cache) -> CREATED
   [10:47:01] revoke createServiceOffering         database = DENY
   [10:47:01] create #2 immediately                -> REFUSED
   [10:47:01] create #3 immediately                -> REFUSED
   ```
   
   Setting `cache.period` back to 0 on the PR build also makes it refuse 
correctly every time.
   
   This works in both directions. A newly *granted* permission also fails to 
take effect until the cache period runs out. That is not a security problem, 
but it will be reported as permissions not working.
   
   **Suggestion.** Clear the remembered permissions for a role when they are 
changed, or read them from the database for this particular check.
   
   ### 2. A long list of escalated APIs makes the API return HTTP 500 with an 
empty response
   
   This one does **not** need the cache. It happens on a default installation.
   
   Every differing API name is put into the error message with no limit. Past a 
certain length the API stops returning the error at all.
   
   Tested by calling `createAccount` directly and checking the response, with 
`dynamic.apichecker.cache.period=0`:
   
   | escalated APIs | HTTP status | readable error |
   |---|---|---|
   | 326 | `531`  | yes |
   | 786 | `500` | no |
   
   **The identical call on a build without the PR returns a proper error**:
   
   | | without the PR | with the PR |
   |---|---|---|
   | HTTP status | **531** | **500** |
   | readable error | yes | no |
   
   **Steps to reproduce**, no special configuration needed:
   
   1. Create a Domain Admin role with only a few rules allowed (`listApis`, 
`listAccounts`, `listDomains`, `createAccount`, `createUser`, `listUsers`, 
`listConfigurations`). An account on that role can see about 140 APIs.
   2. Create an account with that role and generate API keys for it.
   3. As that user, run `createAccount` with a much more privileged role. Using 
the default Domain Admin role gives 326 escalated APIs and returns a proper 
error. Using a role with a `*` ALLOW rule gives 786 and returns `500` with an 
empty response.
   
   cmk shows the failing case as:
   
   ```
   (tpr_minimal) 🐱 > create account accounttype=2 username=tc-chk-c 
password=Password123456 firstname=T lastname=T [email protected] 
domainid=2a43e16e-f8b6-4a93-ba87-74ed1a5ecd9f 
roleid=5c7e8978-d79c-43f8-b2a2-5201e7192d08
   🙈 Error: failed to decode response
   
   ```
   
   So the user gets no error text at all, and a `500` instead of the intended 
`531`, which looks like the server broke rather than a permission being refused.
   
   The escalation is still correctly refused and nothing is written to the 
database, so this is not a security issue.
   
   This also hides the improved message from `81df63ed5c0` whenever the list is 
long, so that improvement is only visible when the list is short.
   
   **Suggestion.** Limit the list in the message, for example the first 10 
followed by `(+N more)`, keeping the full list in the log if it is needed.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to