From 4b53ea8182dc92f8f3a41ead463e53c0408bc38c Mon Sep 17 00:00:00 2001 From: wscalf Date: Sun, 6 Aug 2023 13:03:30 -0400 Subject: [PATCH] Feat: add SpiceDB validations (#1125) ## Fixes Or Enhances Adds support for validating [SpiceDB](https://github.com/authzed/spicedb) object ids, types, and permissions --- README.md | 1 + baked_in.go | 18 ++++++++++++ doc.go | 6 ++++ regexes.go | 6 ++++ validator_test.go | 74 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 105 insertions(+) diff --git a/README.md b/README.md index 7cc0116b..9b93e61b 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ Baked-in Validations | credit_card | Credit Card Number | | mongodb | MongoDB ObjectID | | cron | Cron | +| spicedb | SpiceDb ObjectID/Permission/Type | | datetime | Datetime | | e164 | e164 formatted phone number | | email | E-mail String diff --git a/baked_in.go b/baked_in.go index a0750ccb..f7b55ab3 100644 --- a/baked_in.go +++ b/baked_in.go @@ -230,6 +230,7 @@ var ( "luhn_checksum": hasLuhnChecksum, "mongodb": isMongoDB, "cron": isCron, + "spicedb": isSpiceDB, } ) @@ -2812,6 +2813,23 @@ func isMongoDB(fl FieldLevel) bool { return mongodbRegex.MatchString(val) } +// isSpiceDB is the validation function for validating if the current field's value is valid for use with Authzed SpiceDB in the indicated way +func isSpiceDB(fl FieldLevel) bool { + val := fl.Field().String() + param := fl.Param() + + switch param { + case "permission": + return spicedbPermissionRegex.MatchString(val) + case "type": + return spicedbTypeRegex.MatchString(val) + case "id", "": + return spicedbIDRegex.MatchString(val) + } + + panic("Unrecognized parameter: " + param) +} + // isCreditCard is the validation function for validating if the current field's value is a valid credit card number func isCreditCard(fl FieldLevel) bool { val := fl.Field().String() diff --git a/doc.go b/doc.go index d62bd905..d1eff50f 100644 --- a/doc.go +++ b/doc.go @@ -1382,6 +1382,12 @@ This validates that a string value contains a valid cron expression. Usage: cron +# SpiceDb ObjectID/Permission/Object Type + +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. + + Usage: spicedb=id|permission|type + # Alias Validators and Tags Alias Validators and Tags diff --git a/regexes.go b/regexes.go index ba450b3d..6c8f9856 100644 --- a/regexes.go +++ b/regexes.go @@ -68,6 +68,9 @@ const ( 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 mongodbRegexString = "^[a-f\\d]{24}$" cronRegexString = `(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})` + spicedbIDRegexString = `^(([a-zA-Z0-9/_|\-=+]{1,})|\*)$` + spicedbPermissionRegexString = "^([a-z][a-z0-9_]{1,62}[a-z0-9])?$" + spicedbTypeRegexString = "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)?[a-z][a-z0-9_]{1,62}[a-z0-9]$" ) var ( @@ -134,4 +137,7 @@ var ( cveRegex = regexp.MustCompile(cveRegexString) mongodbRegex = regexp.MustCompile(mongodbRegexString) cronRegex = regexp.MustCompile(cronRegexString) + spicedbIDRegex = regexp.MustCompile(spicedbIDRegexString) + spicedbPermissionRegex = regexp.MustCompile(spicedbPermissionRegexString) + spicedbTypeRegex = regexp.MustCompile(spicedbTypeRegexString) ) diff --git a/validator_test.go b/validator_test.go index 97a50f6c..08878278 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13094,6 +13094,80 @@ func TestMongoDBObjectIDFormatValidation(t *testing.T) { } } +func TestSpiceDBValueFormatValidation(t *testing.T) { + tests := []struct { + value string + tag string + expected bool + }{ + //Must be an asterisk OR a string containing alphanumeric characters and a restricted set a special symbols: _ | / - = + + {"*", "spicedb=id", true}, + {`azAZ09_|/-=+`, "spicedb=id", true}, + {`a*`, "spicedb=id", false}, + {`/`, "spicedb=id", true}, + {"*", "spicedb", true}, + + //Must begin and end with a lowercase letter, may also contain numbers and underscores between, min length 3, max length 64 + {"a", "spicedb=permission", false}, + {"1", "spicedb=permission", false}, + {"a1", "spicedb=permission", false}, + {"a_b", "spicedb=permission", true}, + {"A_b", "spicedb=permission", false}, + {"a_B", "spicedb=permission", false}, + {"abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", true}, + {"abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", false}, + + //Object types follow the same rules as permissions for the type name plus an optional prefix up to 63 characters with a / + {"a", "spicedb=type", false}, + {"1", "spicedb=type", false}, + {"a1", "spicedb=type", false}, + {"a_b", "spicedb=type", true}, + {"A_b", "spicedb=type", false}, + {"a_B", "spicedb=type", false}, + {"abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz", "spicedb=type", true}, + {"abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz", "spicedb=type", false}, + + {`a_b/a`, "spicedb=type", false}, + {`a_b/1`, "spicedb=type", false}, + {`a_b/a1`, "spicedb=type", false}, + {`a_b/a_b`, "spicedb=type", true}, + {`a_b/A_b`, "spicedb=type", false}, + {`a_b/a_B`, "spicedb=type", false}, + {`a_b/abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz`, "spicedb=type", true}, + {`a_b/abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz`, "spicedb=type", false}, + + {`a/a_b`, "spicedb=type", false}, + {`1/a_b`, "spicedb=type", false}, + {`a1/a_b`, "spicedb=type", false}, + {`a_b/a_b`, "spicedb=type", true}, + {`A_b/a_b`, "spicedb=type", false}, + {`a_B/a_b`, "spicedb=type", false}, + {`abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxy/a_b`, "spicedb=type", true}, + {`abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz/a_b`, "spicedb=type", false}, + } + + validate := New() + + for i, test := range tests { + errs := validate.Var(test.value, test.tag) + + if test.expected { + if !IsEqual(errs, nil) { + t.Fatalf("Index: %d spicedb failed Error: %s", i, errs) + } + } else { + if IsEqual(errs, nil) { + t.Fatalf("Index: %d spicedb - expected error but there was none.", i) + } else { + val := getError(errs, "", "") + if val.Tag() != "spicedb" { + t.Fatalf("Index: %d spicedb failed Error: %s", i, errs) + } + } + } + } +} + func TestCreditCardFormatValidation(t *testing.T) { tests := []struct { value string `validate:"credit_card"`