Skip to content

Commit f64a3c0

Browse files
committed
feat: quote CSV fields containing commas in SavePolicy
1 parent 52c2b03 commit f64a3c0

4 files changed

Lines changed: 107 additions & 10 deletions

File tree

persist/adapter.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package persist
1616

1717
import (
18+
"bytes"
1819
"encoding/csv"
1920
"strings"
2021

@@ -40,6 +41,23 @@ func LoadPolicyLine(line string, m model.Model) error {
4041
return LoadPolicyArray(tokens, m)
4142
}
4243

44+
// PolicyLineToCsv serializes a policy type and its rule fields into a CSV-safe line.
45+
// Fields containing commas are properly quoted so the line can be round-tripped
46+
// through LoadPolicyLine without corruption.
47+
func PolicyLineToCsv(ptype string, rule []string) (string, error) {
48+
record := append([]string{ptype}, rule...)
49+
var buf bytes.Buffer
50+
w := csv.NewWriter(&buf)
51+
if err := w.Write(record); err != nil {
52+
return "", err
53+
}
54+
w.Flush()
55+
if err := w.Error(); err != nil {
56+
return "", err
57+
}
58+
return strings.TrimRight(buf.String(), "\r\n"), nil
59+
}
60+
4361
// LoadPolicyArray loads a policy rule to model.
4462
func LoadPolicyArray(rule []string, m model.Model) error {
4563
key := rule[0]

persist/file-adapter/adapter.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323

2424
"github.com/casbin/casbin/v3/model"
2525
"github.com/casbin/casbin/v3/persist"
26-
"github.com/casbin/casbin/v3/util"
2726
)
2827

2928
// Adapter is the file adapter for Casbin.
@@ -68,16 +67,22 @@ func (a *Adapter) SavePolicy(model model.Model) error {
6867

6968
for ptype, ast := range model["p"] {
7069
for _, rule := range ast.Policy {
71-
tmp.WriteString(ptype + ", ")
72-
tmp.WriteString(util.ArrayToString(rule))
70+
line, err := persist.PolicyLineToCsv(ptype, rule)
71+
if err != nil {
72+
return err
73+
}
74+
tmp.WriteString(line)
7375
tmp.WriteString("\n")
7476
}
7577
}
7678

7779
for ptype, ast := range model["g"] {
7880
for _, rule := range ast.Policy {
79-
tmp.WriteString(ptype + ", ")
80-
tmp.WriteString(util.ArrayToString(rule))
81+
line, err := persist.PolicyLineToCsv(ptype, rule)
82+
if err != nil {
83+
return err
84+
}
85+
tmp.WriteString(line)
8186
tmp.WriteString("\n")
8287
}
8388
}

persist/string-adapter/adapter.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121

2222
"github.com/casbin/casbin/v3/model"
2323
"github.com/casbin/casbin/v3/persist"
24-
"github.com/casbin/casbin/v3/util"
2524
)
2625

2726
// Adapter is the string adapter for Casbin.
@@ -58,16 +57,22 @@ func (a *Adapter) SavePolicy(model model.Model) error {
5857
var tmp bytes.Buffer
5958
for ptype, ast := range model["p"] {
6059
for _, rule := range ast.Policy {
61-
tmp.WriteString(ptype + ", ")
62-
tmp.WriteString(util.ArrayToString(rule))
60+
line, err := persist.PolicyLineToCsv(ptype, rule)
61+
if err != nil {
62+
return err
63+
}
64+
tmp.WriteString(line)
6365
tmp.WriteString("\n")
6466
}
6567
}
6668

6769
for ptype, ast := range model["g"] {
6870
for _, rule := range ast.Policy {
69-
tmp.WriteString(ptype + ", ")
70-
tmp.WriteString(util.ArrayToString(rule))
71+
line, err := persist.PolicyLineToCsv(ptype, rule)
72+
if err != nil {
73+
return err
74+
}
75+
tmp.WriteString(line)
7176
tmp.WriteString("\n")
7277
}
7378
}

persist/string-adapter/adapter_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
package stringadapter
1616

1717
import (
18+
"strings"
1819
"testing"
1920

2021
"github.com/casbin/casbin/v3"
2122
"github.com/casbin/casbin/v3/model"
23+
"github.com/casbin/casbin/v3/persist"
2224
)
2325

2426
func Test_KeyMatchRbac(t *testing.T) {
@@ -61,6 +63,73 @@ g, alice, data_group_admin
6163
}
6264
}
6365

66+
// Test_SavePolicyRoundTripWithCommas verifies that a policy rule whose fields contain
67+
// commas (e.g. ABAC condition expressions) survives a SavePolicy → LoadPolicy round trip
68+
// without corruption. See https://github.com/apache/casbin/issues/1733.
69+
func Test_SavePolicyRoundTripWithCommas(t *testing.T) {
70+
conf := `
71+
[request_definition]
72+
r = sub, obj, act, cond
73+
74+
[policy_definition]
75+
p = sub, obj, act, cond
76+
77+
[policy_effect]
78+
e = some(where (p.eft == allow))
79+
80+
[matchers]
81+
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
82+
`
83+
// cond field intentionally contains commas (as in ABAC expressions).
84+
condWithComma := `r.attrs in ('val1','val2')`
85+
line := "p, alice, data1, read, " + `"` + condWithComma + `"`
86+
87+
a := NewAdapter(line)
88+
m := model.NewModel()
89+
if err := m.LoadModelFromText(conf); err != nil {
90+
t.Fatalf("load model: %v", err)
91+
}
92+
e, err := casbin.NewEnforcer(m, a)
93+
if err != nil {
94+
t.Fatalf("new enforcer: %v", err)
95+
}
96+
97+
// SavePolicy serialises in-memory rules back to the adapter string.
98+
err = e.SavePolicy()
99+
if err != nil {
100+
t.Fatalf("SavePolicy: %v", err)
101+
}
102+
103+
// The saved line must be a properly quoted CSV so that re-loading produces
104+
// exactly one rule with the comma-containing cond field intact.
105+
saved := a.Line
106+
reloaded := model.NewModel()
107+
err = reloaded.LoadModelFromText(conf)
108+
if err != nil {
109+
t.Fatalf("reload model: %v", err)
110+
}
111+
for _, l := range strings.Split(saved, "\n") {
112+
if l == "" {
113+
continue
114+
}
115+
err = persist.LoadPolicyLine(l, reloaded)
116+
if err != nil {
117+
t.Fatalf("LoadPolicyLine on saved line %q: %v", l, err)
118+
}
119+
}
120+
121+
rules, err := reloaded.GetPolicy("p", "p")
122+
if err != nil {
123+
t.Fatalf("GetPolicy: %v", err)
124+
}
125+
if len(rules) != 1 {
126+
t.Fatalf("expected 1 rule after round-trip, got %d (saved: %q)", len(rules), saved)
127+
}
128+
if rules[0][3] != condWithComma {
129+
t.Errorf("cond field corrupted after round-trip: got %q, want %q", rules[0][3], condWithComma)
130+
}
131+
}
132+
64133
func Test_StringRbac(t *testing.T) {
65134
conf := `
66135
[request_definition]

0 commit comments

Comments
 (0)