You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@casbin.apache.org by hs...@apache.org on 2026/06/16 13:46:43 UTC

(casbin) 02/02: feat: quote CSV fields containing commas in SavePolicy

This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin.git

commit a76c74043429d713832b70adad67b71f38883544
Author: Yang Luo <hs...@gmail.com>
AuthorDate: Tue Jun 16 21:46:33 2026 +0800

    feat: quote CSV fields containing commas in SavePolicy
---
 persist/adapter.go                     | 18 ++++++++++
 persist/file-adapter/adapter.go        | 15 +++++---
 persist/string-adapter/adapter.go      | 15 +++++---
 persist/string-adapter/adapter_test.go | 66 ++++++++++++++++++++++++++++++++++
 4 files changed, 104 insertions(+), 10 deletions(-)

diff --git a/persist/adapter.go b/persist/adapter.go
index 800e757..c15a4c8 100644
--- a/persist/adapter.go
+++ b/persist/adapter.go
@@ -15,6 +15,7 @@
 package persist
 
 import (
+	"bytes"
 	"encoding/csv"
 	"strings"
 
@@ -40,6 +41,23 @@ func LoadPolicyLine(line string, m model.Model) error {
 	return LoadPolicyArray(tokens, m)
 }
 
+// PolicyLineToCsv serializes a policy type and its rule fields into a CSV-safe line.
+// Fields containing commas are properly quoted so the line can be round-tripped
+// through LoadPolicyLine without corruption.
+func PolicyLineToCsv(ptype string, rule []string) (string, error) {
+	record := append([]string{ptype}, rule...)
+	var buf bytes.Buffer
+	w := csv.NewWriter(&buf)
+	if err := w.Write(record); err != nil {
+		return "", err
+	}
+	w.Flush()
+	if err := w.Error(); err != nil {
+		return "", err
+	}
+	return strings.TrimRight(buf.String(), "\r\n"), nil
+}
+
 // LoadPolicyArray loads a policy rule to model.
 func LoadPolicyArray(rule []string, m model.Model) error {
 	key := rule[0]
diff --git a/persist/file-adapter/adapter.go b/persist/file-adapter/adapter.go
index 454b2d1..223581c 100644
--- a/persist/file-adapter/adapter.go
+++ b/persist/file-adapter/adapter.go
@@ -23,7 +23,6 @@ import (
 
 	"github.com/casbin/casbin/v3/model"
 	"github.com/casbin/casbin/v3/persist"
-	"github.com/casbin/casbin/v3/util"
 )
 
 // Adapter is the file adapter for Casbin.
@@ -68,16 +67,22 @@ func (a *Adapter) SavePolicy(model model.Model) error {
 
 	for ptype, ast := range model["p"] {
 		for _, rule := range ast.Policy {
-			tmp.WriteString(ptype + ", ")
-			tmp.WriteString(util.ArrayToString(rule))
+			line, err := persist.PolicyLineToCsv(ptype, rule)
+			if err != nil {
+				return err
+			}
+			tmp.WriteString(line)
 			tmp.WriteString("\n")
 		}
 	}
 
 	for ptype, ast := range model["g"] {
 		for _, rule := range ast.Policy {
-			tmp.WriteString(ptype + ", ")
-			tmp.WriteString(util.ArrayToString(rule))
+			line, err := persist.PolicyLineToCsv(ptype, rule)
+			if err != nil {
+				return err
+			}
+			tmp.WriteString(line)
 			tmp.WriteString("\n")
 		}
 	}
diff --git a/persist/string-adapter/adapter.go b/persist/string-adapter/adapter.go
index c9655ac..388dc01 100644
--- a/persist/string-adapter/adapter.go
+++ b/persist/string-adapter/adapter.go
@@ -21,7 +21,6 @@ import (
 
 	"github.com/casbin/casbin/v3/model"
 	"github.com/casbin/casbin/v3/persist"
-	"github.com/casbin/casbin/v3/util"
 )
 
 // Adapter is the string adapter for Casbin.
@@ -58,16 +57,22 @@ func (a *Adapter) SavePolicy(model model.Model) error {
 	var tmp bytes.Buffer
 	for ptype, ast := range model["p"] {
 		for _, rule := range ast.Policy {
-			tmp.WriteString(ptype + ", ")
-			tmp.WriteString(util.ArrayToString(rule))
+			line, err := persist.PolicyLineToCsv(ptype, rule)
+			if err != nil {
+				return err
+			}
+			tmp.WriteString(line)
 			tmp.WriteString("\n")
 		}
 	}
 
 	for ptype, ast := range model["g"] {
 		for _, rule := range ast.Policy {
-			tmp.WriteString(ptype + ", ")
-			tmp.WriteString(util.ArrayToString(rule))
+			line, err := persist.PolicyLineToCsv(ptype, rule)
+			if err != nil {
+				return err
+			}
+			tmp.WriteString(line)
 			tmp.WriteString("\n")
 		}
 	}
diff --git a/persist/string-adapter/adapter_test.go b/persist/string-adapter/adapter_test.go
index 5e8727b..37fd08e 100644
--- a/persist/string-adapter/adapter_test.go
+++ b/persist/string-adapter/adapter_test.go
@@ -15,10 +15,12 @@
 package stringadapter
 
 import (
+	"strings"
 	"testing"
 
 	"github.com/casbin/casbin/v3"
 	"github.com/casbin/casbin/v3/model"
+	"github.com/casbin/casbin/v3/persist"
 )
 
 func Test_KeyMatchRbac(t *testing.T) {
@@ -61,6 +63,70 @@ g, alice, data_group_admin
 	}
 }
 
+// Test_SavePolicyRoundTripWithCommas verifies that a policy rule whose fields contain
+// commas (e.g. ABAC condition expressions) survives a SavePolicy → LoadPolicy round trip
+// without corruption. See https://github.com/apache/casbin/issues/1733.
+func Test_SavePolicyRoundTripWithCommas(t *testing.T) {
+	conf := `
+[request_definition]
+r = sub, obj, act, cond
+
+[policy_definition]
+p = sub, obj, act, cond
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
+`
+	// cond field intentionally contains commas (as in ABAC expressions).
+	condWithComma := `r.attrs in ('val1','val2')`
+	line := "p, alice, data1, read, " + `"` + condWithComma + `"`
+
+	a := NewAdapter(line)
+	m := model.NewModel()
+	if err := m.LoadModelFromText(conf); err != nil {
+		t.Fatalf("load model: %v", err)
+	}
+	e, err := casbin.NewEnforcer(m, a)
+	if err != nil {
+		t.Fatalf("new enforcer: %v", err)
+	}
+
+	// SavePolicy serialises in-memory rules back to the adapter string.
+	if err := e.SavePolicy(); err != nil {
+		t.Fatalf("SavePolicy: %v", err)
+	}
+
+	// The saved line must be a properly quoted CSV so that re-loading produces
+	// exactly one rule with the comma-containing cond field intact.
+	saved := a.Line
+	reloaded := model.NewModel()
+	if err := reloaded.LoadModelFromText(conf); err != nil {
+		t.Fatalf("reload model: %v", err)
+	}
+	for _, l := range strings.Split(saved, "\n") {
+		if l == "" {
+			continue
+		}
+		if err := persist.LoadPolicyLine(l, reloaded); err != nil {
+			t.Fatalf("LoadPolicyLine on saved line %q: %v", l, err)
+		}
+	}
+
+	rules, err := reloaded.GetPolicy("p", "p")
+	if err != nil {
+		t.Fatalf("GetPolicy: %v", err)
+	}
+	if len(rules) != 1 {
+		t.Fatalf("expected 1 rule after round-trip, got %d (saved: %q)", len(rules), saved)
+	}
+	if rules[0][3] != condWithComma {
+		t.Errorf("cond field corrupted after round-trip: got %q, want %q", rules[0][3], condWithComma)
+	}
+}
+
 func Test_StringRbac(t *testing.T) {
 	conf := `
 [request_definition]


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@casbin.apache.org
For additional commands, e-mail: commits-help@casbin.apache.org