Step 5. Authorization

Once we’ve established who our user is, we need to implement some way of controlling what operations our user can perform, based on the rights he/she has been assigned. In this tutorial, we will be taking a look at how to install access limitations that are based on user roles. The roles themselves are stored in the Roles microservice and are loaded into a UserSession by the loadSession interceptor we discussed in the previous step.

Our Authorizer class was made to provide flexible access management. We will be using this class to limit access to certain operations in our facade’s RESTful controllers. This class’s implementation can be found in the Authorize.py file, located in the folder controllers/version1. Its code is as follows:

/src/services/version1/Authorize.ts

import { UnauthorizedException } from 'pip-services4-commons-node';
import { HttpResponseSender } from 'pip-services4-http-node';

import { BasicAuthManager } from 'pip-services4-http-node';
import { RoleAuthManager } from 'pip-services4-http-node';
import { OwnerAuthManager } from 'pip-services4-http-node';

export class AuthorizerV1 {
    private basicAuth = new BasicAuthManager();
    private roleAuth = new RoleAuthManager();
    private ownerAuth = new OwnerAuthManager();

    public anybody(): (req: any, res: any, next: () => void) => void {
        return this.basicAuth.anybody();
    }

    public signed(): (req: any, res: any, next: () => void) => void {
        return this.basicAuth.signed();
    }

    public owner(idParam: string = 'user_id'): (req: any, res: any, next: () => void) => void {
        return this.ownerAuth.owner(idParam);
    }
        
    public ownerOrAdmin(idParam: string = 'user_id'): (req: any, res: any, next: () => void) => void {
        return this.ownerAuth.ownerOrAdmin(idParam);
    }

    public siteRoles(roles: string[], idParam: string = 'site_id'): (req: any, res: any, next: () => void) => void {
        return (req, res, next) => {
            let user = req.user;
            if (user == null) {
                HttpResponseSender.sendError(
                    req, res,
                    new UnauthorizedException(
                        null, 'NOT_SIGNED',
                        'User must be signed in to perform this operation'
                    ).withStatus(401)
                );
            } else {
                let siteId = req.params[idParam];
                let authorized = user.roles.includes('admin');
                
                if (siteId != null && !authorized) {
                    for (let role of roles)
                        authorized = authorized || user.roles.includes(siteId + ':' + role);
                }

                if (!authorized) {
                    HttpResponseSender.sendError(
                        req, res,
                        new UnauthorizedException(
                            null, 'NOT_IN_SITE_ROLE',
                            'User must be site:' + roles.join(' or site:') + ' to perform this operation'
                        ).withDetails('roles', roles).withStatus(403)
                    );
                } else {
                    next();
                }
            }
        };
    }

    public admin(): (req: any, res: any, next: () => void) => void {
        return this.roleAuth.userInRole('admin');
    }

    public siteAdmin(idParam: string = 'site_id'): (req: any, res: any, next: () => void) => void {
        return this.siteRoles(['admin'], idParam);
    }

    public siteManager(idParam: string = 'site_id'): (req: any, res: any, next: () => void) => void {
        return this.siteRoles(['admin', 'manager'], idParam);
    }

    public siteUser(idParam: string = 'site_id'): (req: any, res: any, next: () => void) => void {
        return this.siteRoles(['admin', 'manager', 'user'], idParam);
    }

    public siteAdminOrOwner(userIdParam: string = 'user_id', siteIdParam: string = 'site_id'): (req: any, res: any, next: () => void) => void {
        return (req, res, next) => {
            let user = req.user;
            if (user == null) {
                HttpResponseSender.sendError(
                    req, res,
                    new UnauthorizedException(
                        null, 'NOT_SIGNED',
                        'User must be signed in to perform this operation'
                    ).withStatus(401)
                );
            } else {
                let userId = req.params[userIdParam] || req.param(userIdParam);
                if (userId != null && userId == user.user_id) {
                    next();
                } else {
                    let siteId = req.params[siteIdParam];
                    let authorized = user.roles.includes('admin')
                        || user.roles.includes(siteId + ':admin');
                    
                    if (!authorized) {
                        HttpResponseSender.sendError(
                            req, res,
                            new UnauthorizedException(
                                null, 'NOT_IN_SITE_ROLE',
                                'User must be site:admin to perform this operation'
                            ).withDetails('roles', ['admin']).withStatus(403)
                        );
                    } else {
                        next();
                    }
                }
            }
        };
    }
}


/services/version1/Authorize.go

package operations1

import (
	"context"
	"net/http"

	clients1 "github.com/pip-services-samples/client-beacons-go/clients/version1"
	services1 "github.com/pip-services-samples/service-beacons-go/data/version1"

	cref "github.com/pip-services4/pip-services4-go/pip-services4-components-go/refer"
	httpcontr "github.com/pip-services4/pip-services4-go/pip-services4-http-go/controllers"
)

type BeaconsOperationsV1 struct {
	*httpcontr.RestOperations
	beaconsClient clients1.IBeaconsClientV1
	correlationId string
}

func NewBeaconsOperationsV1() *BeaconsOperationsV1 {
	c := BeaconsOperationsV1{
		RestOperations: httpcontr.NewRestOperations(),
	}
	c.DependencyResolver.Put("beacons", cref.NewDescriptor("beacons", "client", "*", "*", "1.0"))
	c.correlationId = "beacons_operations"
	return &c
}

func (c *BeaconsOperationsV1) SetReferences(references cref.IReferences) {
	c.RestOperations.SetReferences(context.Background(), references)

	dependency, _ := c.DependencyResolver.GetOneRequired("beacons")
	client, ok := dependency.(clients1.IBeaconsClientV1)
	if !ok {
		panic("BeaconsOperationsV1: Cant't resolv dependency 'client' to IBeaconsClientV1")
	}
	c.beaconsClient = client
}

func (c *BeaconsOperationsV1) GetBeacons(res http.ResponseWriter, req *http.Request) {
	var filter = c.GetFilterParams(req)
	var paging = c.GetPagingParams(req)

	page, err := c.beaconsClient.GetBeacons(
		c.correlationId, filter, paging)

	if err != nil {
		c.SendError(res, req, err)
	} else {
		c.SendResult(res, req, page, nil)
	}
}

func (c *BeaconsOperationsV1) GetBeaconById(res http.ResponseWriter, req *http.Request) {
	id := c.GetParam(req, "id")
	item, err := c.beaconsClient.GetBeaconById(c.correlationId, id)
	if err != nil {
		c.SendError(res, req, err)
	} else {
		c.SendResult(res, req, item, nil)
	}
}

func (c *BeaconsOperationsV1) GetBeaconByUdi(res http.ResponseWriter, req *http.Request) {
	udi := c.GetParam(req, "udi")
	item, err := c.beaconsClient.GetBeaconByUdi(c.correlationId, udi)
	if err != nil {
		c.SendError(res, req, err)
	} else {
		c.SendResult(res, req, item, nil)
	}
}

func (c *BeaconsOperationsV1) CreateBeacon(res http.ResponseWriter, req *http.Request) {

	data := services1.BeaconV1{}
	err := c.DecodeBody(req, &data)
	if err != nil {
		c.SendError(res, req, err)
	}
	item, err := c.beaconsClient.CreateBeacon(c.correlationId, &data)
	if err != nil {
		c.SendError(res, req, err)
	} else {
		c.SendResult(res, req, item, nil)
	}
}

func (c *BeaconsOperationsV1) UpdateBeacon(res http.ResponseWriter, req *http.Request) {
	data := services1.BeaconV1{}
	err := c.DecodeBody(req, &data)
	if err != nil {
		c.SendError(res, req, err)
	}

	item, err := c.beaconsClient.UpdateBeacon(c.correlationId, &data)
	if err != nil {
		c.SendError(res, req, err)
	} else {
		c.SendResult(res, req, item, nil)
	}
}

func (c *BeaconsOperationsV1) DeleteBeaconById(res http.ResponseWriter, req *http.Request) {
	id := c.GetParam(req, "id")

	item, err := c.beaconsClient.DeleteBeaconById(c.correlationId, id)

	if err != nil {
		c.SendError(res, req, err)
	} else {
		c.SendResult(res, req, item, nil)
	}
}

func (c *BeaconsOperationsV1) CalculatePosition(res http.ResponseWriter, req *http.Request) {

	bodyParams := make(map[string]interface{}, 0)
	err := c.DecodeBody(req, &bodyParams)

	if err != nil {
		c.SendError(res, req, err)
	}

	udiValues, _ := bodyParams["udis"].([]interface{})
	udis := make([]string, 0, 0)
	for _, udi := range udiValues {
		v, _ := udi.(string)
		udis = append(udis, v)
	}
	siteId, _ := bodyParams["site_id"].(string)

	position, err := c.beaconsClient.CalculatePosition(c.correlationId, siteId, udis)
	if err != nil {
		c.SendError(res, req, err)
	} else {
		c.SendResult(res, req, position, nil)
	}
}

Not available

/pip_facades_sample_python/controllers/version1/Authorize.py

# -*- coding: utf-8 -*-
from typing import List

import bottle
from pip_services4_commons.convert import JsonConverter
from pip_services4_commons.errors import UnauthorizedException
from pip_services4_http.auth.BasicAuthorizer import BasicAuthorizer
from pip_services4_http.auth.OwnerAuthorizer import OwnerAuthorizer
from pip_services4_http.auth.RoleAuthorizer import RoleAuthorizer


class AuthorizerV1:

    def __init__(self):
        self.__basic_auth = BasicAuthorizer()
        self.__role_auth = RoleAuthorizer()
        self.__owner_auth = OwnerAuthorizer()

    # Anybody who entered the system
    def anybody(self):
        return self.__basic_auth.anybody()

    # Only registered and authenticated users
    def signed(self):
        return self.__basic_auth.signed()

    # Only the user session owner
    def owner(self, id_param: str = 'user_id'):
        return self.__owner_auth.owner(id_param)

    def owner_or_admin(self, id_param: str = 'user_id'):
        return self.__owner_auth.owner_or_admin(id_param)

    def site_roles(self, roles: List[str], id_param: str = 'site_id'):
        def inner():
            user = getattr(bottle.request, 'user', None)

            if user is None:
                raise UnauthorizedException(
                    None, 'NOT_SIGNED',
                    'User must be signed in to perform this operation'
                ).with_status(401)

            else:
                user.roles = getattr(user, 'roles', False) or []
                site_id = bottle.request.params['kwargs'].get(id_param)
                authorized = 'admin' in user.roles
                if site_id is not None and not authorized:
                    for role in roles:
                        authorized = authorized or (site_id + ':' + role) in user.roles

                if not authorized:
                    raise UnauthorizedException(
                        None, 'NOT_IN_SITE_ROLE',
                        'User must be site:' + ' or site:'.join(roles) + ' to perform this operation'
                    ).with_details('roles', roles).with_status(403)

        return inner

    def admin(self):
        return self.__role_auth.user_in_role('admin')

    def site_admin(self, id_param: str = 'site_id'):
        return self.site_roles(['admin'], id_param)

    def site_manager(self, id_param: str = 'site_id'):
        return self.site_roles(['admin', 'manager'], id_param)

    def site_user(self, id_param: str = 'site_id'):
        return self.site_roles(['admin', 'manager', 'user'], id_param)

    def site_admin_or_owner(self, user_id_param: str = 'user_id', site_id_param: str = 'site_id'):
        def inner():
            user = bottle.request.user
            if user is None:
                raise UnauthorizedException(
                    None, 'NOT_SIGNED',
                    'User must be signed in to perform this operation'
                ).with_status(401)

            else:
                user_id = dict(bottle.request.query.decode()).get(user_id_param) or JsonConverter.to_json(
                    bottle.request.json)
                if user_id is not None and user_id == user.user_id:
                    return
                else:
                    site_id = bottle.request.params.get(site_id_param)
                    authorized = 'admin' in user.roles or site_id + ':admin' in user.roles
                    if not authorized:
                        raise UnauthorizedException(
                            None, 'NOT_IN_SITE_ROLE',
                            'User must be site:admin to perform this operation'
                        ).with_details('roles', ['admin']).with_status(403)

        return inner

Not available

Let’s take a closer look at each of these methods:

  • anybody - allows everyone access, even unauthorized users.
  • signed - access is granted only to authorized users.
  • admin - access is granted only to users with the Administrator role.
  • owner - access is granted only for the session owner.

The logic pretty much boils down to making a decision about whether we should allow further access, or send an answer with the corresponding error. In case of the latter, the error is based on the information provided by the clients and the information about the user that is embedded into the interceptor’s request for the active session.

Setting specific access levels to certain resources is configured when registering routes in the controller. The controller’s implementation is described in Step 6 - REST controllers and versioning.

Step 6 - REST controllers and versioning