In this tutorial, we will start by going over the basic terms we use in the framework, continue with a section for configuring the policy feature to your project, and finish with a few examples.
ent.Policy interface contains two methods:
EvalMutation. The first defines the read-policy, and
the second defines the write-policy. A policy contains zero or more privacy rules (see below). These rules are evaluated
in the same order they are declared in the schema.
If all rules are evaluated without returning an error, the evaluation finishes successfully, and the executed operation gets access to the target nodes.
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.
Each policy (mutation or query) includes one or more privacy rules. The function signature for these rules is as follows:
There are three types of decision that can help you control the privacy rules evaluation.
privacy.Allow- If returned from a privacy rule, the evaluation stops (next rules will be skipped), and the executed operation (query or mutation) gets access to the target nodes.
privacy.Deny- If returned from a privacy rule, the evaluation stops (next rules will be skipped), and the executed operation is cancelled. This equivalent to returning any error.
privacy.Skip- Skip the current rule, and jump to the next privacy rule. This equivalent to returning a
Now, that we’ve covered the basic terms, let’s start writing some code.
In order to enable the privacy option in your code generation, enable the
privacy feature with one of two options:
1. If you are using the default go generate config, add
--feature privacy option to the
ent/generate.go file as follows:
It is recommended to add the
schema/snapshot feature-flag along with the
privacy to enhance the development experience (e.g.
2. If you are using the configuration from the GraphQL documentation, add the feature flag as follows:
You should notice that, similar to schema hooks, if you use the
Policy option in your schema,
you MUST add the following import in the main package, because a circular import is possible between the schema package,
and the generated ent package:
We start with a simple example of an application that lets any user read any data, and accepts mutations only from users with admin role. We will create 2 additional packages for the purpose of the examples:
rule- for holding the different privacy rules in our schema.
viewer- for getting and setting the user/viewer who's executing the operation. In this simple example, it can be either a normal user or an admin.
After running the code-generation (with the feature-flag for privacy), we add the
Policy method with 2 generated policy rules.
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:
As you can see, the first rule
DenyIfNoViewer, makes sure every operation has a viewer in its context,
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:
Since we define the
DenyIfNoViewer first, it will be executed before all other rules, and accessing the
viewer.Viewer object is safe in the
After adding the rules above and running the code-generation, we expect the privacy-layer logic to be applied on
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.
The full example exists in GitHub.
In this example, we're going to create a schema with 3 entity types -
The helper packages
rule (as mentioned above) also exist in this example to help us structure the application.
Let's start building this application piece by piece. We begin by creating 3 different schemas (see the full code here), and since we want to share some logic between them, we create another mixed-in schema and add it to all other schemas as follows:
As explained in the first example, the
DenyIfNoViewer privacy rule, denies the operation if the
context.Context does not
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
Then, we expect the following code to run successfully:
We continue by adding the rest of the edges in our data-model (see image above), and since both
an edge to the
Tenant schema, we create a shared mixed-in schema named
TenantMixin for this:
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.
Note, the filtering option for privacy needs to be enabled using the
entqlfeature-flag (see instructions above).
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.
Then, after running the code-generation, we expect the privacy-rules to take effect on the client operations.
We finish our example with another privacy-rule named
DenyMismatchedTenants on the
DenyMismatchedTenants rule rejects the group creation if the associated users don't belong to
the same tenant as the group.
We add this rule to the
Group schema and run code-generation.
Again, we expect the privacy-rules to take effect on the client operations.
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:
Then, we expect the privacy-rules to take effect on the client operations.
The full example exists in GitHub.
Please note that this documentation is under active development.