One of the the most common questions I get asked is what happens if a user out on the internet sends queries that we don't want run. For example how do we stop him from fetching all users or the emails of users. Our answer to this is that it is not an issue as this cannot happen, let me explain.
Super Graph runs in one of two modes
production, this is controlled via the config value
production: false when it's false it's running in development mode and when true, production. In development mode all the named queries (including mutations) are saved to the allow list
./config/allow.list. While in production mode when Super Graph starts only the queries from this allow list file are registered with the database as prepared statements.
Prepared statements are designed by databases to be fast and secure. They protect against all kinds of sql injection attacks and since they are pre-processed and pre-planned they are much faster to run then raw sql queries. Also there's no GraphQL to SQL compiling happening in production mode which makes your queries lighting fast as they are directly sent to the database with almost no overhead.
In short in production only queries listed in the allow list file
./config/allow.list can be used, all other queries will be blocked.
How to think about the allow list?
The allow list file is essentially a list of all your exposed API calls and the data that passes within them. It's very easy to build tooling to do things like parsing this file within your tests to ensure fields like
credit_card_no are not accidently leaked. It's a great way to build compliance tooling and ensure your user data is always safe.
This is an example of a named query,
getUserWithProducts is the name you've given to this query it can be anything you like but should be unique across all you're queries. Only named queries are saved in the allow list in development mode.
You can only have one type of auth enabled either Rails or JWT.
Ruby on Rails
Almost all Rails apps use Devise or Warden for authentication. Once the user is authenticated a session is created with the users ID. The session can either be stored in the users browser as a cookie, memcache or redis. If memcache or redis is used then a cookie is set in the users browser with just the session id.
Super Graph can handle all these variations including the old and new session formats. Just enable the right
auth config based on how your rails app is configured.
Cookie session store
Memcache session store
Redis session store
For JWT tokens we currently support tokens from a provider like Auth0 or if you have a custom solution then we look for the
user_id in the
subject claim of of the
id token. If you pick Auth0 then we derive two variables from the token
user_id_provider for to use in your filters.
We can get the JWT token either from the
authorization header where we expect it to be a
bearer token or if
cookie is specified then we look there.
For validation a
secret or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file.
Firebase auth also uses JWT the keys are auto-fetched from Google and used according to their documentation mechanism. The
audience config value needs to be set to your project id and everything else is taken care for you.
Header auth is usually the best option to authenticate requests to the action endpoints. For example you might want to use an action to refresh a materalized view every hour and only want a cron service like the Google AppEngine Cron service to make that request in this case a config similar to the one above will do.
exists: true parameter ensures that only the existance of the header is checked not its value. The
value parameter lets you confirm that the value matches the one assgined to the parameter. This helps in the case you are using a shared secret to protect the endpoint.
In addition to the default auth configuration you can create additional named auth configurations to be used
with features like
actions. For example while your main GraphQL endpoint uses JWT for authentication you may want to use a header value to ensure your actions can only be called by clients having access to a shared secret
or security header.
Actions is a very useful feature that is currently work in progress. For now the best use case for actions is to
refresh database tables like materialized views or call a database procedure to refresh a cache table, etc. An action creates an http endpoint that anyone can call to have the SQL query executed. The below example will create an endpoint
/api/v1/actions/refresh_leaderboard_users any request send to that endpoint will cause the sql query to be executed. the
auth_name points to a named auth that should be used to secure this endpoint. In future we have big plans to allow your own custom code to run using actions.
Using CURL to test a query
It's common for APIs to control what information they return or insert based on the role of the user. In Super Graph we have two primary roles
anon the first for users where a
user_id is available the latter for users where it's not.
Define authenticated request?
An authenticated request is one where Super Graph can extract an
user_id based on the configured authentication method (jwt, rails cookies, etc).
user role can be divided up into further roles based on attributes in the database. For example when fetching a list of users, a normal user can only fetch his own entry while an admin can fetch all the users within a company and an admin user can fetch everyone. In some places this is called Attribute based access control. So in way we support both. Role based access control and Attribute based access control.
Super Graph allows you to create roles dynamically using a
match config values.
This configuration is relatively simple to follow the
roles_query parameter is the query that must be run to help figure out a users role. This query can be as complex as you like and include joins with other tables.
The individual roles are defined under the
roles parameter and this includes each table the role has a custom setting for. The role is dynamically matched using the
match parameter for example in the above case
users.id = 1 means that when the
roles_query is executed a user with the id
1 will be assigned the admin role and those that don't match get the
user role if authenticated successfully or the