Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions docs/CycleDetection.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
id: cycle-detection
title: Cycle Detection
description: Detect and prevent role inheritance cycles in RBAC
keywords: [cycle detection, RBAC, role inheritance, detector]
authors: [hsluoyz]
---

```mdx-code-block
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
```

Role inheritance in RBAC creates directed relationships between roles. When these relationships form a cycle (like A inherits B, B inherits C, C inherits A), it creates logical inconsistencies that can break authorization checks. Casbin provides built-in cycle detection to catch these errors early.

## Why Detect Cycles?

Consider this problematic setup:

```csv
g, role_a, role_b
g, role_b, role_c
g, role_c, role_a
```

Here, `role_a` inherits from `role_b`, which inherits from `role_c`, which inherits back to `role_a`. This creates an infinite loop that makes it impossible to determine what permissions any role actually has. Even simpler cases like `g, admin, admin` (a role inheriting from itself) cause similar problems.

Cycle detection helps you catch these mistakes before they cause runtime issues or security vulnerabilities.

## Using the Detector

The `DefaultDetector` uses depth-first search to efficiently scan your role graph for cycles. It works with any `RoleManager` that supports the `Range` method:

```mdx-code-block
<Tabs groupId="langs">
<TabItem value="Go" label="Go" default>
```

```go
import (
"github.com/casbin/casbin/v3"
"github.com/casbin/casbin/v3/detector"
)

e, _ := casbin.NewEnforcer("model.conf", "policy.csv")
d := detector.NewDefaultDetector()

// Check for cycles after loading or modifying roles
if err := d.Check(e.GetRoleManager()); err != nil {
// Handle the cycle error - err describes which roles form the cycle
log.Fatal(err)
}
```

```mdx-code-block
</TabItem>
</Tabs>
```

When a cycle is detected, the error message shows the full path: `"cycle detected: role_a -> role_b -> role_c -> role_a"`.

## When to Check

Run cycle detection:

- After loading policies from storage
- Before deploying role changes to production
- When users modify role hierarchies through admin interfaces
- As part of policy validation in CI/CD pipelines

For systems with complex role structures, consider checking periodically or after bulk updates.

## Performance

The detector runs in linear time relative to the number of roles and relationships. Even with 10,000 roles, cycle detection completes quickly enough for real-time validation. The algorithm only visits each role once, making it efficient for large RBAC systems.

## Custom Detectors

You can implement the `Detector` interface to create custom validation logic:

```go
type Detector interface {
Check(rm rbac.RoleManager) error
}
```

This allows adding checks beyond cycle detection, such as validating role naming conventions or enforcing hierarchy depth limits.
73 changes: 33 additions & 40 deletions docs/RBACWithConditions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ authors: [PokIsemaine]

## Conditional RoleManager

`ConditionalRoleManager` supports custom condition functions at the policy level.
The `ConditionalRoleManager` allows you to apply custom condition functions at the policy level.

For example, when we need a temporary role policy, we can follow the following approach:
When you need role policies that are valid only under certain conditions—such as time-based temporary roles—you can configure this as shown below:

`model.conf`

Expand All @@ -31,8 +31,7 @@ e = some(where (p.eft == allow))
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
```

`g = _, _, (_, _)` uses `(_, _)` to contain a list of arguments to pass to the condition function
and `_` as a parameter placeholder
The syntax `g = _, _, (_, _)` defines a role relationship with additional parameters. The underscores `_` serve as placeholders, while the parentheses `(_, _)` specify the list of arguments that will be passed to the condition function.

`policy.csv`

Expand All @@ -57,10 +56,7 @@ g, alice, data8_admin, 9999-12-30 00:00:00, _

### Basic Usage

Add a conditional function for the role policy(`g`type policy) through `AddNamedLinkConditionFunc`,
and when enforcing is executed, the corresponding parameters will be automatically obtained
and passed in the conditional function for checking.
If the check passes, then the corresponding role policy(`g` type policy) is valid, otherwise it is invalid
Use `AddNamedLinkConditionFunc` to attach a condition function to a role policy (of type `g`). During enforcement, the system automatically retrieves the corresponding parameters and passes them to the condition function. The role policy is considered valid only if the function returns true.

```go
e.AddNamedLinkConditionFunc("g", "alice", "data2_admin", util.TimeMatchFunc)
Expand All @@ -72,25 +68,25 @@ e.AddNamedLinkConditionFunc("g", "alice", "data7_admin", util.TimeMatchFunc)
e.AddNamedLinkConditionFunc("g", "alice", "data8_admin", util.TimeMatchFunc)


e.enforce("alice", "data1", "read") // except: true
e.enforce("alice", "data2", "write") // except: false
e.enforce("alice", "data3", "read") // except: true
e.enforce("alice", "data4", "write") // except: true
e.enforce("alice", "data5", "read") // except: true
e.enforce("alice", "data6", "write") // except: false
e.enforce("alice", "data7", "read") // except: true
e.enforce("alice", "data8", "write") // except: false
e.enforce("alice", "data1", "read") // expect: true
e.enforce("alice", "data2", "write") // expect: false
e.enforce("alice", "data3", "read") // expect: true
e.enforce("alice", "data4", "write") // expect: true
e.enforce("alice", "data5", "read") // expect: true
e.enforce("alice", "data6", "write") // expect: false
e.enforce("alice", "data7", "read") // expect: true
e.enforce("alice", "data8", "write") // expect: false
```

### Custom condition functions

Custom conditional functions need to conform to the following function types
Condition functions must follow this signature:

```go
type LinkConditionFunc = func(args ...string) (bool, error)
```

for example:
Example implementation:

```go
// TimeMatchFunc is the wrapper for TimeMatch.
Expand Down Expand Up @@ -169,10 +165,7 @@ g, alice, data8_admin, domain8, 9999-12-30 00:00:00, _

### Basic Usage

Add a conditional function for the role policy(`g`type policy) through `AddNamedDomainLinkConditionFunc`,
and when enforcing is executed, the corresponding parameters will be automatically obtained
and passed in the conditional function for checking.
If the check passes, then the corresponding role policy(`g` type policy) is valid, otherwise it is invalid
Use `AddNamedDomainLinkConditionFunc` to attach a condition function to a domain-specific role policy (of type `g`). During enforcement, the system automatically retrieves the corresponding parameters and passes them to the condition function. The role policy is considered valid only if the function returns true.

```go
e.AddNamedDomainLinkConditionFunc("g", "alice", "data2_admin", "domain2", util.TimeMatchFunc)
Expand All @@ -184,28 +177,28 @@ e.AddNamedDomainLinkConditionFunc("g", "alice", "data7_admin", "domain7", util.T
e.AddNamedDomainLinkConditionFunc("g", "alice", "data8_admin", "domain8", util.TimeMatchFunc)


e.enforce("alice", "domain1", "data1", "read") // except: true
e.enforce("alice", "domain2", "data2", "write") // except: false
e.enforce("alice", "domain3", "data3", "read") // except: true
e.enforce("alice", "domain4", "data4", "write") // except: true
e.enforce("alice", "domain5", "data5", "read") // except: true
e.enforce("alice", "domain6", "data6", "write") // except: false
e.enforce("alice", "domain7", "data7", "read") // except: true
e.enforce("alice", "domain8", "data8", "write") // except: false
e.enforce("alice", "domain_not_exist", "data1", "write") // except: false
e.enforce("alice", "domain_not_exist", "data2", "read") // except: false
e.enforce("alice", "domain_not_exist", "data3", "write") // except: false
e.enforce("alice", "domain_not_exist", "data4", "read") // except: false
e.enforce("alice", "domain_not_exist", "data5", "write") // except: false
e.enforce("alice", "domain_not_exist", "data6", "read") // except: false
e.enforce("alice", "domain_not_exist", "data7", "write") // except: false
e.enforce("alice", "domain_not_exist", "data8", "read") // except: false
e.enforce("alice", "domain1", "data1", "read") // expect: true
e.enforce("alice", "domain2", "data2", "write") // expect: false
e.enforce("alice", "domain3", "data3", "read") // expect: true
e.enforce("alice", "domain4", "data4", "write") // expect: true
e.enforce("alice", "domain5", "data5", "read") // expect: true
e.enforce("alice", "domain6", "data6", "write") // expect: false
e.enforce("alice", "domain7", "data7", "read") // expect: true
e.enforce("alice", "domain8", "data8", "write") // expect: false
e.enforce("alice", "domain_not_exist", "data1", "write") // expect: false
e.enforce("alice", "domain_not_exist", "data2", "read") // expect: false
e.enforce("alice", "domain_not_exist", "data3", "write") // expect: false
e.enforce("alice", "domain_not_exist", "data4", "read") // expect: false
e.enforce("alice", "domain_not_exist", "data5", "write") // expect: false
e.enforce("alice", "domain_not_exist", "data6", "read") // expect: false
e.enforce("alice", "domain_not_exist", "data7", "write") // expect: false
e.enforce("alice", "domain_not_exist", "data8", "read") // expect: false
```

### Custom condition functions

Like the basic `Conditional RoleManager`, custom functions are supported, and there is no difference in use.
Custom condition functions work the same way as in the basic `Conditional RoleManager`.

Note that `DomainMatchingFunc`, `MatchingFunc`, and `LinkConditionFunc` are at different levels and are used in different situations.
Keep in mind that `DomainMatchingFunc`, `MatchingFunc`, and `LinkConditionFunc` operate at different levels and serve distinct purposes.

![conditional_rolemanager_with_domains](/img/rbac/conditional_rolemanager_with_domains.png)
13 changes: 5 additions & 8 deletions docs/RBACWithConditionsAPI.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ keywords: [RBAC with conditions, API]
authors: [PokIsemaine]
---

A more user-friendly API for [RBAC with conditions](RBACWithConditions.mdx).
This API provides a simplified interface for working with [RBAC with conditions](RBACWithConditions.mdx).

## Reference

### AddNamedLinkConditionFunc

`AddNamedLinkConditionFunc` Add condition function fn for Link `userName->roleName`,
when fn returns true, Link is valid, otherwise invalid
`AddNamedLinkConditionFunc` attaches a condition function to a link between `userName` and `roleName`. The link is valid only when the function returns true.

```mdx-code-block
<Tabs groupId="langs">
Expand All @@ -31,8 +30,7 @@ e.AddNamedLinkConditionFunc("g", "userName", "roleName", YourLinkConditionFunc)

### AddNamedDomainLinkConditionFunc

`AddNamedDomainLinkConditionFunc` Add condition function fn for Link `userName-> {roleName, domain}`,
when fn returns true, Link is valid, otherwise invalid
`AddNamedDomainLinkConditionFunc` attaches a condition function to a link between `userName` and `{roleName, domain}`. The link is valid only when the function returns true.

```mdx-code-block
<Tabs groupId="langs">
Expand All @@ -50,7 +48,7 @@ e.AddNamedDomainLinkConditionFunc("g", "userName", "roleName", "domainName", You

### SetNamedLinkConditionFuncParams

`SetNamedLinkConditionFuncParams` Sets the parameters of the condition function fn for Link `userName->roleName`
`SetNamedLinkConditionFuncParams` configures the parameters passed to the condition function for a link between `userName` and `roleName`.

```mdx-code-block
<Tabs groupId="langs">
Expand All @@ -69,8 +67,7 @@ e.SetNamedLinkConditionFuncParams("g", "userName2", "roleName2", "YourConditionF

### SetNamedDomainLinkConditionFuncParams

`SetNamedDomainLinkConditionFuncParams` Sets the parameters of the condition function fn
for Link `userName->{roleName, domain}`
`SetNamedDomainLinkConditionFuncParams` configures the parameters passed to the condition function for a link between `userName` and `{roleName, domain}`.

```mdx-code-block
<Tabs groupId="langs">
Expand Down
19 changes: 9 additions & 10 deletions docs/RBACWithDomains.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ authors: [hsluoyz]

## Role Definition with Domain Tenants

The RBAC roles in Casbin can be global or domain-specific. Domain-specific roles mean that the roles for a user can be different when the user is in different domains/tenants. This is very useful for large systems like a cloud, as users are usually in different tenants.
In Casbin, RBAC roles can be either global or domain-specific. Domain-specific roles allow a user to hold different roles across different domains or tenants. This capability is particularly valuable in large-scale systems such as cloud platforms, where users frequently operate within multiple distinct tenants.

The role definition with domains/tenants should look like this:
The role definition for domains or tenants should be structured as follows:

```ini
[role_definition]
g = _, _, _
```

The third `_` represents the name of the domain/tenant, and this part should not be changed. Then the policy can be:
The third underscore `_` denotes the domain or tenant name and must remain unchanged. With this setup, policies can be written like this:

```csv
p, admin, tenant1, data1, read
Expand All @@ -27,31 +27,30 @@ g, alice, admin, tenant1
g, alice, user, tenant2
```

This means that the `admin` role in `tenant1` can read `data1`. And `alice` has the `admin` role in `tenant1` and the `user` role in `tenant2`. Therefore, she can read `data1`. However, since `alice` is not an `admin` in `tenant2`, she cannot read `data2`.
This configuration means that the `admin` role in `tenant1` has read access to `data1`. Alice holds the `admin` role in `tenant1` and the `user` role in `tenant2`, so she can read `data1`. However, because Alice is not an `admin` in `tenant2`, she cannot read `data2`.

Then, in a matcher, you should check the role as follows:
In your matcher, verify the role as follows:

```ini
[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
```

Please refer to the [rbac_with_domains_model.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_domains_model.conf) for examples.
For additional examples, refer to [rbac_with_domains_model.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_domains_model.conf).

:::info Token Name Convention

Note: Conventionally, the domain token name in policy definition is `dom` and is placed as the second token (`sub, dom, obj, act`).
Now, Golang Casbin supports customized token names and placement. If the domain token name is `dom`, the domain token can be placed at an arbitrary position without any additional action. If the domain token name is not `dom`, `e.SetFieldIndex()` for `constant.DomainIndex` should be called after the enforcer is initialized, regardless of its position.
By convention, the domain token in policy definitions is named `dom` and is typically positioned as the second token (e.g., `sub, dom, obj, act`). Golang Casbin now supports custom token names and positions. If the domain token is named `dom`, it can be placed anywhere without extra configuration. For other names, call `e.SetFieldIndex()` with `constant.DomainIndex` after initializing the enforcer, regardless of token position.

```ini
# `domain` here for `dom`
# Using `domain` instead of `dom`
[policy_definition]
p = sub, obj, act, domain
```

```go
e.SetFieldIndex("p", constant.DomainIndex, 3) // index starts from 0
users := e.GetAllUsersByDomain("domain1") // without SetFieldIndex, it will raise an error
users := e.GetAllUsersByDomain("domain1") // without SetFieldIndex, this will raise an error
```

:::
Loading