Replies: 2 comments 1 reply
-
Thanks for taking the time to clearly outline your requirements @magicmark. Just to clarify, your ideal solution for this would be that Strawberry facilitates the implementation of the |
Beta Was this translation helpful? Give feedback.
-
Hey @magicmark, first off please excuse the long delay. I've read this when you wrote it together, but was too busy to write down a comprehensive answer. Reviewing your proposal, I see that you want to make RBAC easier to handle using a new Simple permissionsSince we already have a permissions system implement in strawberry, let me derive what you are trying to achieve with class Policy(BasePermission):
def __init__(self, role, scopes):
self.role = role
self.scopes = scopes
def has_permission(
self, source, info, **kwargs
) -> bool:
user = x # get user either from context or other source
return self.role in user.roles and all(set(user.scopes).issuperset(self.roles))
@strawberry.type
class Business:
@strawberry.field(extensions=[PermissionExtension(permissions=[Policy("ADMIN")])])
def revenue_per_month(self, ...):
... So far so simple. Here's also where it gets tricky DevX-Wise: the permissions cannot be ergonomically merged without creating a separate, new extension. #3408 will solve this by defining a new way to chain permissions: [Policy("SelfUser") & Policy("FooMatchesBar", {"editBusinesses"})] This would signify the [Policy("SelfUser") | Policy("FooMatchesBar", {"editBusinesses"})] Schema DirectivesI also see that schema directives are relevant for you in these cases. For Schema Directives, Permissions support overriding the default schema directives function: class PolicyDirective:
@property
def __strawberry_directive__(self):
# build your strawberry directive here
# and fill it with private fields
# partly private API - we should do better on this!
return StrawberrySchemaDirective(
self.__class__.__name__,
self.__class__.__name__,
[Location.FIELD_DEFINITION],
[],
)
class Policy(BasePermission):
def __init__(self, role, scopes):
...
def has_permission(
self, source, info, **kwargs
) -> bool:
...
def schema_directive(self) -> object:
return self._schema_directive As you can see, some things partly use private API and are not yet intuitive on procedure. We could definitely be better than this! ABACNow to the last part, ABAC. This is a problem I'm very split upon, because ABAC in GraphQL is somewhat harder than REST. If we used a SummaryFirst of all, thanks for the time to write all of your suggestions down. I appreciate the effort you put into this and I'm happy about the momentum to make strawberry better instead of building custom solutions! That's awesome 😊 So, speaking about Permissions with roles, my personal vision to this is as follows:
I'm particularly interested in your design decisions that led to the current implementation of ABAC in your backend. Maybe we can discuss and find a more viable and type safe alternative together. Looking forward to hearing your thoughts. Erik |
Beta Was this translation helpful? Give feedback.
-
Hi @patrick91 and @erikwrede!
(As always, thanks so much for the awesome work you've put into Strawberry, we're huge fans!)
As I shared with Patrick, I'm looking to implement a new version of our
@private
directive.We have some additional use-case requirements beyond what is currently (easily) possible with the Permissions class. I don't think these requirements are super specific to our company - so if you think it's worthwhile, I'd love to work together to see if it makes sense to contribute to Strawberry directly, rather than do a hacky in-house version.
I've written out a spec below of what I'm hoping to achieve.
(The examples used in this spec below do not necessarily represent Yelp's actual business logic or policies, and are for illustrative purposes only.)
@private
Directive@private
is a custom GraphQL Directive used at Yelp to secure fields and prevent unauthorized access to private data.Some of the data we expose in the schema may require users to be logged in, or can only be accessed by specific users. For example, we want to make sure that a logged in user can read their own draft reviews, but not other users' draft reviews. That's private!
Example
@private
's policy argument accepts a set of roles. Each role is evaluated, and if any of them pass, access to the field is granted.Scopes
Roles may also have an associated set of scopes. If specified, at least one scope must also pass for access to be granted:
Applying to Types
Where possible,
@private
should be applied to whole types, rather than individual fields. This applies protection globally to the type:Usage
Multiple roles
Multiple roles can be applied with both
AND
andOR
logic:OR
For example, let's say only the logged-in user or a Yelp Admin (with the right scope) can see a user's email address:
AND
For example, on a company-wide internal GraphQL server, to see details about a service's last deployment, you must be
logged in, and your session must carry the
Engineering
role with theCAN_DEPLOY_SERVICES
scope.Using input variables
In the above examples, the ID of the entity being protected is an attribute of the object at runtime being returned.
The logic of the role defintion compares this value to something stored in the context object (derived from the request
object).
We also support protecting fields for which the id is provided as an input variable. In this case, you must provide the
name of the argument which represents the id to compare against. This is most useful for mutations:
This can also be applied to regular queries:
Lazy conditional execution
Sometimes, protection needs to be applied conditionally at runtime.
For example, anyone can view a review. But if at runtime, we find that the business the review is for is "inactive",
then only the business owner can view it.
For this use case, we have a variant of the directive,
@privateLazy
. This works the same way as@private
, exceptthat
@privateLazy
injects a callback into the resolver that you must execute manually if necessary to apply theauth check.
Example
Creating new roles
Each role definition lives in its own file, and must conform to the
AuthRole
interface.API
readFromParent(field)
-- is a function that looks at the parent object and returnsparent[field]
.variableInput
-- if defined, returns the user argument input defined by the role.Example
Appendix
Execution behaviour
When applied to fields, the directive is run before the field's resolver is executed. It operates purely with the inputs
of the request object and the parent object of the field.
When applied to types, the directive is copied and applied to all of the type's fields. Note that the resolver that
returned the type is executed. (This is neccessary to generate the parent object.)
@private
should also be applied at the field that returns the sensitive type - this provides us multiple layers ofprotection.
Beta Was this translation helpful? Give feedback.
All reactions