Data validation in FQL with ABAC

Hello,
What’s the current best practice around data validation in Fauna?

I’d like my web app to interact directly with Fauna which means I need some form of data validation within Fauna itself.

My current solution involves adding a condition (using And) within my ABAC privileges to do some validation. Here is an example for a todo:

q.And(
  q.IsString(q.Select(['data', 'title'], q.Var('newData'), false)),
  q.IsTimestamp(q.Select(['data', 'createdAt'], q.Var('newData'), false)),
  q.Or(
    q.IsTimestamp(q.Select(['data', 'completedAt'], q.Var('newData'), false)),
    q.IsNull(q.Select(['data', 'completedAt'], q.Var('newData'), null))
  ),
  q.IsRef(q.Select(['data', 'user'], q.Var('newData'), false))
)

This does not however account for people adding extra fields on to the todo.

It is also rather verbose so I am wondering if I missed something in the documentation and there is a better way?

Validation in FQL

There is not a best practice on how to do validation in FQL yet.
It’s quite verbose atm but you could of course create a few JavaScript (or whatever your host language is, assuming you are not writing this in the dashboard) to abstract that verbosity away:

function CheckString(attribute, newData) {
   return q.IsString(q.Select(['data', attribute], newData, false))
}

So that you can write:

q.And(
  CheckString('title', q.Var('newData')), 
   ...
)

Which you can do for everything you want to check. FQL is easy to compose, use that feature wherever you can :slight_smile:

Stopping users from adding extra fields or disallowing specific fields

You could even write a role that ranges over the fields of oldData and newData and makes sure no fields are added. A function such as ObjectKeys as provided by Eigils faunadb lib would come in handy there (thanks Eigil! :slight_smile:)

Validation in ABAC roles or in User Defined Functions (UDFs)?

Although it’s possible, it’s not what ABAC was made for. ABAC is meant to write security roles initially yet we see many add validation in there. Another approach that is probably easier to debug is to use User Defined Functions and write the logic there and abort in case the validation is wrong. For example, you could also do something like this:

function RegisterAccount(email, password) {
  const ValidateEmail = FqlStatement =>
    If(
      ContainsStrRegex(
        email,
        "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
      ),
      // If it's valid, we continue with the original statement
      FqlStatement,
      // Else we Abort!
      Abort('Invalid e-mail provided')
    )

  const ValidatePassword = FqlStatement =>
    If(
      GTE(Length(password), 8),
      // If it's valid, we continue with the original statement
      FqlStatement,
      // Else we Abort!
      Abort('Invalid password, please provided at least 8 chars')
    )
  const Query = Create(Collection('accounts'), {
    // credentials is a special field, the contents will never be returned
    // and will be encrypted. { password: ... } is the only format it currently accepts.
    credentials: { password: password },
    // everything you want to store in the document should be scoped under 'data'
    data: {
      email: email
    }
  })

  // Compose, each Validate function will Abort if something is wrong, else it will just call the FQL
  // that was passed.
  return ValidateEmail(ValidatePassword(Query))
}

and then register it as a UDF

const RegisterUDF = CreateOrUpdateFunction({
  name: 'register',
  body: Query(Lambda(['email', 'password'], RegisterAccount(Var('email'), Var('password')))),
  role: Role('functionrole_register')
})

that way you have more control over the error messages and can give feedback to the client.

2 Likes

Besides Is* checks in OP also Equals/Count on incoming date can be checked.

This way you know that all the fields are posted and only those that you check for.

In OP example count of 4 fields should appear in data:
title, createdAt, completedAt and user