Access Control
#
Authorization Levels#
APIIn Symfony, roles (i.e., ROLE_FOO
) represent authorization levels.
In the boilerplate, we defined two hierarchical roles: administrator and user. Hierarchical means that:
- The administrator is the top-level permission: it has its access level and user's access levels.
- A user has only its access level.
In other words, if you limit the access to a resource to users with the administrator authorization level, administrator users can access it too but not the user.
As explained in the previous chapter, we implemented the getRoles
method from the UserInterface
.
This method has to return an array of string.
However, our users have only one authorization level attached to them (thanks to the hierarchy). That's why we always return an array of one element:
Moreover, we create the Role
enumerator, which lists our users'role
property's available values:
These values don't have the prefix ROLE_
because
we don't want to store Symfony specific information in the users
table.
Yet, this prefix is mandatory because otherwise, Symfony will not recognize the permission.
That's why we prefix the role whenever we interact with Symfony in our code:
note
📣  A user must have one authorization level; otherwise authentication won't work.
#
Web ApplicationThe file src/webapp/store/auth/getters.js from the auth
store mimics the role hierarchy from Symfony:
#
Access ControlAccess control in the API is about defining what kind of users (anonymous, authenticated, administrator, etc.) may call (or not) an HTTP entry point.
In the API, there are three sorts :
- Symfony's routes.
- GraphQL mutations/queries.
- The GraphQL fields.
#
Symfony Routes' AnnotationsRestrict to authenticated users:
Restrict to authenticated users with a specific role:
See the security and annotations documentations from Symfony for more details.
#
GraphQLite AnnotationsGraphQLite provides many Symfony like annotations, even if they differ slightly on some occasions. The import statements are also different.
Restrict to authenticated users:
Inject the authenticated user:
Restrict to authenticated users with a specific role:
note
📣  Contrary to Symfony's routes, always put the @Logged
annotation before the @Security
and @InjectUser
annotations
on your GraphQL entry points. The web application needs to know the difference between unauthenticated (401
)
and access denied (403
)!
See GraphQLite documentation for more details.
#
Symfony's VotersSometimes it is not enough to restrict access to authenticated users/users with a specific role. For instance, when a resource is only accessible to the user owning it.
That's when Symfony's voters come in handy!
It comes in two parts:
- The PHP class which is specifying the voter's rules.
- The annotation we put on GraphQL mutations/queries and Symfony's routes.
#
GraphQLFor instance, let's examine the following scenario: an administrator can delete a user, but cannot delete himself:
A voter annotation has two arguments:
- The attribute: in our application, it's equivalent to an action, i.e.,
DELETE_USER
,GET_USER
, etc. - The subject: mostly the model on which we want to check ownership.
Here the annotation asks for a voter that may handle the DELETE_USER
attribute for the user1
subject.
By convention, we've created a voter PHP class per subject. In that case, as the subject is a Uver
, we've made the
UserVoter
class.
Each voters' PHP class consist of three parts:
- The attributes constants.
- The method
supports
: it returnstrue
if the voter supports both the given attribute and subject. - The method
voteOnAttribute
: only called if thesupports
method returnedtrue
. It contains your custom logic for validating (or not) the access.
Take a closer look at those methods from UserVoter
for a better understanding.
#
RESTIn your Symfony's routes, you may not have access to a model directly but an id
instead:
In your Symfony's Voter, you have to check if the subject is either a model or a string:
#
GraphQL FieldsUsually, you define your GraphQL types' fields in your migrations or your Model
's getters in the getters of your
when overriding a base Model
's getter. That's when you must decide if you want to expose or not the field
to your GraphQL clients.
Also, as you are developing both the clients and the API, securing the entry points should be enough. If that's not the
case, you can add the same @Security
annotations to your getters as the ones from the mutations/queries.
#
Web ApplicationThe src/webapp/layouts/error.vue layout handles almost every error.
You can propagate a GraphQL error via context.error(e)
in the asyncData
component's attribute or this.$nuxt.error(e)
in your component's methods (except mixins, where you have to throw it):
In the error layout, we check if:
401
status code: the user has no session in the API. Therefore, we call theresetUser
mutation and redirect the user to the login page. On success, the web application redirects the user to the current page thanks to theredirect
query parameter.404
,403
, or anything else: we display an error page.
Some pages are also not available for the authenticated user (for instance, the login page). You may use the src/webapp/middleware/redirect-if-authenticated.js middleware to redirect the user to the home page:
If a page requires to be authenticated but does not query protected GraphQL entry points / Symfony routes, you may also use the src/webapp/middleware/redirect-if-not- authenticated.js middleware to redirect the user to the home page: