diff --git a/doc/md/privacy.md b/doc/md/privacy.md index 4d13c8715..e6b5578ef 100644 --- a/doc/md/privacy.md +++ b/doc/md/privacy.md @@ -27,7 +27,7 @@ gets access to the target nodes. ![privacy-rules](https://entgo.io/images/assets/permission_1.png) However, if one of the evaluated rules returns an error or a `privacy.Deny` decision (see below), the executed operation -returns an error, and it is cancelled. +returns an error, and it is cancelled. ![privacy-deny](https://entgo.io/images/assets/permission_2.png) @@ -67,7 +67,7 @@ In order to enable the privacy option in your code generation, enable the `priva ```go package ent - + //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature privacy ./schema ``` @@ -76,7 +76,7 @@ It is recommended to add the [`schema/snapshot`](features.md#auto-solve-merge-co 2\. If you are using the configuration from the GraphQL documentation, add the feature flag as follows: -```go +```go {20} // Copyright 2019-present Facebook Inc. All rights reserved. // This source code is licensed under the Apache 2.0 license found // in the LICENSE file in the root directory of this source tree. @@ -130,7 +130,7 @@ with admin role. We will create 2 additional packages for the purpose of the exa After running the code-generation (with the feature-flag for privacy), we add the `Policy` method with 2 generated policy rules. -```go +```go title="examples/privacyadmin/ent/schema/user.go" package schema import ( @@ -161,7 +161,7 @@ func (User) Policy() ent.Policy { We defined a policy that rejects any mutation and accepts any query. However, as mentioned above, in this example, we accept mutations only from viewers with admin role. Let's create 2 privacy rules to enforce this: -```go +```go title="examples/privacyadmin/rule/rule.go" package rule import ( @@ -201,7 +201,7 @@ As you can see, the first rule `DenyIfNoViewer`, makes sure every operation has otherwise, the operation rejected. The second rule `AllowIfAdmin`, accepts any operation from viewer with admin role. Let's add them to the schema, and run the code-generation: -```go +```go title="examples/privacyadmin/ent/schema/user.go" // Policy defines the privacy policy of the User. func (User) Policy() ent.Policy { return privacy.Policy{ @@ -221,9 +221,9 @@ Since we define the `DenyIfNoViewer` first, it will be executed before all other `viewer.Viewer` object is safe in the `AllowIfAdmin` rule. After adding the rules above and running the code-generation, we expect the privacy-layer logic to be applied on - `ent.Client` operations. +`ent.Client` operations. -```go +```go title="examples/privacyadmin/example_test.go" func Do(ctx context.Context, client *ent.Client) error { // Expect operation to fail, because viewer-context // is missing (first mutation rule check). @@ -255,7 +255,7 @@ func Do(ctx context.Context, client *ent.Client) error { Sometimes, we want to bind a specific privacy decision to the `context.Context`. In cases like this, we can use the `privacy.DecisionContext` function to create a new context with a privacy decision attached to it. -```go +```go title="examples/privacyadmin/example_test.go" func Do(ctx context.Context, client *ent.Client) error { // Bind a privacy decision to the context (bypass all other rules). allow := privacy.DecisionContext(ctx, privacy.Allow) @@ -278,7 +278,7 @@ The helper packages `viewer` and `rule` (as mentioned above) also exist in this Let's start building this application piece by piece. We begin by creating 3 different schemas (see the full code [here](https://github.com/ent/ent/tree/master/examples/privacytenant/ent/schema)), and since we want to share some logic between them, we create another [mixed-in schema](schema-mixin.md) and add it to all other schemas as follows: -```go +```go title="examples/privacytenant/ent/schema/mixin.go" // BaseMixin for all schemas in the graph. type BaseMixin struct { mixin.Schema @@ -295,7 +295,9 @@ func (BaseMixin) Policy() ent.Policy { }, } } +``` +```go title="examples/privacytenant/ent/schema/tenant.go" // Mixin of the Tenant schema. func (Tenant) Mixin() []ent.Mixin { return []ent.Mixin{ @@ -310,7 +312,7 @@ contain the `viewer.Viewer` information. Similar to the previous example, we want add a constraint that only admin users can create tenants (and deny otherwise). We do it by copying the `AllowIfAdmin` rule from above, and adding it to the `Policy` of the `Tenant` schema: -```go +```go title="examples/privacytenant/ent/schema/tenant.go" // Policy defines the privacy policy of the User. func (Tenant) Policy() ent.Policy { return privacy.Policy{ @@ -326,7 +328,7 @@ func (Tenant) Policy() ent.Policy { Then, we expect the following code to run successfully: -```go +```go title="examples/privacytenant/example_test.go" func Do(ctx context.Context, client *ent.Client) error { // Expect operation to fail, because viewer-context // is missing (first mutation rule check). @@ -334,18 +336,18 @@ func Do(ctx context.Context, client *ent.Client) error { return fmt.Errorf("expect operation to fail, but got %w", err) } // Deny tenant creation if the viewer is not admin. - viewOnly := viewer.NewContext(ctx, viewer.UserViewer{Role: viewer.View}) - if _, err := client.Tenant.Create().Save(viewOnly); !errors.Is(err, privacy.Deny) { + viewCtx := viewer.NewContext(ctx, viewer.UserViewer{Role: viewer.View}) + if _, err := client.Tenant.Create().Save(viewCtx); !errors.Is(err, privacy.Deny) { return fmt.Errorf("expect operation to fail, but got %w", err) } - // Apply the same operation with "Admin" role. - admin := viewer.NewContext(ctx, viewer.UserViewer{Role: viewer.Admin}) - hub, err := client.Tenant.Create().SetName("GitHub").Save(admin) + // Apply the same operation with "Admin" role, expect it to pass. + adminCtx := viewer.NewContext(ctx, viewer.UserViewer{Role: viewer.Admin}) + hub, err := client.Tenant.Create().SetName("GitHub").Save(adminCtx) if err != nil { return fmt.Errorf("expect operation to pass, but got %w", err) } fmt.Println(hub) - lab, err := client.Tenant.Create().SetName("GitLab").Save(admin) + lab, err := client.Tenant.Create().SetName("GitLab").Save(adminCtx) if err != nil { return fmt.Errorf("expect operation to pass, but got %w", err) } @@ -357,7 +359,7 @@ func Do(ctx context.Context, client *ent.Client) error { We continue by adding the rest of the edges in our data-model (see image above), and since both `User` and `Group` have an edge to the `Tenant` schema, we create a shared [mixed-in schema](schema-mixin.md) named `TenantMixin` for this: -```go +```go title="examples/privacytenant/ent/schema/mixin.go" // TenantMixin for embedding the tenant info in different schemas. type TenantMixin struct { mixin.Schema @@ -374,15 +376,18 @@ func (TenantMixin) Edges() []ent.Edge { ``` Now, we want to enforce that viewers can see only groups and users that are connected to the tenant they belong to. -In this case, there's another type of privacy rule named `FilterRule`. This rule can help us to filters out entities that -are not connected to the same tenant. +In this case, there's another type of privacy rule named `Filter`. +We use it to filter out entities that are not connected to the same tenant. +Unlike previous rules, we only use it for filtering, not for privacy decision-making. -> Note, the filtering option for privacy needs to be enabled using the `entql` feature-flag (see instructions [above](#configuration)). +> Note, the privacy filtering option needs to be enabled using the [`entql`](features.md#entql-filtering) feature-flag (see instructions [above](#configuration)). -```go -// FilterTenantRule is a query rule that filters out entities that are not in the tenant. +```go title="examples/privacytenant/rule/rule.go" +// FilterTenantRule is a query/mutation rule that filters out entities that are not in the tenant. func FilterTenantRule() privacy.QueryMutationRule { - type TeamsFilter interface { + // TenantsFilter is an interface to wrap WhereHasTenantWith() + // predicate that is used by both `Group` and `User` schemas. + type TenantsFilter interface { WhereHasTenantWith(...predicate.Tenant) } return privacy.FilterFunc(func(ctx context.Context, f privacy.Filter) error { @@ -390,7 +395,7 @@ func FilterTenantRule() privacy.QueryMutationRule { if view.Tenant() == "" { return privacy.Denyf("missing tenant information in viewer") } - tf, ok := f.(TeamsFilter) + tf, ok := f.(TenantsFilter) if !ok { return privacy.Denyf("unexpected filter type %T", f) } @@ -405,7 +410,7 @@ func FilterTenantRule() privacy.QueryMutationRule { After creating the `FilterTenantRule` privacy rule, we add it to the `TenantMixin` to make sure **all schemas** that use this mixin, will also have this privacy rule. -```go +```go title="examples/privacytenant/ent/schema/mixin.go" // Policy for all schemas that embed TenantMixin. func (TenantMixin) Policy() ent.Policy { return privacy.Policy{ @@ -421,18 +426,18 @@ func (TenantMixin) Policy() ent.Policy { Then, after running the code-generation, we expect the privacy-rules to take effect on the client operations. -```go +```go title="examples/privacytenant/example_test.go" func Do(ctx context.Context, client *ent.Client) error { // A continuation of the code-block above. - // Create 2 users connected to the 2 tenants we created above (a8m->GitHub, nati->GitLab). - a8m := client.User.Create().SetName("a8m").SetTenant(hub).SaveX(admin) - nati := client.User.Create().SetName("nati").SetTenant(lab).SaveX(admin) + // Create 2 users connected to the 2 tenants we created above + hubUser := client.User.Create().SetName("a8m").SetTenant(hub).SaveX(adminCtx) + labUser := client.User.Create().SetName("nati").SetTenant(lab).SaveX(adminCtx) hubView := viewer.NewContext(ctx, viewer.UserViewer{T: hub}) out := client.User.Query().OnlyX(hubView) // Expect that "GitHub" tenant to read only its users (i.e. a8m). - if out.ID != a8m.ID { + if out.ID != hubUser.ID { return fmt.Errorf("expect result for user query, got %v", out) } fmt.Println(out) @@ -440,7 +445,7 @@ func Do(ctx context.Context, client *ent.Client) error { labView := viewer.NewContext(ctx, viewer.UserViewer{T: lab}) out = client.User.Query().OnlyX(labView) // Expect that "GitLab" tenant to read only its users (i.e. nati). - if out.ID != nati.ID { + if out.ID != labUser.ID { return fmt.Errorf("expect result for user query, got %v", out) } fmt.Println(out) @@ -449,15 +454,14 @@ func Do(ctx context.Context, client *ent.Client) error { ``` We finish our example with another privacy-rule named `DenyMismatchedTenants` on the `Group` schema. -The `DenyMismatchedTenants` rule rejects the group creation if the associated users don't belong to +The `DenyMismatchedTenants` rule rejects group creation if the associated users don't belong to the same tenant as the group. -```go -// DenyMismatchedTenants is a rule runs only on create operations, and returns a deny decision -// if the operation tries to add users to groups that are not in the same tenant. +```go title="examples/privacytenant/rule/rule.go" +// DenyMismatchedTenants is a rule that runs only on create operations and returns a deny +// decision if the operation tries to add users to groups that are not in the same tenant. func DenyMismatchedTenants() privacy.MutationRule { - // Create a rule, and limit it to create operations below. - rule := privacy.GroupMutationRuleFunc(func(ctx context.Context, m *ent.GroupMutation) error { + return privacy.GroupMutationRuleFunc(func(ctx context.Context, m *ent.GroupMutation) error { tid, exists := m.TenantID() if !exists { return privacy.Denyf("missing tenant information in mutation") @@ -469,29 +473,32 @@ func DenyMismatchedTenants() privacy.MutationRule { } // Query the tenant-id of all users. Expect to have exact 1 result, // and it matches the tenant-id of the group above. - uid, err := m.Client().User.Query().Where(user.IDIn(users...)).QueryTenant().OnlyID(ctx) + id, err := m.Client().User.Query().Where(user.IDIn(users...)).QueryTenant().OnlyID(ctx) if err != nil { return privacy.Denyf("querying the tenant-id %w", err) } - if uid != tid { - return privacy.Denyf("mismatch tenant-ids for group/users %d != %d", tid, uid) + if id != tid { + return privacy.Denyf("mismatch tenant-ids for group/users %d != %d", tid, id) } // Skip to the next privacy rule (equivalent to return nil). return privacy.Skip }) - // Evaluate the mutation rule only on group creation. - return privacy.OnMutationOperation(rule, ent.OpCreate) } ``` We add this rule to the `Group` schema and run code-generation. -```go +```go title="examples/privacytenant/ent/schema/group.go" // Policy defines the privacy policy of the Group. func (Group) Policy() ent.Policy { return privacy.Policy{ Mutation: privacy.MutationPolicy{ - rule.DenyMismatchedTenants(), + // Limit DenyMismatchedTenants only for + // Create operation + privacy.OnMutationOperation( + rule.DenyMismatchedTenants(), + ent.OpCreate, + ), }, } } @@ -499,21 +506,21 @@ func (Group) Policy() ent.Policy { Again, we expect the privacy-rules to take effect on the client operations. -```go +```go title="examples/privacytenant/example_test.go" func Do(ctx context.Context, client *ent.Client) error { // A continuation of the code-block above. - // We expect operation to fail, because the DenyMismatchedTenants rule + // Expect operation to fail because the DenyMismatchedTenants rule // makes sure the group and the users are connected to the same tenant. - _, err = client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(nati).Save(admin) + _, err = client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(labUser).Save(adminCtx) if !errors.Is(err, privacy.Deny) { - return fmt.Errorf("expect operatio to fail, since user (nati) is not connected to the same tenant") + return fmt.Errorf("expect operation to fail, since user (nati) is not connected to the same tenant") } - _, err = client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(nati, a8m).Save(admin) + _, err = client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(labUser, hubUser).Save(adminCtx) if !errors.Is(err, privacy.Deny) { - return fmt.Errorf("expect operatio to fail, since some users (nati) are not connected to the same tenant") + return fmt.Errorf("expect operation to fail, since some users (nati) are not connected to the same tenant") } - entgo, err := client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(a8m).Save(admin) + entgo, err := client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(hubUser).Save(adminCtx) if err != nil { return fmt.Errorf("expect operation to pass, but got %w", err) } @@ -523,17 +530,22 @@ func Do(ctx context.Context, client *ent.Client) error { ``` In some cases, we want to reject user operations on entities that don't belong to their tenant **without loading -these entities from the database** (unlike the `DenyMismatchedTenants` example above). To achieve this, we can use the -`FilterTenantRule` rule for mutations as well, but limit it to specific operations as follows: +these entities from the database** (unlike the `DenyMismatchedTenants` example above). +To achieve this, we can use the `FilterTenantRule` rule for mutations as well, but limit it to specific operations as follows: -```go +```go title="examples/privacytenant/ent/schema/group.go" // Policy defines the privacy policy of the Group. func (Group) Policy() ent.Policy { return privacy.Policy{ Mutation: privacy.MutationPolicy{ - rule.DenyMismatchedTenants(), - // Limit the FilterTenantRule only for - // UpdateOne and DeleteOne operations. + // Limit DenyMismatchedTenants only for + // Create operations + privacy.OnMutationOperation( + rule.DenyMismatchedTenants(), + ent.OpCreate, + ), + // Limit the FilterTenantRule only for + // UpdateOne and DeleteOne operations. privacy.OnMutationOperation( rule.FilterTenantRule(), ent.OpUpdateOne|ent.OpDeleteOne, @@ -545,22 +557,22 @@ func (Group) Policy() ent.Policy { Then, we expect the privacy-rules to take effect on the client operations. -```go +```go title="examples/privacytenant/example_test.go" func Do(ctx context.Context, client *ent.Client) error { // A continuation of the code-block above. - // Expect operation to fail, because the FilterTenantRule rule makes sure - // that tenants can update and delete only their groups. - err = entgo.Update().SetName("fail.go").Exec(labView) - if !ent.IsNotFound(err) { - return fmt.Errorf("expect operation to fail, since the group (entgo) is managed by a different tenant (hub)") - } - entgo, err = entgo.Update().SetName("entgo").Save(hubView) - if err != nil { - return fmt.Errorf("expect operation to pass, but got %w", err) - } - fmt.Println(entgo) - return nil + // Expect operation to fail, because the FilterTenantRule rule makes sure + // that tenants can update and delete only their groups. + err = entgo.Update().SetName("fail.go").Exec(labView) + if !ent.IsNotFound(err) { + return fmt.Errorf("expect operation to fail, since the group (entgo) is managed by a different tenant (hub), but got %w", err) + } + entgo, err = entgo.Update().SetName("entgo").Save(hubView) + if err != nil { + return fmt.Errorf("expect operation to pass, but got %w", err) + } + fmt.Println(entgo) + return nil } ``` diff --git a/examples/privacytenant/ent/schema/group.go b/examples/privacytenant/ent/schema/group.go index f6a965fad..3e165b094 100644 --- a/examples/privacytenant/ent/schema/group.go +++ b/examples/privacytenant/ent/schema/group.go @@ -45,7 +45,14 @@ func (Group) Edges() []ent.Edge { func (Group) Policy() ent.Policy { return privacy.Policy{ Mutation: privacy.MutationPolicy{ - rule.DenyMismatchedTenants(), + // Limit DenyMismatchedTenants only for + // Create operations + privacy.OnMutationOperation( + rule.DenyMismatchedTenants(), + ent.OpCreate, + ), + // Limit the FilterTenantRule only for + // UpdateOne and DeleteOne operations. privacy.OnMutationOperation( rule.FilterTenantRule(), ent.OpUpdateOne|ent.OpDeleteOne, diff --git a/examples/privacytenant/example_test.go b/examples/privacytenant/example_test.go index c6b93bb6f..d38b01f12 100644 --- a/examples/privacytenant/example_test.go +++ b/examples/privacytenant/example_test.go @@ -36,7 +36,7 @@ func Example_PrivacyTenant() { // Tenant(id=1, name=GitHub) // Tenant(id=2, name=GitLab) // User(id=1, name=a8m, foods=[]) - // User(id=2, name=nati, foods=[Sushi Burritos]) + // User(id=2, name=nati, foods=[]) // Group(id=1, name=entgo.io) // Group(id=1, name=entgo) } @@ -48,31 +48,31 @@ func Do(ctx context.Context, client *ent.Client) error { return fmt.Errorf("expect operation to fail, but got %w", err) } // Deny tenant creation if the viewer is not admin. - viewOnly := viewer.NewContext(ctx, viewer.UserViewer{Role: viewer.View}) - if _, err := client.Tenant.Create().Save(viewOnly); !errors.Is(err, privacy.Deny) { + viewCtx := viewer.NewContext(ctx, viewer.UserViewer{Role: viewer.View}) + if _, err := client.Tenant.Create().Save(viewCtx); !errors.Is(err, privacy.Deny) { return fmt.Errorf("expect operation to fail, but got %w", err) } - // Apply the same operation with "Admin" role. - admin := viewer.NewContext(ctx, viewer.UserViewer{Role: viewer.Admin}) - hub, err := client.Tenant.Create().SetName("GitHub").Save(admin) + // Apply the same operation with "Admin" role, expect it to pass. + adminCtx := viewer.NewContext(ctx, viewer.UserViewer{Role: viewer.Admin}) + hub, err := client.Tenant.Create().SetName("GitHub").Save(adminCtx) if err != nil { return fmt.Errorf("expect operation to pass, but got %w", err) } fmt.Println(hub) - lab, err := client.Tenant.Create().SetName("GitLab").Save(admin) + lab, err := client.Tenant.Create().SetName("GitLab").Save(adminCtx) if err != nil { return fmt.Errorf("expect operation to pass, but got %w", err) } fmt.Println(lab) - // Create 2 users connected to the 2 tenants we created above (a8m->GitHub, nati->GitLab). - a8m := client.User.Create().SetName("a8m").SetTenant(hub).SaveX(admin) - nati := client.User.Create().SetName("nati").SetTenant(lab).SetFoods([]string{"Sushi", "Burritos"}).SaveX(admin) + // Create 2 users connected to the 2 tenants we created above + hubUser := client.User.Create().SetName("a8m").SetTenant(hub).SaveX(adminCtx) + labUser := client.User.Create().SetName("nati").SetTenant(lab).SaveX(adminCtx) hubView := viewer.NewContext(ctx, viewer.UserViewer{T: hub}) out := client.User.Query().OnlyX(hubView) // Expect that "GitHub" tenant to read only its users (i.e. a8m). - if out.ID != a8m.ID { + if out.ID != hubUser.ID { return fmt.Errorf("expect result for user query, got %v", out) } fmt.Println(out) @@ -80,22 +80,22 @@ func Do(ctx context.Context, client *ent.Client) error { labView := viewer.NewContext(ctx, viewer.UserViewer{T: lab}) out = client.User.Query().OnlyX(labView) // Expect that "GitLab" tenant to read only its users (i.e. nati). - if out.ID != nati.ID { + if out.ID != labUser.ID { return fmt.Errorf("expect result for user query, got %v", out) } fmt.Println(out) // Expect operation to fail, because the DenyMismatchedTenants rule makes sure // the group and the users are connected to the same tenant. - _, err = client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(nati).Save(admin) + _, err = client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(labUser).Save(adminCtx) if !errors.Is(err, privacy.Deny) { return fmt.Errorf("expect operation to fail, since user (nati) is not connected to the same tenant") } - _, err = client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(nati, a8m).Save(admin) + _, err = client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(labUser, hubUser).Save(adminCtx) if !errors.Is(err, privacy.Deny) { return fmt.Errorf("expect operation to fail, since some users (nati) are not connected to the same tenant") } - entgo, err := client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(a8m).Save(admin) + entgo, err := client.Group.Create().SetName("entgo.io").SetTenant(hub).AddUsers(hubUser).Save(adminCtx) if err != nil { return fmt.Errorf("expect operation to pass, but got %w", err) } @@ -105,7 +105,7 @@ func Do(ctx context.Context, client *ent.Client) error { // that tenants can update and delete only their groups. err = entgo.Update().SetName("fail.go").Exec(labView) if !ent.IsNotFound(err) { - return fmt.Errorf("expect operation to fail, since the group (entgo) is managed by a different tenant (hub)") + return fmt.Errorf("expect operation to fail, since the group (entgo) is managed by a different tenant (hub), but got %w", err) } entgo, err = entgo.Update().SetName("entgo").Save(hubView) if err != nil { diff --git a/examples/privacytenant/rule/rule.go b/examples/privacytenant/rule/rule.go index 64c3d1a1d..e22c145df 100644 --- a/examples/privacytenant/rule/rule.go +++ b/examples/privacytenant/rule/rule.go @@ -39,9 +39,11 @@ func AllowIfAdmin() privacy.QueryMutationRule { }) } -// FilterTenantRule is a query rule that filters out entities that are not in the tenant. +// FilterTenantRule is a query/mutation rule that filters out entities that are not in the tenant. func FilterTenantRule() privacy.QueryMutationRule { - type TeamsFilter interface { + // TenantsFilter is an interface to wrap WhereHasTenantWith() + // predicate that is used by both `Group` and `User` schemas. + type TenantsFilter interface { WhereHasTenantWith(...predicate.Tenant) } return privacy.FilterFunc(func(ctx context.Context, f privacy.Filter) error { @@ -49,22 +51,21 @@ func FilterTenantRule() privacy.QueryMutationRule { if view.Tenant() == "" { return privacy.Denyf("missing tenant information in viewer") } - tf, ok := f.(TeamsFilter) + tf, ok := f.(TenantsFilter) if !ok { return privacy.Denyf("unexpected filter type %T", f) } - // Make sure that a tenant is able to read only entities that - // has an edge to it. + // Make sure that a tenant reads only entities that has an edge to it. tf.WhereHasTenantWith(tenant.Name(view.Tenant())) // Skip to the next privacy rule (equivalent to return nil). return privacy.Skip }) } -// DenyMismatchedTenants is a rule that returns a deny decision if the operations -// tries to add users to groups that are not in the same tenant. +// DenyMismatchedTenants is a rule that runs only on create operations and returns a deny +// decision if the operation tries to add users to groups that are not in the same tenant. func DenyMismatchedTenants() privacy.MutationRule { - rule := privacy.GroupMutationRuleFunc(func(ctx context.Context, m *ent.GroupMutation) error { + return privacy.GroupMutationRuleFunc(func(ctx context.Context, m *ent.GroupMutation) error { tid, exists := m.TenantID() if !exists { return privacy.Denyf("missing tenant information in mutation") @@ -76,16 +77,14 @@ func DenyMismatchedTenants() privacy.MutationRule { } // Query the tenant-id of all users. Expect to have exact 1 result, // and it matches the tenant-id of the group above. - uid, err := m.Client().User.Query().Where(user.IDIn(users...)).QueryTenant().OnlyID(ctx) + id, err := m.Client().User.Query().Where(user.IDIn(users...)).QueryTenant().OnlyID(ctx) if err != nil { return privacy.Denyf("querying the tenant-id %v", err) } - if uid != tid { - return privacy.Denyf("mismatch tenant-ids for group/users %d != %d", tid, uid) + if id != tid { + return privacy.Denyf("mismatch tenant-ids for group/users %d != %d", tid, id) } // Skip to the next privacy rule (equivalent to return nil). return privacy.Skip }) - // Evaluate the mutation rule only on group creation. - return privacy.OnMutationOperation(rule, ent.OpCreate) }