Understanding Permissions for Various APIs in Open Event API Server

Since the Open Event Server has various elements, a proper permissions system is essential. This huge list of permissions is well compiled in the developer handbook which can be found here. In this blogpost, permissions listed in the developer handbook are discussed. Let’s start with what we wish to achieve, that is, how to make sense of these permissions and where does each clause fit in the API Server’s codebase.

For example, Sponsors API has the following permissions.

List

View

Create

Update

Delete

Superadmin/admin

Event organizer

✓ [1]

✓ [1]

✓ [1]

✓ [1]

✓ [1]

Registered User

✓ [3]

✓ [3]

✓ [4]

✓ [3]

✓ [3]

Everyone else

✓ [2][4]

✓ [2][4]

  1. Only self-owned events
  2. Only sessions with state approved or accepted
  3. Only self-submitted sessions
  4. Only to events with state published.

Based on flask-rest-jsonapi resource manager, we get list create under ResourceList through ResourceList’s GET and POST methods, whereas View, Update, Delete work on single objects and hence are provided by ResourceDetail’s GET, PATCH and DELETE respectively. Each function of the permission manager has a jwt_required decorator.

@jwt_required
def is_super_admin(view, view_args, view_kwargs, *args, **kwargs):

@jwt_required
def is_session_self_submitted(view, view_args, view_kwargs, *args, **kwargs):


This
 ensures that whenever a check for access control is made to the permission manager, the user is signed in to Open Event. Additionally, the permissions are written in a hierarchical way such that for every permission, first the useris checked for admin or super admin, then for other accesses. Similar hierarchy is kept for organizer accesses like track organizer, registrar, staff or organizer and coorganizer.

Some APIs resources require no authentication for List. To do this we need to add a check for Authentication token in the headers. Since each of the functions of permission manager have jwt_required as decorator, it is important to checkfor the presence of JWT token in request headers, because we can proceed to check for specific permissions in that case only.

if 'Authorizationin request.headers:
 _jwt_required(current_app.config['JWT_DEFAULT_REALM'])


Since the resources are created by endpoints of the form : 
‘/v1/<resource>/` , this is derived from the separate ResourceListPost class. This class is POST only and has a before_create object method where the required relationships and permissions are checked before inserting the data in the tables. In the before_create method, let’s say that event is a required relationship, which will be defined by the ResourceRelationRequired , then we use our custom method

def require_relationship(resource_list, data):
    for resource in resource_list:
        if resource not in data:
            raise UnprocessableEntity({'pointer': '/data/relationships/{}'.format(resource)},
                                      "A valid relationship with {} resource is required".format(resource))


to check if the required relationships are present in the data. The event_id here can also be used to check for organizer or co-organizer access in the permissions manager for a particular event.

Here’s another permissions structure for a different API – Settings.

List

View

Create

Update

Delete

Superadmin/admin

Everyone else

✓ [1]

  1. Only app_nametaglineanalytics_keystripe_publishable_keygoogle_urlgithub_urltwitter_urlsupport_urlfacebook_urlyoutube_urlandroid_app_urlweb_app_url fields .

This API does not allow access to the complete object, but to only some fields which are listed above. The complete details can be checked here.

Resources

Working with Activity API in Open Event API Server


Recently, I added the Activities API with documentation and dredd tests for the same in
Open Event API Server. The Activity Model in the Open Event Server is basically a log of all the things that happen while the server is running, like – event updates, speaker additions, invoice generations and other similar things. This blogpost explains how to implement Activity API in the Open Event API Server’s nextgen branch. In the Open Event Server, we first add the endpoints, then document these so that the consumers ( Open Event Orga App, Open Event Frontend) find it easy to work with all the endpoints.

We also test the documentation against backend implementation to ensure that a end-developer who is working with the APIs is not misled to believe what each endpoint actually does in the server.

We also test the documentation against backend implementation to ensure that a end-developer who is working with the APIs is not misled to believe what each endpoint actually does in the server.
The Activities API endpoints are based on the Activity database model. The Activity table has four columns – 
id, actor, time, action, the names are self-explanatory. Now for the API schema, we need to make fields corresponding these columns.
Since id is auto generated, we do not need to add it as a field for API. Also the activity model’s __init__ method stamps time with the current system time. So this field is also not required in the API fields. We are left with two fields- actor and action.

Defining API Schema

Next, we define the API Schema class for Activities model. This will involve a Meta class and fields for the class.The Meta class contains the metadata of the class. This includes details about type_,

self_view, self_view_kwargs and an inflect parameter to dasherize the input fields from request body.

We define the four fields – id, actor, time and action according to marshmallow fields based on the data type and parameters from the activities model. Since id, actor and action are string columns and time is a DateTime column, the fields are written as following:

The id field is marked as dump only because it is a read-only type field. The other fields are marked with allow_none as they are all non-required field.

ActivityList Class:
The activity list class will provide us with the endpoint: “/v1/activities”

This endpoint will list all the activities. Since we wanted only GET requests to be working for this, so defined method = [‘GET’, ] for ResourceList. The activities are to be internally created based on different actions like creating an event, updating an event, adding speakers to sessions and likewise. Since the activities are to be shown only to the server admin, 
is_admin permission is used from the permission manager.

ActivityDetail Class:

The activity detail gives methods to work with an activity based on the activity id.
The endpoint provided is :  ‘/v1/activity/<int:activity_id>’

Since this is also an admin-only accessible GET only endpoint the following was written:

Writing Documentation:

The documentation is written using API Blueprint. Since we have two endpoints to document : /v1/activities and /v1/activities/<int:activity_id> both GET only.

So we begin by defining the ‘Group Activities’ , under which we first list  ‘Activity Collection’ which essentially is the Activity List class.

For this class, we have the endpoint:  /v1/activities. This is added for GET request. The parameters – actor, time and action are described along with description, type and whether they are required or not.

The request headers are written as part of the docs followed by the expected response.

Next we write the ‘Activity Details’ which represents the ActivityDetail class. Here the endpoint /v1/activities/<int:activity_id> is documented for GET. The parameter here is activity_id, which is the id of the activity to get the details of.

Writing DREDD Test for Documentation

To imitate the request responses, we need a faker script which creates an object of the the class we are testing the docs for then makes the request. For this we use FactoryBoy and dredd hooks to insert data into the database.

Defining Factory Model for Activity db model

The above is the factory model for activity model. It is derived from

factory.alchemy.SQLAlchemyModelFactory. The meta class defines the db model and sqlalchemy session to be used. The actor and action have dummy strings as part of the request body.

Writing Hooks
Now to test these endpoints we need to add objects to the database so that the GET requests have an object to fetch. This is done by dredd hooks. Before each request, an object of the corresponding factory class is initialised and committed into the database. Thus a dummy object is available for dredd to test on. The request is made and the real output is compared with the expected output written in the API Blueprint documentation.

This is what the hooks look like for  this endpoint: GET /activities

Now if the expected responses and actual responses match, the dredd test successfully passes. This dredd test in run on each build of the project on Travis to ensure that documented code does exactly what is says!

This concludes the process to write an API right from Schema to Resources and Documentation and Dredd tests.

Additional Resources:

Using Marshmallow Fields in Open Event API Server

The nextgen Open Event API Server  provides API endpoints to fetch the data, and to modify and update it. These endpoints have been written using flask-rest-jsonapi, which is a flask extension to build APIs around the specifications provided by JSONAPI 1.0. This extension helps you, quoting from their website:

flask-rest-jsonAPI’s data abstraction layer lets us expose the resources in a flexible way. This is achieved by using Marshmallow fields by marshmallow-jsonapi. This blog post explains how we use the marshmallow fields for building API endpoints in Open Event API Server.

The marshmallow library is used to serialize, deserialize and validate input data. Marshmallow uses classes to define output schemas. This makes it easier to reuse and configure the and also extend the schemas. When we write the API Schema for any database model from the Open Event models, all the columns have to be added as schema fields in the API class.

This is the API Server’s event schema using marshmallow fields:

These are the Marshmallow Field classes for various types of data. You can pass the following parameters when creating a field object. The ones which are used in the API Server as described below. For the rest, you can read more on marshmallow docs.

Let’s take a look at each of these fields. Each of the following snippets sample writing fields for API Schema.

identifier = fields.Str(dump_only=True)
  • This is a field of data-type String.
  • dump_only :  This field will be skipped during deserialization as it is set to True here. Setting this true essentially means marking `identifier` as read-only( for HTTP API) 
name = fields.Str(required=True)
  • This again is a field of data-type String.
  • This is a required field and a ValidationError is raised if found missing during deserialization. Taking a look at the database backend:

Since this field is set to non-nullable in the database model, it is made required in API Schema.

 external_event_url = fields.Url(allow_none=True)
  • This is a field of datatype URL.
  • Since this is not a required field, NULL values are allowed for this field in the database model. To reflect the same in the API, we have to add allow_none=True. If missing=None is unset, it defaults to false.
ends_at = fields.DateTime(required=Truetimezone=True)
  • Field of datatype DateTime
  • It is a required field for an event and the time used here is timezone aware.
latitude = fields.Float(validate=lambda n: -90 <= n <= 90allow_none=True)
  • Field of datatype Float.
  • In marshmallow fields, we can use validator clauses on the input value of a field using the validate: parameter. It returns a boolean, which when false raises a Validation Error. These validators are called during deserialization.
is_map_shown = fields.Bool(default=False)
  • Field of datatype boolean.
  • Default value for the marshmallow fields can be set by defining using default: Here, is_map_shown attribute is set to false as default for an event.
privacy = fields.Str(default="public")
  • privacy is set to default “public”.

When the input value for a field is missing during serialization, the default value will be used. This parameter can either be a value or a callable.

As described in the examples above, you can write the field as field.<data-type>(*parameters to marshmallow.fields.Field constructor*).

The parameters passed to the class constructor must reflect the column definition in the database model, else you might run into unexpected errors. An example to quote from Open Event development would be that null values were not being allowed to be posted even for nullable columns. This behavior was because allow_none defaults to false in schema, and it has to be explicitly set to True in order to receive null values. ( Issue for the same: Make non-required attributes nullable and the Pull Request made for fix.)

Fields represent a database model column and are serialized and deserialized, so that these can be used in any format, like JSON objects which we use in API server. Each field corresponds to an attribute of the object type like location, starts-at, ends-at, event-url for an event. marshmallow allows us to define data-types for the fields, validate input data and reinforce column level constraints from database model.

This list is not exhaustive of all the parameters available for marshmallow fields. To read further about them and marshmallow, check out their documentation.

Additional Resources

Code involved in API Server:

Permission Dependent Schema for Admin Settings in Open Event Server

For implementing the next version of the API in Open Event, the schema is a very important thing. It tells you exactly what all information you need to send in the body and how the response will look. In flask-rest-jsonapi, we usually mention a schema for an API which is then used for validating requests and sending response. Using decorators, we restrict who all can create, edit or get responses from a particular API endpoint. However, a scenario may so arise that you need to show data to users at different permissions level, but the amount of data shown significantly varies with the permission.

For example, for the settings API in our case. There are few informations like the app name, app tagline that we want to be available to users at all permission levels. However, informations such as aws secret key, or mailing secret keys or any other secret key, we want that to be available only to the admin and super admin. And the responses should be such that users at different permission level should feel that whatever information shown to them is complete and not missing.

So, what we do is we create different schemas, in our case 2 different schemas. Depending on the permission of the user, we show them a particular schema. In our case, the two schemas are SettingSchemaAdmin and SettingSchemaNonAdmin. In SettingSchemaAdmin, we have all the attributes or fields that are present and is accessible to the Admin and Super Admin. In the SettingSchemaNonAdmin however, we have only those fields and attributes that we want to show to all non admin users.

from flask_jwt import current_identity
 
class SettingDetail(ResourceDetail):
    """
    setting detail by id
    """
 
    def before_get(self, args, kwargs):
        kwargs['id'] = 1
        if current_identity.is_admin or current_identity.is_super_admin:
            self.schema = SettingSchemaAdmin
        else:
            self.schema = SettingSchemaNonAdmin

 

The above code helps us achieve this. If you have read previous blogs about the API server, you would already know that we are using JWT for authenticating our users. In this code, we are importing current_identity from flask_jwt. Current_identity, returns us an object of the User type which has properties such as is_admin, is_super_admin, etc. to help us identify the permission level of that user.
Using this object, we check whether the user who is making the request via jwt authentication is an admin or super admin, or just a normal registered user.

        if current_identity.is_admin or current_identity.is_super_admin:
            self.schema = SettingSchemaAdmin
        else:
            self.schema = SettingSchemaNonAdmin

 

So, if the current user sending the request is an admin, then we set the schema for the Resource manager class of the flask-rest-jsonapi as SettingSchemaAdmin, which we have already declared before containing all the fields, else, we set it as SettingSchemaNonAdmin which has limited number of attributes.