New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support app-specific web apps permissions for web apps users #34421
Conversation
The rest of this was removed in #29767
…tial page data Privileges are already available on the front end.
8de9ec2
to
bc74d48
Compare
bc74d48
to
16ddf89
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exemplary PR, bravo! Great description, commit breakdown, etc
@@ -267,22 +279,6 @@ hqDefine('users/js/roles',[ | |||
allowCheckboxId: null, | |||
allowCheckboxPermission: null, | |||
}, | |||
{ | |||
showOption: root.webAppsPrivilege, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing this was for projects that have plans without access to web apps (is that Community?). Removing this restriction will mean that those projects will now be able to configure web apps permissions even when not applicable. Probably not a big deal, just wanted to check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nevermind, I see it was functionally replaced with
{% if request|request_has_privilege:"CLOUDCARE" %}
below
<legend> | ||
{% trans "Web Apps" %} | ||
</legend> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did this look like a decent target to extract into a template? Not sure how much it has in common with the other parameterized permissions
@@ -510,20 +510,6 @@ def can_restrict_access_by_location(self): | |||
return self.domain_object.has_privilege( | |||
privileges.RESTRICT_ACCESS_BY_LOCATION) | |||
|
|||
@property | |||
@memoized | |||
def release_management_privilege(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice cleanup commit
@@ -556,3 +478,43 @@ <h4 class="modal-title" data-bind="text: modalTitle"></h4> | |||
</form> | |||
</div> | |||
</div> | |||
|
|||
<script type="text/html" id="permission_all_selected_none"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol, you're always a step ahead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😀
for access in ApplicationAccess.objects.filter(restrict=True): | ||
WEB_APPS_PERMISSIONS_VIA_GROUPS.set(access.domain, enabled=True, namespace=NAMESPACE_DOMAIN) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are you anticipating this being fast enough to run during the deploy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, with only a couple hundred ApplicationAccess
objects on prod this should be fast
@@ -944,6 +944,13 @@ def _ensure_valid_randomness(randomness): | |||
""" | |||
) | |||
|
|||
WEB_APPS_PERMISSIONS_VIA_GROUPS = StaticToggle( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might throw in a description pointing to the alternative, unless you did that on the page itself
{% blocktrans %} | ||
This feature is deprecated. Access to specific web apps can now be managed via | ||
<a target="_blank" href="{{ roles_url }}">Roles & Permissions</a>. | ||
{% endblocktrans %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol, yup there it is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a good point that the flag itself could be more descriptive, updated in 7aa89a6
|
||
@skip_on_fresh_install | ||
def enable_flag(apps, schema_editor): | ||
for access in ApplicationAccess.objects.filter(restrict=True): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this as targeted as we can safely do? Like, would it make sense to check whether the domain has access to web apps or groups configured or some such? I guess that could also be a clean-up pass done separately later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's pretty harmless, the view itself is still decorated to require the cloudcare privilege. The risks here are:
- Block access to this page for a domain that should have it: I think this is unlikely, although I could remove the
restrict=True
filter. - Accidentally give access to a domain that doesn't need it: not a big deal, since right now all domains (with cloudcare privilege) can access this page
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidentally give access to a domain that doesn't need it: not a big deal, since right now all domains (with cloudcare privilege) can access this page
Yeah, I agree - this is the situation I was thinking of. Fewer domains on this flag means that it's easier to eventually remove in the future, but I agree it's probably better to play it safe here and work on whittling down the list separately whenever we get around to it
|
||
|
||
@skip_on_fresh_install | ||
def _update_default_mobile_worker_role(apps, schema_editor): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also a bit nervous about the access change associated with this. There's always the possibility that there are mobile workers using roles other than the default and using web apps. I do think the current behavior is arguably a bug (if the role explicitly doesn't have web apps access...), so I think we have some wiggle room. Still, is there anything we could do to de-risk this, or get a sense of how many users it might realistically impact?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Point well taken. I'll do some digging on this and report back.
(You reviewed this a lot faster than I was expecting - thank you!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reverted this change, see longer comment below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great change, thanks! If a web user doesn't have access to any apps can they still get the Web Apps page and use login as?
Can't add anything useful. But it is good learning to read through the PR and Ethan's comments. |
Same here. Reading through this PR and the comments is especially helpful since I'm starting on adjacent permissions work for tableau access. |
I'm reverting the last few commits, which change the mobile worker default role and remove the Many mobile workers have no role set. This makes sense, since the default role is a relatively recent addition and we're lazily migrating mobile workers to use it (see #32057). For those mobile workers, if you ask for their role, you get the default role back (see #32008), but directly accessing their permissions doesn't go through that default role:
That means the migration to add |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does removing this file have any implications for live environments (possibly staging)? Should the change be reversed?
Also, what happens when a future 0061
migration is added to this app? Will it appear to have been applied in any environment where this was applied? Maybe this has not been applied anywhere, in which case there is no concern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important questions, and yes, this migration had already been run on staging. I ran migrate users 0060
on staging to "undo" the migration from django's perspective, and then undid the changes in a shell (running the code that was in the migration but flipping access_web_apps
to False).
@orangejenny that behavior looks very bizarre to me. I just took a look and I think this is a bug, though one that shouldn't affect authorization. Check this out: >>> user.can_access_web_apps()
False
>>> user.can_access_web_apps(self.domain)
True That's the core of the problem, I believe. commcare-hq/corehq/apps/users/models.py Lines 1600 to 1615 in 31ddfc0
So the two calls above resolve to >>> user.has_permission(None, 'access_web_apps')
False
>>> user.has_permission(self.domain, 'access_web_apps')
True That is, this will work if |
(I do still support your decision to separate the main changes from addressing this stuff though) |
@esoergel OH. That makes sense. Yeah, my testing was bad. Which is a relief. But yeah, I think I'm going to keep those changes reverted, I'm just gun-shy about sweeping permissions changes. |
USH is responsible for this deprecation.
permissions.web_apps_list = [ | ||
downstream_id for downstream_id, upstream_id in app_id_map.items() | ||
if upstream_id in permissions.web_apps_list | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this new usage of app ids does not require any data migrations since this is a new feature that is only deployed on staging where old data either does not exist or does not matter. Is that right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch with the linked domains thing, that wasn't on my radar
corehq/apps/cloudcare/views.py
Outdated
def _can_access_web_app(user, domain, app): | ||
if user.can_access_web_app(domain, app.get('copy_of', app.get('_id'))): | ||
return True | ||
if user.can_access_web_app(domain, app.get('upstream_app_id')): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate on this one? This is for the situation where there are two apps on the same domain, one linked to the other? Would these show up as different entries on the roles page where you select which apps are accessible?
role = UserRole.objects.get_by_domain(self.downstream_domain)[0] | ||
self.assertListEqual(role.permissions.web_apps_list, [downstream_app.id]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah this clarifies it, thank you. In my words, it's that a role on an upstream domain with access to an app needs to be propagated to the downstream domain with the app access updated to the corresponding downstream app. Did I get that right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, exactly
Product Description
Overview
https://dimagi.atlassian.net/browse/USH-4289
This work overlaps somewhat with the Login As changes @MartinRiese is doing in #34351 This description is written assuming that those changes are complete. I'm planning to wait on QAing this PR until that work is complete.
Old behavior
There's a single permission in roles & permissions that controls access to web apps.
This permission is checked for the user who is logged into HQ. When Login As is used, permissions of the Login As user are ignored. Also, the web apps permission is not checked if the user logged into HQ is a mobile user. The default role for mobile workers doesn't even include web apps access (see here). All of this is controlled by the @require_cloudcare_access decorator, which is applied to cloudcare views.
In addition, access can be granted to specific applications in web apps using a bespoke set of models and UI:
This is based on mapping applications to user groups. User groups are only available for mobile users. Therefore, app-specific permissions are only available for mobile users.
Impetus for change
As part of the work to move web apps projects away from depending on linked mobile users, we need to support app-specific permissions for web users.
There's an alternative solution where we add support for web users to user groups. This is discussed in the ticket and linked documents, but in short, it was rejected in favor of integrating app-specific web apps permissions with our general roles & permissions framework.
New behavior
I'm clubbing a couple of related changes into this PR in the interest of getting holistic review. I'm happy to split things up if reviewers would find that helpful or believe it would be safer.
This is a straightforward addition to our existing roles & permissions framework.
Having two ways to assign app-specific permissions is confusing. Moving from group-based to role-based permissions requires human intervention. As a compromise, I'm adding a feature flag, marked deprecated, to gate the old UI. Migration
0059
turns this flag on for all domains that currently have anApplicationAccess
object that's restricting app access. I added a warning to the top of the old UI, but I'm not changing behavior at all. ApplicationAccess restrictions will continue to be applied as they are today, in addition to the new app-specific permissions.3. Update default mobile worker role to access web apps, and remove mobile worker check (last few commits)This is the broadest/scariest change. Pushing users to use roles to manage app-specific permissions, for both web and mobile workers, makes it more glaringly apparent that the existing web apps permission doesn't apply to mobile workers. To reduce ambiguity, I'm updatingrequire_cloudcare_access
to check the permission for both web and mobile users. To avoid a change in behavior, migration0060
updates all of the default mobile worker roles to turn on web apps access.How do these changes combine? I'm continuing to decorate cloudcare views with a permissions check on the HQ user, so if you log into HQ and don't have any cloudcare permissions, you always 403, no opportunity to login as someone else. If you can log into HQ and then use login as, that user's permissions are used to determine which apps you see. This does mean that you can log into HQ as a user who has access only to app A, and can login as a user who has access to app B.
Summary of behavior:
access_web_apps
access_web_apps
In addition to this, any legacy
ApplicationAccess
restrictions will continue to be applied, for backwards compatibility.Feature Flag
Introduces WEB_APPS_PERMISSIONS_VIA_GROUPS, but most of this behavior is not gated.
Safety Assurance
Safety story
Permissions are always sensitive, and this does migrations
and changes default-type behavior, so it feels medium-high risk.Automated test coverage
There are tests for roles and permissions.
QA Plan
https://dimagi.atlassian.net/browse/QA-6352
Migrations
No? The migrations are integrated with the code. I'm not terribly concerned that someone will add an
ApplicationAccess
object during deploy, after migration0059
runs but before the new code that restricts the config UI based on that feature flag goes live.I could pull the application access changes (the new feature flag, migration
0059
, and the changes that restrict the old config UI) into a separate PR, or even two separate PRs (one for the flag + migration, a second for the UI restrictions). I'm not going to do that unless anyone else is concerned about the deploy window, because I think the risk here is low.For the0060
migration for the default mobile worker role, the current behavior is at best ambiguous and arguably incorrect, since a mobile worker with a role that doesn't have access to web apps still gets access to web apps. That makes me not concerned about default mobile worker roles getting updated during deploy.Rollback instructions
Rolling back will not be possible as soon as people start adding app-specific permissions to the database, as the wrap function will start to fail.
Labels & Review