diff --git a/adapter.go b/adapter.go index b9f1030..402c412 100644 --- a/adapter.go +++ b/adapter.go @@ -20,12 +20,12 @@ type CasbinRule struct { tableName struct{} `pg:"_"` ID string Ptype string - V0 string - V1 string - V2 string - V3 string - V4 string - V5 string + V0 string `pg:",use_zero"` + V1 string `pg:",use_zero"` + V2 string `pg:",use_zero"` + V3 string `pg:",use_zero"` + V4 string `pg:",use_zero"` + V5 string `pg:",use_zero"` } type Filter struct { @@ -151,6 +151,22 @@ func (a *Adapter) createTableifNotExists() error { return nil } +// getValues returns the V0-V5 values as a slice +func (r *CasbinRule) getValues() []string { + return []string{r.V0, r.V1, r.V2, r.V3, r.V4, r.V5} +} + +// getLastNonEmptyIndex returns the index of the last non-empty value in the given slice +// Returns -1 if all values are empty +func getLastNonEmptyIndex(values []string) int { + for i := len(values) - 1; i >= 0; i-- { + if values[i] != "" { + return i + } + } + return -1 +} + func (r *CasbinRule) String() string { const prefixLine = ", " var sb strings.Builder @@ -162,29 +178,15 @@ func (r *CasbinRule) String() string { ) sb.WriteString(r.Ptype) - if len(r.V0) > 0 { - sb.WriteString(prefixLine) - sb.WriteString(r.V0) - } - if len(r.V1) > 0 { - sb.WriteString(prefixLine) - sb.WriteString(r.V1) - } - if len(r.V2) > 0 { - sb.WriteString(prefixLine) - sb.WriteString(r.V2) - } - if len(r.V3) > 0 { - sb.WriteString(prefixLine) - sb.WriteString(r.V3) - } - if len(r.V4) > 0 { + + values := r.getValues() + lastIndex := getLastNonEmptyIndex(values) + + // Include all values up to and including the last non-empty one + // This preserves empty strings in the middle while trimming trailing empty strings + for i := 0; i <= lastIndex; i++ { sb.WriteString(prefixLine) - sb.WriteString(r.V4) - } - if len(r.V5) > 0 { - sb.WriteString(prefixLine) - sb.WriteString(r.V5) + sb.WriteString(values[i]) } return sb.String() @@ -547,31 +549,17 @@ func (a *Adapter) UpdateFilteredPolicies(sec string, ptype string, newPolicies [ func (c *CasbinRule) queryString() (string, []interface{}) { queryArgs := []interface{}{c.Ptype} - queryStr := "ptype = ?" - if c.V0 != "" { - queryStr += " and v0 = ?" - queryArgs = append(queryArgs, c.V0) - } - if c.V1 != "" { - queryStr += " and v1 = ?" - queryArgs = append(queryArgs, c.V1) - } - if c.V2 != "" { - queryStr += " and v2 = ?" - queryArgs = append(queryArgs, c.V2) - } - if c.V3 != "" { - queryStr += " and v3 = ?" - queryArgs = append(queryArgs, c.V3) - } - if c.V4 != "" { - queryStr += " and v4 = ?" - queryArgs = append(queryArgs, c.V4) - } - if c.V5 != "" { - queryStr += " and v5 = ?" - queryArgs = append(queryArgs, c.V5) + + values := c.getValues() + lastIndex := getLastNonEmptyIndex(values) + + // Include all fields up to and including the last non-empty one + // This ensures empty strings in the middle are matched explicitly + fields := []string{"v0", "v1", "v2", "v3", "v4", "v5"} + for i := 0; i <= lastIndex; i++ { + queryStr += " and " + fields[i] + " = ?" + queryArgs = append(queryArgs, values[i]) } return queryStr, queryArgs @@ -582,24 +570,16 @@ func (c *CasbinRule) toStringPolicy() []string { if c.Ptype != "" { policy = append(policy, c.Ptype) } - if c.V0 != "" { - policy = append(policy, c.V0) - } - if c.V1 != "" { - policy = append(policy, c.V1) - } - if c.V2 != "" { - policy = append(policy, c.V2) - } - if c.V3 != "" { - policy = append(policy, c.V3) - } - if c.V4 != "" { - policy = append(policy, c.V4) - } - if c.V5 != "" { - policy = append(policy, c.V5) + + values := c.getValues() + lastIndex := getLastNonEmptyIndex(values) + + // Include all values up to and including the last non-empty one + // This preserves empty strings in the middle while trimming trailing empty strings + for i := 0; i <= lastIndex; i++ { + policy = append(policy, values[i]) } + return policy } diff --git a/adapter_test.go b/adapter_test.go index e390ca1..7bc45e8 100644 --- a/adapter_test.go +++ b/adapter_test.go @@ -345,6 +345,132 @@ func (s *AdapterTestSuite) TestUpdateFilteredPolicies() { s.assertPolicy(s.e.GetPolicy(), [][]string{{"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"alice", "data2", "write"}, {"bob", "data1", "read"}}) } + +func (s *AdapterTestSuite) TestEmptyStringSupport() { + // Test that empty strings in the middle of rules are properly stored and retrieved + // Note: Casbin trims trailing empty strings, so we focus on empty strings in the middle + var err error + + // Add policies with empty strings in the middle + _, err = s.e.AddPolicy("alice", "", "read") + s.Require().NoError(err) + + _, err = s.e.AddPolicy("bob", "", "write") + s.Require().NoError(err) + + // Reload to ensure they were saved correctly + err = s.e.LoadPolicy() + s.Require().NoError(err) + + // Check that all policies including empty strings are present + policies := s.e.GetPolicy() + + // Should have original 4 policies plus 2 new ones with empty strings + s.Assert().Len(policies, 6) + + // Verify the new policies with empty strings exist + hasAliceEmpty := false + hasBobEmpty := false + + for _, p := range policies { + if len(p) == 3 && p[0] == "alice" && p[1] == "" && p[2] == "read" { + hasAliceEmpty = true + } + if len(p) == 3 && p[0] == "bob" && p[1] == "" && p[2] == "write" { + hasBobEmpty = true + } + } + + s.Assert().True(hasAliceEmpty, "Policy with alice and empty string in middle not found") + s.Assert().True(hasBobEmpty, "Policy with bob and empty string in middle not found") + + // Test removing a policy with empty string + _, err = s.e.RemovePolicy("alice", "", "read") + s.Require().NoError(err) + + err = s.e.LoadPolicy() + s.Require().NoError(err) + + policies = s.e.GetPolicy() + s.Assert().Len(policies, 5) + + // Verify alice policy with empty string was removed + for _, p := range policies { + if len(p) == 3 && p[0] == "alice" && p[1] == "" && p[2] == "read" { + s.Fail("Policy with alice and empty string should have been removed") + } + } + + // Test updating a policy with empty string + _, err = s.e.UpdatePolicy([]string{"bob", "", "write"}, []string{"bob", "", "read"}) + s.Require().NoError(err) + + err = s.e.LoadPolicy() + s.Require().NoError(err) + + policies = s.e.GetPolicy() + hasBobEmptyRead := false + for _, p := range policies { + if len(p) == 3 && p[0] == "bob" && p[1] == "" && p[2] == "read" { + hasBobEmptyRead = true + } + // Should not have the old policy anymore + if len(p) == 3 && p[0] == "bob" && p[1] == "" && p[2] == "write" { + s.Fail("Old policy with bob and write should have been updated") + } + } + s.Assert().True(hasBobEmptyRead, "Updated policy with bob and read not found") +} + +func (s *AdapterTestSuite) TestEmptyStringVsWildcard() { + // Test that empty strings in rules are different from wildcards in filters + var err error + + // Add a policy with an empty string + _, err = s.e.AddPolicy("user1", "", "write") + s.Require().NoError(err) + + // Add a policy with a non-empty value + _, err = s.e.AddPolicy("user1", "resource1", "write") + s.Require().NoError(err) + + // Reload + err = s.e.LoadPolicy() + s.Require().NoError(err) + + // Both should exist + policies := s.e.GetPolicy() + hasEmpty := false + hasNonEmpty := false + + for _, p := range policies { + if len(p) >= 3 && p[0] == "user1" && p[2] == "write" { + if p[1] == "" { + hasEmpty = true + } else if p[1] == "resource1" { + hasNonEmpty = true + } + } + } + + s.Assert().True(hasEmpty, "Policy with empty string should exist") + s.Assert().True(hasNonEmpty, "Policy with resource1 should exist") + + // Remove using wildcard (empty string in filter) should remove both + _, err = s.e.RemoveFilteredPolicy(0, "user1", "", "write") + s.Require().NoError(err) + + err = s.e.LoadPolicy() + s.Require().NoError(err) + + policies = s.e.GetPolicy() + for _, p := range policies { + if len(p) >= 3 && p[0] == "user1" && p[2] == "write" { + s.Fail("Policies with user1 and write should have been removed by wildcard filter") + } + } +} + func TestAdapterTestSuite(t *testing.T) { suite.Run(t, new(AdapterTestSuite)) }