package blocks import ( "fmt" "regexp" "slices" "strings" yamlv3 "gopkg.in/yaml.v3" "github.com/ccoveille/go-safecast" "github.com/authzed/spicedb/pkg/spiceerrors" "github.com/authzed/spicedb/pkg/tuple" ) // ParsedExpectedRelations represents the expected relations defined in the validation // file. type ParsedExpectedRelations struct { // ValidationMap is the parsed expected relations validation map. ValidationMap ValidationMap // SourcePosition is the position of the expected relations in the file. SourcePosition spiceerrors.SourcePosition } // UnmarshalYAML is a custom unmarshaller. func (per *ParsedExpectedRelations) UnmarshalYAML(node *yamlv3.Node) error { err := node.Decode(&per.ValidationMap) if err != nil { return convertYamlError(err) } per.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} return nil } // ValidationMap is a map from an Object Relation (as a Relationship) to the // validation strings containing the Subjects for that Object Relation. type ValidationMap map[ObjectRelation][]ExpectedSubject // ObjectRelation represents an ONR defined as a string in the key for // the ValidationMap. type ObjectRelation struct { // ObjectRelationString is the string form of the object relation. ObjectRelationString string // ObjectAndRelation is the parsed object and relation. ObjectAndRelation tuple.ObjectAndRelation // SourcePosition is the position of the expected relations in the file. SourcePosition spiceerrors.SourcePosition } // UnmarshalYAML is a custom unmarshaller. func (ors *ObjectRelation) UnmarshalYAML(node *yamlv3.Node) error { err := node.Decode(&ors.ObjectRelationString) if err != nil { return convertYamlError(err) } line, err := safecast.ToUint64(node.Line) if err != nil { return err } column, err := safecast.ToUint64(node.Column) if err != nil { return err } parsed, err := tuple.ParseONR(ors.ObjectRelationString) if err != nil { return spiceerrors.NewWithSourceError( fmt.Errorf("could not parse %s: %w", ors.ObjectRelationString, err), ors.ObjectRelationString, line, column, ) } ors.ObjectAndRelation = parsed ors.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} return nil } var ( vsSubjectRegex = regexp.MustCompile(`(.*?)\[(?P.*)](.*?)`) vsObjectAndRelationRegex = regexp.MustCompile(`(.*?)<(?P[^>]+)>(.*?)`) vsSubjectWithExceptionsOrCaveatRegex = regexp.MustCompile(`^(?P[^]\s]+)(?P\[\.\.\.])?(\s+-\s+\{(?P[^}]+)})?$`) ) // ExpectedSubject is a subject expected for the ObjectAndRelation. type ExpectedSubject struct { // ValidationString holds a validation string containing a Subject and one or // more Relations to the parent Object. // Example: `[tenant/user:someuser#...] is ` ValidationString ValidationString // Subject is the subject expected. May be nil if not defined in the line. SubjectWithExceptions *SubjectWithExceptions // Resources are the resources under which the subject is found. Resources []tuple.ObjectAndRelation // SourcePosition is the position of the expected subject in the file. SourcePosition spiceerrors.SourcePosition } // SubjectAndCaveat returns a subject and whether it is caveated. type SubjectAndCaveat struct { // Subject is the subject found. Subject tuple.ObjectAndRelation // IsCaveated indicates whether the subject is caveated. IsCaveated bool } // SubjectWithExceptions returns the subject found in a validation string, along with any exceptions. type SubjectWithExceptions struct { // Subject is the subject found. Subject SubjectAndCaveat // Exceptions are those subjects removed from the subject, if it is a wildcard. Exceptions []SubjectAndCaveat } // UnmarshalYAML is a custom unmarshaller. func (es *ExpectedSubject) UnmarshalYAML(node *yamlv3.Node) error { err := node.Decode(&es.ValidationString) if err != nil { return convertYamlError(err) } line, err := safecast.ToUint64(node.Line) if err != nil { return err } column, err := safecast.ToUint64(node.Column) if err != nil { return err } subjectWithExceptions, subErr := es.ValidationString.Subject() if subErr != nil { return spiceerrors.NewWithSourceError( subErr, subErr.SourceCodeString, line+subErr.LineNumber, column+subErr.ColumnPosition, ) } onrs, onrErr := es.ValidationString.ONRS() if onrErr != nil { return spiceerrors.NewWithSourceError( onrErr, onrErr.SourceCodeString, line+onrErr.LineNumber, column+onrErr.ColumnPosition, ) } es.SubjectWithExceptions = subjectWithExceptions es.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} es.Resources = onrs return nil } // ValidationString holds a validation string containing a Subject and one or // more Relations to the parent Object. // Example: `[tenant/user:someuser#...] is ` type ValidationString string // SubjectString returns the subject contained in the ValidationString, if any. func (vs ValidationString) SubjectString() (string, bool) { result := vsSubjectRegex.FindStringSubmatch(string(vs)) if len(result) != 4 { return "", false } return result[2], true } // Subject returns the subject contained in the ValidationString, if any. If // none, returns nil. func (vs ValidationString) Subject() (*SubjectWithExceptions, *spiceerrors.WithSourceError) { subjectStr, ok := vs.SubjectString() if !ok { return nil, nil } subjectStr = strings.TrimSpace(subjectStr) groups := vsSubjectWithExceptionsOrCaveatRegex.FindStringSubmatch(subjectStr) if len(groups) == 0 { bracketedSubjectString := "[" + subjectStr + "]" return nil, spiceerrors.NewWithSourceError(fmt.Errorf("invalid subject: `%s`", subjectStr), bracketedSubjectString, 0, 0) } subjectONRString := groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "subject_onr")] subjectONR, err := tuple.ParseSubjectONR(subjectONRString) if err != nil { return nil, spiceerrors.NewWithSourceError(fmt.Errorf("invalid subject: `%s`: %w", subjectONRString, err), subjectONRString, 0, 0) } exceptionsString := strings.TrimSpace(groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "exceptions")]) var exceptions []SubjectAndCaveat if len(exceptionsString) > 0 { exceptionsStringsSlice := strings.Split(exceptionsString, ",") exceptions = make([]SubjectAndCaveat, 0, len(exceptionsStringsSlice)) for _, exceptionString := range exceptionsStringsSlice { isCaveated := false if strings.HasSuffix(exceptionString, "[...]") { exceptionString = strings.TrimSuffix(exceptionString, "[...]") isCaveated = true } exceptionONR, err := tuple.ParseSubjectONR(strings.TrimSpace(exceptionString)) if err != nil { return nil, spiceerrors.NewWithSourceError(fmt.Errorf("invalid subject: `%s`: %w", exceptionString, err), exceptionString, 0, 0) } exceptions = append(exceptions, SubjectAndCaveat{exceptionONR, isCaveated}) } } isCaveated := len(strings.TrimSpace(groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "caveat")])) > 0 return &SubjectWithExceptions{SubjectAndCaveat{subjectONR, isCaveated}, exceptions}, nil } // ONRStrings returns the ONRs contained in the ValidationString, if any. func (vs ValidationString) ONRStrings() []string { results := vsObjectAndRelationRegex.FindAllStringSubmatch(string(vs), -1) onrStrings := []string{} for _, result := range results { onrStrings = append(onrStrings, result[2]) } return onrStrings } // ONRS returns the subject ONRs in the ValidationString, if any. func (vs ValidationString) ONRS() ([]tuple.ObjectAndRelation, *spiceerrors.WithSourceError) { onrStrings := vs.ONRStrings() onrs := []tuple.ObjectAndRelation{} for _, onrString := range onrStrings { found, err := tuple.ParseONR(onrString) if err != nil { return nil, spiceerrors.NewWithSourceError(fmt.Errorf("invalid resource and relation: `%s`: %w", onrString, err), onrString, 0, 0) } onrs = append(onrs, found) } return onrs, nil } // ParseExpectedRelationsBlock parses the given contents as an expected relations block. func ParseExpectedRelationsBlock(contents []byte) (*ParsedExpectedRelations, error) { per := ParsedExpectedRelations{} err := yamlv3.Unmarshal(contents, &per) if err != nil { return nil, convertYamlError(err) } return &per, nil }