Fine-grained Data Permissions
Vendia Share has controls to allow data producers to limit the actions of data consumers on stored data.
Vendia's fine-grained data permissions are designed to be both simple to use and highly flexible. This feature enables administrators to easily limit who can take action on data written to a Uni.
NOTE: Defining fine-grained permissions on data is optional. Not including the Access Control List (ACL) will result in the default behavior of giving full Create, Read, Update and Delete (CRUD) access to all Node partners in the Uni network. This default behavior can be overridden by applying a Sharing Policy.
Scope of Fine-grained Data Permissions
Fine-grained data permissions serve to constrain the visibility of data in a Uni. They also set boundaries on the the ability to modify data in a Uni. It does not affect how users interact with Vendia APIs. If you're interested in defining how to control how Vendia Share users create and manage Unis, please refer to Role-based Access Controls.
Key Terms
-
ACL: Access Control List (ACL) is a list of objects that contain the fine-grained definitions that control access to data in a Uni.
-
Principal: Principal is a list of nodes to which the ACL is applied.
-
Path: [Optional] The path defines the attribute of the object to apply the ACL operations. This field is optional. If it is not specified, the specified operations are applied to the entire object.
-
Operations: Operations define what action a Principal can take on data.
-
aclInput: aclInput is a GraphQL object that contains a list of Access Control List (ACL) definitions that are applied when data is added or updated in a Uni.
Operations
Vendia Share allows the following operations to be assigned to Principals when data is added or updated in a Uni:
READ
: Permits the Principal to list and get data.WRITE
: Permits the Principal to update, put, and delete data.ALL
: Permits the Principal to bothREAD
andWRITE
data.UPDATE_ACL
: Permits the Principal to update the ACL. This permission can only be granted by the Principal who added the data.
NOTE: The ALL
operation does not include UPDATE_ACL
.
![Screenshot of item permissions in the Share UI](https://res.cloudinary.com/vendia/image/upload/v1689267451/image_11_sricv6.png)
Schema Definition Requirement
In order to take advantage of fine-grained data permissions, the JSON schema must include the x-vendia-acls
top-level type. For example, if my JSON schema as two distinct types - Foo
and Bar
- and I wish to enable fine-grained permissions on each type, I would set x-vendia-acls
as the following:
"x-vendia-acls": {
"FooAcl": {
"type": "Foo"
},
"BarAcl": {
"type": "Bar"
}
}
NOTE: The x-vendia-acls
top-level type must be defined when the Uni is created. Schemas cannot be updated to include this definition after the Uni has been created.
Restricting Data Access
When creating data on the Vendia Share platform, data producers can optionally restrict access to Node partners in the Network. The restriction can be either global (across all fields), or fine-grained (different for each field).
![Screenshot of the Vendia Share UI restricting access](https://res.cloudinary.com/vendia/image/upload/v1689263095/253319903-c3bf51fa-b87f-49ba-9d85-a75b320b9674.png_dur8wb.png)
Sharing Policies
Instead of relying on data producers to set the appropriate ACL on every item entry, each Node partner can configure sharing policies. Sharing policies are ACLs that are globally defined. If an item is created without an explicit ACL, the matching sharing policy for the entity will be used.
![Screenshot of Vendia Share UI sharing policies](https://res.cloudinary.com/vendia/image/upload/v1689270715/Screenshot_2023-07-13_at_12.51.40_PM_ah1eb2.png)
Creating a Sharing Policy
In the Vendia Share UI, sharing policies can be configured in the Node Settings. After configuration, newly created items can begin to utilize the policy configuration. Sharing policies are also available to be created through the GraphQL API, CLI, and SDK.
Using a Sharing Policy
When configured, sharing policies are used without any additional configuration for API requests. A sharing policy's configuration will be used has the default ACL when an explicit ACL is not defined in the GraphQL mutation.
GraphQL Request
add_Product(input: {price: 10, sku: "test-123"} ) {
result {
... on Self_Product {
_acl {
operations
path
principal {
nodes
}
}
sku
price
}
}
}
Response
{
"data": {
"add_Product": {
"result": {
"_acl": [
{
"operations": ["READ"],
"path": null,
"principal": {
"nodes": ["Acme"]
}
},
{
"operations": ["ALL"],
"path": "title",
"principal": {
"nodes": ["Helix"]
}
}
],
"sku": "test-123",
"price": 10
}
}
}
}
Advanced
API Usage Examples
Now that we have a sense of the operations that can be allowed, how to define our list of principals, and the how to specify that our JSON schema types will use fine-grained permissions, let's go through a very simple example to illustrate how to assign controls when adding or updating data in a Uni.
We will provide an example to demonstrate how nodes can share recipe information in a controlled manner in a Uni. We will also demonstrate how to query data that has controls associated with it.
Data Model and Uni Configuration
Below is the JSON schema that will be used as the basis for our queries.
Click to view Recipe Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://vendia.com/schemas/blog/fine_grained_control.json",
"title": "Sample schema for setting fine-grained access controls",
"description": "Model bakery recipes",
"x-vendia-acls": {
"RecipeAcl": {
"type": "Recipe"
}
},
"type": "object",
"properties": {
"Recipe": {
"description": "Recipe information",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"description": "The common name of recipe",
"type": "string"
},
"sku": {
"description": "SKU of the recipe",
"type": "string"
},
"price": {
"description": "Sales price of the recipe in USD",
"type": "number"
},
"recipeType": {
"description": "Type of recipe",
"type": "string",
"enum": ["cake", "cupcake", "pie", "muffin"]
},
"recipeYield": {
"description": "Quanitity yielded by the recipe",
"type": "number"
},
"ingredients": {
"description": "Ingredient listing needed for the recipe",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"description": "Name of the ingredient",
"type": "string"
},
"quantity": {
"description": "Quantity of the ingredient needed for the yield",
"type": "string"
}
}
}
},
"directions": {
"description": "Steps to make the recipe",
"type": "array",
"items": {
"description": "Discrete step",
"type": "string"
}
}
}
}
}
}
}
Click to view sample Recipe node configuration
Note: This example uses a 3-node Uni configuration, which is not viable for Starter tier users given the node limits enforced. See full details on the Pricing page. Starter tier users can still follow this example by omitting one of the nodes in the configuration shown below.
Our Uni is made of 3 nodes - Alice
, Bob
, and Eve
. All queries will make references to these node names.
[
{
"name": "Alice",
"userId": "alice@recipe-creator.com",
"region": "us-east-2"
},
{
"name": "Bob",
"userId": "bob@bobs-bakery.com",
"region": "us-west-2"
},
{
"name": "Eve",
"userId": "eve@eves-bakery.com",
"region": "us-east-1"
}
]
NOTE: Be sure to update the userId
and region
appropriately if you are deploying the example.
Writing Data with Fine-grained Control
The following mutations should be run from the Alice
node.
Sample Mutation - Allow Read-Only Access to a Recipe
In this example, we will specify that all nodes in a Uni will have the ability to READ
all attributes of our Red Velvet Cake
recipe. However, only the node Alice
will have the ability to WRITE
changes.
This mutation adds our aclInput
in addition to our standard input
.
mutation addRedVelvetCake {
add_Recipe(
input: {
name: "Red Velvet Cake"
sku: "ca001"
price: 5.00
recipeType: "cake"
recipeYield: 1
ingredients: [
{ name: "All-purpose Flour", quantity: "453 grams" }
{ name: "Granulated Sugar", quantity: "680.3 grams" }
]
directions: ["Mix dry ingredients", "Bake", "Profit"]
}
aclInput: { acl: [{ principal: { nodes: ["*"] }, operations: [READ] }] }
syncMode: ASYNC
) {
transaction {
_id
_owner
}
}
}
Sample Mutation - Allow Different Operators By Node
In this mutation, we will specify that the Bob
node has the ability to view all attributes of our Sprinkles Cupcake
recipe. Eve
will only be able to read the name
, price
, recipeType
, and recipeYield
. The attributes ingredients
and directions
will not be visible in query results from Eve
.
This mutation adds our aclInput
in addition to our standard input
.
mutation addSprinklesCupcake {
add_Recipe(
input: {
name: "Sprinkles Cupcake"
sku: "cc001"
price: 5.99
recipeType: cupcake
recipeYield: 100
ingredients: [
{ name: "All-purpose Flour", quantity: "783.33 grams" }
{ name: "Granulated Sugar", quantity: "833 grams" }
]
directions: [
"Mix dry ingredients"
"Bake"
"Let cupcakes cool for 20min"
"Make icing"
"Put icing on cupcakes"
"Profit"
]
}
aclInput: {
acl: [
{ principal: { nodes: ["*"] }, path: "name", operations: [READ] }
{ principal: { nodes: ["*"] }, path: "sku", operations: [READ] }
{ principal: { nodes: ["*"] }, path: "price", operations: [READ] }
{ principal: { nodes: ["*"] }, path: "recipeType", operations: [READ] }
{ principal: { nodes: ["*"] }, path: "recipeYield", operations: [READ] }
]
}
syncMode: ASYNC
) {
transaction {
_id
_owner
}
}
}
Reading Data with Fine-grained Control
Let's go ahead and issue queries from our nodes and see how the results change depending upon the node from which the query was issued.
Sample Query - Read data that may or may not be shared
query listRecipes {
list_RecipeItems {
_RecipeItems {
... on Self_Recipe {
_id
name
price
sku
recipeType
recipeYield
directions
ingredients {
name
quantity
}
}
... on Self_Recipe_Partial_ {
_id
name
price
sku
recipeType
recipeYield
directions
ingredients {
name
quantity
}
}
}
}
}
Alice
Since Alice
added the recipes, the node is the owner of the two Recipes
. The query will return all attributes of each Recipe
.
Sample Query Result - Alice Results
NOTE: The _id
of each Recipe
will be different in your results.
{
"data": {
"listRecipes": {
"Recipes": [
{
"_id": "017b3bc0-fe35-893f-5c88-ac73eddd88df",
"name": "Sprinkles Cupcake",
"sku": "cc001",
"price": 5.99,
"recipeType": "cupcake",
"ingredients": [
{
"name": "All-purpose Flour",
"quantity": "783.33 grams"
},
{
"name": "Granulated Sugar",
"quantity": "833 grams"
}
],
"directions": ["Mix dry ingredients", "Bake", "Profit"]
},
{
"_id": "017b3bf0-43e6-26ac-119f-81d5a60ef574",
"name": "Red Velvet Cake",
"sku": "ca001",
"price": 5,
"recipeType": "cake",
"ingredients": [
{
"name": "All-purpose Flour",
"quantity": "453 grams"
},
{
"name": "Granulated Sugar",
"quantity": "680.3 grams"
}
],
"directions": ["Mix dry ingredients", "Bake", "Profit"]
}
]
}
}
}
Bob Results
Bob
has been granted READ
access to all attributes of both Recipes
- implicitly for Red Velvet Cake
and explicitly for Sprinkles Cupcake
. The results of our query will match the results of Alice
.
Sample Query Result - Bob Results
NOTE: The _id
of each Recipe
will be different in your results.
{
"data": {
"listRecipes": {
"Recipes": [
{
"_id": "017b3bc0-fe35-893f-5c88-ac73eddd88df",
"name": "Sprinkles Cupcake",
"sku": "cc001",
"price": 5.99,
"recipeType": "cupcake",
"ingredients": [
{
"name": "All-purpose Flour",
"quantity": "783.33 grams"
},
{
"name": "Granulated Sugar",
"quantity": "833 grams"
}
],
"directions": ["Mix dry ingredients", "Bake", "Profit"]
},
{
"_id": "017b3bf0-43e6-26ac-119f-81d5a60ef574",
"name": "Red Velvet Cake",
"sku": "ca001",
"price": 5,
"recipeType": "cake",
"ingredients": [
{
"name": "All-purpose Flour",
"quantity": "453 grams"
},
{
"name": "Granulated Sugar",
"quantity": "680.3 grams"
}
],
"directions": ["Mix dry ingredients", "Bake", "Profit"]
}
]
}
}
}
Eve Results
Eve
has been granted READ
access to all attributes of Red Velvet Cake
but can only view a subset of Sprinkles Cupcake
attributes. Eve
cannot view the data for sku
, ingredients
, or directions
.
Sample Query Result - Eve Results
NOTE: The _id
of each Recipe
will be different in your results.
{
"data": {
"listRecipes": {
"Recipes": [
{
"_id": "017b3bc0-fe35-893f-5c88-ac73eddd88df",
"name": "Sprinkles Cupcake",
"sku": null,
"price": 5.99,
"recipeType": "cupcake",
"ingredients": null,
"directions": null
},
{
"_id": "017b3bf0-43e6-26ac-119f-81d5a60ef574",
"name": "Red Velvet Cake",
"sku": "ca001",
"price": 5,
"recipeType": "cake",
"ingredients": [
{
"name": "All-purpose Flour",
"quantity": "453 grams"
},
{
"name": "Granulated Sugar",
"quantity": "680.3 grams"
}
],
"directions": ["Mix dry ingredients", "Bake", "Profit"]
}
]
}
}
}
Updating Data with Fine-grained Control
Since Alice
created both Recipe
records, Alice
has full access and can run a updateRecipe
mutation to update any of the attributes of both records.
Since neither Bob
nor Eve
have the ability to write data for either record, both will receive an Unauthorized
exception when running the updateRecipe
mutation.
Sample mutation - updating data without access
NOTE: The _id
you use will be different in your Uni.
mutation updateSprinklesCupcake {
updateRecipe(
id: "017b3bc0-fe35-893f-5c88-ac73eddd88df"
input: { name: "Super Awesome Sprinkles Cupcake" }
syncMode: ASYNC
) {
transaction {
_id
_owner
}
}
}
Sample Mutation Result - Bob Results
{
"data": {
"updateRecipe": null
},
"errors": [
{
"message": "unauthorized",
"locations": [
{
"line": 2,
"column": 5
}
],
"path": ["updateRecipe"]
}
]
}
Sample Mutation Result - Eve Results
{
"data": {
"updateRecipe": null
},
"errors": [
{
"message": "unauthorized",
"locations": [
{
"line": 33,
"column": 5
}
],
"path": ["updateRecipe"]
}
]
}
Effects of Fine-grained Controls on Blocks
Vendia Share will take fine-grained permissions into account when writing transactions into a block. The block record for each node will reflect the operations permitted.
If a node has permission to the data then it will be visible in the transactions
. The list of mutations
will contain clear transparency regarding what was changed in the Uni. The redactedTxHash
value will be null. If, however, a node does not have full visibility into the data that is part of the block then it will be reflected. The mutations
will only display the subset of data to which the node should have access. The redactedTxHash
will not be null.
For example, if the following list_Blocks
query is run from Eve
, we will only see the transaction data that should be visible.
query listBlocks {
listVendia_BlockItems {
Vendia_BlockItems {
_id
redactedBlockHash
previousRedactedBlockHash
previousBlockHash
previousBlockId
transactions {
mutations
owner
redactedTxHash
}
}
}
}
Eve Results
The Mutations
for the addition of the Sprinkles Cupcake
only includes data Eve
has access to.
{
"data": {
"listVendia_BlockItems": {
"Vendia_BlockItems": [
...
{
"_id": "000000000000002",
"redactedBlockHash": "20d1968ddf5bee2831830c7353e62aefb33cbbd4b726cb9a0524508593af810d",
"previousRedactedBlockHash": "7b4ebbf3052b5300bf4315364c00d1582abc892bf94d1abd9c7ff6ec09d698d2",
"previousBlockHash": "ff0b499d269bbd5e2255c340da466f22376dc32b3429a7d324a00a49c8db53a9",
"previousBlockId": "000000000000001",
"transactions": [
{
"mutations": [
"mutation m{addRecipe(id:\"017b3bc0-fe35-893f-5c88-ac73eddd88df\",input: {name: \"Sprinkles Cupcake\", price: 5.99, recipeType: \"cupcake\", recipeYield: 100},aclInput: {acl: [{principal: {nodes: [\"Bob\"]}, operations: [READ]}, {principal: {nodes: [\"Eve\"]}, path: \"name\", operations: [READ]}, {principal: {nodes: [\"Eve\"]}, path: \"price\", operations: [READ]}, {principal: {nodes: [\"Eve\"]}, path: \"recipeType\", operations: [READ]}, {principal: {nodes: [\"Eve\"]}, path: \"recipeYield\", operations: [READ]}]}){error}}"
],
"_owner": "Alice",
"redactedTxHash": "2fc17018d68bc5852d4912cf95f55a82d0e04a18e94ef7dd2bb629f9eb12136b"
}
]
},
{
"_id": "000000000000003",
"redactedBlockHash": "d83b3edde7b0079bae9fc3dbc814891f11a85d0104d12b7c6ad3f63b10466272",
"previousRedactedBlockHash": "20d1968ddf5bee2831830c7353e62aefb33cbbd4b726cb9a0524508593af810d",
"previousBlockHash": "5bb22779e063938fed2cc32f335dab2f3b0092881874b328c9c3bd7a359a2245",
"previousBlockId": "000000000000002",
"transactions": [
{
"mutations": [
"addRecipe(id:\"017b3bf0-43e6-26ac-119f-81d5a60ef574\",input: {directions: [\"Mix dry ingredients\", \"Bake\", \"Profit\"], ingredients: [{name: \"All-purpose Flour\", quantity: \"453 grams\"}, {name: \"Granulated Sugar\", quantity: \"680.3 grams\"}], name: \"Red Velvet Cake\", price: 5, recipeType: \"cake\", recipeYield: 1, sku: \"ca001\"},aclInput: {acl: [{principal: {nodes: [\"*\"]}, operations: [READ]}]}){error}"
],
"_owner": "Alice",
"redactedTxHash": null
}
]
}
]
}
}
}