Privar queries e mutations graphQL

Boa tarde, pessoal.

1 - Alguém sabe como privar queries e mutations GraphQL apenas para determinado app executar ou desde que o usuário seja autenticado?
Exemplo: se eu acesso o endpoint /_v/private/graphql/v1 no Postman e executo uma query, os dados são retornados. O ideal seria privar essa query para ambientes externos e quando acessar pelo Postman por exemplo ser retornado 403.
Tentei usar Role-based policies: Policies porém independente das regras que coloco no resources eu sempre consigo executar a query no Postman com os dados retornados.
Não sei se de fato essas policies funcionam para esse objetivo.

2 - Alguém já implementou rate limit no Node da VTEX ou sabem se é possível fazer essa implementação?

Obrigado.

1 Like

Bom dia João
Sobre o ponto 1, o role-based policies funciona para bloquear chamadas do graphql, se quiser bloquear as rotas, teria que bloquear usando o Resource-based policies no service.json quem tem a explicação nesse mesmo documento que você mandou, só quem mais embaixo

Sobre o ponto 2, eu nunca cheguei a implementar o rate limit, mas há bibliotecas que podem ser usadas para poder utilizar para implementar esse rate limit

3 Likes

Obrigado, Ana.

“Sobre o ponto 1, o role-based policies funciona para bloquear chamadas do graphql”
O que seria exatamente ‘bloquear chamadas do graphql’? Qual o comportamento disso?

Sobre o rate limit, as bibliotecas sempre predem para adicionar por exemplo dentro de um app.use(), adicionar antes das rotas etc. Como a infra VTEX é um pouco diferente, ainda não consegui achar nenhum que se encaixa nesse cenário. Conhece alguma?

Obrigado novamente.

Bom dia João
Seria para bloquear a busca direto no GraphQL, como o GraphQL IDE ou chamadas no react.

Sobre o rate limite tem 2 formas que eu vi o pessoal fazendo, uma é usando uma função do VTEX/api, tem esse exemplo aqui e outro usando o sleep

Bom dia, Ana.

Obrigado mais uma vez.

Tentei usar este exemplo, porém não parece estar funcionando.
A função é executada, porém os tokensLeft sempre é o mesmo independente da quantidade de requests que faço. Acredito que deveria ser subtraído sempre que uma nova request for feita.
Executei mais de 2 mil requisições com intervalo de 1ms e não teve essa limitação.

Código:

import { perMinuteRateLimiter } from "@vtex/api/lib/service/worker/runtime/http/middlewares/rateLimit";
import { createTokenBucket } from "@vtex/api/lib/service/worker/runtime/utils/tokenBucket";

const teste = async (_obj: any, args: any, ctx: Context, _info: any) => {
    const { clients } = ctx;

    const globalLimiter = createTokenBucket(2 * 1000);
    console.log(
        " ~ file: _teste.ts:8 ~ teste~ globalLimiter:",
        globalLimiter
    );

    ctx.set("Cache-Control", "no-cache, no-store");

    const request = await clients.teste.teste(args.email);

    perMinuteRateLimiter(2 * 400, globalLimiter);
    return request;
};

export { teste};

Retorno a cada request:

09:35:09.852 - info: ![🚀](https://fonts.gstatic.com/s/e/notoemoji/15.0/1f680/32.png) ~ file: _teste.ts:8 ~ teste ~ globalLimiter: TokenBucket {
loadSaved: [Function],
save: [Function],
removeTokensSync: [Function],
removeTokens: [Function],
size: 1000,
tokensToAddPerInterval: 2000,
interval: 60000,
tokensLeft: 1000,
lastFill: 1677674110378,
spread: true,
parentBucket: undefined,
maxWait: undefined
} service-node@6.36.3
09:35:10.097 - info: [12:35:10.626Z] [95] ***/routesapi:__graphql 200 POST /**/routesapi/_v/graphql 268 ms service-node@6.36.3
09:35:11.743 - info: ![🚀](https://fonts.gstatic.com/s/e/notoemoji/15.0/1f680/32.png) ~ file: _teste.ts:8 ~ teste ~ globalLimiter: TokenBucket {
loadSaved: [Function],
save: [Function],
removeTokensSync: [Function],
removeTokens: [Function],
size: 1000,
tokensToAddPerInterval: 2000,
interval: 60000,
tokensLeft: 1000,
lastFill: 1677674112270,
spread: true,
parentBucket: undefined,
maxWait: undefined
} service-node@6.36.3
09:35:11.802 - info: [12:35:12.329Z] [95] ***/routesapi:__graphql 200 POST /**/routesapi/_v/graphql 60 ms service-node@6.36.3
09:35:13.465 - info: ![🚀](https://fonts.gstatic.com/s/e/notoemoji/15.0/1f680/32.png) ~ file: _teste.ts:8 ~ teste ~ globalLimiter: TokenBucket {
loadSaved: [Function],
save: [Function],
removeTokensSync: [Function],
removeTokens: [Function],
size: 1000,
tokensToAddPerInterval: 2000,
interval: 60000,
tokensLeft: 1000,
lastFill: 1677674113975,
spread: true,
parentBucket: undefined,
maxWait: undefined
} service-node@6.36.3

Estou tentando ver isso via tickets, porém se tiver alguma ideia ou sugestão, agradeço.

Obrigado!!

Qual seria o rate limit que você deseja?

Para realizar os testes e ter a confirmação de que está funcionando, o ideal seria entre 2 e 5 requisições para limitação.

Achei um exemplo aqui para teste
Talvez te ajude:

describe('Rate limit per minute', () => {

  test('Test per minute middleware', async () => {
    // 2 * 1000 because bucket size is / 2 in order to not overflow amount of requests in 1 minute
    const globalLimiter: TokenBucket | undefined = createTokenBucket(2 * 1000)
    const perMinuteRateLimiterMiddleware = perMinuteRateLimiter(2 * 400, globalLimiter)

    await expect(
      execRequests(300, perMinuteRateLimiterMiddleware)
    ).resolves.not.toThrowError(TooManyRequestsError)

    await expect(
      execRequests(300, perMinuteRateLimiterMiddleware)
    ).rejects.toThrowError(TooManyRequestsError)
  })
})

Obrigado Ana.

Só não consegui achar o execRequests. É alguma importação de biblioteca? Função?

É uma função!
Esqueci de incluir ela, desculpe

type Middleware = (ctx: ServiceContext, next: () => Promise<any>) => Promise<void>

async function execRequests(requestsAmount: number, middleware: Middleware) {
  await Promise.all(
    new Array(requestsAmount).fill(null).map(_ => middleware(nopCtx, nopNext))
  )
}

Boa tarde João.
Na busca dessa função, acredito ter achado outra forma de implementação que deve resolver seu problema
Vou deixar aqui pra você!

import TokenBucket from 'tokenbucket'

export function createTokenBucket(rateLimit?: number, globalRateTokenBucket?: TokenBucket){
  return rateLimit ? new TokenBucket({
    interval: 'minute',
    parentBucket: globalRateTokenBucket,
    size: Math.ceil(rateLimit / 2.0),
    spread: true,
    tokensToAddPerInterval: rateLimit,
  }) : globalRateTokenBucket!
}

export function perMinuteRateLimiter(rateLimit?: number, globalLimiter?: TokenBucket) {
  if (!rateLimit && !globalLimiter) {
    return noopMiddleware
  }

  const tokenBucket: TokenBucket = createTokenBucket(rateLimit, globalLimiter)

  return function perMinuteRateMiddleware(ctx: ServiceContext, next: () => Promise<void>) {
    if (!tokenBucket.removeTokensSync(1)) {
      throw new TooManyRequestsError(responseMessagePerMinute)
    }
    return next()
  } 
}

Lembrando que seria necessário importar a biblioteca tokenbucket para poder usar as funções!

Bom dia, Ana.

Obrigado pela ajuda.

Acredito que entendi o porque de não subtrair os tokensLeft toda vez que a request é feita.
Toda vez que o meu código é executado, ele define o createBucket e o perMinuteRateLimiter com seus respectivos valores, ou seja, ele sempre está executando com os mesmos valores. O correto seria esses valores serem armazenados em memória por exemplo, dessa forma, toda vez que eu executar o código, o espaço da memória já vai ler esse valor e não vai definir novamente. Dessa forma, a função para remover os tokens iria subtraindo desse valor na memória.

Não sei se por trás dessa biblioteca VTEX está sendo feito isso, mas parece que não.