Skip to content
Open
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
203 changes: 203 additions & 0 deletions RBAC_WITH_RESOURCE_SCOPE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# RBAC with Resource Scope

This document explains how to implement Azure RBAC-like functionality in Casbin, where the same role can be assigned to different users scoped to specific resources, preventing permission leakage.

## Problem Statement

In traditional RBAC implementations, when you assign a role to a user, that user gets access to all resources that the role has permissions for. This can lead to permission leakage when you want to reuse roles but scope them to specific resources.

For example, consider a scenario where:
- `user1` should have `reader` role for `resource1` only
- `user2` should have `reader` role for `resource2` only

In traditional RBAC, if you assign the `reader` role to both users, they would both get access to both resources if the role has permissions for both.

## Solution

Casbin provides a solution using 3-parameter grouping (`g = _, _, _`) to scope roles by resource. This allows you to assign the same role to different users with different resource scopes.

## Simple Resource Scope

### Model Configuration

```ini
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow))

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

### Policy Configuration

```csv
p, reader, resource1, read
p, reader, resource2, read
p, writer, resource1, write
p, writer, resource2, write

g, user1, reader, resource1
g, user2, reader, resource2
g, user3, writer, resource1
```

### Usage

```go
e, _ := casbin.NewEnforcer("rbac_with_resource_scope_model.conf", "rbac_with_resource_scope_policy.csv")

// Check if user1 can read resource1 (returns true)
e.Enforce("user1", "resource1", "read")

// Check if user1 can read resource2 (returns false - different scope)
e.Enforce("user1", "resource2", "read")

// Check if user2 can read resource2 (returns true)
e.Enforce("user2", "resource2", "read")

// Get roles for user1 in resource1 scope
e.GetRolesForUser("user1", "resource1") // Returns ["reader"]

// Get roles for user1 in resource2 scope
e.GetRolesForUser("user1", "resource2") // Returns []

// Add a role for a user with resource scope
e.AddRoleForUser("user4", "reader", "resource1")

// Delete a role for a user with resource scope
e.DeleteRoleForUser("user4", "reader", "resource1")
```

## Multi-Tenant Resource Scope

For applications with multi-tenancy requirements, you can combine tenant and resource scoping by concatenating them in the grouping relationship.

### Model Configuration

```ini
[request_definition]
r = sub, tenant, obj, act

[policy_definition]
p = sub, tenant, obj, act

[role_definition]
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub, r.tenant + "::" + r.obj) && r.tenant == p.tenant && r.obj == p.obj && r.act == p.act
```

### Policy Configuration

```csv
p, reader, tenant1, resource1, read
p, reader, tenant1, resource2, read
p, reader, tenant2, resource1, read
p, writer, tenant1, resource1, write

g, user1, reader, tenant1::resource1
g, user2, reader, tenant1::resource2
g, user3, reader, tenant2::resource1
g, user4, writer, tenant1::resource1
```

### Usage

```go
e, _ := casbin.NewEnforcer("rbac_with_resource_scope_tenant_model.conf", "rbac_with_resource_scope_tenant_policy.csv")

// Check if user1 can read resource1 in tenant1 (returns true)
e.Enforce("user1", "tenant1", "resource1", "read")

// Check if user1 can read resource2 in tenant1 (returns false - different resource scope)
e.Enforce("user1", "tenant1", "resource2", "read")

// Check if user1 can read resource1 in tenant2 (returns false - different tenant)
e.Enforce("user1", "tenant2", "resource1", "read")

// Get roles for user1 in tenant1::resource1 scope
e.GetRolesForUser("user1", "tenant1::resource1") // Returns ["reader"]

// Add a role for a user with tenant::resource scope
e.AddRoleForUser("user5", "reader", "tenant1::resource1")

// Delete a role for a user with tenant::resource scope
e.DeleteRoleForUser("user5", "reader", "tenant1::resource1")
```

## Comparison with Azure RBAC

This implementation provides functionality similar to Azure RBAC where:

1. **Role Definitions**: Define what actions can be performed (like Azure's built-in or custom roles)
2. **Role Assignments**: Assign roles to users with specific scopes (like Azure's role assignments at different scopes)
3. **Scope Hierarchy**: Support for multi-level scoping (tenant::resource is similar to Azure's subscription/resource group/resource hierarchy)

### Key Differences

- **Azure RBAC**: Uses a hierarchical scope model where permissions at a parent scope automatically apply to child scopes
- **Casbin Resource Scope**: Explicit scoping - permissions must be explicitly granted for each scope level

## API Compatibility

The standard Casbin RBAC APIs work seamlessly with resource-scoped roles by passing the scope as the domain parameter:

```go
// Get roles for a user in a specific scope
e.GetRolesForUser("user1", "resource1")

// Get users who have a role in a specific scope
e.GetUsersForRole("reader", "resource1")

// Add a role for a user in a specific scope
e.AddRoleForUser("user3", "writer", "resource1")

// Delete a role for a user in a specific scope
e.DeleteRoleForUser("user3", "writer", "resource1")

// Check if a user has a role in a specific scope
e.HasRoleForUser("user1", "reader", "resource1")
```

For multi-tenant scenarios, use the concatenated scope:

```go
e.GetRolesForUser("user1", "tenant1::resource1")
e.AddRoleForUser("user5", "reader", "tenant1::resource1")
```

## Benefits

1. **Role Reusability**: Define roles once and reuse them across different resources
2. **Permission Isolation**: Users with the same role but different scopes cannot access each other's resources
3. **No Core Changes**: Uses existing Casbin capabilities (multi-domain role manager) without requiring library modifications
4. **Flexible Scoping**: Can be adapted for single-level (resource) or multi-level (tenant::resource) scoping
5. **Standard APIs**: Works with existing Casbin RBAC API methods

## Examples

See the following files for complete working examples:
- `examples/rbac_with_resource_scope_model.conf` - Simple resource scope model
- `examples/rbac_with_resource_scope_policy.csv` - Simple resource scope policy
- `examples/rbac_with_resource_scope_tenant_model.conf` - Multi-tenant resource scope model
- `examples/rbac_with_resource_scope_tenant_policy.csv` - Multi-tenant resource scope policy

## Tests

See `rbac_api_with_resource_scope_test.go` for comprehensive test cases covering:
- Simple resource scope
- Multi-tenant resource scope
- Multi-tenancy isolation
137 changes: 137 additions & 0 deletions examples/rbac_with_resource_scope_demo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright 2017 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"fmt"
"log"

"github.com/casbin/casbin/v3"
)

func main() {
fmt.Println("=== RBAC with Resource Scope Demo ===")

// Simple Resource Scope Example
fmt.Println("--- Simple Resource Scope Example ---")
e1, err := casbin.NewEnforcer("examples/rbac_with_resource_scope_model.conf", "examples/rbac_with_resource_scope_policy.csv")
if err != nil {
log.Fatalf("Failed to create enforcer: %v", err)
}

fmt.Println("\nInitial Policy:")
fmt.Println("p, reader, resource1, read")
fmt.Println("p, reader, resource2, read")
fmt.Println("p, writer, resource1, write")
fmt.Println("p, writer, resource2, write")
fmt.Println("\nInitial Role Assignments:")
fmt.Println("g, user1, reader, resource1")
fmt.Println("g, user2, reader, resource2")
fmt.Println("g, user3, writer, resource1")

// Test enforcement
fmt.Println("\n--- Enforcement Tests ---")
testEnforce(e1, "user1", "resource1", "read", "user1 can read resource1")
testEnforce(e1, "user1", "resource2", "read", "user1 can read resource2 (should be false)")
testEnforce(e1, "user2", "resource1", "read", "user2 can read resource1 (should be false)")
testEnforce(e1, "user2", "resource2", "read", "user2 can read resource2")
testEnforce(e1, "user3", "resource1", "write", "user3 can write to resource1")
testEnforce(e1, "user3", "resource2", "write", "user3 can write to resource2 (should be false)")

// Get roles for users
fmt.Println("\n--- Role Queries ---")
printRoles(e1, "user1", "resource1")
printRoles(e1, "user1", "resource2")
printRoles(e1, "user2", "resource1")
printRoles(e1, "user2", "resource2")

// Add a new role assignment
fmt.Println("\n--- Adding New Role Assignment ---")
_, err = e1.AddRoleForUser("user4", "reader", "resource1")
if err != nil {
log.Fatalf("Failed to add role: %v", err)
}
fmt.Println("Added: g, user4, reader, resource1")
testEnforce(e1, "user4", "resource1", "read", "user4 can read resource1")
testEnforce(e1, "user4", "resource2", "read", "user4 can read resource2 (should be false)")

// Multi-Tenant Resource Scope Example
fmt.Println("\n\n--- Multi-Tenant Resource Scope Example ---")
e2, err := casbin.NewEnforcer("examples/rbac_with_resource_scope_tenant_model.conf", "examples/rbac_with_resource_scope_tenant_policy.csv")
if err != nil {
log.Fatalf("Failed to create enforcer: %v", err)
}

fmt.Println("\nInitial Policy:")
fmt.Println("p, reader, tenant1, resource1, read")
fmt.Println("p, reader, tenant1, resource2, read")
fmt.Println("p, reader, tenant2, resource1, read")
fmt.Println("p, writer, tenant1, resource1, write")
fmt.Println("\nInitial Role Assignments:")
fmt.Println("g, user1, reader, tenant1::resource1")
fmt.Println("g, user2, reader, tenant1::resource2")
fmt.Println("g, user3, reader, tenant2::resource1")
fmt.Println("g, user4, writer, tenant1::resource1")

// Test enforcement with tenants
fmt.Println("\n--- Enforcement Tests with Tenants ---")
testEnforceWithTenant(e2, "user1", "tenant1", "resource1", "read", "user1 can read resource1 in tenant1")
testEnforceWithTenant(e2, "user1", "tenant1", "resource2", "read", "user1 can read resource2 in tenant1 (should be false)")
testEnforceWithTenant(e2, "user1", "tenant2", "resource1", "read", "user1 can read resource1 in tenant2 (should be false)")
testEnforceWithTenant(e2, "user2", "tenant1", "resource2", "read", "user2 can read resource2 in tenant1")
testEnforceWithTenant(e2, "user3", "tenant2", "resource1", "read", "user3 can read resource1 in tenant2")
testEnforceWithTenant(e2, "user4", "tenant1", "resource1", "write", "user4 can write to resource1 in tenant1")

// Get roles for users in tenant context
fmt.Println("\n--- Role Queries with Tenants ---")
printRoles(e2, "user1", "tenant1::resource1")
printRoles(e2, "user1", "tenant1::resource2")
printRoles(e2, "user2", "tenant1::resource2")
printRoles(e2, "user3", "tenant2::resource1")

// Demonstrate isolation
fmt.Println("\n--- Demonstrating Multi-Tenancy Isolation ---")
fmt.Println("Both user1 and user3 have 'reader' role, but for different tenant::resource combinations")
testEnforceWithTenant(e2, "user1", "tenant1", "resource1", "read", "user1 can read resource1 in tenant1")
testEnforceWithTenant(e2, "user1", "tenant2", "resource1", "read", "user1 can read resource1 in tenant2 (should be false - different tenant)")
testEnforceWithTenant(e2, "user3", "tenant2", "resource1", "read", "user3 can read resource1 in tenant2")
testEnforceWithTenant(e2, "user3", "tenant1", "resource1", "read", "user3 can read resource1 in tenant1 (should be false - different tenant)")

fmt.Println("\n=== Demo Complete ===")
}

func testEnforce(e *casbin.Enforcer, sub, obj, act, description string) {
result, err := e.Enforce(sub, obj, act)
if err != nil {
log.Printf("Error during enforcement: %v", err)
}
fmt.Printf("✓ %s: %t\n", description, result)
}

func testEnforceWithTenant(e *casbin.Enforcer, sub, tenant, obj, act, description string) {
result, err := e.Enforce(sub, tenant, obj, act)
if err != nil {
log.Printf("Error during enforcement: %v", err)
}
fmt.Printf("✓ %s: %t\n", description, result)
}

func printRoles(e *casbin.Enforcer, user, scope string) {
roles, err := e.GetRolesForUser(user, scope)
if err != nil {
log.Printf("Error getting roles: %v", err)
}
fmt.Printf("Roles for %s in scope '%s': %v\n", user, scope, roles)
}
14 changes: 14 additions & 0 deletions examples/rbac_with_resource_scope_model.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub, r.obj) && r.obj == p.obj && r.act == p.act
8 changes: 8 additions & 0 deletions examples/rbac_with_resource_scope_policy.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
p, reader, resource1, read
p, reader, resource2, read
p, writer, resource1, write
p, writer, resource2, write

g, user1, reader, resource1
g, user2, reader, resource2
g, user3, writer, resource1
14 changes: 14 additions & 0 deletions examples/rbac_with_resource_scope_tenant_model.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[request_definition]
r = sub, tenant, obj, act

[policy_definition]
p = sub, tenant, obj, act

[role_definition]
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub, r.tenant + "::" + r.obj) && r.tenant == p.tenant && r.obj == p.obj && r.act == p.act
9 changes: 9 additions & 0 deletions examples/rbac_with_resource_scope_tenant_policy.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
p, reader, tenant1, resource1, read
p, reader, tenant1, resource2, read
p, reader, tenant2, resource1, read
p, writer, tenant1, resource1, write

g, user1, reader, tenant1::resource1
g, user2, reader, tenant1::resource2
g, user3, reader, tenant2::resource1
g, user4, writer, tenant1::resource1
Loading
Loading