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 both READ and WRITE 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

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

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

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: ["NodeOne"] }, path: "price", operations: [READ] }
        { principal: { nodes: ["NodeOne"] }, path: "recipeType", operations: [READ] }
        { principal: { nodes: ["NodeOne"] }, path: "recipeYield", operations: [READ] }
      ]
    }
    syncMode: ASYNC
  ) {
    transaction {
      _id
      _owner
    }
  }
}

In the example above, all nodes have the ability to read the name and sku, while only NodeOne has the ability to read the price and recipe information.

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 {
      _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
            }
          ]
        }
      ]
    }
  }
}