diff --git a/preset/preset.go b/preset/preset.go new file mode 100644 index 00000000..21cb319f --- /dev/null +++ b/preset/preset.go @@ -0,0 +1,63 @@ +// 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" +) + +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: +// +// [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 { + // 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 +} diff --git a/preset/preset_test.go b/preset/preset_test.go new file mode 100644 index 00000000..21885db2 --- /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" + + casbin "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..f650c91f --- /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 ( + casbin "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..44d581a5 --- /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" + + casbin "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)") + } +}