Contextual Based Relationships Suggestion

Hi guys,

I’ve been reading up on how to do contextual based relationships

https://docs.fga.dev/modeling/basics/organization-context-authorization

and i’ve mocked something up that is sort of akin to what i would like to represent our objects and relationships as:

model
  schema 1.1

type user

type organization
  relations
    define base_org_viewer: [user] or org_viewer
    define base_org_writer: [user] or org_writer
    define has_view_access: base_org_viewer and user_in_context
    define has_write_access: base_org_writer and user_in_context
    define member: [user]
    define org_viewer: [user] and user_in_context
    define org_writer: [user] and user_in_context
    define user_in_context: [user]

type subscription
  relations
    define can_view: viewer
    define can_write: writer
    define subscriber: [organization]
    define viewer: has_view_access from subscriber
    define writer: has_write_access from subscriber

type application
  relations
    define can_view: can_view from subscription
    define can_write: can_write from subscription
    define subscription: [subscription]

so in this case, if a user is part of an organization that has view access and the organization has a relationship with a subscription that also has a relationship with the application that the user is trying to view, then the it should return allow.

i ran this snippet of code while passing the context into the contextTuple and i am getting the expected response of true

const fgaClient = new OpenFgaClient({
    apiUrl: FGA_API_URL,
    storeId: FGA_STORE_ID,
    authorizationModelId: FGA_MODEL_ID,
    credentials: {
      method: CredentialsMethod.ClientCredentials,
      config: {
        apiTokenIssuer: FGA_API_TOKEN_ISSUER,
        apiAudience: FGA_API_AUDIENCE,
        clientId: FGA_CLIENT_ID,
        clientSecret: FGA_CLIENT_SECRET,
      },
    }
});

const check = async () => {
    const { allowed } = await fgaClient.check({
        user: 'user:anne',
        relation: 'can_view',
        object: 'application:app1',
        contextualTuples: [
          {"_description":"Anne is authorizing from the context of organization:A","user":"user:anne","relation":"user_in_context","object":"organization:org1"}
        ],
      }, {
      authorization_model_id: FGA_MODEL_ID,
    });

    console.log(allowed);

}

check()

// logs out true

the tuples i used are :

 await fgaClient.write({
    writes: [
        // Anne has a `base_org_viewer` role in org1
        {"_description":"Anne has a `base_org_viewer` role at org1","user":"user:anne","relation":"base_org_viewer","object":"organization:org1"},
        // org1 has a subscription to subscription1
        {"_description":"org1 has a subscription to subscription1","user":"organization:org1","relation":"subscriber","object":"subscription:subscription1"},
        // subscription1 has a relation to app 1
        {"_description":"subscription1 has a relation to app 1","user":"subscription:subscription1","relation":"subscription","object":"application:app1"},
    ],
  }, {
    authorization_model_id: FGA_MODEL_ID
  });

My question was whether or not there was a way to simplify my schema as this seems a bit confusing to me when reading it as there feels like a lot of duplications in organization, subscription and application model

Thanks

Hi!

It seems you can simplify the organization type. Unless I’m missing something this would be equivalent:


  type organization
    relations
      define org_viewer: [user] and user_in_context
      define org_writer: [user] and user_in_context
      define user_in_context: [user]
      
      define has_view_access: org_viewer 
      define has_write_access: org_writer 

You can also simplify the subscription type:

type subscription
    relations
      define subscriber: [organization]
      define can_view: has_view_access from subscriber
      define can_write: has_write_access from subscriber

Let me know if it helps, thanks!

Hey Andres! it worked like a charm! thank you very much :smiley:

I was wondering if i could ask for your input in regards to extending my schema with two new models.

I am trying to see if i can introduce the concept of api keys as a model and additional definition in subscription whereby in subscription, i am able to check if an api key is valid based on if the key is active or not. I’ve created a seperate model called active_keys but i am not sure if this is the best approach.

model
  schema 1.1

type user

type organization
  relations
    define has_view_access: org_viewer
    define has_write_access: org_writer
    define org_viewer: [user] and user_in_context
    define org_writer: [user] and user_in_context
    define user_in_context: [user]

type active_key
  relations
    define active_key: [api_key]
    define key_in_context: [api_key]
    define key_is_active: active_key and key_in_context

type api_key
  relations
    define owner: [user]

type subscription
  relations
    define active_key: [active_key]
    define can_view: has_view_access from subscriber
    define can_write: has_write_access from subscriber
    define is_valid_api_key: key_is_active from active_key
    define subscriber: [organization]

type application
  relations
    define can_view: can_view from subscription
    define can_write: can_write from subscription
    define subscription: [subscription]

type api_bundle
  relations
    define has_access: can_view from subscription or is_valid_api_key from subscription
    define subscription: [subscription]

type api_resource
  relations
    define api_bundle: [api_bundle]
    define has_access: has_access from api_bundle

The problem here it seems is that i had to pass additional contexual tuples into the check call and i was wondering if this could somehow be avoided? and if there was just a way to do so without having to know the schema of the fga models? If there were a simpler way of doing this it would be greatly appreciated if you could guide me on the ways :smiley:

const check = async () => {
    const response = await fgaClient.check({
        user: 'api_key:api_key_1',
        relation: 'has_access',
        object: 'api_resource:api_resource_1',
        contextualTuples: [
          {"_description":"api_key_1 authorized via context of active key to subscription","user":"api_key:api_key_1","relation":"key_in_context","object":"active_key:active_key_1"},
          {"_description":"api_key_1 authorized via context of active key -> subscription -> api_bundle -> api_resources","user":"subscription:subscription_1","relation":"subscription","object":"api_bundle:api_bundle_1"}
        ],
      }, {
      authorization_model_id: FGA_MODEL_ID,
    });

    console.log(response);

}

This is my seed for the tuples


const seed = async () => {
  await fgaClient.write({
    writes: [
      // Anne has a `base_org_viewer` role in org1
      {"_description":"Anne has a `org_viewer` role at org1","user":"user:anne","relation":"org_viewer","object":"organization:org1"},
      // org1 has a subscription to subscription1
      {"_description":"org1 has a subscription to subscription1","user":"organization:org1","relation":"subscriber","object":"subscription:subscription1"},
      // subscription1 has a relation to app 1
      {"_description":"subscription1 has a relation to app 1","user":"subscription:subscription1","relation":"subscription","object":"application:app1"},
      {"_description":"subscription1 has a relation to api_bundle_1","user":"subscription:subscription1","relation":"subscription","object":"api_bundle:api_bundle_1"},
      {"_description":"api_bundle_1 has a relation to api_resource_1","user":"api_bundle:api_bundle_1","relation":"api_bundle","object":"api_resource:api_resource_1"},
      //----
      {"_description":"Anne has a `org_viewer` role at org2","user":"user:anne","relation":"org_viewer","object":"organization:org2"},
      {"_description":"org2 has a subscription to subscription2","user":"organization:org2","relation":"subscriber","object":"subscription:subscription2"},
      {"_description":"subscription2 has a relation to api_bundle_2","user":"subscription:subscription2","relation":"subscription","object":"api_bundle:api_bundle_2"},
      {"_description":"api_bundle_2 has a relation to api_resource_2","user":"api_bundle:api_bundle_2","relation":"api_bundle","object":"api_resource:api_resource_2"},
      //-----
      {"_description":"api_key_1 has a relation to active_key","user":"api_key:api_key_1","relation":"active_key","object":"active_key:active_key_1"},
      {"_description":"api_key_1 has a relation to subscription_1","user":"active_key:active_key_1","relation":"active_key","object":"subscription:subscription_1"},
    ],
  }, {
    authorization_model_id: FGA_MODEL_ID
  });
}

A few comments/questions:

  • You can considering deleting inactive keys to simplify the model.
  • Why do you need ‘key_in_context’?
  • Can you elaborate on what api_bundle/api_resources are?
  • Shouldn’t { “user”:“subscription:subscription_1”,“relation”:“subscription”,“object”:“api_bundle:api_bundle_1”} be a tuple that’s written to FGA?

Hey Andres!

Thanks for taking your time to assist and sorry about that, let me add more context!

So i was hoping to be able to establish a relationship between a user and an app / api_resource in which i would be able to perform the following check and ideally without having to pass in any contextual tuples (if possible)

const check = async () => {

// does api_key_1 have access to api_resource_1

    const response = await fgaClient.check({
        user: 'api_key:api_key_1',
        relation: 'has_access',
        object: 'api_resource:api_resource_1',
        contextualTuples: [
        ],
      }, {
      authorization_model_id: FGA_MODEL_ID,
    });

    console.log(response);
}

-----

const check = async () => {

// does user have access to api_resource_1

    const response = await fgaClient.check({
        user: 'user:user_1',
        relation: 'has_access',
        object: 'api_resource:api_resource_1',
        contextualTuples: [
        ],
      }, {
      authorization_model_id: FGA_MODEL_ID,
    });

    console.log(response);
}


----
const check = async () => {

// does user have access to app_1

    const response = await fgaClient.check({
        user: 'user:user_1',
        relation: 'has_access',
        object: 'application:application_1',
        contextualTuples: [
        ],
      }, {
      authorization_model_id: FGA_MODEL_ID,
    });

    console.log(response);
}

However, i was hoping to be able to perform this check via an indirect relationship through a few other models of which have the following relationship:

  • Users are members of organizations
  • An organization can have subscriptions
  • Subscriptions can have application relations as well as api_bundle relations
  • Api_bundles are a group of api_resources
  • Api Keys are owned by users and belong to a subscription from an organization

For api_bundles and api_resources; an api resource is an endpoint which a user can call and an api_bundle are just groupings of api_resources.

As such :
A user: user_1 will have access to application: application_1 if they are part of an organization: organization_1 that has a subscription: subscription_1 which has a relationship to application: application_1

A user: user_1 will have access to api_resource: api_resource_1 if they are part of an organization: organization_1 that has a subscription: subscription_9 which has a relationship with api_bundle: api_bundle_1 which includes api_resource: api_resource_1

An api_key: api_key_1 will have access to api_resource_1 if they have a relation to a subscription: subscription_9 that has a relationship with api_bundle: api_bundle_1 which includes api_resource: api_resource_1

I hope that clarifies things and thanks again for helping :smiley: