Skip to content

Commit 0dd2ea7

Browse files
committed
fix: keep ", " separator in SavePolicy output
1 parent 65cb985 commit 0dd2ea7

2 files changed

Lines changed: 116 additions & 7 deletions

File tree

persist/adapter.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,31 @@ func LoadPolicyLine(line string, m model.Model) error {
4545
}
4646

4747
// PolicyLineToCsv serializes a policy type and its rule fields into a CSV-safe line.
48+
// Fields are separated by ", " to match the conventional layout of Casbin policy
49+
// files, which LoadPolicyLine accepts because its reader trims leading spaces.
4850
// Fields containing commas are properly quoted so the line can be round-tripped
4951
// through LoadPolicyLine without corruption.
5052
func PolicyLineToCsv(ptype string, rule []string) (string, error) {
5153
record := append([]string{ptype}, rule...)
54+
55+
// Each field is written as its own single-field record so that the standard
56+
// library decides on quoting, then the fields are joined with ", ".
5257
var buf bytes.Buffer
5358
w := csv.NewWriter(&buf)
54-
if err := w.Write(record); err != nil {
55-
return "", err
56-
}
57-
w.Flush()
58-
if err := w.Error(); err != nil {
59-
return "", err
59+
fields := make([]string, 0, len(record))
60+
for _, field := range record {
61+
buf.Reset()
62+
if err := w.Write([]string{field}); err != nil {
63+
return "", err
64+
}
65+
w.Flush()
66+
if err := w.Error(); err != nil {
67+
return "", err
68+
}
69+
fields = append(fields, strings.TrimRight(buf.String(), "\r\n"))
6070
}
61-
return strings.TrimRight(buf.String(), "\r\n"), nil
71+
72+
return strings.Join(fields, ", "), nil
6273
}
6374

6475
// LoadPolicyArray loads a policy rule to model.

persist/persist_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package persist_test
1919

2020
import (
21+
"reflect"
2122
"testing"
2223

2324
"github.com/casbin/casbin/v3"
@@ -54,3 +55,100 @@ func TestDuplicateRuleInAdapter(t *testing.T) {
5455

5556
testRuleCount(t, e.GetModel(), 1, "p", "p", "LoadPolicyArray")
5657
}
58+
59+
func TestPolicyLineToCsv(t *testing.T) {
60+
tests := []struct {
61+
name string
62+
ptype string
63+
rule []string
64+
expected string
65+
}{
66+
{
67+
// Fields are separated by ", ", the layout used by the policy files
68+
// under examples/, so that SavePolicy does not reformat them.
69+
name: "plain fields keep the space after the comma",
70+
ptype: "p",
71+
rule: []string{"alice", "data1", "read"},
72+
expected: "p, alice, data1, read",
73+
},
74+
{
75+
name: "field containing a comma is quoted",
76+
ptype: "p",
77+
rule: []string{"alice", "data1", "read", "r.attrs in ('val1','val2')"},
78+
expected: `p, alice, data1, read, "r.attrs in ('val1','val2')"`,
79+
},
80+
{
81+
name: "field containing a quote is escaped",
82+
ptype: "p",
83+
rule: []string{"alice", `say "hi"`, "read"},
84+
expected: `p, alice, "say ""hi""", read`,
85+
},
86+
{
87+
// A leading space must stay quoted: LoadPolicyLine trims leading
88+
// spaces on unquoted fields.
89+
name: "field with a leading space is quoted",
90+
ptype: "g",
91+
rule: []string{" alice", "admin"},
92+
expected: `g, " alice", admin`,
93+
},
94+
{
95+
name: "empty field",
96+
ptype: "p",
97+
rule: []string{"alice", "", "read"},
98+
expected: "p, alice, , read",
99+
},
100+
}
101+
102+
for _, tt := range tests {
103+
t.Run(tt.name, func(t *testing.T) {
104+
line, err := persist.PolicyLineToCsv(tt.ptype, tt.rule)
105+
if err != nil {
106+
t.Fatalf("PolicyLineToCsv: %v", err)
107+
}
108+
if line != tt.expected {
109+
t.Errorf("line: %q, expected %q", line, tt.expected)
110+
}
111+
})
112+
}
113+
}
114+
115+
func TestPolicyLineToCsvRoundTrip(t *testing.T) {
116+
conf := `
117+
[request_definition]
118+
r = sub, obj, act, cond
119+
120+
[policy_definition]
121+
p = sub, obj, act, cond
122+
123+
[policy_effect]
124+
e = some(where (p.eft == allow))
125+
126+
[matchers]
127+
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
128+
`
129+
rule := []string{" alice", `say "hi"`, "read", "r.attrs in ('val1','val2')"}
130+
131+
line, err := persist.PolicyLineToCsv("p", rule)
132+
if err != nil {
133+
t.Fatalf("PolicyLineToCsv: %v", err)
134+
}
135+
136+
m := model.NewModel()
137+
if err := m.LoadModelFromText(conf); err != nil {
138+
t.Fatalf("load model: %v", err)
139+
}
140+
if err := persist.LoadPolicyLine(line, m); err != nil {
141+
t.Fatalf("LoadPolicyLine on %q: %v", line, err)
142+
}
143+
144+
rules, err := m.GetPolicy("p", "p")
145+
if err != nil {
146+
t.Fatalf("GetPolicy: %v", err)
147+
}
148+
if len(rules) != 1 {
149+
t.Fatalf("rule count: %d, expected 1 (line: %q)", len(rules), line)
150+
}
151+
if !reflect.DeepEqual(rules[0], rule) {
152+
t.Errorf("rule after round-trip: %q, expected %q (line: %q)", rules[0], rule, line)
153+
}
154+
}

0 commit comments

Comments
 (0)