diff --git a/RBAC_WITH_RESOURCE_SCOPE.md b/RBAC_WITH_RESOURCE_SCOPE.md new file mode 100644 index 00000000..a5eba982 --- /dev/null +++ b/RBAC_WITH_RESOURCE_SCOPE.md @@ -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 diff --git a/examples/rbac_with_resource_scope_demo.go b/examples/rbac_with_resource_scope_demo.go new file mode 100644 index 00000000..242bc650 --- /dev/null +++ b/examples/rbac_with_resource_scope_demo.go @@ -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) +} diff --git a/examples/rbac_with_resource_scope_model.conf b/examples/rbac_with_resource_scope_model.conf new file mode 100644 index 00000000..d04a469e --- /dev/null +++ b/examples/rbac_with_resource_scope_model.conf @@ -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 diff --git a/examples/rbac_with_resource_scope_policy.csv b/examples/rbac_with_resource_scope_policy.csv new file mode 100644 index 00000000..bbc8a4d6 --- /dev/null +++ b/examples/rbac_with_resource_scope_policy.csv @@ -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 diff --git a/examples/rbac_with_resource_scope_tenant_model.conf b/examples/rbac_with_resource_scope_tenant_model.conf new file mode 100644 index 00000000..a6f4f4cd --- /dev/null +++ b/examples/rbac_with_resource_scope_tenant_model.conf @@ -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 diff --git a/examples/rbac_with_resource_scope_tenant_policy.csv b/examples/rbac_with_resource_scope_tenant_policy.csv new file mode 100644 index 00000000..976f6369 --- /dev/null +++ b/examples/rbac_with_resource_scope_tenant_policy.csv @@ -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 diff --git a/rbac_api_with_resource_scope_test.go b/rbac_api_with_resource_scope_test.go new file mode 100644 index 00000000..e6b45ae0 --- /dev/null +++ b/rbac_api_with_resource_scope_test.go @@ -0,0 +1,151 @@ +// 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 casbin + +import "testing" + +func testEnforceWithTenant(t *testing.T, e *Enforcer, sub string, tenant string, obj interface{}, act string, res bool) { + t.Helper() + if myRes, _ := e.Enforce(sub, tenant, obj, act); myRes != res { + t.Errorf("%s, %s, %v, %s: %t, supposed to be %t", sub, tenant, obj, act, myRes, res) + } +} + +func TestRBACWithResourceScope(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_resource_scope_model.conf", "examples/rbac_with_resource_scope_policy.csv") + + // Test user1 has reader role for resource1 + testGetRoles(t, e, []string{"reader"}, "user1", "resource1") + testGetRoles(t, e, []string{}, "user1", "resource2") + + // Test user2 has reader role for resource2 + testGetRoles(t, e, []string{"reader"}, "user2", "resource2") + testGetRoles(t, e, []string{}, "user2", "resource1") + + // Test user3 has writer role for resource1 + testGetRoles(t, e, []string{"writer"}, "user3", "resource1") + testGetRoles(t, e, []string{}, "user3", "resource2") + + // Test enforcement - user1 can read resource1 but not resource2 + testEnforce(t, e, "user1", "resource1", "read", true) + testEnforce(t, e, "user1", "resource2", "read", false) + testEnforce(t, e, "user1", "resource1", "write", false) + + // Test enforcement - user2 can read resource2 but not resource1 + testEnforce(t, e, "user2", "resource1", "read", false) + testEnforce(t, e, "user2", "resource2", "read", true) + testEnforce(t, e, "user2", "resource2", "write", false) + + // Test enforcement - user3 can write to resource1 + testEnforce(t, e, "user3", "resource1", "write", true) + testEnforce(t, e, "user3", "resource2", "write", false) + testEnforce(t, e, "user3", "resource1", "read", false) + + // Test GetUsersForRole with resource scope + testGetUsers(t, e, []string{"user1"}, "reader", "resource1") + testGetUsers(t, e, []string{"user2"}, "reader", "resource2") + testGetUsers(t, e, []string{"user3"}, "writer", "resource1") + + // Test AddRoleForUser with resource scope + _, _ = e.AddRoleForUser("user4", "reader", "resource1") + testGetRoles(t, e, []string{"reader"}, "user4", "resource1") + testEnforce(t, e, "user4", "resource1", "read", true) + testEnforce(t, e, "user4", "resource2", "read", false) + + // Test DeleteRoleForUser with resource scope + _, _ = e.DeleteRoleForUser("user4", "reader", "resource1") + testGetRoles(t, e, []string{}, "user4", "resource1") + testEnforce(t, e, "user4", "resource1", "read", false) +} + +func TestRBACWithResourceScopeAndTenant(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_resource_scope_tenant_model.conf", "examples/rbac_with_resource_scope_tenant_policy.csv") + + // Test user1 has reader role for tenant1::resource1 + testGetRoles(t, e, []string{"reader"}, "user1", "tenant1::resource1") + testGetRoles(t, e, []string{}, "user1", "tenant1::resource2") + + // Test user2 has reader role for tenant1::resource2 + testGetRoles(t, e, []string{"reader"}, "user2", "tenant1::resource2") + testGetRoles(t, e, []string{}, "user2", "tenant1::resource1") + + // Test user3 has reader role for tenant2::resource1 + testGetRoles(t, e, []string{"reader"}, "user3", "tenant2::resource1") + testGetRoles(t, e, []string{}, "user3", "tenant1::resource1") + + // Test user4 has writer role for tenant1::resource1 + testGetRoles(t, e, []string{"writer"}, "user4", "tenant1::resource1") + + // Test enforcement - user1 can read resource1 in tenant1 only + testEnforceWithTenant(t, e, "user1", "tenant1", "resource1", "read", true) + testEnforceWithTenant(t, e, "user1", "tenant1", "resource2", "read", false) + testEnforceWithTenant(t, e, "user1", "tenant2", "resource1", "read", false) + testEnforceWithTenant(t, e, "user1", "tenant1", "resource1", "write", false) + + // Test enforcement - user2 can read resource2 in tenant1 only + testEnforceWithTenant(t, e, "user2", "tenant1", "resource1", "read", false) + testEnforceWithTenant(t, e, "user2", "tenant1", "resource2", "read", true) + testEnforceWithTenant(t, e, "user2", "tenant2", "resource2", "read", false) + + // Test enforcement - user3 can read resource1 in tenant2 only + testEnforceWithTenant(t, e, "user3", "tenant1", "resource1", "read", false) + testEnforceWithTenant(t, e, "user3", "tenant2", "resource1", "read", true) + + // Test enforcement - user4 can write to resource1 in tenant1 + testEnforceWithTenant(t, e, "user4", "tenant1", "resource1", "write", true) + testEnforceWithTenant(t, e, "user4", "tenant1", "resource2", "write", false) + testEnforceWithTenant(t, e, "user4", "tenant2", "resource1", "write", false) + + // Test GetUsersForRole with tenant::resource scope + testGetUsers(t, e, []string{"user1"}, "reader", "tenant1::resource1") + testGetUsers(t, e, []string{"user2"}, "reader", "tenant1::resource2") + testGetUsers(t, e, []string{"user3"}, "reader", "tenant2::resource1") + + // Test AddRoleForUser with tenant::resource scope + _, _ = e.AddRoleForUser("user5", "reader", "tenant1::resource1") + testGetRoles(t, e, []string{"reader"}, "user5", "tenant1::resource1") + testEnforceWithTenant(t, e, "user5", "tenant1", "resource1", "read", true) + testEnforceWithTenant(t, e, "user5", "tenant1", "resource2", "read", false) + + // Test DeleteRoleForUser with tenant::resource scope + _, _ = e.DeleteRoleForUser("user5", "reader", "tenant1::resource1") + testGetRoles(t, e, []string{}, "user5", "tenant1::resource1") + testEnforceWithTenant(t, e, "user5", "tenant1", "resource1", "read", false) +} + +func TestRBACWithResourceScopeMultitenancy(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_resource_scope_tenant_model.conf", "examples/rbac_with_resource_scope_tenant_policy.csv") + + // Verify isolation: user1 and user3 both have reader role, but for different tenant::resource combinations + // user1 -> reader -> tenant1::resource1 + // user3 -> reader -> tenant2::resource1 + + // user1 should only access tenant1::resource1 + testEnforceWithTenant(t, e, "user1", "tenant1", "resource1", "read", true) + testEnforceWithTenant(t, e, "user1", "tenant2", "resource1", "read", false) + + // user3 should only access tenant2::resource1 + testEnforceWithTenant(t, e, "user3", "tenant2", "resource1", "read", true) + testEnforceWithTenant(t, e, "user3", "tenant1", "resource1", "read", false) + + // Verify that adding a role to one user doesn't affect another user with the same role in a different scope + _, _ = e.AddRoleForUser("user1", "writer", "tenant1::resource1") + testEnforceWithTenant(t, e, "user1", "tenant1", "resource1", "write", true) + testEnforceWithTenant(t, e, "user3", "tenant2", "resource1", "write", false) // user3 should not be affected + + // Clean up + _, _ = e.DeleteRoleForUser("user1", "writer", "tenant1::resource1") + testEnforceWithTenant(t, e, "user1", "tenant1", "resource1", "write", false) +}