Custom lambda not working on Dedicated Cluster with namespaces and ACL

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: