From a30d77b92d04a1ba1c4b4e1a6c5cc8a93bcffe9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:14:11 +0000 Subject: [PATCH 1/4] Initial plan From d510de00e2190773c8486cfd95a7d9659c9a2e3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:18:54 +0000 Subject: [PATCH 2/4] Add RBAC preset model and helper functions Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- preset/preset.go | 57 +++++++++++ preset/preset_test.go | 161 +++++++++++++++++++++++++++++++ preset/rbac/rbac.go | 33 +++++++ preset/rbac/rbac_test.go | 200 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 451 insertions(+) create mode 100644 preset/preset.go create mode 100644 preset/preset_test.go create mode 100644 preset/rbac/rbac.go create mode 100644 preset/rbac/rbac_test.go diff --git a/preset/preset.go b/preset/preset.go new file mode 100644 index 00000000..8749d9a9 --- /dev/null +++ b/preset/preset.go @@ -0,0 +1,57 @@ +// 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 preset + +import ( + "github.com/casbin/casbin/v3/model" +) + +// RBAC returns a standard RBAC model. +// This is equivalent to a model.conf with: +// +// [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 == p.obj && r.act == p.act +func RBAC() model.Model { + text := ` +[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 == p.obj && r.act == p.act +` + m, _ := model.NewModelFromString(text) + return m +} diff --git a/preset/preset_test.go b/preset/preset_test.go new file mode 100644 index 00000000..07368287 --- /dev/null +++ b/preset/preset_test.go @@ -0,0 +1,161 @@ +// 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 preset + +import ( + "testing" + + "github.com/casbin/casbin/v3" + fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" +) + +func TestRBACPreset(t *testing.T) { + m := RBAC() + if m == nil { + t.Fatal("RBAC() returned nil model") + } + + // Create enforcer with preset model and a file adapter + adapter := fileadapter.NewAdapter("../examples/rbac_policy.csv") + e, err := casbin.NewEnforcer(m, adapter) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Test basic enforcement + ok, err := e.Enforce("alice", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("alice should be able to read data1") + } + + ok, err = e.Enforce("alice", "data2", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("alice should be able to read data2 (via data2_admin role)") + } + + ok, err = e.Enforce("bob", "data2", "write") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("bob should be able to write data2") + } + + ok, err = e.Enforce("bob", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if ok { + t.Error("bob should not be able to read data1") + } +} + +func TestRBACPresetWithoutPolicies(t *testing.T) { + m := RBAC() + e, err := casbin.NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Initially, no permissions should be granted + ok, err := e.Enforce("alice", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if ok { + t.Error("alice should not have any permissions initially") + } + + // Add a policy + _, err = e.AddPolicy("alice", "data1", "read") + if err != nil { + t.Fatalf("AddPolicy failed: %v", err) + } + + // Now alice should have permission + ok, err = e.Enforce("alice", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("alice should be able to read data1 after adding policy") + } + + // Add role + _, err = e.AddRoleForUser("bob", "alice") + if err != nil { + t.Fatalf("AddRoleForUser failed: %v", err) + } + + // Bob should inherit alice's permission + ok, err = e.Enforce("bob", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("bob should be able to read data1 (via alice role)") + } +} + +func TestRBACPresetModelEquivalence(t *testing.T) { + // Test that the preset model is equivalent to the standard rbac_model.conf + presetModel := RBAC() + presetAdapter := fileadapter.NewAdapter("../examples/rbac_policy.csv") + presetEnforcer, err := casbin.NewEnforcer(presetModel, presetAdapter) + if err != nil { + t.Fatalf("Failed to create preset enforcer: %v", err) + } + + fileEnforcer, err := casbin.NewEnforcer("../examples/rbac_model.conf", "../examples/rbac_policy.csv") + if err != nil { + t.Fatalf("Failed to create file enforcer: %v", err) + } + + // Test cases that should behave the same + testCases := []struct { + sub string + obj string + act string + }{ + {"alice", "data1", "read"}, + {"alice", "data2", "read"}, + {"alice", "data2", "write"}, + {"bob", "data2", "write"}, + {"bob", "data1", "read"}, + {"data2_admin", "data2", "read"}, + } + + for _, tc := range testCases { + presetOk, err := presetEnforcer.Enforce(tc.sub, tc.obj, tc.act) + if err != nil { + t.Fatalf("Preset enforce failed for %v: %v", tc, err) + } + + fileOk, err := fileEnforcer.Enforce(tc.sub, tc.obj, tc.act) + if err != nil { + t.Fatalf("File enforce failed for %v: %v", tc, err) + } + + if presetOk != fileOk { + t.Errorf("Mismatch for %v: preset=%v, file=%v", tc, presetOk, fileOk) + } + } +} diff --git a/preset/rbac/rbac.go b/preset/rbac/rbac.go new file mode 100644 index 00000000..3d61f5fd --- /dev/null +++ b/preset/rbac/rbac.go @@ -0,0 +1,33 @@ +// 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 rbac + +import ( + "github.com/casbin/casbin/v3" +) + +// AssignRole assigns a role to a user. +// This is a convenience wrapper around AddRoleForUser. +// Returns false if the user already has the role (aka not affected). +func AssignRole(e *casbin.Enforcer, user string, role string) (bool, error) { + return e.AddRoleForUser(user, role) +} + +// Grant grants a permission to a subject (user or role). +// This is a convenience wrapper around AddPolicy. +// Returns false if the permission already exists (aka not affected). +func Grant(e *casbin.Enforcer, subject string, object string, action string) (bool, error) { + return e.AddPolicy(subject, object, action) +} diff --git a/preset/rbac/rbac_test.go b/preset/rbac/rbac_test.go new file mode 100644 index 00000000..c916eb96 --- /dev/null +++ b/preset/rbac/rbac_test.go @@ -0,0 +1,200 @@ +// 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 rbac + +import ( + "testing" + + "github.com/casbin/casbin/v3" + "github.com/casbin/casbin/v3/preset" +) + +func TestAssignRole(t *testing.T) { + m := preset.RBAC() + e, err := casbin.NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Assign a role to alice + ok, err := AssignRole(e, "alice", "admin") + if err != nil { + t.Fatalf("AssignRole failed: %v", err) + } + if !ok { + t.Error("AssignRole should return true when adding a new role") + } + + // Verify the role was assigned + roles, err := e.GetRolesForUser("alice") + if err != nil { + t.Fatalf("GetRolesForUser failed: %v", err) + } + if len(roles) != 1 || roles[0] != "admin" { + t.Errorf("Expected alice to have admin role, got %v", roles) + } + + // Try to assign the same role again + ok, err = AssignRole(e, "alice", "admin") + if err != nil { + t.Fatalf("AssignRole failed: %v", err) + } + if ok { + t.Error("AssignRole should return false when role already exists") + } +} + +func TestGrant(t *testing.T) { + m := preset.RBAC() + e, err := casbin.NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Grant permission to admin role + ok, err := Grant(e, "admin", "data1", "read") + if err != nil { + t.Fatalf("Grant failed: %v", err) + } + if !ok { + t.Error("Grant should return true when adding a new permission") + } + + // Verify the permission was granted + ok, err = e.Enforce("admin", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("admin should be able to read data1 after grant") + } + + // Try to grant the same permission again + ok, err = Grant(e, "admin", "data1", "read") + if err != nil { + t.Fatalf("Grant failed: %v", err) + } + if ok { + t.Error("Grant should return false when permission already exists") + } +} + +func TestIntegrationAssignRoleAndGrant(t *testing.T) { + m := preset.RBAC() + e, err := casbin.NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Grant permission to admin role + _, err = Grant(e, "admin", "data1", "read") + if err != nil { + t.Fatalf("Grant failed: %v", err) + } + + // Assign admin role to alice + _, err = AssignRole(e, "alice", "admin") + if err != nil { + t.Fatalf("AssignRole failed: %v", err) + } + + // Alice should be able to read data1 through the admin role + ok, err := e.Enforce("alice", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("alice should be able to read data1 (via admin role)") + } + + // Alice should not have direct permission + hasPolicy, err := e.HasPolicy("alice", "data1", "read") + if err != nil { + t.Fatalf("HasPolicy failed: %v", err) + } + if hasPolicy { + t.Error("alice should not have direct policy") + } + + // But she should have it through the role + hasRole, err := e.HasRoleForUser("alice", "admin") + if err != nil { + t.Fatalf("HasRoleForUser failed: %v", err) + } + if !hasRole { + t.Error("alice should have admin role") + } +} + +func TestGrantToUserDirectly(t *testing.T) { + m := preset.RBAC() + e, err := casbin.NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Grant permission directly to a user (not a role) + _, err = Grant(e, "alice", "data1", "read") + if err != nil { + t.Fatalf("Grant failed: %v", err) + } + + // Alice should be able to read data1 + ok, err := e.Enforce("alice", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("alice should be able to read data1 after direct grant") + } + + // Alice should not have any roles + roles, err := e.GetRolesForUser("alice") + if err != nil { + t.Fatalf("GetRolesForUser failed: %v", err) + } + if len(roles) != 0 { + t.Errorf("alice should not have any roles, got %v", roles) + } +} + +func TestExampleFromIssue(t *testing.T) { + // This test demonstrates the example usage from the issue description + m := preset.RBAC() + e, err := casbin.NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Use the optional helpers + _, err = AssignRole(e, "alice", "admin") + if err != nil { + t.Fatalf("AssignRole failed: %v", err) + } + + _, err = Grant(e, "admin", "data1", "read") + if err != nil { + t.Fatalf("Grant failed: %v", err) + } + + // Test enforcement + ok, err := e.Enforce("alice", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("alice should be able to read data1 (as shown in the issue example)") + } +} From 5b29afc97b17a2a94a18032b0c7763aa52411974 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:22:02 +0000 Subject: [PATCH 3/4] Fix import statements for linter compatibility Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- preset/preset_test.go | 2 +- preset/rbac/rbac.go | 2 +- preset/rbac/rbac_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/preset/preset_test.go b/preset/preset_test.go index 07368287..21885db2 100644 --- a/preset/preset_test.go +++ b/preset/preset_test.go @@ -17,7 +17,7 @@ package preset import ( "testing" - "github.com/casbin/casbin/v3" + casbin "github.com/casbin/casbin/v3" fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" ) diff --git a/preset/rbac/rbac.go b/preset/rbac/rbac.go index 3d61f5fd..f650c91f 100644 --- a/preset/rbac/rbac.go +++ b/preset/rbac/rbac.go @@ -15,7 +15,7 @@ package rbac import ( - "github.com/casbin/casbin/v3" + casbin "github.com/casbin/casbin/v3" ) // AssignRole assigns a role to a user. diff --git a/preset/rbac/rbac_test.go b/preset/rbac/rbac_test.go index c916eb96..44d581a5 100644 --- a/preset/rbac/rbac_test.go +++ b/preset/rbac/rbac_test.go @@ -17,7 +17,7 @@ package rbac import ( "testing" - "github.com/casbin/casbin/v3" + casbin "github.com/casbin/casbin/v3" "github.com/casbin/casbin/v3/preset" ) From fd5eb8a7b421fa080fa092a871fecc0ce948ef51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:24:06 +0000 Subject: [PATCH 4/4] Address code review feedback: extract model text to constant and improve error handling Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- preset/preset.go | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/preset/preset.go b/preset/preset.go index 8749d9a9..21cb319f 100644 --- a/preset/preset.go +++ b/preset/preset.go @@ -18,6 +18,23 @@ import ( "github.com/casbin/casbin/v3/model" ) +const rbacModelText = ` +[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 == p.obj && r.act == p.act +` + // RBAC returns a standard RBAC model. // This is equivalent to a model.conf with: // @@ -36,22 +53,11 @@ import ( // [matchers] // m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act func RBAC() model.Model { - text := ` -[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 == p.obj && r.act == p.act -` - m, _ := model.NewModelFromString(text) + // Model creation from this hardcoded valid text should never fail. + // If it does, it indicates a programming error in the preset definition. + m, err := model.NewModelFromString(rbacModelText) + if err != nil { + panic("preset: failed to create RBAC model: " + err.Error()) + } return m }