Custom lambda not working on Dedicated Cluster with namespaces and ACL

I have decided to move this post to here, since it makes more sense to discuss this separately.

The Problem

I have the feeling that the header forwarding is an issue. It seems that the X-Dgraph-AccessToken never gets forwarded (and somehow also not added, even when accessing lambda resolvers on the very same namespace). If this is the case then Dgraph Cloud with multi tennency is simply not working! Correct me, but I have no clue how we would have @auth rules in combination with the X-Dgraph-AccessToken at the same time.

If I remember right Dgraph Lambda ist hosted on Cloud. Then this could be an issue here

async function dqlMutate(mutate: string | Object): Promise<GraphQLResponse> {
  const response = await fetch(`${process.env.DGRAPH_URL}/mutate?commitNow=true`, {
    method: "POST",
    headers: {
      "Content-Type": typeof mutate === 'string' ? "application/rdf" : "application/json",
      "X-Auth-Token": process.env.DGRAPH_TOKEN || ""
    },
    body: typeof mutate === 'string' ? mutate : JSON.stringify(mutate)
  })
  if (response.status !== 200) {
    throw new Error("Failed to execute DQL Mutate")
  }
  return response.json();
}

since no X-Dgraph-AccessToken is getting submitted! I have also tried to manually do a fetch request inside a lambda resolver, where I have included a valid access token.

const response = await fetch(`https://MY_BACKEND/mutate?commitNow=true`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Dgraph-AccessToken": "MY_TOKEN",
          
      },
      body: JSON.stringify({
        query: `{ q(func: uid(${input.id})) { uid } }`,
        mutations: [{}],
      }),
    });

   
    const test = await response.json();

Doing this results in an access denied error

{\"data\":null,\"errors\":[{\"message\":\"Access Denied\"}]}

Update

Alright, further investigation revealed (at least with my understanding) that this is definitely a bug!

I managed to get my custom fetch query, against the /mutate endpoint, working for my namespace if I additionally attach the DG-Auth header with the Admin Key to the request…

const response = await fetch(`https://MY_ENDPOINT/mutate?commitNow=true`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Dgraph-AccessToken": "MY_TOKEN",
        "DG-Auth": "MY_ADMIN_KEY",
      },
      body: JSON.stringify({
        query: `{ q(func: uid(${input.id})) { uid } }`,
        mutations: [{}],
      }),
    });

const test = await response.json();

Great! This means that if I want custom lambdas to be working, I need to:

  • Login in the frontend to get the access token for the user on the namespace so I can mutate against the /graphql endpoint
  • Either submit the access token as a parameter(!) or run the login mutation inside the lambda resolver again to get another access token.
  • We obviously can’t use the built in functions dql.mutate and dql.query anymore, so we have to write our own wrapper wich performs a custom fetch request where we manually attach the X-Dgraph-AccessToken and the DG-Auth with the admin key as value.

So either I did not get this entirely or this is such a big fail! How can it be that no one ever experienced this before? Am I the only one paying for a Dedicated Cluster and tried namespaces?

Once again the trust in Dgraph is put under test…

1 Like

Update 2

This topic keeps getting more strange! I have decided to write a custom wrapper for dql.mutate and dql.query which fetches a X-Dgraph-AccessToken from inside the lambda resolver. I have also created a user (Test) on the namespace I want to mutate, including a user group (TestGroup) where I have ticked everything for

  • read
  • modify and
  • write

So the user should be allowed everything, right? However, if I submit this token it allows me only to query for data but I’m not allowed to mutate anything! If I login as groot I can do both.

1 Like

I just bumped into the same issue…

I can’t use dql.mutate and transforming my existing queries to make it work is a pain…

Did you manage to make it work?
Also can you share your custom wrapper code? That would be helpful.
Thanks


Edit:
It seems this issue is only with dql. When using Graphql mutations or queries on webhooks with ACL it works fine

const hasTimestamp = await graphql(hasTimestampQuery);

Hey @Konarium!

Yeah, I’ve solved the issue and after consultation with members of the Core Team, it is up to now the only possible solution.

Login

First things first. I’m not sure how you access your namespaces yet since only Tenant-0 is publicly exposed and thus reachable directly without submitting an X-Dgraph-AccessToken. Since the only way of receiving the token for a given namespace is querying against the /admin endpoint, hence requireing an admin key which I do not want to expose in my application bundle, I wrote a custom query to fetch tokens via Tenant-0.

This is my schema on Tenant-0:

type Tokens @generate(
  query: {get: false, query: false, aggregate: false}
  mutation: {add: false, update: false, delete: false}
  subscription: false
){
  accessJWT: String
  refreshJWT: String
}

type LoginPayload @generate(
  query: {get: false, query: false, aggregate: false}
  mutation: {add: false, update: false, delete: false}
  subscription: false
){
  response: Tokens
}

type Mutation {
		getTokenForNamespace(userId: String, password: String, namespace: Int, refreshToken: String): LoginPayload @custom(http: {
			url: "https://old-meadow.eu-central-1.aws.cloud.dgraph.io/admin",
			method: POST,
			secretHeaders: ["DG-Auth:AdminKey"],
    	introspectionHeaders:["DG-Auth:AdminKey"],
      graphql: "mutation($userId: String, $password: String, $namespace: Int, $refreshToken: String) { login(userId: $userId, password: $password, namespace: $namespace, refreshToken: $refreshToken) }"
		})
}

# Dgraph.Secret AdminKey "YOUR-ADMIN-KEY-HERE"

DQL Mutations/Queries from Custom Lambda Resolver

As I have stated in my previous post, the Dgraph-Lambda package is indeed implemented in the cloud. Looking at the source reveals that there is no token forwarding for DQL operations and for GraphQL operations only one header will be submitted. I have not tested this, but I guess your GraphQL operations only work because you have no @auth rules on the types which are subject to your queries. Under my understanding also GraphQL operations would fail if you require both tokens, the

  • X-Dgraph-AccessToken → to query against the right namespace, and the
  • X-Auth-Token → which includes the user claim for your @auth rules

However, since all methods which are accessible as arguments from inside a custom lambda resolver, are simple wrappers for a fetch query, I wrote my own wrapper with an additional login step to fetch an X-Dgraph-AccessToken inside the resolver - I know! Sounds weird to login twice but this is how it is at the moment :man_shrugging: The only possible argument here is that:

You have to require an X-Dgrap-AccessToken from inside the lambda resolver again so you have full control over what the lambda is allowed to do via ACL.

I have simply created a lambda user with special read/write permissions valid for my custom lambda operations. This user is the one I’m logging in with from inside the custom resolver.

Request access token

export const getAccessToken: GetAccessToken = async params => {
  const { userId, password, namespace, refreshToken } = params;

  if (!refreshToken && !(userId && password && namespace)) {
    throw new Error("Not all paramteters for logging in to a namespace are provided. Either submit a refresh token or userId, password and namespace.");
  }

  const res = await fetch(`YOUR_CLUSTER_ENDPOINT/graphql`, {
    headers: {
      "Content-Type": "application/json",
    },
    method: "POST",
    body: JSON.stringify({
      query: `mutation GetToken($userId: String, $password: String, $namespace: Int, $refreshToken: String) {
        getTokenForNamespace(userId: $userId, password: $password, namespace: $namespace, refreshToken: $refreshToken) {
          response {
            accessJWT
            refreshJWT
          }
        }
      }`,
      variables: {
        userId: userId,
        password: password,
        namespace: namespace,
        refreshToken: refreshToken,
      },
    }),
  });

  const result = (await res.json()) as GetAccessTokenResult;

  if (result.errors) {
    throw new Error(result.errors[0].message);
  }

  return result.data.getTokenForNamespace.response;
};

DQL Request Wrapper

export const dqlRequest = async <T extends DqlRequest<any>>(params: T["params"]): Promise<T["result"] | never> => {
  const { type, query, errorPos, commitNow, secretHeaders, user } = params;

  // Get the lambda access token for the current namespace
  const accessToken = await getAccessToken(user);

  // Set the content header type according to submitted query data
  const contentType =
    typeof query === "string" ? (type === "mutate" ? "application/rdf" : "application/dql") : "application/json";

  // Set the request url
  const requestUrl = `YOUR_CLUSTER_ENDPOINT/${type}${commitNow ? "?commitNow=true" : ""}`;

  // Perform fetch request
  const request = await fetch(requestUrl, {
    method: "POST",
    headers: {
      "Content-Type": contentType,
      "X-Dgraph-AccessToken": accessToken.accessJWT,
      ...secretHeaders,
    },
    body: JSON.stringify(query),
  });

  const result = (await request.json()) as DqlRequest<any>["result"];

  if (result.errors) {
    throw new Error(result.errors[0].message);
  }

  return result.data;
};

The Secret Headers object MUST CONTAIN the DG-Auth header value! In my case I have set this to the Client Key!

Hope this helps! :raised_hands:

@Poolshark

Thanks a lot mate! This is awesome I will help me a lot (an hopefully other people too) :pray:

1 Like