Skip to content
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

Remove IdentityBasedPolicy base class #8859

Merged
merged 1 commit into from
Aug 15, 2024

Conversation

seanh
Copy link
Contributor

@seanh seanh commented Aug 6, 2024

Factor out the IdentityBasedPolicy base class, favouring helper methods that're used by composition instead.

This base class is making future refactorings (see #8860 and #8861) more difficult to reason about by coupling together different security policy classes that I want to evolve in separate directions.

The base class is also responsible for some disturbing unintended behaviours. For example, on main:

  • IdentityBasedPolicy.authenticated_userid() calls identity() (which must be implemented by IdentityBasedPolicy sub classes).

  • CookiePolicy has an identity() method and inherits from IdentityBasedPolicy, so CookiePolicy inherits IdentityBasedPolicy's authenticated_userid() method that calls CookiePolicy.identity()

  • As with all h's security policies CookiePolicy is never used directly as a security policy, rather it's always delegated to by TopLevelPolicy

  • TopLevelPolicy has an identity() method that delegates to CookiePolicy.identity() for HTML pages, and TopLevelPolicy also inherits from IdentityBasedPolicy, so TopLevelPolicy inherits an authenticated_userid() method that calls TopLevelPolicy.identity().

  • So the end result is that CookiePolicy.authenticated_userid() (which is inherited from IdentityBasedPolicy) is never actually used! Instead TopLevelPolicy's copy of authenticated_userid() (also inherited from TopLevelPolicy) is used and calls TopLevelPolicy.identity() which delegates to CookiePolicy.identity().

The same thing happens when APIPolicy (which also inherits from IdentityBasedPolicy) delegates to API subpolicies.

This is true for the authenticated_userid() and permits() methods of all h's security policies other than TopLevelPolicy: none of them are ever called. In fact you could remove the IdentityBasedPolicy base class from all classes except TopLevelPolicy and it would make no difference, the methods that all those classes inherit from IdentityBasedPolicy are never called.

This hasn't resulted in any bugs because all the sub-policies and TopLevelPolicy inherit same authenticated_userid() and permits() methods from IdentityBasedPolicy, so TopLevelPolicy's by-passing the authenticated_userid() and permits() methods of sub-policies makes no difference.

But clearly this is very confusing and unintended, and would lead to bugs if there's ever a sub-policy in future that has a different authenticated_userid() or permits() method.

Testing

  1. Non-API requests can be authenticated with cookies. To test this just go to http://localhost:5000/login, log in, and visit some pages.
  2. API requests can be authenticated with bearer tokens. To test this go to http://localhost:5000/account/developer, generate a developer token, and make an API request with that token. For example: http POST 'http://localhost:5000/api/annotations' uri=foo Authorization:'Bearer 6879-***'
  3. Requests to certain API endpoints can be authenticated using an auth client. To test this log in to https://hypothesis.instructure.com/ and launch a localhost test assignment
  4. Requests to certain API endpoints can be authenticated using the auth cookie. To test this log in as devdata_admin, go to http://localhost:5000/admin/features and enable the preact_create_group_form feature flag, then go to http://localhost:5000/groups/new and create a new group.

@seanh seanh changed the base branch from main to auth-ticket-service August 6, 2024 14:42
@seanh seanh changed the title remove IdentityBasedPolicy base class Remove IdentityBasedPolicy base class Aug 6, 2024
Comment on lines +98 to +104
@staticmethod
def authenticated_userid(identity: Self | None) -> str | None:
"""Return the authenticated_userid from the given identity."""
if identity and identity.user:
return identity.user.userid

return None
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the IdentityBasedPolicy.authenticated_userid() method to a static method on the Identity class itself. Alternatively this could just be a standalone helper function.

Comment on lines +29 to +30
def permits(self, request, context, permission) -> Allowed | Denied:
return identity_permits(self.identity(request), context, permission)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each sub-policy class that previously inherited authenticated_userid() and permits() from IdentityBasedPolicy now instead needs to provide these two one-liner methods to delegate authenticated_userid() and permits() to the new helpers.

As is usual with composition-over-inheritance this leads to more duplication, but this is trivial boiler-plate code and the advantage is better code readability and decoupling.

Comment on lines +34 to +35
def permits(self, request, context, permission) -> Allowed | Denied:
return get_subpolicy(request).permits(request, context, permission)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike with the various sub-policies, TopLevelPolicy's authenticated_userid() and permits() methods do not delegate to the new helpers. Instead, they delegate to the applicable sub-policy the same way that TopLevelPolicy's other methods do. I think this makes sense: TopLevelPolicy's only job is to choose the applicable sub-policy and delegate all calls to the sub-policy. It's the sub-policy's business how those methods are implemented (and whether or not they work by calling the new helpers). This also makes the behaviour of all of TopLevelPolicy's methods consistent with each other.

@seanh seanh requested a review from marcospri August 7, 2024 14:40
@seanh seanh force-pushed the remove-IdentityBasedPolicy-base-class branch from e61c731 to 7997448 Compare August 7, 2024 14:59
@seanh seanh marked this pull request as ready for review August 7, 2024 15:15
Copy link
Member

@marcospri marcospri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense 👍

I have struggled to remember how identity_permits actually works to get the full picture but that's completely out of scope of this PR.

Base automatically changed from auth-ticket-service to main August 15, 2024 11:53
Factor out the `IdentityBasedPolicy` base class, favouring helper
methods that're used by composition instead.

This base class is making future refactorings (see
#8860 and
#8861) more difficult to reason
about by coupling together different security policy classes that I want
to evolve in separate directions.

The base class is also responsible for some disturbing unintended
behaviours. For example, on `main`:

* `IdentityBasedPolicy.authenticated_userid()` calls `identity()` (which
  must be implemented by `IdentityBasedPolicy` sub classes).

* `CookiePolicy` has an `identity()` method and inherits from
  `IdentityBasedPolicy`, so `CookiePolicy` inherits
  `IdentityBasedPolicy`'s `authenticated_userid()` method that calls
  `CookiePolicy.identity()`

* As with all h's security policies `CookiePolicy` is never used
  directly as a security policy, rather it's always delegated to by
  `TopLevelPolicy`

* `TopLevelPolicy` has an `identity()` method that delegates to
  `CookiePolicy.identity()` for HTML pages, and `TopLevelPolicy` also
  inherits from `IdentityBasedPolicy`, so `TopLevelPolicy` inherits an
  `authenticated_userid()` method that calls `TopLevelPolicy.identity()`.

* So the end result is that `CookiePolicy.authenticated_userid()` (which
  is inherited from `IdentityBasedPolicy`) is never actually used!
  Instead `TopLevelPolicy`'s copy of `authenticated_userid()` (also
  inherited from `TopLevelPolicy`) is used and calls
  `TopLevelPolicy.identity()` which delegates to
  `CookiePolicy.identity()`.

The same thing happens when `APIPolicy` (which also inherits from
`IdentityBasedPolicy`) delegates to API subpolicies.

This is true for the `authenticated_userid()` and `permits()` methods of
all h's security policies other than `TopLevelPolicy`: none of them are
ever called.

This hasn't resulted in any bugs because all the sub-policies and
`TopLevelPolicy` inherit same `authenticated_userid()` and `permits()`
methods from `IdentityBasedPolicy`, so `TopLevelPolicy`'s by-passing the
`authenticated_userid()` and `permits()` methods of sub-policies makes
no difference. In fact you could remove the `IdentityBasedPolicy` base
class from all classes except `TopLevelPolicy` and it would make no
difference, the methods that all those classes inherit from
`IdentityBasedPolicy` are never called.

But clearly this is very confusing and unintended, and would lead to
bugs if there's ever a sub-policy in future that has a different
`authenticated_userid()` or `permits()` method.
@seanh seanh force-pushed the remove-IdentityBasedPolicy-base-class branch from 7997448 to ad9b438 Compare August 15, 2024 12:16
@seanh seanh merged commit ec6ec7f into main Aug 15, 2024
9 checks passed
@seanh seanh deleted the remove-IdentityBasedPolicy-base-class branch August 15, 2024 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants