From dfe0ca533c7f889b23279cc7ac367ef47452d0e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:09:19 +0000 Subject: [PATCH 1/3] Initial plan From d1008a958f8702bcdb92967c736d8d5ef3282a3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:14:28 +0000 Subject: [PATCH 2/3] Add programmatic model builder with RBAC and ABAC tests Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- builder_test.go | 74 +++++++++++++++++++++++++ model/builder.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 builder_test.go create mode 100644 model/builder.go diff --git a/builder_test.go b/builder_test.go new file mode 100644 index 00000000..798f0fca --- /dev/null +++ b/builder_test.go @@ -0,0 +1,74 @@ +// Copyright 2026 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" + + "github.com/casbin/casbin/v3/model" + fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" +) + +// TestBuilderRBAC tests the builder version of the RBAC model. +func TestBuilderRBAC(t *testing.T) { + // Build the RBAC model programmatically + m, _ := model.New(). + Request("sub", "obj", "act"). + Policy("sub", "obj", "act"). + Role("_", "_"). + Effect("some(where (p.eft == allow))"). + Matcher("g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act"). + Build() + + // Use the same policy file as the original test + a := fileadapter.NewAdapter("examples/rbac_policy.csv") + e, _ := NewEnforcer(m, a) + + // Same test cases as TestRBACModel + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +// TestBuilderABAC tests the builder version of the ABAC model. +func TestBuilderABAC(t *testing.T) { + // Build the ABAC model programmatically + m, _ := model.New(). + Request("sub", "obj", "act"). + Policy("sub", "obj", "act"). + Effect("some(where (p.eft == allow))"). + Matcher("r.sub == r.obj.Owner"). + Build() + + e, _ := NewEnforcer(m) + + data1 := newTestResource("data1", "alice") + data2 := newTestResource("data2", "bob") + + // Same test cases as TestABACModel + testEnforce(t, e, "alice", data1, "read", true) + testEnforce(t, e, "alice", data1, "write", true) + testEnforce(t, e, "alice", data2, "read", false) + testEnforce(t, e, "alice", data2, "write", false) + testEnforce(t, e, "bob", data1, "read", false) + testEnforce(t, e, "bob", data1, "write", false) + testEnforce(t, e, "bob", data2, "read", true) + testEnforce(t, e, "bob", data2, "write", true) +} diff --git a/model/builder.go b/model/builder.go new file mode 100644 index 00000000..558210f5 --- /dev/null +++ b/model/builder.go @@ -0,0 +1,140 @@ +// Copyright 2026 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 model + +import ( + "strings" +) + +// Builder provides a programmatic way to construct Casbin models. +type Builder struct { + requestDef string + policyDef string + roleDef string + effectDef string + matcherDef string +} + +// New creates a new model builder. +func New() *Builder { + return &Builder{} +} + +// Request sets the request definition with the provided fields. +// Example: Request("sub", "obj", "act") +func (b *Builder) Request(fields ...string) *Builder { + b.requestDef = strings.Join(fields, ", ") + return b +} + +// Policy sets the policy definition with the provided fields. +// Example: Policy("sub", "obj", "act") +func (b *Builder) Policy(fields ...string) *Builder { + b.policyDef = strings.Join(fields, ", ") + return b +} + +// Role sets the role definition. +// Example: Role("_", "_") for basic RBAC +func (b *Builder) Role(fields ...string) *Builder { + b.roleDef = strings.Join(fields, ", ") + return b +} + +// Effect sets the policy effect. +// Example: Effect("some(where (p.eft == allow))") +func (b *Builder) Effect(effect string) *Builder { + b.effectDef = effect + return b +} + +// Matcher sets the matcher expression. +// Example: Matcher("r.sub == p.sub && r.obj == p.obj && r.act == p.act") +func (b *Builder) Matcher(matcher string) *Builder { + b.matcherDef = matcher + return b +} + +// Build creates a Model from the builder configuration. +func (b *Builder) Build() (Model, error) { + m := NewModel() + + if b.requestDef != "" { + m.AddDef("r", "r", b.requestDef) + } + + if b.policyDef != "" { + m.AddDef("p", "p", b.policyDef) + } + + if b.roleDef != "" { + m.AddDef("g", "g", b.roleDef) + } + + if b.effectDef != "" { + m.AddDef("e", "e", b.effectDef) + } + + if b.matcherDef != "" { + m.AddDef("m", "m", b.matcherDef) + } + + return m, nil +} + +// ToString returns the model as a string in CONF format. +func (b *Builder) ToString() string { + var sb strings.Builder + + sb.WriteString("[request_definition]\n") + if b.requestDef != "" { + sb.WriteString("r = ") + sb.WriteString(b.requestDef) + sb.WriteString("\n") + } + sb.WriteString("\n") + + sb.WriteString("[policy_definition]\n") + if b.policyDef != "" { + sb.WriteString("p = ") + sb.WriteString(b.policyDef) + sb.WriteString("\n") + } + sb.WriteString("\n") + + if b.roleDef != "" { + sb.WriteString("[role_definition]\n") + sb.WriteString("g = ") + sb.WriteString(b.roleDef) + sb.WriteString("\n\n") + } + + sb.WriteString("[policy_effect]\n") + if b.effectDef != "" { + sb.WriteString("e = ") + sb.WriteString(b.effectDef) + sb.WriteString("\n") + } + sb.WriteString("\n") + + sb.WriteString("[matchers]\n") + if b.matcherDef != "" { + sb.WriteString("m = ") + sb.WriteString(b.matcherDef) + sb.WriteString("\n") + } + + return sb.String() +} From 53b6cfc3417120f2d2f91b6f2af0a3b9a21b2d91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:17:16 +0000 Subject: [PATCH 3/3] Add documentation to Builder and format code Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- model/builder.go | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/model/builder.go b/model/builder.go index 558210f5..6ee8c0b3 100644 --- a/model/builder.go +++ b/model/builder.go @@ -19,6 +19,19 @@ import ( ) // Builder provides a programmatic way to construct Casbin models. +// It allows creating models without needing a model.conf file. +// +// Example usage: +// +// m, _ := model.New(). +// Request("sub", "obj", "act"). +// Policy("sub", "obj", "act"). +// Role("_", "_"). +// Effect("some(where (p.eft == allow))"). +// Matcher("g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act"). +// Build() +// +// The resulting model is equivalent to one loaded from a model.conf file. type Builder struct { requestDef string policyDef string @@ -70,34 +83,34 @@ func (b *Builder) Matcher(matcher string) *Builder { // Build creates a Model from the builder configuration. func (b *Builder) Build() (Model, error) { m := NewModel() - + if b.requestDef != "" { m.AddDef("r", "r", b.requestDef) } - + if b.policyDef != "" { m.AddDef("p", "p", b.policyDef) } - + if b.roleDef != "" { m.AddDef("g", "g", b.roleDef) } - + if b.effectDef != "" { m.AddDef("e", "e", b.effectDef) } - + if b.matcherDef != "" { m.AddDef("m", "m", b.matcherDef) } - + return m, nil } // ToString returns the model as a string in CONF format. func (b *Builder) ToString() string { var sb strings.Builder - + sb.WriteString("[request_definition]\n") if b.requestDef != "" { sb.WriteString("r = ") @@ -105,7 +118,7 @@ func (b *Builder) ToString() string { sb.WriteString("\n") } sb.WriteString("\n") - + sb.WriteString("[policy_definition]\n") if b.policyDef != "" { sb.WriteString("p = ") @@ -113,14 +126,14 @@ func (b *Builder) ToString() string { sb.WriteString("\n") } sb.WriteString("\n") - + if b.roleDef != "" { sb.WriteString("[role_definition]\n") sb.WriteString("g = ") sb.WriteString(b.roleDef) sb.WriteString("\n\n") } - + sb.WriteString("[policy_effect]\n") if b.effectDef != "" { sb.WriteString("e = ") @@ -128,13 +141,13 @@ func (b *Builder) ToString() string { sb.WriteString("\n") } sb.WriteString("\n") - + sb.WriteString("[matchers]\n") if b.matcherDef != "" { sb.WriteString("m = ") sb.WriteString(b.matcherDef) sb.WriteString("\n") } - + return sb.String() }