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 | |
---|---|---|
Read | x | |
Update | x | |
Delete | x | |
List | x |
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.