Chapter 3: Let's get into Fauna: a guide to understanding Fauna while creating a social media database
Tags: javascript, fauna, nextjs, serverless
Published: 2020-11-06 22:33:07

In chapter 1, we created a database using the [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) and Next.js tutorials together, we created some users and logged them in using a project from Next.js.
In chapter 2, we added functionality to follow and post as well as the necessary permissions to do it. Now you have a repository to clone and use it for this purpose.

In this chapter we are going to make use of [Fauna’s](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) access control, ABAC (Attribute Based Access Control) to limit what users are allowed to do. For example, they will only be able to create, edit or delete posts if they are the owners. We will create private content and reject access to vip. Also, upper roles and configure post's permissions.

#What is ABAC?

We already mentioned what ABAC stands for, but what does it mean? [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) can access a specific document and the user trying to access it, with this information it can determine if the user trying to access/change the document actually has the permissions to do it. This can help a lot to keep a portion of the user’s information private or prevent changes in a document (e.g. a post) which doesn’t belong to the user trying to change it.

ABAC is composed of two sections: The first one is the Membership, we did already do something about membership in our second chapter: you created a `Lambda` function that checks the documents in a collection, if the `Lambda` function returns `true`, the document has this role.

Let’s use the Fauna’s Dashboard and navigate to Security/Manage [Roles/basicUser/Membership] (https://dashboard.fauna.com/roles-edit/basicUser/@db/social-network-test)

If you followed through the second chapter, you can see the Users collection and should be able to expand it to see a `Lambda` function. This function has “ref” as a parameter and returns true every time. This means all the users from the Users collection will have this permissions schema. This Lambda function can be changed to include any attribute relatable to the Users collection. For example, the default Lambda function when you are creating a membership goes like this:
```javascript
Lambda("ref", Select(["data", "vip"], Get(Var("ref"))))
```

This function uses `Get` (reads) on the “ref” and then `Select`(s) the document path *data / vip*. Here, *vip* is supposed to contain a boolean stating if the user has a vip (premium) status. You can also check if the user is in a secondary list, like another collection where you can find the references of the admins. Here, we will go through some examples and apply them.

##Example 1: Premium content:

Let’s think about this scenario: You don’t have a social network but some premium content your users will be able to see after paying a fee, a lifetime subscription to a service.

1. Create a collection called *PaidVideos*.

```javascript
CreateCollection({name:'PaidVideos'})
```

2. Create some documents on it with a field called video:

```javascript
Map(
  [
    {video:"dQw4w9WgXcQ",text:"Old but gold"},
    {video:"XcgoZO-p9tI",text:"Secret of the forest"},
    {video:"D5q094yRsbA",text:"Tyrano lair"},
    {video:"re0A23CSvpw",text:"Imp’s song"},
    {video:"cM4kqL13jGM",text:"Rebirth of slick"}
  ],
  Lambda(
    "docPayload",
    Create(Collection('PaidVideos'),{data:Var('docPayload')})
  )
)
```

3. Create a new function called *premiumContent* with the following body

```javascript
Query(
  Lambda(
    [],
    Map(
      Paginate(Documents(Collection("PaidVideos"))),
      Lambda("videoRef", Select("data",Get(Var("videoRef"))))
    )
  )
)
```

You can see there’s a new method called `Documents`. This method returns a set containing all the documents of the collection in the argument. 

Now, let’s create a new role, head to *Security/Manage Roles* and press on *New Role*.

![Dashboard: Head to security/ manage roles](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/Fauna-new-role-VIP_oeK5uPz7Xx.png)

This new role will require the collection *PaidVideos*, we will grant *view* permissions, also, the function `premiumContent`, we will grant *call* permissions.

![Dashboard: Create new role and add permissions](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/Fauna-new-role-VIP-2_d-p_-bHvB.png)

Why only these two permissions? As you may remember, any document in *Users* will have the basicUser’s permissions. Their predicate function always returns `true`. Any document in *Users* who also has a field called *vip* with a value of `true` will have the basicUser’s permissions as well as the premiumUser’s.

Now, head to the Membership tab, add the collection Users and use the function provided by Fauna.

![Dashboard: Add membership predicate as recommended by Fauna](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/Fauna-new-role-VIP-3_j1zhsgb2l.png)

Put a name on your new role, I used *premiumUser*, you can pick any name you like, but we will use this name here when referring to this role.

Take one or two of your existing users and `Update` them to have a new field `vip:true`, this will enable them as *premiumUser*

![Dashboard: Update user to have a field called VIP and value true](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/Fauna-update-user-VIP_yXnSyS2RM.png)

If you cloned this repository at the beginning of the lesson, you can switch to the branch called *third-chapter-end* to update your repository to test this functionality.

Log in with any user valid to premiumUser (the ones we updated to have vip:true), click on the new tab labeled *premium*.

![Navigation: Premium view when user is vip](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/chrome-vip-view_DSqVClcRp.png)

If the user has the attribute vip set to true, it will be able to access the content inside the *PaidVideos* collection, otherwise, the function will throw an error stating the user doesn’t have permissions to access these documents.

In this case, we handled the error from the API, we can also handle it from the front end to get a 403 status, indicating further payment is required.

##Example 2: Monthly subscription

This scenario is very similar to the previous one, except your subscription expires after some time.

Create a new role with the same permissions as *premiumUser*, in this case, we will name it *subscribedUser*. Head to the *Membership* tab, add the *Users* collection and add this function to the predicate function:

```javascript
Lambda(
  "ref",
  Let(
    {
      subExpiration: Select(
        ["data", "expiration"],
        Get(Var("ref")),
        TimeSubtract(Now(),1,"day")
      ),
      remainingTime: TimeDiff(Var("subExpiration"), Now(), "seconds")
    },
    GTE(Var("remainingTime"),0)
  )
)
```

This function seems a little more complex, but don’t panic. We use the method `Let` to create 2 variables: *subExpiration* which `Get`(s) the `User` info, `Select`(s) the path *data/expiration* which will be a timestamp with the expiration date of the subscription (the third argument means if there’s no data at that value, return the current time but yesterday); as well as *remainingTime*, which subtracts (using `TimeDiff`) the current time to the time stored on *subExpiration* and returns the value in seconds. Now, `GTE` returns true if the variable *remainingTime* is greater or equal to 0, which means the user still has time on its subscription.

As you can see, this status can change if the user’s subscription expires during the day. When the User’s expiration date has passed, it will no longer have the attributes to be a *subscribedUser*. Thus, when the user requests the `PaidVideos`, it will get a “permission denied” response.

Let’s update any non-vip user to have an expiration date for tomorrow.

![Dashboard: Update expiration date for tomorrow](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/Fauna-update-user-expiration_5a8EdYfI4.png)

When updating a file on the dashboard, if you use the method `TimeAdd` (or any other method), [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) will update the field to the result of such method instead of a fixed value.

Let’s login with our updated user and head to the premium tab of our Next.js project. You should see the content we defined as *PaidVideos*.

If you try with a non-vip, non-subscribed user, you should see a message stating you are not premium

##Example 3: Admins only

Let’s suppose your website has a section for admins, managers or any other role which is only granted to some handpicked users. 

Create a new collection and name it *UpperRoles*. Create a new index called *roles_by_user_id* by using the next command on the Fauna’s shell:

```javascript
CreateIndex(
  {
    name: "roles_by_user_id",
    unique: true,
    serialized: true,
    source: Collection("UpperRoles"),
    terms: [
      {
        field: ["data", "userId"]
      }
    ],
    values: [
      {
        field: ["data", "roles"]
      }
    ]
  }
)
```

We mark unique as `true` to have a simple user in the collection with all the roles assigned to it.

Create a new role with the same permissions we had on *premiumUser* and *subscribedUser*, now, let’s head again to the *membership* tab, select the *Users* collection and add this as predicate function:

```javascript
Lambda(
  "ref", 
  Let(
    {
      rolesPage:Paginate(Match(Index('roles_by_user_id'),Var("ref"))),
      roles:Select(["data"], Var("rolesPage"),[]),
    },
    IsNonEmpty(Intersection(["admin"],Var("roles")))
  )
)
```

We bring the results of the index we just created, it’s expected to bring a single result as it has the *unique* flag marked as `true`, on *roles* we bring the first result of the page and set the default as an empty array, we expect *roles* to be an array of all the roles a user has available. Finally, we get the `Intersection` of our roles array and an array containing the role “admin”. If it’s a non-empty array, the user will have this role’s privileges.

Now, let’s grant a user these admin privileges:

Copy the ref of any non-premium, non-subscribed user.

![Dashboard: Go to users collection and copy the reference for an user](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/Fauna-get-ref_sMh8QoXdl.png)

Create a new document on the *UpperRoles* collection with the following data:

```javascript
Create(
  Collection("UpperRoles"),
  {data:{
    userId:Ref(Collection("Users"), "277425124024517138"), //The reference you just copied
    Roles:["admin","accountant","manager"] //additional roles as reference
  }}
)
```

As you can see, this user will have some roles, including “admin”, which is the value we will look for.

Login with the user you’ve set up and try to access the premium tab. This user is now an admin.

We’ve explored some scenarios to define the role of a user based on its attributes. Next, we are going to determine if a user has access to read/update/create/delete a document.

In chapter two, we set the access to posts in a way that allowed any *basicUser* to change any document in the collection *Posts*, to create and delete any document in the collection *Followers* as well as many other privileges that give way too much freedom and may cause undesired behaviour.

Let’s head to the *Security* section, click on *manage roles*, find the *basicUser* and click the cogwheel on the right side. Let’s click the Users collection to expand it. Look at the </> symbols below each action. When clicked, it allows us to create a predicate function or script to grant privileges.

![Dashboard: Predicate functions on privileges](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/Fauna-roles-update-1_bOHmCXh4p.png)

When you click any of them, [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) provides a simple template script to hint you into a useful function.

If you don’t want to use a script but you already clicked on the button, just find the *clear* option on the lower right part of the script area.

Let’s expand the *Posts* collection and see what we can do regarding write permissions.

![Dashboard: Default predicate function for write privilege](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/Fauna-roles-update-2_iIqck5MMD.png)

When writing a document, [Fauna’s](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) ABAC calls this function with 3 arguments: the previous document (olData), the document’s future state (newData) and the document’s id (usually ref). Let’s check what is new here, the `Equals` method compares the arguments inside it and returns `true` if all of them are equal. The `And` method returns `true` if all the arguments are true, just like a regular `AND` logical gate.

In this example, we check if the document belongs to the user trying to modify it by using Equals:

```javascript
Equals(Identity(), Select(["data", "owner"], Var("oldData")))
```

As you can see, it checks the path data/owner in the previous document and compares it with the `Identity` of the logged in user, which means you can only edit the posts you own. Also, we want the data to remain of the same user, so we check the field in data/owner in both, previous and new, documents to check if the owner will remain the same.

As both `Equals` methods are inside an `And` method, both have to return `true` to confirm the document write. You can also add another field, for example, the creation date of the previous document must be equal to the new date.

```javascript
Equals(
  Select(["data", "date"], Var("oldData")),
  Select(["data", "date"], Var("newData"))
)
```

If the function returns `true`, the document will be updated as if the user had full permissions to do it, otherwise, it will throw an error and the document will remain unchanged.

**Note:** newData contains the whole document’s new state. If you modify a single field, newData will contain the whole document with the change on the modified field. There’s no need to send the fields you want ABAC to compare.

This is very useful to keep some fields of the database static, for example, the owner of a post. For now, uncomment the functional part of the script so we use it, then, click on the *</>* symbol under the *Create* action.

![Dashboard: Default predicate function for create privileges](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/Fauna-roles-update-3_e4_Go_RYo.png)

You can see the function here is very similar to the other one, except we only have one argument on the `Lambda` function which is *values*, these are the values that are about to be written on the database. The path *data/owner* has to be equal to the `Identity` to allow a user to create a post, otherwise, no document is created at all. Let’s uncomment this function as well and check on the *Delete* action.

![Dashboard: Default predicate function for delete privileges ](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/Fauna-roles-update-4_Udd3qkFmM.png)

This function gets the document’s id as argument and names it *ref*, It performs a `Get` method on the argument and checks the path data/owner to compare it with the Identity. If it’s the owner who’s deleting the post, the action is performed. Let’s uncomment this functional part as well and scroll down to save our changes.

Let’s try to create a post under the regular method. Navigate to chrome, create a post and you should see nothing has changed since we did it in the previous chapter.

Now, let’s break our application:
Copy the ref of any user different from the one you’re logged in, just like we did for the upper roles. Go to the functions section, select the *createPost* function and change the field on the path data/owner to look like this:

```javascript
Query(
  Lambda(
    "description",
    Create(Collection("Posts"), {
      data: {
        description: Var("description"),
        date: Now(),
        owner: Ref(Collection("Users"), "277945843461390867"), // The ref you just copied
        likes: 0,
        comments: 0
      }
    })
  )
)

```

As the reference in the field *owner* is different from our logged in user, our permission will be denied. Save the broken function and try creating a post again.

![Shell: Permission denied error](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/shell-permission-denied_B3K5cgnqS.png)

This error message is quite large, but the punchline is in the *responseRaw* field (also, you can catch the field *responseContent.errors*), you will find the reason for the error is “permission denied” and the description states you don’t have the permissions for the action. This is the error you will find every time you try to perform an action you’re not allowed to. This is not the expected behaviour of your app, but a failsafe in case someone tries to break havoc in your app. Now you can repair the broken function, we have tested what we wanted.

#Debugging ABAC

Well, we’ve set up some permissions and we want to know if the functions we defined are actually doing what we need them. We will use the [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) Shell to compare our results with our expectations.

For example, let’s bring our predicate function for premiumUser:

```javascript
Lambda(
  "ref",
  Select(
    ["data", "vip"],
    Get(Var("ref"))
  )
)
```

The variable `ref` will be the user’s ref. So, let’s head to the Fauna’s shell, use the `Let` method to bring a variable with the name `ref`.

```javascript
Let(
  {
    ref:Ref(Collection("Users"),"277945843461390867")
  },
  Select(
    ["data", "vip"],
    Get(Var("ref"))
  )
)
```

We changed the `Lambda` method for a `Let`, and created the variable *ref* with the reference of a user. In this case, this is the user’s document:

```javascript
{
  "ref": Ref(Collection("Users"), "277945843461390867"),
  "ts": 1603515727810000,
  "data": {
    "email": "testmail5@mail.com",
    "posts": 0,
    "activeSince": Time("2020-09-28T21:31:02.124870Z"),
    "vip": true
  }
}
```

When you execute on the shell, you’ll realize that getting the document and selecting the value in the path data/vip will return `true`.

When you try with another user, for example this:

```javascript
{
  "ref": Ref(Collection("Users"), "280324497574199812"),
  "ts": 1603600132565000,
  "data": {
    "email": "testmail4@mail.com",
    "posts": 0,
    "activeSince": Time("2020-10-25T03:38:43.365515Z"),
    "expiration": Time("2020-10-26T04:28:52.453007Z"),
    "vip":false
  }
}

```

The function will return `false`. Meaning the user will not be included in the *premiumUser* role.

The only way an ABAC function grants privileges or includes a document within a role is by having the predicate function return `true`, having a function that returns an error will deny the privileges or the role. This means that you can have users that don’t contain the field vip and this won’t break the functionality of ABAC.

Now, let’s try with the predicate functions to update a post:

```javascript
Lambda(
  ["oldData", "newData"],
  And(
    Equals(Identity(), Select(["data", "owner"], Var("oldData"))),
    Equals(
      Select(["data", "owner"], Var("oldData")),
      Select(["data", "owner"], Var("newData"))
    )
  )
)
```

This one requires the definition of 3 variables: oldData, newData and the user’s id which will replace the `Identity` method, this is because [Fauna's](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) Shell has no identity nor document associated.

Copy and paste the whole existing document for the oldData, do the same for the newData, but change the owner to some other user id (or just something random, it doesn’t matter). When executed on the Fauna shell, you’ll see this returns `false` because the new value for the owner is not Equal to the previous one.

```javascript
Let(
  {
    oldData:{
      "ref": Ref(Collection("Posts"), "280597810560107014"),
      "ts": 1603857775247000,
      "data": {
        "description": "I like turtles",
        "date": Time("2020-10-28T04:02:55.038172Z"),
        "owner": Ref(Collection("Users"), "277425124024517138"),
        "likes": 0,
        "comments": 0
      }
    }, 
    newData:{
      "ref": Ref(Collection("Posts"), "280597810560107014"),
      "ts": 1603857775247000,
      "data": {
        "description": "I like turtles",
        "date": Time("2020-10-28T04:02:55.038172Z"),
        "owner": Ref(Collection("Users"), "280324497574199812"),
        "likes": 0,
        "comments": 0
      }
    },
    userId:Ref(Collection("Users"), "277425124024517138")
  },
  And(
    Equals(Var("userId"), Select(["data", "owner"], Var("oldData"))),
    Equals(
      Select(["data", "owner"], Var("oldData")),
      Select(["data", "owner"], Var("newData"))
    )
  )
)
```

The reason we copied the whole document instead of just the path we needed is to show you how ABAC will see the information when you are trying to perform the *write* action on a document. Something similar will happen when you try to read/create/delete a document in this collection due to the predicate functions. 

This is basically it, copy the functional part of the `Lambda` within a `Let` and set the expected (and some unexpected) values as the `Let` definitions, with this, you will be able to predict the behaviour of any predicate function you declare.

#Password reset for your users

Let’s think about this common scenario: One of your users doesn’t remember the password used for signup. How do you recover it? [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) won’t show you the password or allow you to see the user’s login keys. Even if you are an admin. However, [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) allows admins to create Login tokens for any user, no passwords required. This way, you may try to send the user’s token via email or any other confirmation method defined prior to the password loss.

We are going to create a function on [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) to perform this action. We are going to receive the user’s email, look for it on our database to get the user’s id, create the token and return it to the API, we are expecting this API won’t return the token to the user directly, instead, the API will send an email to the user.

```javascript
Query(
  Lambda(
    "email",
    Let(
      {
        userId: Select(
          ["data", 0],
          Paginate(
              Match(Index("users_by_email"), Var("email")),
          )
        ),
        returnData: Create(Tokens(), {
          instance: Var("userId"),
          data: { message: "you can add some information here" },
          ttl: TimeAdd(Now(), 5, "minutes") // add time to live
        })
      },
      Var("returnData")
    )
  )
)
```

We use this function to create a new document in the collection Tokens(), this is the collection where [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) stores the Login tokens for all Users, this information is partially visible, we will not be able to see the current key nor the password used, but we can see the instance, which should be the user’s id, and the data field, which we used to store a message. We also added a ttl or time to live, this works as an expiration date, so the user has a limited time to reset the password with this token.

Last function on [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaSocialMediaDatabase_SJamie) is *resetPassword*, this function will update the user’s password to the one provided on the parameters.

```javascript
Query(
  Lambda(
    "password",
    Do(
      Update(Identity(), { credentials: { password: Var("password") } }),
      Logout(false)
    )
  )
)
```

As this will update the own user, we need to add privileges to the *Users* collection to update itself. Add this as the predicate function under the Write action.

```javascript
Lambda(
  ["oldData"],
  Equals(Identity(), Select("ref", Var("oldData")))
)
```

Also, add the *resetPassword* function to the privileges and check the privilege to *Call* the function.

In our repository, we added a tab called Recover, sign up with a reachable email address and try to reset your password.

![Navigation: Tab called forgot takes you to the password recover view](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/chrome-forgot-tab_q8DUMTWUU.png)

You will get an email similar to this:

![Navigation: The email you receive to recover your passsword](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/chrome-forgot-email_L2fOtqWjJ.png)

And when you click on the link, you’ll be here

![Navigation: Reset password by clicking on email](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/chrome-forgot-reset_ak4djV082.png)

Add a new password and you’ll be able to login with it.

#Are you test launching now? Here some advice:

When setting up your environment variables on an actual server, it’s recommended you don’t use a key with administrator or server privileges. Using a key with minimum privileges may keep the functionality intact and your application will be safer.

In Our case, we can have permissions to create and read on the *Users* collection, add read privileges to the index *users_by_email*, the function *signupUsers* and *recoverPassword* will have call permissions.

With this, you will have a public role with limited functionality, create a key for this role, you don’t need to add a collection or a predicate function, just add the key from the security menu.

![Dashboard: Creating public key](https://ik.imagekit.io/z9wrsfvqnb/FaunaForum/fauna-public-user_p6PpM8ctu.png)

And that’s it. Add some styles to make it look fancy, add some features to make it more interesting. It’s up to you.

Thank you so much for following this blog series, I hope it's useful to your projects or your new interests, perhaps.