github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/validationfile/blocks/expectedrelations.go (about) 1 package blocks 2 3 import ( 4 "fmt" 5 "regexp" 6 "slices" 7 "strings" 8 9 yamlv3 "gopkg.in/yaml.v3" 10 11 core "github.com/authzed/spicedb/pkg/proto/core/v1" 12 13 "github.com/authzed/spicedb/pkg/spiceerrors" 14 "github.com/authzed/spicedb/pkg/tuple" 15 ) 16 17 // ParsedExpectedRelations represents the expected relations defined in the validation 18 // file. 19 type ParsedExpectedRelations struct { 20 // ValidationMap is the parsed expected relations validation map. 21 ValidationMap ValidationMap 22 23 // SourcePosition is the position of the expected relations in the file. 24 SourcePosition spiceerrors.SourcePosition 25 } 26 27 // UnmarshalYAML is a custom unmarshaller. 28 func (per *ParsedExpectedRelations) UnmarshalYAML(node *yamlv3.Node) error { 29 err := node.Decode(&per.ValidationMap) 30 if err != nil { 31 return convertYamlError(err) 32 } 33 34 per.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} 35 return nil 36 } 37 38 // ValidationMap is a map from an Object Relation (as a Relationship) to the 39 // validation strings containing the Subjects for that Object Relation. 40 type ValidationMap map[ObjectRelation][]ExpectedSubject 41 42 // ObjectRelation represents an ONR defined as a string in the key for 43 // the ValidationMap. 44 type ObjectRelation struct { 45 // ObjectRelationString is the string form of the object relation. 46 ObjectRelationString string 47 48 // ObjectAndRelation is the parsed object and relation. 49 ObjectAndRelation *core.ObjectAndRelation 50 51 // SourcePosition is the position of the expected relations in the file. 52 SourcePosition spiceerrors.SourcePosition 53 } 54 55 // UnmarshalYAML is a custom unmarshaller. 56 func (ors *ObjectRelation) UnmarshalYAML(node *yamlv3.Node) error { 57 err := node.Decode(&ors.ObjectRelationString) 58 if err != nil { 59 return convertYamlError(err) 60 } 61 62 parsed := tuple.ParseONR(ors.ObjectRelationString) 63 if parsed == nil { 64 return spiceerrors.NewErrorWithSource( 65 fmt.Errorf("could not parse %s", ors.ObjectRelationString), 66 ors.ObjectRelationString, 67 uint64(node.Line), 68 uint64(node.Column), 69 ) 70 } 71 72 ors.ObjectAndRelation = parsed 73 ors.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} 74 return nil 75 } 76 77 var ( 78 vsSubjectRegex = regexp.MustCompile(`(.*?)\[(?P<user_str>.*)](.*?)`) 79 vsObjectAndRelationRegex = regexp.MustCompile(`(.*?)<(?P<onr_str>[^>]+)>(.*?)`) 80 vsSubjectWithExceptionsOrCaveatRegex = regexp.MustCompile(`^(?P<subject_onr>[^]\s]+)(?P<caveat>\[\.\.\.])?(\s+-\s+\{(?P<exceptions>[^}]+)})?$`) 81 ) 82 83 // ExpectedSubject is a subject expected for the ObjectAndRelation. 84 type ExpectedSubject struct { 85 // ValidationString holds a validation string containing a Subject and one or 86 // more Relations to the parent Object. 87 // Example: `[tenant/user:someuser#...] is <tenant/document:example#viewer>` 88 ValidationString ValidationString 89 90 // Subject is the subject expected. May be nil if not defined in the line. 91 SubjectWithExceptions *SubjectWithExceptions 92 93 // Resources are the resources under which the subject is found. 94 Resources []*core.ObjectAndRelation 95 96 // SourcePosition is the position of the expected subject in the file. 97 SourcePosition spiceerrors.SourcePosition 98 } 99 100 // SubjectAndCaveat returns a subject and whether it is caveated. 101 type SubjectAndCaveat struct { 102 // Subject is the subject found. 103 Subject *core.ObjectAndRelation 104 105 // IsCaveated indicates whether the subject is caveated. 106 IsCaveated bool 107 } 108 109 // SubjectWithExceptions returns the subject found in a validation string, along with any exceptions. 110 type SubjectWithExceptions struct { 111 // Subject is the subject found. 112 Subject SubjectAndCaveat 113 114 // Exceptions are those subjects removed from the subject, if it is a wildcard. 115 Exceptions []SubjectAndCaveat 116 } 117 118 // UnmarshalYAML is a custom unmarshaller. 119 func (es *ExpectedSubject) UnmarshalYAML(node *yamlv3.Node) error { 120 err := node.Decode(&es.ValidationString) 121 if err != nil { 122 return convertYamlError(err) 123 } 124 125 subjectWithExceptions, subErr := es.ValidationString.Subject() 126 if subErr != nil { 127 return spiceerrors.NewErrorWithSource( 128 subErr, 129 subErr.SourceCodeString, 130 uint64(node.Line)+subErr.LineNumber, 131 uint64(node.Column)+subErr.ColumnPosition, 132 ) 133 } 134 135 onrs, onrErr := es.ValidationString.ONRS() 136 if onrErr != nil { 137 return spiceerrors.NewErrorWithSource( 138 onrErr, 139 onrErr.SourceCodeString, 140 uint64(node.Line)+onrErr.LineNumber, 141 uint64(node.Column)+onrErr.ColumnPosition, 142 ) 143 } 144 145 es.SubjectWithExceptions = subjectWithExceptions 146 es.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} 147 es.Resources = onrs 148 return nil 149 } 150 151 // ValidationString holds a validation string containing a Subject and one or 152 // more Relations to the parent Object. 153 // Example: `[tenant/user:someuser#...] is <tenant/document:example#viewer>` 154 type ValidationString string 155 156 // SubjectString returns the subject contained in the ValidationString, if any. 157 func (vs ValidationString) SubjectString() (string, bool) { 158 result := vsSubjectRegex.FindStringSubmatch(string(vs)) 159 if len(result) != 4 { 160 return "", false 161 } 162 163 return result[2], true 164 } 165 166 // Subject returns the subject contained in the ValidationString, if any. If 167 // none, returns nil. 168 func (vs ValidationString) Subject() (*SubjectWithExceptions, *spiceerrors.ErrorWithSource) { 169 subjectStr, ok := vs.SubjectString() 170 if !ok { 171 return nil, nil 172 } 173 174 subjectStr = strings.TrimSpace(subjectStr) 175 groups := vsSubjectWithExceptionsOrCaveatRegex.FindStringSubmatch(subjectStr) 176 if len(groups) == 0 { 177 bracketedSubjectString := "[" + subjectStr + "]" 178 return nil, spiceerrors.NewErrorWithSource(fmt.Errorf("invalid subject: `%s`", subjectStr), bracketedSubjectString, 0, 0) 179 } 180 181 subjectONRString := groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "subject_onr")] 182 subjectONR := tuple.ParseSubjectONR(subjectONRString) 183 if subjectONR == nil { 184 return nil, spiceerrors.NewErrorWithSource(fmt.Errorf("invalid subject: `%s`", subjectONRString), subjectONRString, 0, 0) 185 } 186 187 exceptionsString := strings.TrimSpace(groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "exceptions")]) 188 var exceptions []SubjectAndCaveat 189 190 if len(exceptionsString) > 0 { 191 exceptionsStringsSlice := strings.Split(exceptionsString, ",") 192 exceptions = make([]SubjectAndCaveat, 0, len(exceptionsStringsSlice)) 193 for _, exceptionString := range exceptionsStringsSlice { 194 isCaveated := false 195 if strings.HasSuffix(exceptionString, "[...]") { 196 exceptionString = strings.TrimSuffix(exceptionString, "[...]") 197 isCaveated = true 198 } 199 200 exceptionONR := tuple.ParseSubjectONR(strings.TrimSpace(exceptionString)) 201 if exceptionONR == nil { 202 return nil, spiceerrors.NewErrorWithSource(fmt.Errorf("invalid subject: `%s`", exceptionString), exceptionString, 0, 0) 203 } 204 205 exceptions = append(exceptions, SubjectAndCaveat{exceptionONR, isCaveated}) 206 } 207 } 208 209 isCaveated := len(strings.TrimSpace(groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "caveat")])) > 0 210 return &SubjectWithExceptions{SubjectAndCaveat{subjectONR, isCaveated}, exceptions}, nil 211 } 212 213 // ONRStrings returns the ONRs contained in the ValidationString, if any. 214 func (vs ValidationString) ONRStrings() []string { 215 results := vsObjectAndRelationRegex.FindAllStringSubmatch(string(vs), -1) 216 onrStrings := []string{} 217 for _, result := range results { 218 onrStrings = append(onrStrings, result[2]) 219 } 220 return onrStrings 221 } 222 223 // ONRS returns the subject ONRs in the ValidationString, if any. 224 func (vs ValidationString) ONRS() ([]*core.ObjectAndRelation, *spiceerrors.ErrorWithSource) { 225 onrStrings := vs.ONRStrings() 226 onrs := []*core.ObjectAndRelation{} 227 for _, onrString := range onrStrings { 228 found := tuple.ParseONR(onrString) 229 if found == nil { 230 return nil, spiceerrors.NewErrorWithSource(fmt.Errorf("invalid resource and relation: `%s`", onrString), onrString, 0, 0) 231 } 232 233 onrs = append(onrs, found) 234 } 235 return onrs, nil 236 } 237 238 // ParseExpectedRelationsBlock parses the given contents as an expected relations block. 239 func ParseExpectedRelationsBlock(contents []byte) (*ParsedExpectedRelations, error) { 240 per := ParsedExpectedRelations{} 241 err := yamlv3.Unmarshal(contents, &per) 242 if err != nil { 243 return nil, convertYamlError(err) 244 } 245 return &per, nil 246 }