Skip to content

Commit 4b53ea8

Browse files
authored
Feat: add SpiceDB validations (#1125)
## Fixes Or Enhances Adds support for validating [SpiceDB](https://github.com/authzed/spicedb) object ids, types, and permissions
1 parent 0f6b874 commit 4b53ea8

File tree

5 files changed

+105
-0
lines changed

5 files changed

+105
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ Baked-in Validations
158158
| credit_card | Credit Card Number |
159159
| mongodb | MongoDB ObjectID |
160160
| cron | Cron |
161+
| spicedb | SpiceDb ObjectID/Permission/Type |
161162
| datetime | Datetime |
162163
| e164 | e164 formatted phone number |
163164
| email | E-mail String

baked_in.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ var (
230230
"luhn_checksum": hasLuhnChecksum,
231231
"mongodb": isMongoDB,
232232
"cron": isCron,
233+
"spicedb": isSpiceDB,
233234
}
234235
)
235236

@@ -2812,6 +2813,23 @@ func isMongoDB(fl FieldLevel) bool {
28122813
return mongodbRegex.MatchString(val)
28132814
}
28142815

2816+
// isSpiceDB is the validation function for validating if the current field's value is valid for use with Authzed SpiceDB in the indicated way
2817+
func isSpiceDB(fl FieldLevel) bool {
2818+
val := fl.Field().String()
2819+
param := fl.Param()
2820+
2821+
switch param {
2822+
case "permission":
2823+
return spicedbPermissionRegex.MatchString(val)
2824+
case "type":
2825+
return spicedbTypeRegex.MatchString(val)
2826+
case "id", "":
2827+
return spicedbIDRegex.MatchString(val)
2828+
}
2829+
2830+
panic("Unrecognized parameter: " + param)
2831+
}
2832+
28152833
// isCreditCard is the validation function for validating if the current field's value is a valid credit card number
28162834
func isCreditCard(fl FieldLevel) bool {
28172835
val := fl.Field().String()

doc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,12 @@ This validates that a string value contains a valid cron expression.
13821382
13831383
Usage: cron
13841384
1385+
# SpiceDb ObjectID/Permission/Object Type
1386+
1387+
This validates that a string is valid for use with SpiceDb for the indicated purpose. If no purpose is given, a purpose of 'id' is assumed.
1388+
1389+
Usage: spicedb=id|permission|type
1390+
13851391
# Alias Validators and Tags
13861392
13871393
Alias Validators and Tags

regexes.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ const (
6868
cveRegexString = `^CVE-(1999|2\d{3})-(0[^0]\d{2}|0\d[^0]\d{1}|0\d{2}[^0]|[1-9]{1}\d{3,})$` // CVE Format Id https://cve.mitre.org/cve/identifiers/syntaxchange.html
6969
mongodbRegexString = "^[a-f\\d]{24}$"
7070
cronRegexString = `(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})`
71+
spicedbIDRegexString = `^(([a-zA-Z0-9/_|\-=+]{1,})|\*)$`
72+
spicedbPermissionRegexString = "^([a-z][a-z0-9_]{1,62}[a-z0-9])?$"
73+
spicedbTypeRegexString = "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)?[a-z][a-z0-9_]{1,62}[a-z0-9]$"
7174
)
7275

7376
var (
@@ -134,4 +137,7 @@ var (
134137
cveRegex = regexp.MustCompile(cveRegexString)
135138
mongodbRegex = regexp.MustCompile(mongodbRegexString)
136139
cronRegex = regexp.MustCompile(cronRegexString)
140+
spicedbIDRegex = regexp.MustCompile(spicedbIDRegexString)
141+
spicedbPermissionRegex = regexp.MustCompile(spicedbPermissionRegexString)
142+
spicedbTypeRegex = regexp.MustCompile(spicedbTypeRegexString)
137143
)

validator_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13094,6 +13094,80 @@ func TestMongoDBObjectIDFormatValidation(t *testing.T) {
1309413094
}
1309513095
}
1309613096

13097+
func TestSpiceDBValueFormatValidation(t *testing.T) {
13098+
tests := []struct {
13099+
value string
13100+
tag string
13101+
expected bool
13102+
}{
13103+
//Must be an asterisk OR a string containing alphanumeric characters and a restricted set a special symbols: _ | / - = +
13104+
{"*", "spicedb=id", true},
13105+
{`azAZ09_|/-=+`, "spicedb=id", true},
13106+
{`a*`, "spicedb=id", false},
13107+
{`/`, "spicedb=id", true},
13108+
{"*", "spicedb", true},
13109+
13110+
//Must begin and end with a lowercase letter, may also contain numbers and underscores between, min length 3, max length 64
13111+
{"a", "spicedb=permission", false},
13112+
{"1", "spicedb=permission", false},
13113+
{"a1", "spicedb=permission", false},
13114+
{"a_b", "spicedb=permission", true},
13115+
{"A_b", "spicedb=permission", false},
13116+
{"a_B", "spicedb=permission", false},
13117+
{"abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", true},
13118+
{"abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", false},
13119+
13120+
//Object types follow the same rules as permissions for the type name plus an optional prefix up to 63 characters with a /
13121+
{"a", "spicedb=type", false},
13122+
{"1", "spicedb=type", false},
13123+
{"a1", "spicedb=type", false},
13124+
{"a_b", "spicedb=type", true},
13125+
{"A_b", "spicedb=type", false},
13126+
{"a_B", "spicedb=type", false},
13127+
{"abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz", "spicedb=type", true},
13128+
{"abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz", "spicedb=type", false},
13129+
13130+
{`a_b/a`, "spicedb=type", false},
13131+
{`a_b/1`, "spicedb=type", false},
13132+
{`a_b/a1`, "spicedb=type", false},
13133+
{`a_b/a_b`, "spicedb=type", true},
13134+
{`a_b/A_b`, "spicedb=type", false},
13135+
{`a_b/a_B`, "spicedb=type", false},
13136+
{`a_b/abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz`, "spicedb=type", true},
13137+
{`a_b/abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz`, "spicedb=type", false},
13138+
13139+
{`a/a_b`, "spicedb=type", false},
13140+
{`1/a_b`, "spicedb=type", false},
13141+
{`a1/a_b`, "spicedb=type", false},
13142+
{`a_b/a_b`, "spicedb=type", true},
13143+
{`A_b/a_b`, "spicedb=type", false},
13144+
{`a_B/a_b`, "spicedb=type", false},
13145+
{`abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxy/a_b`, "spicedb=type", true},
13146+
{`abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz/a_b`, "spicedb=type", false},
13147+
}
13148+
13149+
validate := New()
13150+
13151+
for i, test := range tests {
13152+
errs := validate.Var(test.value, test.tag)
13153+
13154+
if test.expected {
13155+
if !IsEqual(errs, nil) {
13156+
t.Fatalf("Index: %d spicedb failed Error: %s", i, errs)
13157+
}
13158+
} else {
13159+
if IsEqual(errs, nil) {
13160+
t.Fatalf("Index: %d spicedb - expected error but there was none.", i)
13161+
} else {
13162+
val := getError(errs, "", "")
13163+
if val.Tag() != "spicedb" {
13164+
t.Fatalf("Index: %d spicedb failed Error: %s", i, errs)
13165+
}
13166+
}
13167+
}
13168+
}
13169+
}
13170+
1309713171
func TestCreditCardFormatValidation(t *testing.T) {
1309813172
tests := []struct {
1309913173
value string `validate:"credit_card"`

0 commit comments

Comments
 (0)