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
feat(list-users): ListUsers with Excluded Users #1604
Conversation
Minder Vulnerability Report ✅Minder analyzed this PR and found no vulnerable dependencies.
|
Minder analyzed this PR with Trusty and found no dependencies scored lower than your profile threshold. |
c38697b
to
a1ca45f
Compare
Supersedes #1590 |
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.
Still need to look over more but some immediate comments.
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 wrote two tests that I've corroborated with Check API manually (not sure why we're not doing that within runListUsersTestCases
) and that are failing with this PR:
{
name: "intersection",
req: &openfgav1.ListUsersRequest{
Object: &openfgav1.Object{Type: "document", Id: "1"},
Relation: "viewer",
UserFilters: []*openfgav1.UserTypeFilter{
{
Type: "user",
},
},
},
model: `model
schema 1.1
type user
type document
relations
define b: [user:*] but not b1
define b1: [user]
define a: [user:*] but not a1
define a1: [user]
define viewer: a and b`,
tuples: []*openfgav1.TupleKey{
tuple.NewTupleKey("document:1", "b", "user:*"),
tuple.NewTupleKey("document:1", "b1", "user:will"),
// b -> user:* but not user:will
tuple.NewTupleKey("document:1", "a", "user:*"),
tuple.NewTupleKey("document:1", "a1", "user:maria"),
// a -> user:* but not user:maria
// therefore:
// a and -b -> (user:* but not user:maria) and (user:* but not user:will)
// -> user:* but not (user:maria, user:will)
},
expectedUsers: []string{"user:*"},
butNot: []string{"user:maria", "user:will"},
},
{
name: "union",
req: &openfgav1.ListUsersRequest{
Object: &openfgav1.Object{Type: "document", Id: "1"},
Relation: "viewer",
UserFilters: []*openfgav1.UserTypeFilter{
{
Type: "user",
},
},
},
model: `model
schema 1.1
type user
type document
relations
define b: [user:*] but not b1
define b1: [user]
define a: [user:*] but not a1
define a1: [user]
define viewer: a or b`,
tuples: []*openfgav1.TupleKey{
tuple.NewTupleKey("document:1", "b", "user:*"),
tuple.NewTupleKey("document:1", "b1", "user:will"),
// b -> user:* but not user:will
tuple.NewTupleKey("document:1", "a", "user:*"),
tuple.NewTupleKey("document:1", "a1", "user:maria"),
// a -> user:* but not user:maria
// therefore:
// a or -b -> (user:* but not user:maria) or (user:* but not user:will)
// -> user:*, user:maria, user:will
},
expectedUsers: []string{"user:*", "user:maria", "user:will"},
},
I'm not very sure on the changes you made but it seems to be that we need changes in union and intersection handlers too.
More specifically I think we need a helper method that recursively computes the transformations of intersection, union and exclusion.
E.g.
(user:* but not user:maria) or (user:* but not user:will) -> user:*, user:maria, user:will
(user:* but not user:maria) and (user:* but not user:will) -> user:* but not (user:maria, user:will)
I'm pretty sure this is https://en.wikipedia.org/wiki/De_Morgan%27s_laws 🤔
…openfga into listUsers-excludedUsers
Are we going to address the case of excluded usersets in a different PR? {
name: "usersets",
model: `model
schema 1.1
type user
type group
relations
define member: [user]
type document
relations
define blocked: [group#member]
define viewer: [group#member] but not blocked
`,
tuples: []*openfgav1.TupleKey{
tuple.NewTupleKey("document:1", "viewer", "group:eng#member"),
tuple.NewTupleKey("document:1", "blocked", "group:fga#member"),
},
req: &openfgav1.ListUsersRequest{
Object: &openfgav1.Object{Type: "document", Id: "1"},
Relation: "viewer",
UserFilters: []*openfgav1.UserTypeFilter{{Type: "group", Relation: "member"}},
},
expectedUsers: []string{"group:eng#member"},
butNot: []string{"group:fga#member"},
}, |
if !userIsSubtracted && !wildcardSubtracted { | ||
switch { | ||
case baseWildcardExists: | ||
if !wildcardSubtracted { |
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 conditional behavior is not quite correct and has deviated from the prior commit. Previously we were only producing the wildcard subject if baseWildcardExists && !wildcardSubtracted
. See
user: tuple.StringToUserProto(wildcardKey), |
And we'd only produce an explicit (non-wildcard) subject if baseWildcardExists && !userIsSubtracted && !wildcardSubtracted
. See
openfga/pkg/server/commands/listusers/list_users_rpc.go
Lines 673 to 678 in b872f91
if baseWildcardExists { | |
if !userIsSubtracted && !wildcardSubtracted { | |
trySendResult(ctx, foundUser{ | |
user: tuple.StringToUserProto(userKey), | |
}, foundUsersChan) | |
} |
Consider the following scenario:
type user
type document
relations
define restricted: [user, user:*]
define viewer: [user, user:*] but not restricted
with tuples:
- document:1#viewer@user:*
- document:1#viewer@user:jon
- document:1#restricted@user:jon
When ranging over the baseFoundUsersMap
we'll find user:jon
and yield it here, because there is not a subtracted wildcard. The only reason a test related to this scenario is passing against this commit is because of other code in the callstack above this function, which isn't obvious and is actually misleading when reading the code. See
openfga/pkg/server/commands/listusers/list_users_rpc.go
Lines 251 to 253 in 5f20ffb
if userIsExcluded { | |
continue | |
} |
But this logic itself is not quite correct, becase we're producing on the foundUsersChan
superfluously only to be filtered out above upstream since it is not applicable (the subject user:jon
is not just excluded but is explicitly subtracted). The rewrite handler should only produce accurate "found user candidates" with respect to the found users channel.
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 ok with proving your above theory with a unit test? Then we can address appropriately in code.
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 one test case I'm still pondering though, could see it going either way:
|
For consistency,
Maria is explicitly blocked, but then subsequently unblocked, so she explicitly does not have the blocked relationshiop. For any subject that shows up in the |
ds := memory.New() | ||
t.Cleanup(ds.Close) | ||
|
||
t.Run("avoid_producing_explicitly_negated_subjects", func(t *testing.T) { |
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.
…openfga into listUsers-excludedUsers
} | ||
} | ||
} | ||
case subtractWildcardExists: |
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 case can be collapsed along with the case below
case userIsSubtracted, subtractWildcardExists:
trySendResult(ctx, foundUser{
user: tuple.StringToUserProto(userKey),
relationshipStatus: subtractedUser.relationshipStatus.InvertRelationshipStatus(),
}, foundUsersChan)
…listUsers-excludedUsers
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.
🙏
Description
This PR introduces the concept of excluded users, which represent the list of users that do not apply to an exclusion clause. This occurs in models that have chained exclusion where the intermediate exclusion includes a typed public wildcard.
Example:
In this example, the "unblocked" status of
user:jon
was being lost when evaluating the outer exclusion because the exception to the exclusion was not being communicated. This is considered an "excluded user". The solution here is to keep track of the excluded users so that the outer negation context can un-exclude the proper subjects. We also return the excluded users along with the response for the client to handle appropriately.Consider the same model above but the following tuples:
ListUsers(document:1#viewer, user_filters=[{"type": "user"}])
will/should propagate up theuser:jon
subject from the inner most exclusion. That is, intermediate expansions should always yield a result, but that result may indicate the subject does or does not have the relationship for that intermediate relationships. In this case, that means the following:References
Related API PR: openfga/api#144
Review Checklist
main