Chroma Authorization Model with OpenFGA

Source Code

The source code for this article can be found here.

This article will not provide any code that you can use immediately but will set the stage for our next article, which will introduce the actual Chroma-OpenFGA integration.

With that in mind, let’s get started.

Who is this article for? The intended audience is DevSecOps, but engineers and architects could also use this to learn about Chroma and the authorization models.

Authorization Model

Authorization models are an excellent way to abstract the way you wish your users to access your application form the actual implementation.

There are many ways to do authz, ranging from commercial Auth0 FGA to OSS options like Ory Keto/Kratos, CASBIN, Permify, and Kubescape, but for this article, we’ve decided to use OpenFGA (which technically is Auth0’s open-source framework for FGA).

Why OpenFGA, I hear you ask? Here are a few reasons:

  • Apache-2 licensed
  • CNCF Incubating project
  • Zanzibar alignment in that it is a ReBAC (Relation-based access control) system
  • DSL for modeling and testing permissions (as well as JSON-base version for those with masochistic tendencies)

OpenFGA has done a great job explaining the steps to building an Authorization model, which you can read here. We will go over those while keeping our goal of creating an authorization model for Chroma.

It is worth noting that the resulting authorization model that we will create here will be suitable for many GenAI applications, such as general-purpose RAG systems. Still, it is not a one-size-fits-all solution to all problems. For instance, if you want to implement authz in Chroma within your organization, OpenFGA might not be the right tool for the job, and you should consult with your IT/Security department for guidance on integrating with existing systems.

The Goal

Our goal is to achieve the following:

  • Allow fine-grained access to the following resources - collection, database, tenant, and Chroma server.
  • AlGrouping of users for improved permission management.
  • Individual user access to resources
  • Roles - owner, writer, reader

Document-Level Access

Although granting access to individual documents in a collection can be beneficial in some contexts, we have left that part out of our goals to keep things as simple and short as possible. If you are interested in this topic, reach out, and we will help you.

This article will not cover user management, commonly called Identity Access Management (IAM). We’ll cover that in a subsequent article.

Modeling Fundamentals

Let’s start with the fundamentals:

Why could user U perform an action A on an object O?

We will attempt to answer the question in the context of Chroma by following OpenFGA approach to refining the model. The steps are:

  1. Pick the most important features.
  2. List of object types
  3. List of relations for the types
  4. Test the model
  5. Iterate

Given that OpenFGA is Zanzibar inspired, the basic primitive for it is a tuple of the following format:

  1. (User,Relation,Object)

With the above we can express any relation between a user (or a team or even another object) the action the user performs (captured by object relations) and the object (aka API resource).

Pick the features

In the context of Chroma, the features are the actions the user can perform on Chroma API (as of this writing v0.4.24).

Let’s explore what are the actions that users can perform:

  • Create a tenant
  • Get a tenant
  • Create a database for a tenant
  • Get a database for a tenant
  • Create a collection in a database
  • Delete a collection from a database
  • Update collection name and metadata
  • List collections in a database
  • Count collections in a database
  • Add records to a collection
  • Delete records from a collection
  • Update records in a collection
  • Upsert records in a collection
  • Count records in a collection
  • Get records from a collection
  • Query records in a collection
  • Get pre-flight-checks

Open Endpoints

Note we will omit get hearbeat and get versionactions as this is generally a good idea to be open so that orchestrators (docker/k8s) can get the health status of chroma.

To make it easy to reason about relations in our authorization model we will rephrase the above to the following format:

  1. A user {user} can perform action {action} to/on/in {object types} ... IF {conditions}
  • A user can perform action create tenant on Chroma server if they are owner of the server
  • A user can perform action get tenant on Chroma server if they are a reader or writer or owner of the server
  • A user can perform action create database on a tenant if they are an owner of the tenant
  • A user can perform action get database on a tenant if they are reader, writer or owner of the tenant
  • A user can perform action create collection on a database if they are a writer or an owner of the database
  • A user can perform action delete collection on a database if they are a writer or an owner of the database
  • A user can perform action update collection name or metadata on a database if they are a writer or an owner of the database
  • A user can perform action list collections in a database if they are a writer or an owner of the database
  • A user can perform action count collections in a database if they are a writer or an owner of the database
  • A user can perform action add records on a collection if they are writer or owner of the collection
  • A user can perform action delete records on a collection if they are writer or owner of the collection
  • A user can perform action update records on a collection if they are writer or owner of the collection
  • A user can perform action upsert records on a collection if they are writer or owner of the collection
  • A user can perform action get records on a collection if they are writer or owner or reader of the collection
  • A user can perform action count records on a collection if they are writer or owner or reader of the collection
  • A user can perform action query records on a collection if they are writer or owner or reader of the collection
  • A user can perform action get pre-flight-checks on a Chroma server if they are writer or owner or reader of the server

We don’t have to get it all right in the first iteration, but the above is a good starting point that can be adapted further.

The above statements alone are already a great introspection as to what we can do within Chroma and who is supposed to be able to do what. Please note that your mileage may vary, as per your authz requirements, but in our experience the variations are generally around the who.

As an astute reader you have already noted that we’re generally outlined some RBAC stuff in the form of owner, writer and reader.

List the objects!!!

Now that we know what our users can do, let’s figure solidify our understanding of on what our users will be performing these actions, aka the object types.

Let’s call them out:

  • User - this is basic and pretty obvious object type that we want to model our users after
  • Chroma server - this is our top level object in the access relations
  • Tenant - for most Chroma developers this will equate to a team or a group
  • Database
  • Collection

We can also examine all of the of the <object> in the above statements to ensure we haven’t missed any objects. So far seems we’re all good.

Now that we have our objects let’s create a first iteration of our authorization model using OpenFGA DSL:

  1. model
  2. schema 1.1
  3. type server
  4. type user
  5. type tenant
  6. type database
  7. type collection

OpenFGA CLI

You will need to install openfga CLI - https://openfga.dev/docs/getting-started/install-sdk. Also check the VSCode extension for OpenFGA.

Let’s validate our work:

  1. fga model validate --file model-article-p1.fga

You should see the following output:

  1. {
  2. "is_valid":true
  3. }

Relations

Now that we have the actions and the objects, let us figure out the relationships we want to build into our model.

To come up with our relations we can follow these two rules:

  • Any noun of the type {noun} of a/an/the {type} expression (e.g. of the collection)
  • Any verb or action described with can {action} on/in {type}

So now let’s work on our model to expand it with relationships:

  1. model
  2. schema 1.1
  3. type user
  4. type server
  5. relations
  6. define owner: [user]
  7. define reader: [user]
  8. define writer: [user]
  9. define can_get_preflight: reader or owner or writer
  10. define can_create_tenant: owner or writer
  11. type tenant
  12. relations
  13. define owner: [user]
  14. define reader: [user]
  15. define writer: [user]
  16. define belongsTo: [server]
  17. define can_create_database: owner from belongsTo or writer from belongsTo or owner or writer
  18. define can_get_database: reader or owner or writer or owner from belongsTo or reader from belongsTo or writer from belongsTo
  19. type database
  20. relations
  21. define owner: [user]
  22. define reader: [user]
  23. define writer: [user]
  24. define belongsTo: [tenant]
  25. define can_create_collection: owner from belongsTo or writer from belongsTo or owner or writer
  26. define can_delete_collection: owner from belongsTo or writer from belongsTo or owner or writer
  27. define can_list_collections: owner or writer or owner from belongsTo or writer from belongsTo
  28. define can_get_collection: owner or writer or owner from belongsTo or writer from belongsTo
  29. define can_get_or_create_collection: owner or writer or owner from belongsTo or writer from belongsTo
  30. define can_count_collections: owner or writer or owner from belongsTo or writer from belongsTo
  31. type collection
  32. relations
  33. define owner: [user]
  34. define reader: [user]
  35. define writer: [user]
  36. define belongsTo: [database]
  37. define can_add_records: writer or reader or owner from belongsTo or writer from belongsTo
  38. define can_delete_records: writer or owner from belongsTo or writer from belongsTo
  39. define can_update_records: writer or owner from belongsTo or writer from belongsTo
  40. define can_get_records: reader or owner or writer or owner from belongsTo or reader from belongsTo or writer from belongsTo
  41. define can_upsert_records: writer or owner from belongsTo or writer from belongsTo
  42. define can_count_records: reader or owner or writer or owner from belongsTo or reader from belongsTo or writer from belongsTo
  43. define can_query_records: reader or owner or writer or owner from belongsTo or reader from belongsTo or writer from belongsTo

Let’s validated:

  1. fga model validate --file model-article-p2.fga

This seems mostly accurate and should do ok as Authorization model. But let us see if we can make it better. If we are to implement the above we will end up with lots of permissions in OpenFGA, not that it can’t handle them, but as we go into the implementation details it will become cumbersome to update and maintain all these permissions. So let’s look for opportunity to simplify things a little.

Can we make the model a little simpler and the first question we ask is do we really need owner, reader, writer on every object or can we make a decision about our model and simplify this. As it turns out we can. The way that most multi-user systems work is that they tend to gravitate to grouping things as a way to reduce the need to maintain a large number of permissions. In our case we can group our users into team and in each team we’ll have owner, writer, reader

Let’s see the results:

  1. model
  2. schema 1.1
  3. type user
  4. type team
  5. relations
  6. define owner: [user]
  7. define writer: [user]
  8. define reader: [user]
  9. type server
  10. relations
  11. define can_get_preflight: [user, team#owner, team#writer, team#reader]
  12. define can_create_tenant: [user, team#owner, team#writer]
  13. define can_get_tenant: [user, team#owner, team#writer, team#reader]
  14. type tenant
  15. relations
  16. define can_create_database: [user, team#owner, team#writer]
  17. define can_get_database: [user, team#owner, team#writer, team#reader]
  18. type database
  19. relations
  20. define can_create_collection: [user, team#owner, team#writer]
  21. define can_list_collections: [user, team#owner, team#writer, team#reader]
  22. define can_get_or_create_collection: [user, team#owner, team#writer]
  23. define can_count_collections: [user, team#owner, team#writer, team#reader]
  24. type collection
  25. relations
  26. define can_delete_collection: [user, team#owner, team#writer]
  27. define can_get_collection: [user, team#owner, team#writer, team#reader]
  28. define can_update_collection: [user, team#owner, team#writer]
  29. define can_add_records: [user, team#owner, team#writer]
  30. define can_delete_records: [user, team#owner, team#writer]
  31. define can_update_records: [user, team#owner, team#writer]
  32. define can_get_records: [user, team#owner, team#writer, team#reader]
  33. define can_upsert_records: [user, team#owner, team#writer]
  34. define can_count_records: [user, team#owner, team#writer, team#reader]
  35. define can_query_records: [user, team#owner, team#writer, team#reader]

That is arguably more readable.

As you will observe we have also added [user] in the permissions of each object, why is that you may ask. The reason is that we want to build a fine-grained authorization, which means while a collection can be belong to a team, we can also grant individual permissions to users. This gives us a great way to play around with permissions at the cost of a more complex implementation of how permissions are managed, but we will get to that in the next post.

We have also removed the belongsTo relationship as we no longer need it. Reason: OpenFGA does not allow access of relations more than a single layer into the hierarchy thus a collection cannot use the owner of its team for permissions (there are other ways to implement that outside of the scope of this article).

Let’s recap what is our model capable of doing:

  • Fine-grained access control to objects is possible via relations
  • Users can be grouped into teams (a single user per team is also acceptable for cases where you need a user to be the sole owner of a collection or a database)
  • Access to resources can be granted to individual users via object relations
  • Define roles within a team (this can be extended to allow roles per resource, but is outside of the scope of this article)

In short we have achieved the goals we have initially set, with a relatively simple and understandable model. However, does our model work? Let’s find out in the next section.

Testing the model

Luckily OpenFGA folks have provided a great developer experience by making it easy to write and run tests. This is a massive W and time-saver.

  • An individual user can be given access to specific resources via relations
  • Users can be part of any of the team roles
  • An object can access by a team
  1. name: Chroma Authorization Model Tests # optional
  2. model_file: ./model-article-p4.fga # you can specify an external .fga file, or include it inline
  3. # tuple_file: ./tuples.yaml # you can specify an external file, or include it inline
  4. tuples:
  5. - user: user:jane
  6. relation: owner
  7. object: team:chroma
  8. - user: user:john
  9. relation: writer
  10. object: team:chroma
  11. - user: user:jill
  12. relation: reader
  13. object: team:chroma
  14. - user: user:sam
  15. relation: can_create_tenant
  16. object: server:server1
  17. - user: user:sam
  18. relation: can_get_tenant
  19. object: server:server1
  20. - user: user:sam
  21. relation: can_get_preflight
  22. object: server:server1
  23. - user: user:michelle
  24. relation: can_create_tenant
  25. object: server:server1
  26. - user: team:chroma#owner
  27. relation: can_get_preflight
  28. object: server:server1
  29. - user: team:chroma#owner
  30. relation: can_create_tenant
  31. object: server:server1
  32. - user: team:chroma#owner
  33. relation: can_get_tenant
  34. object: server:server1
  35. - user: team:chroma#writer
  36. relation: can_get_preflight
  37. object: server:server1
  38. - user: team:chroma#writer
  39. relation: can_create_tenant
  40. object: server:server1
  41. - user: team:chroma#writer
  42. relation: can_get_tenant
  43. object: server:server1
  44. - user: team:chroma#reader
  45. relation: can_get_preflight
  46. object: server:server1
  47. - user: team:chroma#reader
  48. relation: can_get_tenant
  49. object: server:server1
  50. tests:
  51. - name: Users should have team roles
  52. check:
  53. - user: user:jane
  54. object: team:chroma
  55. assertions:
  56. owner: true
  57. writer: false
  58. reader: false
  59. - user: user:john
  60. object: team:chroma
  61. assertions:
  62. writer: true
  63. owner: false
  64. reader: false
  65. - user: user:jill
  66. object: team:chroma
  67. assertions:
  68. writer: false
  69. owner: false
  70. reader: true
  71. - user: user:unknown
  72. object: team:chroma
  73. assertions:
  74. writer: false
  75. owner: false
  76. reader: false
  77. - user: user:jane
  78. object: team:unknown
  79. assertions:
  80. writer: false
  81. owner: false
  82. reader: false
  83. - user: user:unknown
  84. object: team:unknown
  85. assertions:
  86. writer: false
  87. owner: false
  88. reader: false
  89. - name: Users should have direct access to server
  90. check:
  91. - user: user:sam
  92. object: server:server1
  93. assertions:
  94. can_get_preflight: true
  95. can_create_tenant: true
  96. can_get_tenant: true
  97. - user: user:michelle
  98. object: server:server1
  99. assertions:
  100. can_get_preflight: false
  101. can_create_tenant: true
  102. can_get_tenant: false
  103. - user: user:unknown
  104. object: server:server1
  105. assertions:
  106. can_get_preflight: false
  107. can_create_tenant: false
  108. can_get_tenant: false
  109. - user: user:jill
  110. object: server:serverX
  111. assertions:
  112. can_get_preflight: false
  113. can_create_tenant: false
  114. can_get_tenant: false
  115. - name: Users of a team should have access to server
  116. check:
  117. - user: user:jane
  118. object: server:server1
  119. assertions:
  120. can_create_tenant: true
  121. can_get_tenant: true
  122. can_get_preflight: true
  123. - user: user:john
  124. object: server:server1
  125. assertions:
  126. can_create_tenant: true
  127. can_get_tenant: true
  128. can_get_preflight: true
  129. - user: user:jill
  130. object: server:server1
  131. assertions:
  132. can_create_tenant: false
  133. can_get_tenant: true
  134. can_get_preflight: true
  135. - user: user:unknown
  136. object: server:server1
  137. assertions:
  138. can_create_tenant: false
  139. can_get_tenant: false
  140. can_get_preflight: false

Let’s run the tests:

  1. fga model test --tests test.model-article-p4.fga.yaml

This will result in the following output:

  1. # Test Summary #
  2. Tests 3/3 passing
  3. Checks 42/42 passing

That is all folks. We try to keep things as concise as possible and this article has already our levels of comfort in that area. The bottom line is that authorization is no joke and it should take as long of a time as needed.

Writing out all tests will not be concise (maybe we’ll add that to the repo).

Conclusion

In this article we’ve have built an authorization model for Chroma from scratch using OpenFGA. Admittedly it is a simple model, it still gives is a lot of flexibility to control access to Chroma resources.

Resources

April 15, 2024