Recap
to-do-list
branch, where we built a to-do list generator using React and Fluree. If you are not familiar with Fluree, please refer to the first part of this tutorial series to learn the basics.Getting started
git clone https://github.com/fluree/version-2-lists-generator.git
, or any other preferred method.cd
into the repo and run npm install
.npm start
to locally serve the app in your browser at http://localhost:3000Fluree
Identity and Permissions
User, Auth, Rules, and Roles
_user
, _auth
, _rule
, and _role
are built in collections within Fluree that hold the criteria needed to implement user permissions and control identity. Below is a diagram showcasing how each collection is connected to each other by their predicates.
["_role/id","root"]
, this role has full read and write access to the db. Users can have multiple roles connected to their auth record, and each role can have a different set of rules as well. Also users can have multiple auth records, but multiple users cannot share the same auth record.It is worth noting that an auth record does not need to be tied to a user, they can be used independently with a collection of your choosing.
Generating Auth records with Public-Private Keys
The way auth records control identity in Fluree are by tying the record to a specific public-private key pair (specifically generated using the secp256k1 elliptic curve). This cryptographic identity enables the user to sign their transactions and queries according to their permissions. There are a number of ways to generate the public-private key/auth id triple, and can be found here in the docs. In this tutorial we will be issuing them using the Admin UI in the sections below.
Signing queries and transactions can be done in different ways found here, in this tutorial we use the @fluree/crypto-utils
library.
Integrating identity and permissions into the Application
Make sure to change the network and db variables located in the appConfig.js
to mirror your Fluree network and db namespace, and double-check that you're using the correct port (e.g. :8090)
Schema Changes
_user
collection was not used, but now that we need to leverage permissioning we will utilize the _user
collection, along with their username
predicate. The only other additions are the following predicates: list/listOwner
, assignee/user
, and task/issuedBy
, where all of these predicates reference the _user
collection. The list/listOwner
predicate references a single user, while the assignee/user
predicate is multi: true
, meaning it can reference multiple users, given that a list can have more than one assignee. The entire schema can be found here and transacted to a new Fluree ledger.Roles and Rules

[
{
"_id": "_role?endUser",
"id": "endUser"
}
]

_fn
collection, the _rule
collection has a predicate called _rule/fns
which is a reference to this collection. So when an action takes place the smart function that is connected to a certain rule is evaluated and will either return true or false depending if the user in question has the permission to execute the desired action.*Currently, Smart Functions are only usable in Clojure, although we are alpha-testing a version of Fluree that allows for JavaScript-written Smart Functions as well. Stay tuned for updates!
Rules and Smart Functions
_collection/spec
, _predicate/spec
, _predicate/txSpec
and even in transactions, an example of each can be found here. But for the purpose of demonstrating permissions, all of our smart functions are connected to the rules mentioned above. Now lets create a rule that is connected to a smart function:
[
{
"_id": "_rule$fullAccessOwnListData",
"id": "fullAccessOwnListData",
"doc": "A _user with this rule can only view and edit their own list data",
"fns": ["_fn$fullAccessOwnListData"],
"ops": ["all"],
"collection": "list",
"predicates": ["*"]
},
{
"_id": "_fn$fullAccessOwnListData",
"name": "fullAccessOwnListData?",
"doc": "A _user can query and edit their own list data",
"code": "(relationship? (?sid) [\"list/listOwner\" \"_user/auth\"] (?auth_id))"
}
]
rule
and its accompanying fn
. Starting with the rule object it includes a temporary id (for the transaction) that will evaluate to a integer once it’s in Fluree. It also includes an id
, a description
, the fns
associated with the rule in the form of a temporary id, ops
is set to all
(meaning this applies to both actions: transact and query), and the collection and its predicates to which this rule is attributed to. In this case it applies to the list
collection and all of its predicates.fullAccessOwnListData
smart function will be run every time a user with this rule either transacts or queries any data field available to records in the list
collection.
(relationship? (?sid) [\"list/listOwner\" \"_user/auth\"] (?auth_id))
There are a subset of Clojure functions that can be used in the code, but in this tutorial all of our smart functions use the relationship?
function. The syntax for the relationship?
function is as follows (function startSubject path endSubject)
.
In our implementation, this maps to the following:
function
:relationship?
startSubject
:(?sid)
(?sid)
is a context-dependent function which refers to the specific subject id being queried/transacted in the context of the current function evaluation.path
:[\"list/listOwner\" \"_user/auth\"]
- The
path
can be a single predicate or a vector of predicates that potentially connect the two subjects. Essentially, for therelationship?
smart function to be satisfied, we use this schema-defined path to evaluate whether a relationship exists between two subjects. In our example, if the data links a list to the user accessing the list, via the path oflist/listOwner
, then we can understand the user accessing the list is the owner and should be entitled to specific permissions. endSubject
:(?auth_id)
(?auth_id)
is also a context-dependent function and refers to the specific auth record (i.e. public cryptographic identity) that is querying/transacting against the data at the time of the smart function evaluation.
listOwner
to view their list data, their id must have a connection to the auth record id, via the path [list/listOwner _user/auth]
that bridges that connection within the data & the schema that models it._rule/fns
to "fns": [[ "_fn/name","true"]]
, the same can be done if a rule must always evaluate to false.
{
"_id": "_rule$canViewAssignees",
"id": "canViewAssignees",
"doc": "A _user with this rule can view all assignee data",
"fns": [["_fn/name", "true"]],
"ops": ["query"],
"collection": "assignee",
"predicates": ["*"]
}
The transaction above is a rule where all users can view assignee data, so this rule will always evaluate to true.
Auth records and public-private keys
[
{
"_id": "_user$6",
"username": "rootUser",
"auth": [
{
"_id": "_auth",
"id": "Tf3VDot4jSKHFcLY8HSPsuf2yA5YBnRsEPU",
"roles": [["_role/id", "root"]]
}
]
}
]
Signing Queries and Transactions
@fluree/crypto-utils
, but other methods can be found here.Signing Queries
signQuery
function. It takes a private key, param, queryType, host, and db as parameters (defined below), then it returns an object with keys: header
, method
and body
, which can be sent to the /query
endpoint (or other possible endpoints such as /multi-query
, history
, and block
). This specific query can be found here, it is triggered on load and defaults to the rootUser on the tab component. Each tab option is a different user with different permissions. The lists view changes given their identity.
import { signQuery } from '@fluree/crypto-utils';
import usersAuth from './data/usersAuth';
const fetchListData = {
select: [
'*',
{
tasks: [
'*',
{
assignedTo: ['*'],
},
],
},
],
from: 'list',
opts: {
compact: true,
orderBy: ['ASC', '_id'],
},
};
const privateKey = selectedUser.privateKey;
const queryType = 'query';
const host = 'localhost';
const db = `${network}/${database}`;
const param = JSON.stringify(fetchListData);
let signed = signQuery(privateKey, param, queryType, host, db);
fetch(`${baseURL}/query`, signed) //fetch issues the request to the given url and takes the output of signedQuery.
.then((res) => res.json())
.then((res) => {
setLists(res);
})
.catch((err) => {
if (/not found/.test(err.message)) {
return console.log("this didn't work");
}
});
Signing Transactions
signTransaction
function, which takes an auth id, db, expire, fuel, private key, tx, and optional deps. It is similar to the query version, but the output of signTransaction
is then placed within the body of the POST request. Please refer to deleteTaskFromFluree and editTaskProps for their full implementation.
import { signTranaction } from '@fluree/crypto-utils';
import usersAuth from './data/usersAuth';
let deleteTaskFromFluree = () => {
const privateKey = selectedUser.privateKey;
const auth = selectedUser.authId;
const db = `${network}/${database}`;
const expire = Date.now() + 1000;
const fuel = 100000;
const nonce = 1;
const tx = JSON.stringify([
{
_id: chosenTask._id,
_action: 'delete',
},
]);
let signedCommandOne = signTransaction(
auth,
db,
expire,
fuel,
nonce,
privateKey,
tx
);
const fetchOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedCommandOne),
};
fetch(`${baseURL}command`, fetchOpts).then((res) => {
return;
});
};
let editTaskProps = () => {
const privateKey = selectedUser.privateKey;
const auth = selectedUser.authId;
const db = `${network}/${database}`;
const expire = Date.now() + 1000;
const fuel = 100000;
const nonce = 1;
const tx = JSON.stringify(taskChangeTransact);
let signedCommandTwo = signTransaction(
auth,
db,
expire,
fuel,
nonce,
privateKey,
tx
);
const fetchOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedCommandTwo),
};
fetch(`${baseURL}command`, fetchOpts).then((res) => {
return;
});
};