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  }