OpenFGA Authorization Bypass
Description
OpenFGA is a high-performance authorization/permission engine inspired by Google Zanzibar. Versions prior to 0.2.5 are vulnerable to authorization bypass under certain conditions. You are affected by this vulnerability if you added a tuple with a wildcard (*) assigned to a tupleset relation (the right hand side of a ‘from’ statement). This issue has been patched in version v0.2.5. This update is not backward compatible with any authorization model that uses wildcard on a tupleset relation.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/openfga/openfgaGo | < 0.2.5 | 0.2.5 |
Affected products
1Patches
1776e80505e8dMerge pull request from GHSA-3gfj-fxx4-f22w
11 files changed · +609 −88
pkg/tuple/validation.go+0 −1 modified@@ -57,7 +57,6 @@ func (i *IndirectWriteError) Error() string { return fmt.Sprintf("Cannot write tuple '%s'. Reason: %s", i.TupleKey, i.Reason) } -// ValidateUser returns whether the user is valid. If not, return error func ValidateUser(tk *openfgapb.TupleKey) error { if !IsValidUser(tk.GetUser()) { return &InvalidTupleError{Reason: "the 'user' field is invalid", TupleKey: tk}
pkg/typesystem/typesystem.go+45 −0 modified@@ -545,6 +545,7 @@ func isUsersetRewriteValid( func validateRelationTypeRestrictions(model *openfgapb.AuthorizationModel) error { t := New(model) + allTupleToUsersetDefinitions := t.GetAllTupleToUsersetsDefinitions() for objectType := range t.typeDefinitions { relations, err := t.GetRelations(objectType) @@ -576,6 +577,15 @@ func validateRelationTypeRestrictions(model *openfgapb.AuthorizationModel) error if _, err := t.GetRelation(relatedObjectType, relatedRelation); err != nil { return InvalidRelationTypeError(objectType, name, relatedObjectType, relatedRelation) } + + // you cannot specify a userset if the relation is being used in a `x from y` definition (in the `y` part) + for _, arrayOfTtus := range allTupleToUsersetDefinitions[objectType] { + for _, tupleToUserSetDef := range arrayOfTtus { + if tupleToUserSetDef.Tupleset.Relation == name { + return &InvalidRelationError{ObjectType: objectType, Relation: name} + } + } + } } } } @@ -725,3 +735,38 @@ func InvalidRelationTypeError(objectType, relation, relatedObjectType, relatedRe return fmt.Errorf("the relation type '%s' on '%s' in object type '%s' is not valid", relationType, relation, objectType) } + +// GetAllTupleToUsersetsDefinitions returns a map where the key is the object type and the value +// is another map where key=relationName, value=list of tuple to usersets declared in that relation +func (t *TypeSystem) GetAllTupleToUsersetsDefinitions() map[string]map[string][]*openfgapb.TupleToUserset { + response := make(map[string]map[string][]*openfgapb.TupleToUserset, 0) + for typeName, typeDef := range t.GetTypeDefinitions() { + response[typeName] = make(map[string][]*openfgapb.TupleToUserset, 0) + for relationName, relationDef := range typeDef.GetRelations() { + ttus := make([]*openfgapb.TupleToUserset, 0) + response[typeName][relationName] = t.getAllTupleToUsersetsDefinitions(relationDef, &ttus) + } + } + return response +} + +func (t *TypeSystem) getAllTupleToUsersetsDefinitions(relationDef *openfgapb.Userset, resp *[]*openfgapb.TupleToUserset) []*openfgapb.TupleToUserset { + if relationDef.GetTupleToUserset() != nil { + *resp = append(*resp, relationDef.GetTupleToUserset()) + } + if relationDef.GetUnion() != nil { + for _, child := range relationDef.GetUnion().GetChild() { + t.getAllTupleToUsersetsDefinitions(child, resp) + } + } + if relationDef.GetIntersection() != nil { + for _, child := range relationDef.GetIntersection().GetChild() { + t.getAllTupleToUsersetsDefinitions(child, resp) + } + } + if relationDef.GetDifference() != nil { + t.getAllTupleToUsersetsDefinitions(relationDef.GetDifference().GetBase(), resp) + t.getAllTupleToUsersetsDefinitions(relationDef.GetDifference().GetSubtract(), resp) + } + return *resp +}
pkg/typesystem/typesystem_test.go+115 −0 modified@@ -868,6 +868,121 @@ func TestInvalidRelationTypeRestrictionsValidations(t *testing.T) { }, err: NonAssignableRelationError("document", "reader"), }, + { + name: "userset specified as allowed type, but the relation is used in a TTU rewrite", + model: &openfgapb.AuthorizationModel{ + SchemaVersion: SchemaVersion1_1, + TypeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "user", + }, + { + Type: "folder", + Relations: map[string]*openfgapb.Userset{ + "member": This(), + }, + Metadata: &openfgapb.Metadata{ + Relations: map[string]*openfgapb.RelationMetadata{ + "member": { + DirectlyRelatedUserTypes: []*openfgapb.RelationReference{ + { + Type: "user", + }, + }, + }, + }, + }, + }, + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": This(), + "can_view": TupleToUserset("parent", "member"), + }, + Metadata: &openfgapb.Metadata{ + Relations: map[string]*openfgapb.RelationMetadata{ + "parent": { + DirectlyRelatedUserTypes: []*openfgapb.RelationReference{ + { + Type: "folder", + Relation: "member", //this isn't allowed + }, + }, + }, + }, + }, + }, + }, + }, + err: &InvalidRelationError{ObjectType: "document", Relation: "parent"}, + }, + { + name: "userset specified as allowed type, but the relation is used in a TTU rewrite included in a union", + model: &openfgapb.AuthorizationModel{ + SchemaVersion: SchemaVersion1_1, + TypeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "user", + }, + { + Type: "folder", + Relations: map[string]*openfgapb.Userset{ + "parent": This(), + "viewer": This(), + }, + Metadata: &openfgapb.Metadata{ + Relations: map[string]*openfgapb.RelationMetadata{ + "parent": { + DirectlyRelatedUserTypes: []*openfgapb.RelationReference{ + { + Type: "folder", + }, + }, + }, + "viewer": { + DirectlyRelatedUserTypes: []*openfgapb.RelationReference{ + { + Type: "user", + }, + }, + }, + }, + }, + }, + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": This(), + "viewer": Union(TupleToUserset("parent", "viewer"), This()), + }, + Metadata: &openfgapb.Metadata{ + Relations: map[string]*openfgapb.RelationMetadata{ + "parent": { + DirectlyRelatedUserTypes: []*openfgapb.RelationReference{ + { + Type: "folder", + Relation: "parent", // this isn't allowed + }, + }, + }, + "viewer": { + DirectlyRelatedUserTypes: []*openfgapb.RelationReference{ + { + Type: "folder", + Relation: "parent", + }, + { + Type: "user", + }, + }, + }, + }, + }, + }, + }, + }, + err: &InvalidRelationError{ObjectType: "document", Relation: "parent"}, + }, } for _, test := range tests {
server/commands/check.go+48 −17 modified@@ -104,6 +104,7 @@ func (query *CheckQuery) Execute(ctx context.Context, req *openfgapb.CheckReques }, nil } +// getTypeDefinitionRelationUsersets validates a tuple and returns the userset corresponding to the "object" and "relation" func (query *CheckQuery) getTypeDefinitionRelationUsersets(ctx context.Context, rc *resolutionContext) (*openfgapb.Userset, error) { ctx, span := query.tracer.Start(ctx, "getTypeDefinitionRelationUsersets") defer span.End() @@ -452,6 +453,27 @@ func (query *CheckQuery) resolveDifference( return nil } +// Given this auth model: +// +// type document +// relations +// define parent as self +// define viewer as reader from parent +// type folder +// relations +// define reader as self +// +// and this rc.tk: +// +// document:budget#viewer@anne +// +// and these tuples: +// +// folder:budgets#reader@anne +// document:budget#parent@folder:budget +// +// resolveTupleToUserset first finds all the entities that are related to "document:budget" via the "parent" relation +// and then, for each of those (in this case "folder:budgets"), checks the rc.tk.User (anne) against the "reader" relation of that entity func (query *CheckQuery) resolveTupleToUserset( ctx context.Context, rc *resolutionContext, @@ -463,7 +485,7 @@ func (query *CheckQuery) resolveTupleToUserset( relation = rc.tk.GetRelation() } - findTK := tupleUtils.NewTupleKey(rc.tk.GetObject(), relation, "") + findTK := tupleUtils.NewTupleKey(rc.tk.GetObject(), relation, "") //findTk=document:budget#parent@ tracer := rc.tracer.AppendTupleToUserset().AppendString(tupleUtils.ToObjectRelationString(findTK.GetObject(), relation)) iter, err := rc.read(ctx, query.datastore, findTK) @@ -490,14 +512,15 @@ func (query *CheckQuery) resolveTupleToUserset( break // the user was resolved already, avoid launching extra lookups } - userObj, userRel := tupleUtils.SplitObjectRelation(tuple.GetUser()) + userObj, userRel := tupleUtils.SplitObjectRelation(tuple.GetUser()) // userObj=folder:budgets, userRel="" + + objectType, _ := tupleUtils.SplitObject(rc.tk.GetObject()) if userObj == Wildcard { - objectType, _ := tupleUtils.SplitObject(rc.tk.GetObject()) query.logger.WarnWithContext( ctx, - fmt.Sprintf("unexpected wildcard evaluated on tupleset relation '%s'", relation), + fmt.Sprintf("unexpected wildcard evaluated on tupleset relation '%s#%s'", objectType, relation), zap.String("store_id", rc.store), zap.String("authorization_model_id", rc.modelID), zap.String("object_type", objectType), @@ -509,27 +532,35 @@ func (query *CheckQuery) resolveTupleToUserset( ) } + if tupleUtils.UserSet == tupleUtils.GetUserTypeFromUser(tuple.GetUser()) { + query.logger.WarnWithContext( + ctx, + fmt.Sprintf("unexpected userset evaluated on tupleset relation '%s#%s'", objectType, relation), + zap.String("store_id", rc.store), + zap.String("authorization_model_id", rc.modelID), + zap.String("object_type", objectType), + ) + + return serverErrors.InvalidTuple( + fmt.Sprintf("unexpected userset evaluated on relation '%s#%s'", tupleUtils.GetType(rc.tk.GetObject()), relation), + tupleUtils.NewTupleKey(tuple.GetObject(), relation, tuple.GetUser()), + ) + } + if !tupleUtils.IsValidObject(userObj) { continue // TupleToUserset tuplesets should be of the form 'objectType:id' or 'objectType:id#relation' but are not guaranteed to be because it is neither a user or userset } - usersetRel := node.TupleToUserset.GetComputedUserset().GetRelation() + usersetRel := node.TupleToUserset.GetComputedUserset().GetRelation() //reader - // userRel may be empty, and in this case we set it to usersetRel. if userRel == "" { - userRel = usersetRel - } - // We only proceed in the case that userRel == usersetRel (=node.TupleToUserset.GetComputedUserset().GetRelation()). - if userRel != usersetRel { - continue + userRel = usersetRel // userRel=reader } tupleKey := &openfgapb.TupleKey{ - // user from previous lookup - Object: userObj, - Relation: userRel, - // original tk user - User: rc.tk.GetUser(), + Object: userObj, //folder:budgets + Relation: userRel, //reader + User: rc.tk.GetUser(), //anne } tracer := tracer.AppendString(tupleUtils.ToObjectRelationString(userObj, userRel)) nestedRC := rc.fork(tupleKey, tracer, false) @@ -538,7 +569,7 @@ func (query *CheckQuery) resolveTupleToUserset( go func(c chan<- *chanResolveResult) { defer wg.Done() - userset, err := query.getTypeDefinitionRelationUsersets(ctx, nestedRC) + userset, err := query.getTypeDefinitionRelationUsersets(ctx, nestedRC) // folder:budgets#reader if err == nil { err = query.resolveNode(ctx, nestedRC, userset, typesys) }
server/commands/expand.go+8 −4 modified@@ -258,17 +258,21 @@ func (query *ExpandQuery) resolveTupleToUserset( continue } + userType := tupleUtils.GetUserTypeFromUser(user) + if userType == tupleUtils.UserSet { + return nil, serverErrors.InvalidTuple( + fmt.Sprintf("unexpected userset evaluated on relation '%s#%s'", objectType, tupleset), + tupleUtils.NewTupleKey(targetObject, tupleset, user), + ) + } + tObject, tRelation := tupleUtils.SplitObjectRelation(user) // We only proceed in the case that tRelation == userset.GetComputedUserset().GetRelation(). // tRelation may be empty, and in this case, we set it to userset.GetComputedUserset().GetRelation(). if tRelation == "" { tRelation = userset.GetComputedUserset().GetRelation() } - if tRelation != userset.GetComputedUserset().GetRelation() { - continue - } - cs := &openfgapb.TupleKey{ Object: tObject, Relation: tRelation,
server/commands/write.go+25 −0 modified@@ -84,6 +84,10 @@ func (c *WriteCommand) validateTuplesets(ctx context.Context, req *openfgapb.Wri return serverErrors.HandleTupleValidateError(&tupleUtils.IndirectWriteError{Reason: IndirectWriteErrorReason, TupleKey: tk}) } + if err := c.validateNoUsersetForRelationReferencedInTupleset(authModel, tk); err != nil { + return err + } + if err := c.validateTypesForTuple(authModel, tk); err != nil { return err } @@ -103,6 +107,27 @@ func (c *WriteCommand) validateTuplesets(ctx context.Context, req *openfgapb.Wri return nil } +func (c *WriteCommand) validateNoUsersetForRelationReferencedInTupleset(authModel *openfgapb.AuthorizationModel, tk *openfgapb.TupleKey) error { + if !tupleUtils.IsObjectRelation(tk.GetUser()) { + return nil + } + + objType := tupleUtils.GetType(tk.GetObject()) + + // at this point we know tk.User is a userset + // if tk.Relation is used in a `x from y` definition (in the `y` part), throw an error + ts := typesystem.New(authModel) + for _, arrayOfTtus := range ts.GetAllTupleToUsersetsDefinitions()[objType] { + for _, tupleToUserSetDef := range arrayOfTtus { + if tupleToUserSetDef.Tupleset.Relation == tk.Relation { + return serverErrors.InvalidTuple(fmt.Sprintf("Userset '%s' is not allowed to have relation '%s' with '%s'", tk.User, tk.Relation, tk.Object), tk) + } + } + } + + return nil +} + // validateTypesForTuple makes sure that when writing a tuple, the types are compatible. // 1. If the tuple is of the form (user=person:bob, relation=reader, object=doc:budget), then the type "doc", relation "reader" must allow type "person". // 2. If the tuple is of the form (user=group:abc#member, relation=reader, object=doc:budget), then the type "doc", relation "reader" must allow type "group", relation "member".
server/test/check.go+86 −10 modified@@ -764,7 +764,7 @@ func TestCheckQuery(t *testing.T, datastore storage.OpenFGADatastore) { }, }, { - name: "ExecuteReturnsAllowedForTupleToUserset", + name: "Error if userset encountered in tupleset relation of a TTU definition", typeDefinitions: []*openfgapb.TypeDefinition{ { Type: "repo", @@ -779,7 +779,6 @@ func TestCheckQuery(t *testing.T, datastore storage.OpenFGADatastore) { Relation: "manager", }, ComputedUserset: &openfgapb.ObjectRelation{ - Object: "$TUPLE_USERSET_OBJECT", Relation: "repo_admin", }, }}}, @@ -789,26 +788,26 @@ func TestCheckQuery(t *testing.T, datastore storage.OpenFGADatastore) { }, }, { - Type: "org", + Type: "group", Relations: map[string]*openfgapb.Userset{ - // implicit direct? - "repo_admin": {}, + "member": typesystem.This(), }, }, }, tuples: []*openfgapb.TupleKey{ tuple.NewTupleKey("repo:openfga/canaveral", "manager", "org:openfga#repo_admin"), - tuple.NewTupleKey("org:openfga", "repo_admin", "github|jose@openfga"), + tuple.NewTupleKey("org:openfga", "repo_admin", "group:eng#member"), + tuple.NewTupleKey("group:eng", "member", "github|jose@openfga"), }, resolveNodeLimit: defaultResolveNodeLimit, request: &openfgapb.CheckRequest{ TupleKey: tuple.NewTupleKey("repo:openfga/canaveral", "admin", "github|jose@openfga"), Trace: true, }, - response: &openfgapb.CheckResponse{ - Allowed: true, - Resolution: ".union.1(tuple-to-userset).repo:openfga/canaveral#manager.org:openfga#repo_admin.(direct).", - }, + err: serverErrors.InvalidTuple( + "unexpected userset evaluated on relation 'repo#manager'", + tuple.NewTupleKey("repo:openfga/canaveral", "manager", "org:openfga#repo_admin"), + ), }, { name: "ExecuteCanResolveRecursiveComputedUserSets", @@ -1288,6 +1287,83 @@ func TestCheckQuery(t *testing.T, datastore storage.OpenFGADatastore) { errors.New("unexpected rewrite on relation 'document#parent'"), ), }, + { + //type org + // relations + // define viewer as self + // define can_view as viewer + //type document + // relations + // define parent as self + // define viewer as viewer from parent + name: "Fails if expanding the computed userset of a tupleToUserset rewrite", + resolveNodeLimit: defaultResolveNodeLimit, + typeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "viewer": typesystem.TupleToUserset("parent", "viewer"), + }, + }, + { + Type: "org", + Relations: map[string]*openfgapb.Userset{ + "viewer": typesystem.This(), + "can_view": typesystem.ComputedUserset("viewer"), + }, + }, + }, + tuples: []*openfgapb.TupleKey{ + tuple.NewTupleKey("org:x", "viewer", "org:y"), + tuple.NewTupleKey("document:1", "parent", "org:y#can_view"), + tuple.NewTupleKey("document:1", "parent", "org:z#can_view"), //not relevant + }, + request: &openfgapb.CheckRequest{ + TupleKey: tuple.NewTupleKey("document:1", "viewer", "org:y"), + ContextualTuples: &openfgapb.ContextualTupleKeys{}, + }, + err: serverErrors.InvalidTuple( + "unexpected userset evaluated on relation 'document#parent'", + tuple.NewTupleKey("document:1", "parent", "org:y#can_view"), + ), + }, + { + //type org + // relations + // define viewer as self + // define can_view as viewer + //type document + // relations + // define parent as self + // define viewer as viewer from parent + name: "Fails if expanding the computed userset of a tupleToUserset rewrite", + resolveNodeLimit: defaultResolveNodeLimit, + typeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "viewer": typesystem.Union( + typesystem.This(), + typesystem.TupleToUserset("parent", "viewer"), + ), + }, + }, + }, + tuples: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:1", "parent", "document:2#viewer"), + tuple.NewTupleKey("document:2", "viewer", "jon"), + }, + request: &openfgapb.CheckRequest{ + TupleKey: tuple.NewTupleKey("document:1", "viewer", "org:y"), + ContextualTuples: &openfgapb.ContextualTupleKeys{}, + }, + err: serverErrors.InvalidTuple( + "unexpected userset evaluated on relation 'document#parent'", + tuple.NewTupleKey("document:1", "parent", "document:2#viewer"), + ), + }, } ctx := context.Background()
server/test/expand.go+89 −0 modified@@ -706,6 +706,40 @@ func TestExpandQuery(t *testing.T, datastore storage.OpenFGADatastore) { }, }, }, + { + name: "Tuple involving userset that is not involved in TTU rewrite", + typeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "editor": typesystem.This(), + }, + }, + }, + tuples: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:1", "parent", "document:2#editor"), + }, + request: &openfgapb.ExpandRequest{ + TupleKey: tuple.NewTupleKey("document:1", "parent", ""), + }, + expected: &openfgapb.ExpandResponse{ + Tree: &openfgapb.UsersetTree{ + Root: &openfgapb.UsersetTree_Node{ + Name: "document:1#parent", + Value: &openfgapb.UsersetTree_Node_Leaf{ + Leaf: &openfgapb.UsersetTree_Leaf{ + Value: &openfgapb.UsersetTree_Leaf_Users{ + Users: &openfgapb.UsersetTree_Users{ + Users: []string{"document:2#editor"}, + }, + }, + }, + }, + }, + }, + }, + }, } require := require.New(t) @@ -858,6 +892,61 @@ func TestExpandQueryErrors(t *testing.T, datastore storage.OpenFGADatastore) { errors.Errorf("unexpected rewrite on relation '%s#%s'", "document", "parent"), ), }, + { + name: "Tuple involving userset returns error if it is referenced in a TTU rewrite", + typeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "folder", + Relations: map[string]*openfgapb.Userset{ + "viewer": typesystem.This(), + }, + }, + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "editor": typesystem.This(), + "viewer": typesystem.TupleToUserset("parent", "viewer"), + }, + }, + }, + tuples: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:1", "parent", "document:2#editor"), + }, + request: &openfgapb.ExpandRequest{ + TupleKey: tuple.NewTupleKey("document:1", "viewer", ""), + }, + expected: serverErrors.InvalidTuple( + "unexpected userset evaluated on relation 'document#parent'", + tuple.NewTupleKey("document:1", "parent", "document:2#editor"), + ), + }, + { + name: "Tuple involving userset returns error if same ComputedUserset involved in TTU rewrite", + typeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "viewer": typesystem.Union( + typesystem.This(), + typesystem.TupleToUserset("parent", "viewer"), + ), + }, + }, + }, + tuples: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:1", "parent", "document:2#viewer"), + tuple.NewTupleKey("document:2", "viewer", "jon"), + }, + request: &openfgapb.ExpandRequest{ + TupleKey: tuple.NewTupleKey("document:1", "viewer", ""), + }, + expected: serverErrors.InvalidTuple( + "unexpected userset evaluated on relation 'document#parent'", + tuple.NewTupleKey("document:1", "parent", "document:2#viewer"), + ), + }, } require := require.New(t)
server/test/write_authzmodel.go+25 −54 modified@@ -35,7 +35,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err error }{ { - name: "succeeds", + name: "succeeds with a simple model", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -49,7 +49,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor }, }, { - name: "succeeds part II", + name: "succeeds with a complex model", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: "somestoreid", TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -112,7 +112,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.ExceededEntityLimit("type definitions in an authorization model", datastore.MaxTypesInTypeDefinition()), }, { - name: "empty relations is valid", + name: "succeeds with empty relations", request: &openfgapb.WriteAuthorizationModelRequest{ TypeDefinitions: []*openfgapb.TypeDefinition{ { @@ -122,7 +122,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor }, }, { - name: "zero length relations is valid", + name: "succeeds with zero length relations", request: &openfgapb.WriteAuthorizationModelRequest{ TypeDefinitions: []*openfgapb.TypeDefinition{ { @@ -133,7 +133,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor }, }, { - name: "ExecuteWriteFailsIfSameTypeTwice", + name: "fails if the same type appears twice", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -154,7 +154,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(typesystem.ErrDuplicateTypes), }, { - name: "ExecuteWriteFailsIfEmptyRewrites", + name: "fails if a relation is not defined", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -169,7 +169,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{ObjectType: "repo", Relation: "owner"}), }, { - name: "ExecuteWriteFailsIfUnknownRelationInComputedUserset", + name: "fails if unknown relation in computed userset definition", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -191,7 +191,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.RelationUndefinedError{ObjectType: "repo", Relation: "owner"}), }, { - name: "ExecuteWriteFailsIfUnknownRelationInTupleToUserset", + name: "fails if unknown relation in tuple to userset definition (computed userset component)", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -218,7 +218,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.RelationUndefinedError{ObjectType: "", Relation: "owner"}), }, { - name: "ExecuteWriteFailsIfUnknownRelationInUnion", + name: "fails if unknown relation in union", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -253,7 +253,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.RelationUndefinedError{ObjectType: "repo", Relation: "owner"}), }, { - name: "ExecuteWriteFailsIfUnknownRelationInDifferenceBaseArgument", + name: "fails if unknown relation in difference base argument", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -288,7 +288,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.RelationUndefinedError{ObjectType: "repo", Relation: "owner"}), }, { - name: "ExecuteWriteFailsIfUnknownRelationInDifferenceSubtractArgument", + name: "fails if unknown relation in difference subtract argument", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -323,7 +323,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.RelationUndefinedError{ObjectType: "repo", Relation: "owner"}), }, { - name: "ExecuteWriteFailsIfUnknownRelationInTupleToUsersetTupleset", + name: "fails if unknown relation in tuple to userset definition (tupleset component)", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -352,36 +352,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.RelationUndefinedError{ObjectType: "repo", Relation: "owner"}), }, { - name: "ExecuteWriteFailsIfUnknownRelationInTupleToUsersetComputedUserset", - request: &openfgapb.WriteAuthorizationModelRequest{ - StoreId: storeID, - TypeDefinitions: []*openfgapb.TypeDefinition{ - { - Type: "repo", - Relations: map[string]*openfgapb.Userset{ - "writer": { - Userset: &openfgapb.Userset_This{}, - }, - "viewer": { - Userset: &openfgapb.Userset_TupleToUserset{ - TupleToUserset: &openfgapb.TupleToUserset{ - Tupleset: &openfgapb.ObjectRelation{ - Relation: "writer", - }, - ComputedUserset: &openfgapb.ObjectRelation{ - Relation: "owner", - }, - }, - }, - }, - }, - }, - }, - }, - err: serverErrors.InvalidAuthorizationModelInput(&typesystem.RelationUndefinedError{ObjectType: "", Relation: "owner"}), - }, - { - name: "ExecuteWriteFailsIfTupleToUsersetReferencesUnknownRelation", + name: "fails if unknown relation in computed userset", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -417,7 +388,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.RelationUndefinedError{ObjectType: "bar", Relation: "writer"}), }, { - name: "ExecuteWriteFailsIfUnknownRelationInIntersection", + name: "fails if unknown relation in intersection", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -450,7 +421,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.RelationUndefinedError{ObjectType: "repo", Relation: "owner"}), }, { - name: "ExecuteWriteFailsIfDifferenceIncludesSameRelationTwice", + name: "fails if difference includes same relation twice", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -480,7 +451,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{ObjectType: "repo", Relation: "viewer"}), }, { - name: "ExecuteWriteFailsIfUnionIncludesSameRelationTwice", + name: "fails if union includes same relation twice", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -507,7 +478,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{ObjectType: "repo", Relation: "viewer"}), }, { - name: "ExecuteWriteFailsIfIntersectionIncludesSameRelationTwice", + name: "fails if intersection includes same relation twice", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -533,7 +504,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{ObjectType: "repo", Relation: "viewer"}), }, { - name: "Union Rewrite Contains Repeated Definitions", + name: "Success if Union Rewrite Contains Repeated Definitions", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -556,7 +527,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor }, }, { - name: "Intersection Rewrite Contains Repeated Definitions", + name: "Success if Intersection Rewrite Contains Repeated Definitions", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -579,7 +550,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor }, }, { - name: "Exclusion Rewrite Contains Repeated Definitions", + name: "Success if Exclusion Rewrite Contains Repeated Definitions", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -602,7 +573,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor }, }, { - name: "Tupleset relation involves ComputedUserset rewrite", + name: "Fails if Tupleset relation involves ComputedUserset rewrite", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -621,7 +592,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor ), }, { - name: "Tupleset relation involves Union rewrite", + name: "Fails if Tupleset relation involves Union rewrite", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -640,7 +611,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor ), }, { - name: "Tupleset relation involves Intersection rewrite", + name: "Fails if Tupleset relation involves Intersection rewrite", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -659,7 +630,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor ), }, { - name: "Tupleset relation involves Exclusion rewrite", + name: "Fails if Tupleset relation involves Exclusion rewrite", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{ @@ -678,7 +649,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor ), }, { - name: "Tupleset relation involves TupleToUserset rewrite", + name: "Fails if Tupleset relation involves TupleToUserset rewrite", request: &openfgapb.WriteAuthorizationModelRequest{ StoreId: storeID, TypeDefinitions: []*openfgapb.TypeDefinition{
server/test/write.go+164 −0 modified@@ -1268,6 +1268,170 @@ var writeCommandTests = []writeCommandTest{ }, err: serverErrors.NewInternalError("invalid authorization model", errors.New("invalid authorization model")), }, + + { + _name: "Write fails if a. schema version is 1.0 b. user is a userset c. relation is referenced in a tupleset of a tupleToUserset relation", + model: &openfgapb.AuthorizationModel{ + Id: ulid.Make().String(), + SchemaVersion: typesystem.SchemaVersion1_0, + TypeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "folder", + Relations: map[string]*openfgapb.Userset{ + "owner": typesystem.This(), + "admin": typesystem.This(), + }, + }, + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "can_view": typesystem.TupleToUserset("parent", "owner"), //owner from parent + }, + }, + }, + }, + request: &openfgapb.WriteRequest{ + Writes: &openfgapb.TupleKeys{TupleKeys: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:budget", "parent", "folder:budgets#admin"), + }}, + }, + err: serverErrors.InvalidTuple("Userset 'folder:budgets#admin' is not allowed to have relation 'parent' with 'document:budget'", + tuple.NewTupleKey("document:budget", "parent", "folder:budgets#admin"), + ), + }, + { + _name: "Write fails if a. schema version is 1.0 b. user is a userset c. relation is referenced in a tupleset of a tupleToUserset relation (defined as union)", + model: &openfgapb.AuthorizationModel{ + Id: ulid.Make().String(), + SchemaVersion: typesystem.SchemaVersion1_0, + TypeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "folder", + Relations: map[string]*openfgapb.Userset{ + "owner": typesystem.This(), + "admin": typesystem.This(), + }, + }, + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "can_view": typesystem.Union( + typesystem.TupleToUserset("parent", "owner"), //owner from parent + typesystem.TupleToUserset("parent", "admin"), //admin from parent + ), + }, + }, + }, + }, + request: &openfgapb.WriteRequest{ + Writes: &openfgapb.TupleKeys{TupleKeys: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:budget", "parent", "folder:budgets#admin"), + }}, + }, + err: serverErrors.InvalidTuple("Userset 'folder:budgets#admin' is not allowed to have relation 'parent' with 'document:budget'", + tuple.NewTupleKey("document:budget", "parent", "folder:budgets#admin"), + ), + }, + { + _name: "Write fails if a. schema version is 1.0 b. user is a userset c. relation is referenced in a tupleset of a tupleToUserset relation (defined as intersection)", + model: &openfgapb.AuthorizationModel{ + Id: ulid.Make().String(), + SchemaVersion: typesystem.SchemaVersion1_0, + TypeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "folder", + Relations: map[string]*openfgapb.Userset{ + "owner": typesystem.This(), + "admin": typesystem.This(), + }, + }, + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "can_view": typesystem.Intersection( + typesystem.TupleToUserset("parent", "owner"), //owner from parent + typesystem.TupleToUserset("parent", "admin"), //admin from parent + ), + }, + }, + }, + }, + request: &openfgapb.WriteRequest{ + Writes: &openfgapb.TupleKeys{TupleKeys: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:budget", "parent", "folder:budgets#admin"), + }}, + }, + err: serverErrors.InvalidTuple("Userset 'folder:budgets#admin' is not allowed to have relation 'parent' with 'document:budget'", + tuple.NewTupleKey("document:budget", "parent", "folder:budgets#admin"), + ), + }, + { + _name: "Write fails if a. schema version is 1.0 b. user is a userset c. relation is referenced in a tupleset of a tupleToUserset relation (defined as difference)", + model: &openfgapb.AuthorizationModel{ + Id: ulid.Make().String(), + SchemaVersion: typesystem.SchemaVersion1_0, + TypeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "folder", + Relations: map[string]*openfgapb.Userset{ + "owner": typesystem.This(), + "admin": typesystem.This(), + }, + }, + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "can_view": typesystem.Difference( + typesystem.TupleToUserset("parent", "owner"), //owner from parent + typesystem.TupleToUserset("parent", "admin"), //admin from parent + ), + }, + }, + }, + }, + request: &openfgapb.WriteRequest{ + Writes: &openfgapb.TupleKeys{TupleKeys: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:budget", "parent", "folder:budgets#admin"), + }}, + }, + err: serverErrors.InvalidTuple("Userset 'folder:budgets#admin' is not allowed to have relation 'parent' with 'document:budget'", + tuple.NewTupleKey("document:budget", "parent", "folder:budgets#admin"), + ), + }, + { + _name: "Write succeeds if a. schema version is 1.0 b. user is a userset c. relation is referenced in a tupleset of a tupleToUserset relation of another type", + model: &openfgapb.AuthorizationModel{ + Id: ulid.Make().String(), + SchemaVersion: typesystem.SchemaVersion1_0, + TypeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "folder", + Relations: map[string]*openfgapb.Userset{ + "owner": typesystem.This(), + "parent": typesystem.Union( // let's confuse the code. if this were defined in 'document' type, it would fail + typesystem.TupleToUserset("parent", "owner"), + ), + }, + }, + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "owner": typesystem.This(), + "parent": typesystem.This(), + }, + }, + }, + }, + request: &openfgapb.WriteRequest{ + Writes: &openfgapb.TupleKeys{TupleKeys: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:budget", "parent", "folder:budgets#admin"), + }}, + }, + }, } func TestWriteCommand(t *testing.T, datastore storage.OpenFGADatastore) {
server/validation/validation.go+4 −2 modified@@ -9,15 +9,17 @@ import ( openfgapb "go.buf.build/openfga/go/openfga/api/openfga/v1" ) -// ValidateTuple returns whether a *openfgapb.TupleKey is valid +// ValidateTuple checks whether a tuple is valid. If it is not, returns as error. +// If it is, it grabs the type of the tuple's object, and the tuple's relation, and returns its corresponding userset. func ValidateTuple(ctx context.Context, backend storage.TypeDefinitionReadBackend, store, authorizationModelID string, tk *openfgapb.TupleKey) (*openfgapb.Userset, error) { if err := tuple.ValidateUser(tk); err != nil { return nil, err } return ValidateObjectsRelations(ctx, backend, store, authorizationModelID, tk) } -// ValidateObjectsRelations returns whether a tuple's object and relations are valid +// ValidateObjectsRelations checks whether a tuple's object and relations are valid. If they are not, returns an error. +// If they are, it grabs the type of the tuple's object, and the tuple's relation, and returns its corresponding userset. func ValidateObjectsRelations(ctx context.Context, backend storage.TypeDefinitionReadBackend, store, modelID string, t *openfgapb.TupleKey) (*openfgapb.Userset, error) { if !tuple.IsValidRelation(t.GetRelation()) { return nil, &tuple.InvalidTupleError{Reason: "invalid relation", TupleKey: t}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-3gfj-fxx4-f22wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-39352ghsaADVISORY
- github.com/openfga/openfga/commit/776e80505e8d184b2286acc8268d8d74f36a9984ghsaWEB
- github.com/openfga/openfga/releases/tag/v0.2.5ghsaWEB
- github.com/openfga/openfga/security/advisories/GHSA-3gfj-fxx4-f22wghsaWEB
News mentions
0No linked articles in our index yet.