Access Control

In this guide we'll look at how to add access control to your services. Access control provides a means to restrict which entities can be accessed by different users, protecting sensitive information. First we'll cover the #readable and #writable metadata tags, then we'll look at their behaviour in projects with and without authentication configured. After that we'll see how these tags affect structs. Finally we'll mention the limitations of this access control support. It's highly recommended you check out the Authentication guide before following this guide.

Metadata Tags

We have two metadata tags for access control, #readable and #writable. These tags define how specific users can access existing entities for different endpoints:

#readable#writable
Readx
Updatex
Deletex
Listx

The List endpoint is not generated by default, to find out how to add it, check out the Enumeration guide. The Identify endpoint added to services with #auth defined, as discussed in the Authentication guide, is not applicable to these access control tags. This is because by definition the Identify endpoint is only accessible by the corresponding authenticated user.

These tags can be set to either by: this or by: all. For example, taking the ExampleService from the Getting Started guide, we would write:

ExampleService: service {
foo: string;
bar: int;
#readable(by: all);
}

But what do by: this and by: all mean? Let us consider a project with authentication.

Projects with Authentication

Here we've taken the ExampleProject from the Authentication guide.

ExampleProject: project {
#language(go);
#database(postgres);
#provider(dockerCompose);
#authMethod(email);
}
ExampleUser: service {
name: string;
#auth;
}
ExampleService: service {
foo: string;
bar: int;
}

Since the project has authentication, every service actually has implicit default access control tags defined:

ExampleUser: service {
name: string;
#auth;
// implicit #readable(by: this);
// implicit #writable(by: this);
}
ExampleService: service {
foo: string;
bar: int;
// implicit #readable(by: this);
// implicit #writable(by: this);
}

The by: this means that only the authenticated user that created an entity in a service can be the one to read (make Read or List requests) the entity or write (make Update or Delete requests) to the entity.

For example, if User A makes a Create request to the ExampleService service with their access token, the resulting entity created can only be read or written by User A. If User B then tries to make an Update request to User A's entity with their own access token, User B will receive a 401 Unauthorized error.

If by: all is used, any user can make the corresponding requests unimpeded. So if instead we define ExampleService like this:

ExampleService: service {
foo: string;
bar: int;
#readable(by: all);
#writable(by: all);
}

When User A makes a Create request to this ExampleService, the resulting entity can be read and written by any other authenticated user. Good news for User B.

The #readable and #writable tags can be set to differing values, for example:

ExampleService: service {
foo: string;
bar: int;
#readable(by: all);
#writable(by: this);
}

This means that any authenticated user can Read or List (if defined) a created ExampleService entity, but only the user that created the entity may Update or Delete it.

Defaults

For projects with authentication, #readable and #writable behaviour defaults to by: this for security, forcing by: all to specified if desired. This reduces the likelihood of entities being exposed to users that should not have access.

Projects without Authentication

For projects without authentication it doesn't really make sense to have access control, since users cannot be identified. Therefore all services have #readable(by: all) and #writable(by: all) implicitly defined on them:

ExampleProject: project {
#language(go);
#database(postgres);
#provider(dockerCompose);
}
ExampleService: service {
foo: string;
bar: int;
// implicit #readable(by: all);
// implicit #writable(by: all);
}

Structs

Structs inherit #readable and #writable behaviour from their parent service. The only exception to this is the Create endpoint. For example, let's add a struct to the ExampleService service:

ExampleService: service {
foo: string;
bar: int;
Photo: struct {
image: data(5M);
caption: string;
}
#readable(by: this);
#writable(by: this);
}

Making a Create request to create a Photo entity requires a parent ExampleService entity UUID in the URL, e.g. /api/example-service/43cc65f5-823c-11ea-9dc4-0242ac180003/photo. If like we have here, the parent service has #writable(by: this) defined, then only the creator of the parent entity can create struct entities on it. You can think of struct entities as belonging to the creator of the parent service entity.

For more details on structs check out the Structs guide.

Limitations

We acknowledge that this current implementation is limited in the granularity of access control it can support. Access roles is top of the list of features we wish to support next.

In the meantime, for projects with auth, the authentication token of a request is exposed to the hooks, so you can add your own access control there. Though note in the hooks you can only restrict access further, not widen. For more information on hooks, check out the Business Logic & Hooks guide.