Slash DGraph Backend Security, Field Validation, and Timestamps

Backend Security, Field Validation, and Timestamps

Please correct me if I’m wrong, but currently there is no way currently to:

  • Secure Backend
  • Secure createdAt / updatedAt - automatic timestamps
  • Field Validation
  • Auto edit / generate fields / clear fields
  • Complex user validation not having to do with graphql queries directly (@auth rules)

Other than… @lambdas and @custom resolvers.

@custom resolvers will break your subscriptions and require you to fetch an external resource, so that is not a real viable option for backend security.

Future Development:

Although some of this may be fixed as early as this month (March 2021)…

  • Custom Timestamps
  • Custom Hooks
  • Authorization / Validation Rules

Example with @lambda

So, I thought I would post how we can do this with @lambdas in the meantime. I figure this will also serve for other ideas for future dgraph app developers…

First we create the Mutation Schemas

# secure add and update, add createdAt and updatedAt fields

type Post @withSubscription @auth(
  add: { rule:  "{$DENIED: { eq: \"DENIED\" } }"}
  update: { rule:  "{$DENIED: { eq: \"DENIED\" } }"}
  delete: { rule:  "{$ROLE: { eq: \"ADMIN\" } }"}
){
  id: ID!
  name: String! @search(by: [fulltext])
  description: String! @search(by: [fulltext])
  nameKebab: String @search(by: [exact])
  isPublished: Boolean!
  createdAt: DateTime
  updatedAt: DateTime
}

# create input fields

input NewPostInput {
  name: String!
  description: String!
  isPublished: Boolean!
}

input EditPostInput {
  id: ID!
  name: String
  description: String
  isPublished: Boolean
}

# add new lambda mutations

type Mutation {
  newPost(input: NewPostInput!): String @lambda
  editPost(input: EditPostInput!): String @lambda
}

Since add and update are taken, I chose new and edit.

newPost

async function newPost({args, dql, authHeader}) {

  // get claim data
  
  const headerName = authHeader.key;
  const headerValue = authHeader.value;
  const [algo, claimsBase64, signature] = headerValue.split(".")
  const claims = JSON.parse(atob(claimsBase64));
  
  // verify claims, depends on your setup
  ...
  // claims example
  if (claims['ROLE'] === 'ADMIN') {
    // run dql query here
  }
  ...
  const post = args.input;
  
  // change data example
  const nameKebab = post.name.replace(/\s/g, '-').toLowerCase();

  // timestamp example
  const createdAt = new Date().toISOString();
  
  // validation example
  if (post.name.length < 3) {
    return 'error-min-length'; 
  }

  let newArgs = `
  upsert {
    query {
      q(func: eq(User.email, "${post.email}")) {
        v as uid
      }
    }
    mutation {
      set {
        _:blank-0 <Post.name> "${post.name}" .
        _:blank-0 <Post.description> "${post.description}" .
        _:blank-0 <Post.nameKebab> "${nameKebab}" .
        _:blank-0 <Post.user> uid(v) .
        _:blank-0 <Post.isPublished> "${post.isPublished}" .
        _:blank-0 <Post.createdAt> "${createdAt}" .
        _:blank-0 <dgraph.type> "Post" .
        uid(v) <User.posts> _:blank-0 .
      }
    }
  }
  `;
  const results = await dql.mutate(newArgs);
  return results.data.uids['blank-0'];
}

editPost

async function newPost({args, dql, authHeader}) {

  // header validation here as well if you need it
  ...
  const post = args.input;
  const id = args.input.id

  // updatedAt instead of createdAt
  const updatedAt = new Date().toISOString();

  let newArgs = `
  upsert {
    query {
      q(func: eq(User.email, "${post.email}")) {
        v as uid
      }
    }
    mutation {
      set {
        <${id}> <Post.name> "${post.name}" .
        <${id}> <Post.description> "${post.description}" .
        <${id}> <Post.nameKebab> "${nameKebab}" .
        <${id}> <Post.user> uid(v) .
        <${id}> <Post.isPublished> "${post.isPublished}" .
        <${id}> <Post.createdAt> "${createdAt}" .
        <${id}> <dgraph.type> "Post" .
        uid(v) <User.posts> <${id}> .
      }
    }
  }
  `;
  const results = await dql.mutate(newArgs);
  return results.data.code === 'Success'
    ? id
    : null;
}

As you can see, this can get complicated very very quickly, and another reason why we NEED for DQL to accept JSON Format Mutations in lambdas… please add this!

So, I hope this helps someone.

Your Humble Dgraph Newbie,

J

1 Like

Hi @jdgamble555,
I couldn’t really understand this bit. Can you elaborate a bit more on this?

This I understand is a pain at present. We would get it fixed soon for 21.03

1 Like

@abhimanyusinghgaur

1.) From my understanding, @withSubscription on a type with lambdas or custom resolvers will not work. Please correct me if I’m wrong, but I don’t believe I could get it to work.

2.) Nice. Thank you. I submitted my PR for that.

Indeed, this is also my understanding and therefore a major challenge for me in securing my backend since mirroring the auto-generated mutations (add/update) with @lambda will require reinventing the wheel for subscription, deep querying, filtering, sorting, etc.

Nevertheless, I found the solution proposed by @abhimanyusinghgaur here GraphQL error: Non-nullable field was not present in result from Dgraph - #6 by abhimanyusinghgaur to potentially satisfy my use-case. Using the example in this thread, the createdAt and updatedAt fields can simply be disabled from mutations then post-hook can be used to populate the disabled fields. With this, everything else should work as expected.

@jdgamble555 does this make sense for your use-case?

@abhimanyusinghgaur Will be great if this feature will be available soon. Already in the roadmap, or any ETA?

@iyinoluwaayoola The example I gave above uses a Mutation type with a lamda in it, which is a functioning work-around for the moment. If you do it this way, there should be no issues with subscriptions, as it only affects mutations and not the queries.

Should be ready by end of this month, although I suspect it could be late as they want to get everything right…

@jdgamble555 I agree. However, using this @lambda mutation approach only for securing the createdAt and updatedAt fields and having to manage all other fields as well (especially when dealing with a complex type) defeats the purpose of the auto-generated mutation types. Given the ability to toggle these secure fields and managing them on pre/post hooks seems neat.

Agreed.

You can subscribe to queries for such types, but the only thing is those queries can’t query for a lambda/custom field (PR). I think the reason for this restriction is that lambda/custom fields could make subscriptions slow because of network latency.
Apart from that, subscriptions now work with @custom(dql: ...) too with this PR: feat(GraphQL):This PR adds subscriptions to custom DQL. by JatinDev543 · Pull Request #7385 · dgraph-io/dgraph · GitHub

Thanks! I will test that, and merge it soon.

Not just disabled, I was also aiming to solve the default value problem with that. So, you may not need to use post mutation hooks at all. That was the most neat way I could think of to achieve all this.

I hope it would make it to 21.07

2 Likes