From 102ac124dad1d08ef488d2959e56d86ae4214330 Mon Sep 17 00:00:00 2001 From: ivanauth Date: Mon, 30 Mar 2026 14:44:30 -0400 Subject: [PATCH 1/2] feat: allow underscore prefix for private identifiers Update regex patterns across proto validation rules, tuple parsing, and schema DSL to accept identifiers starting with underscore. This enables a convention for marking definitions, relations, and permissions as private/internal (e.g., _private_resource, _internal_owner). Regenerated proto files from updated core.proto. Fixes #2066 --- pkg/development/wasm/operations_test.go | 2 +- pkg/proto/core/v1/core.pb.go | 78 ++++++++++++------------- pkg/schemadsl/compiler/compiler_test.go | 64 +++++++++++++++++++- pkg/schemadsl/lexer/lex_test.go | 18 ++++++ pkg/tuple/parsing.go | 6 +- proto/internal/core/v1/core.proto | 30 +++++----- 6 files changed, 138 insertions(+), 60 deletions(-) diff --git a/pkg/development/wasm/operations_test.go b/pkg/development/wasm/operations_test.go index 970102fa6b..dade295b27 100644 --- a/pkg/development/wasm/operations_test.go +++ b/pkg/development/wasm/operations_test.go @@ -133,7 +133,7 @@ func TestCheckOperation(t *testing.T) { tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"), nil, &devinterface.DeveloperError{ - Message: "error in object definition fo: validation error: name: does not match regex pattern `^([a-z][a-z0-9_]{1,62}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$`", + Message: "error in object definition fo: validation error: name: does not match regex pattern `^([a-z_][a-z0-9_]{1,62}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$`", Kind: devinterface.DeveloperError_SCHEMA_ISSUE, Source: devinterface.DeveloperError_SCHEMA, Line: 1, diff --git a/pkg/proto/core/v1/core.pb.go b/pkg/proto/core/v1/core.pb.go index 4478de0487..7738a77d87 100644 --- a/pkg/proto/core/v1/core.pb.go +++ b/pkg/proto/core/v1/core.pb.go @@ -3237,14 +3237,14 @@ const file_core_v1_core_proto_rawDesc = "" + "\ttype_name\x18\x01 \x01(\tR\btypeName\x12I\n" + "\vchild_types\x18\x02 \x03(\v2\x1c.core.v1.CaveatTypeReferenceB\n" + "\xbaH\a\x92\x01\x04\b\x00\x10\x01R\n" + - "childTypes\"\x91\x02\n" + - "\x11ObjectAndRelation\x12f\n" + - "\tnamespace\x18\x01 \x01(\tBH\xbaHErC(\x80\x012>^([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$R\tnamespace\x12F\n" + - "\tobject_id\x18\x02 \x01(\tB)\xbaH&r$(\x80\b2\x1f^(([a-zA-Z0-9/_|\\-=+]{1,})|\\*)$R\bobjectId\x12L\n" + - "\brelation\x18\x03 \x01(\tB0\xbaH-r+(@2'^(\\.\\.\\.|[a-z][a-z0-9_]{1,62}[a-z0-9])$R\brelation\"\xc9\x01\n" + - "\x11RelationReference\x12f\n" + - "\tnamespace\x18\x01 \x01(\tBH\xbaHErC(\x80\x012>^([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$R\tnamespace\x12L\n" + - "\brelation\x18\x03 \x01(\tB0\xbaH-r+(@2'^(\\.\\.\\.|[a-z][a-z0-9_]{1,62}[a-z0-9])$R\brelation\"'\n" + + "childTypes\"\x94\x02\n" + + "\x11ObjectAndRelation\x12h\n" + + "\tnamespace\x18\x01 \x01(\tBJ\xbaHGrE(\x80\x012@^([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$R\tnamespace\x12F\n" + + "\tobject_id\x18\x02 \x01(\tB)\xbaH&r$(\x80\b2\x1f^(([a-zA-Z0-9/_|\\-=+]{1,})|\\*)$R\bobjectId\x12M\n" + + "\brelation\x18\x03 \x01(\tB1\xbaH.r,(@2(^(\\.\\.\\.|[a-z_][a-z0-9_]{1,62}[a-z0-9])$R\brelation\"\xcc\x01\n" + + "\x11RelationReference\x12h\n" + + "\tnamespace\x18\x01 \x01(\tBJ\xbaHGrE(\x80\x012@^([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$R\tnamespace\x12M\n" + + "\brelation\x18\x03 \x01(\tB1\xbaH.r,(@2(^(\\.\\.\\.|[a-z_][a-z0-9_]{1,62}[a-z0-9])$R\brelation\"'\n" + "\x06Zookie\x12\x1d\n" + "\x05token\x18\x01 \x01(\tB\a\xbaH\x04r\x02 \x01R\x05token\"\xd8\x01\n" + "\x13RelationTupleUpdate\x12N\n" + @@ -3278,14 +3278,14 @@ const file_core_v1_core_proto_rawDesc = "" + "\x0eDirectSubjects\x122\n" + "\bsubjects\x18\x01 \x03(\v2\x16.core.v1.DirectSubjectR\bsubjects\"\xb7\x01\n" + "\bMetadata\x12\xaa\x01\n" + - "\x10metadata_message\x18\x01 \x03(\v2\x14.google.protobuf.AnyBi\xbaHf\xc8\x01\x01\x92\x01`\b\x01\"\\\xc8\x01\x01\xa2\x01V\x12&type.googleapis.com/impl.v1.DocComment\x12,type.googleapis.com/impl.v1.RelationMetadataR\x0fmetadataMessage\"\x93\x02\n" + - "\x13NamespaceDefinition\x12\\\n" + - "\x04name\x18\x01 \x01(\tBH\xbaHErC(\x80\x012>^([a-z][a-z0-9_]{1,62}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$R\x04name\x12-\n" + + "\x10metadata_message\x18\x01 \x03(\v2\x14.google.protobuf.AnyBi\xbaHf\xc8\x01\x01\x92\x01`\b\x01\"\\\xc8\x01\x01\xa2\x01V\x12&type.googleapis.com/impl.v1.DocComment\x12,type.googleapis.com/impl.v1.RelationMetadataR\x0fmetadataMessage\"\x95\x02\n" + + "\x13NamespaceDefinition\x12^\n" + + "\x04name\x18\x01 \x01(\tBJ\xbaHGrE(\x80\x012@^([a-z_][a-z0-9_]{1,62}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$R\x04name\x12-\n" + "\brelation\x18\x02 \x03(\v2\x11.core.v1.RelationR\brelation\x12-\n" + "\bmetadata\x18\x03 \x01(\v2\x11.core.v1.MetadataR\bmetadata\x12@\n" + - "\x0fsource_position\x18\x04 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\"\x9c\x03\n" + - "\bRelation\x12;\n" + - "\x04name\x18\x01 \x01(\tB'\xbaH$r\"(@2\x1e^[a-z][a-z0-9_]{1,62}[a-z0-9]$R\x04name\x12@\n" + + "\x0fsource_position\x18\x04 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\"\x9d\x03\n" + + "\bRelation\x12<\n" + + "\x04name\x18\x01 \x01(\tB(\xbaH%r#(@2\x1f^[a-z_][a-z0-9_]{1,62}[a-z0-9]$R\x04name\x12@\n" + "\x0fuserset_rewrite\x18\x02 \x01(\v2\x17.core.v1.UsersetRewriteR\x0eusersetRewrite\x12C\n" + "\x10type_information\x18\x03 \x01(\v2\x18.core.v1.TypeInformationR\x0ftypeInformation\x12-\n" + "\bmetadata\x18\x04 \x01(\v2\x11.core.v1.MetadataR\bmetadata\x12@\n" + @@ -3320,10 +3320,10 @@ const file_core_v1_core_proto_rawDesc = "" + "\x1cREACHABLE_CONDITIONAL_RESULT\x10\x00\x12\x1b\n" + "\x17DIRECT_OPERATION_RESULT\x10\x01J\x04\b\x03\x10\x04\"e\n" + "\x0fTypeInformation\x12R\n" + - "\x18allowed_direct_relations\x18\x01 \x03(\v2\x18.core.v1.AllowedRelationR\x16allowedDirectRelations\"\x95\x04\n" + - "\x0fAllowedRelation\x12f\n" + - "\tnamespace\x18\x01 \x01(\tBH\xbaHErC(\x80\x012>^([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$R\tnamespace\x12N\n" + - "\brelation\x18\x03 \x01(\tB0\xbaH-r+(@2'^(\\.\\.\\.|[a-z][a-z0-9_]{1,62}[a-z0-9])$H\x00R\brelation\x12R\n" + + "\x18allowed_direct_relations\x18\x01 \x03(\v2\x18.core.v1.AllowedRelationR\x16allowedDirectRelations\"\x98\x04\n" + + "\x0fAllowedRelation\x12h\n" + + "\tnamespace\x18\x01 \x01(\tBJ\xbaHGrE(\x80\x012@^([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$R\tnamespace\x12O\n" + + "\brelation\x18\x03 \x01(\tB1\xbaH.r,(@2(^(\\.\\.\\.|[a-z_][a-z0-9_]{1,62}[a-z0-9])$H\x00R\brelation\x12R\n" + "\x0fpublic_wildcard\x18\x04 \x01(\v2'.core.v1.AllowedRelation.PublicWildcardH\x00R\x0epublicWildcard\x12@\n" + "\x0fsource_position\x18\x05 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\x12?\n" + "\x0frequired_caveat\x18\x06 \x01(\v2\x16.core.v1.AllowedCaveatR\x0erequiredCaveat\x12I\n" + @@ -3357,28 +3357,28 @@ const file_core_v1_core_proto_rawDesc = "" + "\x03Nil\x1a\x06\n" + "\x04SelfB\x13\n" + "\n" + - "child_type\x12\x05\xbaH\x02\b\x01\"\xb6\x02\n" + + "child_type\x12\x05\xbaH\x02\b\x01\"\xb7\x02\n" + "\x0eTupleToUserset\x12D\n" + "\btupleset\x18\x01 \x01(\v2 .core.v1.TupleToUserset.TuplesetB\x06\xbaH\x03\xc8\x01\x01R\btupleset\x12K\n" + "\x10computed_userset\x18\x02 \x01(\v2\x18.core.v1.ComputedUsersetB\x06\xbaH\x03\xc8\x01\x01R\x0fcomputedUserset\x12@\n" + - "\x0fsource_position\x18\x03 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\x1aO\n" + - "\bTupleset\x12C\n" + - "\brelation\x18\x01 \x01(\tB'\xbaH$r\"(@2\x1e^[a-z][a-z0-9_]{1,62}[a-z0-9]$R\brelation\"\xe8\x03\n" + + "\x0fsource_position\x18\x03 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\x1aP\n" + + "\bTupleset\x12D\n" + + "\brelation\x18\x01 \x01(\tB(\xbaH%r#(@2\x1f^[a-z_][a-z0-9_]{1,62}[a-z0-9]$R\brelation\"\xe9\x03\n" + "\x18FunctionedTupleToUserset\x12R\n" + "\bfunction\x18\x01 \x01(\x0e2*.core.v1.FunctionedTupleToUserset.FunctionB\n" + "\xbaH\a\x82\x01\x04\x10\x01 \x00R\bfunction\x12N\n" + "\btupleset\x18\x02 \x01(\v2*.core.v1.FunctionedTupleToUserset.TuplesetB\x06\xbaH\x03\xc8\x01\x01R\btupleset\x12K\n" + "\x10computed_userset\x18\x03 \x01(\v2\x18.core.v1.ComputedUsersetB\x06\xbaH\x03\xc8\x01\x01R\x0fcomputedUserset\x12@\n" + - "\x0fsource_position\x18\x04 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\x1aO\n" + - "\bTupleset\x12C\n" + - "\brelation\x18\x01 \x01(\tB'\xbaH$r\"(@2\x1e^[a-z][a-z0-9_]{1,62}[a-z0-9]$R\brelation\"H\n" + + "\x0fsource_position\x18\x04 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\x1aP\n" + + "\bTupleset\x12D\n" + + "\brelation\x18\x01 \x01(\tB(\xbaH%r#(@2\x1f^[a-z_][a-z0-9_]{1,62}[a-z0-9]$R\brelation\"H\n" + "\bFunction\x12\x18\n" + "\x14FUNCTION_UNSPECIFIED\x10\x00\x12\x10\n" + "\fFUNCTION_ANY\x10\x01\x12\x10\n" + - "\fFUNCTION_ALL\x10\x02\"\x91\x02\n" + + "\fFUNCTION_ALL\x10\x02\"\x92\x02\n" + "\x0fComputedUserset\x12A\n" + - "\x06object\x18\x01 \x01(\x0e2\x1f.core.v1.ComputedUserset.ObjectB\b\xbaH\x05\x82\x01\x02\x10\x01R\x06object\x12C\n" + - "\brelation\x18\x02 \x01(\tB'\xbaH$r\"(@2\x1e^[a-z][a-z0-9_]{1,62}[a-z0-9]$R\brelation\x12@\n" + + "\x06object\x18\x01 \x01(\x0e2\x1f.core.v1.ComputedUserset.ObjectB\b\xbaH\x05\x82\x01\x02\x10\x01R\x06object\x12D\n" + + "\brelation\x18\x02 \x01(\tB(\xbaH%r#(@2\x1f^[a-z_][a-z0-9_]{1,62}[a-z0-9]$R\brelation\x12@\n" + "\x0fsource_position\x18\x03 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\"4\n" + "\x06Object\x12\x10\n" + "\fTUPLE_OBJECT\x10\x00\x12\x18\n" + @@ -3397,19 +3397,19 @@ const file_core_v1_core_proto_rawDesc = "" + "\aUNKNOWN\x10\x00\x12\x06\n" + "\x02OR\x10\x01\x12\a\n" + "\x03AND\x10\x02\x12\a\n" + - "\x03NOT\x10\x03\"\xee\x03\n" + - "\x12RelationshipFilter\x12p\n" + - "\rresource_type\x18\x01 \x01(\tBK\xbaHHrF(\x80\x012A^(([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9])?$R\fresourceType\x12W\n" + + "\x03NOT\x10\x03\"\xf1\x03\n" + + "\x12RelationshipFilter\x12r\n" + + "\rresource_type\x18\x01 \x01(\tBM\xbaHJrH(\x80\x012C^(([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9])?$R\fresourceType\x12W\n" + "\x14optional_resource_id\x18\x02 \x01(\tB%\xbaH\"r (\x80\b2\x1b^([a-zA-Z0-9/_|\\-=+]{1,})?$R\x12optionalResourceId\x12d\n" + - "\x1boptional_resource_id_prefix\x18\x05 \x01(\tB%\xbaH\"r (\x80\b2\x1b^([a-zA-Z0-9/_|\\-=+]{1,})?$R\x18optionalResourceIdPrefix\x12W\n" + - "\x11optional_relation\x18\x03 \x01(\tB*\xbaH'r%(@2!^([a-z][a-z0-9_]{1,62}[a-z0-9])?$R\x10optionalRelation\x12N\n" + - "\x17optional_subject_filter\x18\x04 \x01(\v2\x16.core.v1.SubjectFilterR\x15optionalSubjectFilter\"\x86\x03\n" + - "\rSubjectFilter\x12k\n" + - "\fsubject_type\x18\x01 \x01(\tBH\xbaHErC(\x80\x012>^([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$R\vsubjectType\x12Z\n" + + "\x1boptional_resource_id_prefix\x18\x05 \x01(\tB%\xbaH\"r (\x80\b2\x1b^([a-zA-Z0-9/_|\\-=+]{1,})?$R\x18optionalResourceIdPrefix\x12X\n" + + "\x11optional_relation\x18\x03 \x01(\tB+\xbaH(r&(@2\"^([a-z_][a-z0-9_]{1,62}[a-z0-9])?$R\x10optionalRelation\x12N\n" + + "\x17optional_subject_filter\x18\x04 \x01(\v2\x16.core.v1.SubjectFilterR\x15optionalSubjectFilter\"\x89\x03\n" + + "\rSubjectFilter\x12m\n" + + "\fsubject_type\x18\x01 \x01(\tBJ\xbaHGrE(\x80\x012@^([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$R\vsubjectType\x12Z\n" + "\x13optional_subject_id\x18\x02 \x01(\tB*\xbaH'r%(\x80\b2 ^(([a-zA-Z0-9/_|\\-=+]{1,})|\\*)?$R\x11optionalSubjectId\x12R\n" + - "\x11optional_relation\x18\x03 \x01(\v2%.core.v1.SubjectFilter.RelationFilterR\x10optionalRelation\x1aX\n" + - "\x0eRelationFilter\x12F\n" + - "\brelation\x18\x01 \x01(\tB*\xbaH'r%(@2!^([a-z][a-z0-9_]{1,62}[a-z0-9])?$R\brelation\"\xef\x04\n" + + "\x11optional_relation\x18\x03 \x01(\v2%.core.v1.SubjectFilter.RelationFilterR\x10optionalRelation\x1aY\n" + + "\x0eRelationFilter\x12G\n" + + "\brelation\x18\x01 \x01(\tB+\xbaH(r&(@2\"^([a-z_][a-z0-9_]{1,62}[a-z0-9])?$R\brelation\"\xef\x04\n" + "\fStoredSchema\x12\x18\n" + "\aversion\x18\x01 \x01(\rR\aversion\x126\n" + "\x02v1\x18\x02 \x01(\v2$.core.v1.StoredSchema.V1StoredSchemaH\x00R\x02v1\x1a\xfb\x03\n" + diff --git a/pkg/schemadsl/compiler/compiler_test.go b/pkg/schemadsl/compiler/compiler_test.go index 92addbb4e3..891b94bd84 100644 --- a/pkg/schemadsl/compiler/compiler_test.go +++ b/pkg/schemadsl/compiler/compiler_test.go @@ -795,7 +795,7 @@ func TestCompile(t *testing.T) { "invalid definition name", nilPrefix, `definition someTenant/fo {}`, - "parse error in `invalid definition name`, line 1, column 1: error in object definition someTenant/fo: validation error: name: does not match regex pattern `^([a-z][a-z0-9_]{1,62}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$`", + "parse error in `invalid definition name`, line 1, column 1: error in object definition someTenant/fo: validation error: name: does not match regex pattern `^([a-z_][a-z0-9_]{1,62}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$`", []SchemaDefinition{}, }, { @@ -804,9 +804,69 @@ func TestCompile(t *testing.T) { `definition some_tenant/foos { relation ab: some_tenant/foos }`, - "parse error in `invalid relation name`, line 2, column 5: error in relation ab: validation error: name: does not match regex pattern `^[a-z][a-z0-9_]{1,62}[a-z0-9]$`", + "parse error in `invalid relation name`, line 2, column 5: error in relation ab: validation error: name: does not match regex pattern `^[a-z_][a-z0-9_]{1,62}[a-z0-9]$`", []SchemaDefinition{}, }, + { + "underscore prefix namespace", + AllowUnprefixedObjectType(), + `definition _private_resource { + relation owner: user + }`, + "", + []SchemaDefinition{ + namespace.Namespace("_private_resource", + namespace.MustRelation("owner", nil, namespace.AllowedRelation("user", "...")), + ), + }, + }, + { + "underscore prefix relation", + withTenantPrefix, + `definition resource { + relation _internal_owner: sometenant/user + relation viewer: sometenant/user + permission view = viewer + _internal_owner + }`, + "", + []SchemaDefinition{ + namespace.Namespace("sometenant/resource", + namespace.MustRelation("_internal_owner", nil, namespace.AllowedRelation("sometenant/user", "...")), + namespace.MustRelation("viewer", nil, namespace.AllowedRelation("sometenant/user", "...")), + namespace.MustRelation("view", + namespace.Union( + namespace.ComputedUserset("viewer"), + namespace.ComputedUserset("_internal_owner"), + ), + ), + ), + }, + }, + { + "underscore prefix permission", + withTenantPrefix, + `definition document { + relation owner: sometenant/user + permission _internal_edit = owner + permission edit = _internal_edit + }`, + "", + []SchemaDefinition{ + namespace.Namespace("sometenant/document", + namespace.MustRelation("owner", nil, namespace.AllowedRelation("sometenant/user", "...")), + namespace.MustRelation("_internal_edit", + namespace.Union( + namespace.ComputedUserset("owner"), + ), + ), + namespace.MustRelation("edit", + namespace.Union( + namespace.ComputedUserset("_internal_edit"), + ), + ), + ), + }, + }, { "no implicit tenant with specified tenant on type ref", nilPrefix, diff --git a/pkg/schemadsl/lexer/lex_test.go b/pkg/schemadsl/lexer/lex_test.go index 8eb575b006..126f59e2f0 100644 --- a/pkg/schemadsl/lexer/lex_test.go +++ b/pkg/schemadsl/lexer/lex_test.go @@ -67,6 +67,24 @@ var lexerTests = []lexerTest{ tEOF, }}, + // Underscore prefix tests + {"underscore identifier", "_private", []Lexeme{{TokenTypeIdentifier, 0, "_private", ""}, tEOF}}, + {"underscore relation", "_internal_permission", []Lexeme{{TokenTypeIdentifier, 0, "_internal_permission", ""}, tEOF}}, + {"underscore namespace", "_system", []Lexeme{{TokenTypeIdentifier, 0, "_system", ""}, tEOF}}, + {"underscore typepath", "_tenant/_resource", []Lexeme{ + {TokenTypeIdentifier, 0, "_tenant", ""}, + {TokenTypeDiv, 0, "/", ""}, + {TokenTypeIdentifier, 0, "_resource", ""}, + tEOF, + }}, + {"mixed underscore path", "_private/public/_internal", []Lexeme{ + {TokenTypeIdentifier, 0, "_private", ""}, + {TokenTypeDiv, 0, "/", ""}, + {TokenTypeIdentifier, 0, "public", ""}, + {TokenTypeDiv, 0, "/", ""}, + {TokenTypeIdentifier, 0, "_internal", ""}, + tEOF, + }}, {"unicode identifier", "一级", []Lexeme{{TokenTypeIdentifier, 0, "一级", ""}, tEOF}}, {"unicode singlequoted string literal", "'一级'", []Lexeme{{TokenTypeString, 0, "'一级'", ""}, tEOF}}, {"ascii singlequoted string literal", "'foo'", []Lexeme{{TokenTypeString, 0, "'foo'", ""}, tEOF}}, diff --git a/pkg/tuple/parsing.go b/pkg/tuple/parsing.go index 3e03678cc7..d2a336b168 100644 --- a/pkg/tuple/parsing.go +++ b/pkg/tuple/parsing.go @@ -16,11 +16,11 @@ import ( ) const ( - namespaceNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]" + namespaceNameExpr = "([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]" resourceIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})" subjectIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})|\\*" - relationExpr = "[a-z][a-z0-9_]{1,62}[a-z0-9]" - caveatNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]" + relationExpr = "[a-z_][a-z0-9_]{1,62}[a-z0-9]" + caveatNameExpr = "([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]" ) var onrExpr = fmt.Sprintf( diff --git a/proto/internal/core/v1/core.proto b/proto/internal/core/v1/core.proto index 527c125058..825f0ab91e 100644 --- a/proto/internal/core/v1/core.proto +++ b/proto/internal/core/v1/core.proto @@ -88,7 +88,7 @@ message CaveatTypeReference { message ObjectAndRelation { /** namespace is the full namespace path for the referenced object */ string namespace = 1 [(buf.validate.field).string = { - pattern: "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$" + pattern: "^([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$" max_bytes: 128 }]; @@ -100,7 +100,7 @@ message ObjectAndRelation { /** relation is the name of the referenced relation or permission under the namespace */ string relation = 3 [(buf.validate.field).string = { - pattern: "^(\\.\\.\\.|[a-z][a-z0-9_]{1,62}[a-z0-9])$" + pattern: "^(\\.\\.\\.|[a-z_][a-z0-9_]{1,62}[a-z0-9])$" max_bytes: 64 }]; } @@ -108,13 +108,13 @@ message ObjectAndRelation { message RelationReference { /** namespace is the full namespace path */ string namespace = 1 [(buf.validate.field).string = { - pattern: "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$" + pattern: "^([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$" max_bytes: 128 }]; /** relation is the name of the referenced relation or permission under the namespace */ string relation = 3 [(buf.validate.field).string = { - pattern: "^(\\.\\.\\.|[a-z][a-z0-9_]{1,62}[a-z0-9])$" + pattern: "^(\\.\\.\\.|[a-z_][a-z0-9_]{1,62}[a-z0-9])$" max_bytes: 64 }]; } @@ -188,7 +188,7 @@ message Metadata { message NamespaceDefinition { /** name is the unique for the namespace, including prefixes (which are optional) */ string name = 1 [(buf.validate.field).string = { - pattern: "^([a-z][a-z0-9_]{1,62}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$" + pattern: "^([a-z_][a-z0-9_]{1,62}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$" max_bytes: 128 }]; @@ -208,7 +208,7 @@ message NamespaceDefinition { message Relation { /** name is the full name for the relation or permission */ string name = 1 [(buf.validate.field).string = { - pattern: "^[a-z][a-z0-9_]{1,62}[a-z0-9]$" + pattern: "^[a-z_][a-z0-9_]{1,62}[a-z0-9]$" max_bytes: 64 }]; @@ -394,7 +394,7 @@ message AllowedRelation { /** namespace is the full namespace path of the allowed object type */ string namespace = 1 [(buf.validate.field).string = { - pattern: "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$" + pattern: "^([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$" max_bytes: 128 }]; @@ -403,7 +403,7 @@ message AllowedRelation { */ oneof relation_or_wildcard { string relation = 3 [(buf.validate.field).string = { - pattern: "^(\\.\\.\\.|[a-z][a-z0-9_]{1,62}[a-z0-9])$" + pattern: "^(\\.\\.\\.|[a-z_][a-z0-9_]{1,62}[a-z0-9])$" max_bytes: 64 }]; PublicWildcard public_wildcard = 4; @@ -492,7 +492,7 @@ message SetOperation { message TupleToUserset { message Tupleset { string relation = 1 [(buf.validate.field).string = { - pattern: "^[a-z][a-z0-9_]{1,62}[a-z0-9]$" + pattern: "^[a-z_][a-z0-9_]{1,62}[a-z0-9]$" max_bytes: 64 }]; } @@ -511,7 +511,7 @@ message FunctionedTupleToUserset { message Tupleset { string relation = 1 [(buf.validate.field).string = { - pattern: "^[a-z][a-z0-9_]{1,62}[a-z0-9]$" + pattern: "^[a-z_][a-z0-9_]{1,62}[a-z0-9]$" max_bytes: 64 }]; } @@ -533,7 +533,7 @@ message ComputedUserset { Object object = 1 [(buf.validate.field).enum.defined_only = true]; string relation = 2 [(buf.validate.field).string = { - pattern: "^[a-z][a-z0-9_]{1,62}[a-z0-9]$" + pattern: "^[a-z_][a-z0-9_]{1,62}[a-z0-9]$" max_bytes: 64 }]; SourcePosition source_position = 3; @@ -567,7 +567,7 @@ message RelationshipFilter { // resource_type is the *optional* resource type of the relationship. // NOTE: It is not prefixed with "optional_" for legacy compatibility. string resource_type = 1 [(buf.validate.field).string = { - pattern: "^(([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9])?$" + pattern: "^(([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9])?$" max_bytes: 128 }]; @@ -587,7 +587,7 @@ message RelationshipFilter { // relation is the *optional* relation of the relationship. string optional_relation = 3 [(buf.validate.field).string = { - pattern: "^([a-z][a-z0-9_]{1,62}[a-z0-9])?$" + pattern: "^([a-z_][a-z0-9_]{1,62}[a-z0-9])?$" max_bytes: 64 }]; @@ -602,13 +602,13 @@ message RelationshipFilter { message SubjectFilter { message RelationFilter { string relation = 1 [(buf.validate.field).string = { - pattern: "^([a-z][a-z0-9_]{1,62}[a-z0-9])?$" + pattern: "^([a-z_][a-z0-9_]{1,62}[a-z0-9])?$" max_bytes: 64 }]; } string subject_type = 1 [(buf.validate.field).string = { - pattern: "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$" + pattern: "^([a-z_][a-z0-9_]{1,61}[a-z0-9]/)*[a-z_][a-z0-9_]{1,62}[a-z0-9]$" max_bytes: 128 }]; From d92b493b3a54f2a0c046e1fd7539130d8a563210 Mon Sep 17 00:00:00 2001 From: ivanauth Date: Tue, 31 Mar 2026 11:29:39 -0400 Subject: [PATCH 2/2] chore: add CHANGELOG entry for underscore prefix identifiers --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e5f1f79e3..cddee64cb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [1.56.0] - 2026-07-24 ### Added - New metric `check_permissionship_total` for CheckPermission and CheckBulkPermissions that counts the number of requests that returned HAS_PERMISSION. Also, `write_relationships_updates` also includes BulkImport calls (https://github.com/authzed/spicedb/pull/3240) +- Allow underscore prefix for private identifiers in definitions, relations, and permissions (https://github.com/authzed/spicedb/pull/2733) ### Changed - Schema: reads inside write transactions now use a cheap hash-only lookup (`schema_revision`) to check the cache before loading the full schema blob, reducing DB round-trips on cache hits (https://github.com/authzed/spicedb/pull/3160)