# Authorization

By default we use client scopes for authorizing access to certain parts of the API. While this is a good and quick solution for a start when using a client credentials flow, it might not be suitable for your environment and can be easily customized.

# Default Privileges

We will use the term privilege when access to an API resource in TiloRes is required. The default privileges are defined in the cognito module in deployment/tilores/main.tf in the available_scopes property.

These default privileges are then assigned within the schema to certain API fields. E.g. the privilege mutation.submit is used in schema/mutation.graphls using the @hasPrivilege directive.

Each client that is defined in the cognito module then can be assigned a list of the allowed requestable privileges. When the client then makes a token request, it can narrow the requested privileges down even further.

# Add New Privilege

Let's assume we want to allow only certain clients to request the myCustomField from the Record. First we need to introduce this new permission by extending the available_scopes and assigning it to a client in deployment/tilores/main.tf:

module "cognito" {
  # other properties
  available_scopes = [
    # other scopes
    {
      "name": "record.myCustomField"
      "description": "allows querying the myCustomField on a record"
    }
  ]
  clients = {
    # other clients
    special_client = {
      allowed_scopes = [
        "tilores/query.search",
        "tilores/record.myCustomField"
      ]
    }
  }
}

Now we only need to assign that new privilege to the Record in schema/record.graphqls:

type Record {
  id: ID!
  myCustomField: String! @hasPrivilege(privilege: "tilores/record.myCustomField")
}

After redeploying, the client can request a new token like this:

curl -u <client_id>:<client_secret> \
--url <token_url> \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data 'scope=tilores/query.search tilores/tilores/record.myCustomField'

It is also possible to assign a privilege to more than one field or even assign it to an object, ensuring that wherever that object is used in your API, that it is properly secured.

type CustomObject @hasPrivilege(privilege: "tilores/customObject") {
  someField: String!
}

type Record {
  customObjectA: CustomObject!
  customObjectB: CustomObject!
}

In that example the client must have the tilores/customObject privilege for accessing customObjectA or customObjectB.

# Custom Privileges

We are fully aware, that this is not how scopes were intended to be used. But we believe, that for a machine to machine communication this approach is totally fine. However, if you want to implement your own logic, that is not dependent on scopes, you can change that.

Let's assume we want to load the privileges from somewhere else. In that case all you have to do is to modify the implementation for the hasPrivilege directive. The default implementation for that can be found in graph/directive/hasprivilege.go.

The PreparePrivilegeContext function is responsible for enriching the context with the information that is needed to perform the actual privilege check. That can be enriching user information or even already enriching the list of privileges a user has. This function will be called exactly once per GraphQL request.

The HasPrivilege function is responsible for verifying that a client has the required privilege. This function is called multiple times, perhaps even in parallel, for each object or field that needs to be accessed. If the client has the required privilege, then this function must invoke the next resolver.

A very basic implementation might looks something like this:

type contextKey string

var privilegesContext = contextKey("privileges")

func PreparePrivilegeContext(ctx context.Context, request *events.APIGatewayProxyRequest) (context.Context, error) {
	// load privileges e.g. for a user extracted from the request
	privileges := map[string]bool{
		"privilege.a": true,
		"privilege.b": false,
	}
	return context.WithValue(ctx, privilegesContext, privileges), nil
}

func HasPrivilege(ctx context.Context, obj interface{}, next graphql.Resolver, privilege string) (interface{}, error) {
	privileges := ctx.Value(privilegesContext).(map[string]bool)
	hasPrivilege, ok := privileges[privilege]
	if ok && hasPrivilege {
		return next(ctx)
	}
	return nil, fmt.Errorf("access denied, required privilege: %s", privilege)
}

When a user tries to access a resource annotated with @hasPrivilege(privilege: "privilege.a") the value for that resource will be returned. For resources with @hasPrivilege(privilege: "privilege.a") directives access will be denied.

Obviously, we skipped the part where we extract the user information from the request and make a lookup for the actual privileges with an external service or database.