mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: improve rbac and add benchmark tooling (#18584)
## Description This PR improves the RBAC package by refactoring the policy, enhancing documentation, and adding utility scripts. ## Changes * Refactored `policy.rego` for clarity and readability * Updated README with OPA section * Added `benchmark_authz.sh` script for authz performance testing and comparison * Added `gen_input.go` to generate input for `opa eval` testing
This commit is contained in:
+91
-3
@@ -102,18 +102,106 @@ Example of a scope for a workspace agent token, using an `allow_list` containing
|
||||
}
|
||||
```
|
||||
|
||||
## OPA (Open Policy Agent)
|
||||
|
||||
Open Policy Agent (OPA) is an open source tool used to define and enforce policies.
|
||||
Policies are written in a high-level, declarative language called Rego.
|
||||
Coder’s RBAC rules are defined in the [`policy.rego`](policy.rego) file under the `authz` package.
|
||||
|
||||
When OPA evaluates policies, it binds input data to a global variable called `input`.
|
||||
In the `rbac` package, this structured data is defined as JSON and contains the action, object and subject (see `regoInputValue` in [astvalue.go](astvalue.go)).
|
||||
OPA evaluates whether the subject is allowed to perform the action on the object across three levels: `site`, `org`, and `user`.
|
||||
This is determined by the final rule `allow`, which aggregates the results of multiple rules to decide if the user has the necessary permissions.
|
||||
Similarly to the input, OPA produces structured output data, which includes the `allow` variable as part of the evaluation result.
|
||||
Authorization succeeds only if `allow` explicitly evaluates to `true`. If no `allow` is returned, it is considered unauthorized.
|
||||
To learn more about OPA and Rego, see https://www.openpolicyagent.org/docs.
|
||||
|
||||
### Application and Database Integration
|
||||
|
||||
- [`rbac/authz.go`](authz.go) – Application layer integration: provides the core authorization logic that integrates with Rego for policy evaluation.
|
||||
- [`database/dbauthz/dbauthz.go`](../database/dbauthz/dbauthz.go) – Database layer integration: wraps the database layer with authorization checks to enforce access control.
|
||||
|
||||
There are two types of evaluation in OPA:
|
||||
|
||||
- **Full evaluation**: Produces a decision that can be enforced.
|
||||
This is the default evaluation mode, where OPA evaluates the policy using `input` data that contains all known values and returns output data with the `allow` variable.
|
||||
- **Partial evaluation**: Produces a new policy that can be evaluated later when the _unknowns_ become _known_.
|
||||
This is an optimization in OPA where it evaluates as much of the policy as possible without resolving expressions that depend on _unknown_ values from the `input`.
|
||||
To learn more about partial evaluation, see this [OPA blog post](https://blog.openpolicyagent.org/partial-evaluation-162750eaf422).
|
||||
|
||||
Application of Full and Partial evaluation in `rbac` package:
|
||||
|
||||
- **Full Evaluation** is handled by the `RegoAuthorizer.Authorize()` method in [`authz.go`](authz.go).
|
||||
This method determines whether a subject (user) can perform a specific action on an object.
|
||||
It performs a full evaluation of the Rego policy, which returns the `allow` variable to decide whether access is granted (`true`) or denied (`false` or undefined).
|
||||
- **Partial Evaluation** is handled by the `RegoAuthorizer.Prepare()` method in [`authz.go`](authz.go).
|
||||
This method compiles OPA’s partial evaluation queries into `SQL WHERE` clauses.
|
||||
These clauses are then used to enforce authorization directly in database queries, rather than in application code.
|
||||
|
||||
Authorization Patterns:
|
||||
|
||||
- Fetch-then-authorize: an object is first retrieved from the database, and a single authorization check is performed using full evaluation via `Authorize()`.
|
||||
- Authorize-while-fetching: Partial evaluation via `Prepare()` is used to inject SQL filters directly into queries, allowing efficient authorization of many objects of the same type.
|
||||
`dbauthz` methods that enforce authorization directly in the SQL query are prefixed with `Authorized`, for example, `GetAuthorizedWorkspaces`.
|
||||
|
||||
## Testing
|
||||
|
||||
You can test outside of golang by using the `opa` cli.
|
||||
- OPA Playground: https://play.openpolicyagent.org/
|
||||
- OPA CLI (`opa eval`): useful for experimenting with different inputs and understanding how the policy behaves under various conditions.
|
||||
`opa eval` returns the constraints that must be satisfied for a rule to evaluate to `true`.
|
||||
- `opa eval` requires an `input.json` file containing the input data to run the policy against.
|
||||
You can generate this file using the [gen_input.go](../../scripts/rbac-authz/gen_input.go) script.
|
||||
Note: the script currently produces a fixed input. You may need to tweak it for your specific use case.
|
||||
|
||||
**Evaluation**
|
||||
### Full Evaluation
|
||||
|
||||
```bash
|
||||
opa eval --format=pretty "data.authz.allow" -d policy.rego -i input.json
|
||||
```
|
||||
|
||||
**Partial Evaluation**
|
||||
This command fully evaluates the policy in the `policy.rego` file using the input data from `input.json`, and returns the result of the `allow` variable:
|
||||
|
||||
- `data.authz.allow` accesses the `allow` rule within the `authz` package.
|
||||
- `data.authz` on its own would return the entire output object of the package.
|
||||
|
||||
This command answers the question: “Is the user allowed?”
|
||||
|
||||
### Partial Evaluation
|
||||
|
||||
```bash
|
||||
opa eval --partial --format=pretty 'data.authz.allow' -d policy.rego --unknowns input.object.owner --unknowns input.object.org_owner --unknowns input.object.acl_user_list --unknowns input.object.acl_group_list -i input.json
|
||||
```
|
||||
|
||||
This command performs a partial evaluation of the policy, specifying a set of unknown input parameters.
|
||||
The result is a set of partial queries that can be converted into `SQL WHERE` clauses and injected into SQL queries.
|
||||
|
||||
This command answers the question: “What conditions must be met for the user to be allowed?”
|
||||
|
||||
### Benchmarking
|
||||
|
||||
Benchmark tests to evaluate the performance of full and partial evaluation can be found in `authz_test.go`.
|
||||
You can run these tests with the `-bench` flag, for example:
|
||||
|
||||
```bash
|
||||
go test -bench=BenchmarkRBACFilter -run=^$
|
||||
```
|
||||
|
||||
To capture memory and CPU profiles, use the following flags:
|
||||
|
||||
- `-memprofile memprofile.out`
|
||||
- `-cpuprofile cpuprofile.out`
|
||||
|
||||
The script [`benchmark_authz.sh`](../../scripts/rbac-authz/benchmark_authz.sh) runs the `authz` benchmark tests on the current Git branch or compares benchmark results between two branches using [`benchstat`](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat).
|
||||
`benchstat` compares the performance of a baseline benchmark against a new benchmark result and highlights any statistically significant differences.
|
||||
|
||||
- To run benchmark on the current branch:
|
||||
|
||||
```bash
|
||||
benchmark_authz.sh --single
|
||||
```
|
||||
|
||||
- To compare benchmarks between 2 branches:
|
||||
|
||||
```bash
|
||||
benchmark_authz.sh --compare main prebuild_policy
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user