Security
This section provides security guidelines and recommendations for the Appointment Manager.
ACL
The Appointment Manager does not provide a built-in authorization mechanism, but we strongly encourage to protect your endpoints with policies or using a custom middleware.
We recommend using [Rönd policies] for simple use cases, like checking user properties (groups, roles, etc.) to verify if the current user has rights to perform a certain operation, and relying on a custom middleware for more advanced cases, like manipulating a response to anonymize sensitive information or enrich it with ACL information.
In the rest of the section, we will guide you through the basic steps to write a custom middleware based on Mia Platform Node.js template. In the given example, we are enriching the response of the GET /calendar/
endpoint, which is used by the Care Kit Calendar component, with a permissions
field both at the slot and appointment level, according to the following specifications:
- the slot
permissions
field can include the following permissions:CREATE
: book an appointment on an available slot;
- the appointment
permissions
field can include any combination of the following permissions:VIEW
: view the details of the appointment (participants, teleconsultation links, etc.);EDIT
: change the details of the appointment (start date, participants, etc.);DELETE
: delete the appointment.
We also assume the AM is configured in full mode, the resource ID field is named resourceId
and the following custom user fields are used:
doctor
: the ID of the doctor, which is also the value of the resource ID field;patients
: a list of patients.
Slot permissions
A basic authorization policy could grant the CREATE
permission to the current user if any of the following conditions is satisfied:
- the user has an administrative account with full privileges (i.e. belongs to the
admins
group); - the user is the owner of the availability (i.e. the availability
resourceId
field value matches the user ID); - the user is a patient (i.e. belongs to the
patients
group)
Given an availability event, you could write a function looking like this to determine the permissions:
// User groups
const ADMINS_GROUP = 'admins'
const PATIENTS_GROUP = 'patients'
/**
* Compute the slot permissions
*
* @param {string} resourceId Availability resource ID
* @param {string} userId User ID
* @param {string[]} userGroups User groups
* @returns {string[]} List of slot permissions
*/
function getSlotPermissions(resourceId, userId, userGroups) {
const isOwner = resourceId === userId
const isAdmin = userGroups.includes(ADMINS_GROUP)
const isPatient = userGroups.includes(PATIENTS_GROUP)
return isOwner || isAdmin || isPatient ? ['CREATE'] : []
}
Appointment permissions
In the response of the GET /calendar/
you can find appointments withing an availability slot or as standalone events, so you need to process them in a slightly different way.
Apart from that, the main attention point is if you have enabled the participants
field, which we strongly recommend, since it reconciles all the different user custom fields and provides an easy way to check participants and grand appropriate permissions. In the code sample below, though, we provide an implementation that works seamlessly in both scenarios, i.e. with or without the participants
field.
So, a basic authorization policy could grant permissions according to the following rules:
- if the current user has an administrative account with full privileges (i.e. belongs to the
admins
group), we grant all permissions (VIEW
,EDIT
,DELETE
); - if the current user is a doctor involved in the appointment, we grant again all permissions (
VIEW
,EDIT
,DELETE
); - if the current user is a patient involved in the appointment, we grant only the
VIEW
andDELETE
permissions; - if the current user is a generic participant (supporting staff, for example), we only grant the
VIEW
permission.
Given an appointment, no matter if associated or not to a slot, you could write a function looking like this to compute the permissions:
// User groups
const ADMINS_GROUP = 'admins'
const PATIENTS_GROUP = 'patients'
// Custom user fields
const PATIENT_USER_FIELD = 'patients'
const DOCTOR_USER_FIELD = 'doctor'
/**
* Compute the appointment permissions
* @param {Object} appointment Appointment
* @param {string} userId User ID
* @param {string[]} userGroups User groups
* @returns {string[]} List of appointment permissions
*/
function getAppointmentPermissions(appointment, userId, userGroups) {
const isAdmin = userGroups.includes(ADMINS_GROUP)
if (isAdmin) {
return ['VIEW', 'EDIT', 'DELETE']
}
const permissions = new Set()
if (appointment.participants) {
const userParticipant = appointment.participants.find(participant => participant.id === userId)
const isDoctor = userParticipant?.type === DOCTOR_USER_FIELD
const isPatient = userParticipant?.type === PATIENT_USER_FIELD
if (userParticipant) {
permissions.add('VIEW')
if (isDoctor) {
permissions.add('EDIT')
permissions.add('DELETE')
} else if (isPatient) {
permissions.add('DELETE')
}
}
} else {
const doctors = appointment[DOCTOR_USER_FIELD]
const isDoctor = Array.isArray(doctors) ? doctors.includes(userId) : doctors === userId
const patients = appointment[PATIENT_USER_FIELD]
const isPatient = Array.isArray(patients) ? patients.includes(userId) : patients === userId
if (isDoctor) {
permissions.add('VIEW')
permissions.add('EDIT')
permissions.add('DELETE')
} else if (isPatient) {
permissions.add('VIEW')
permissions.add('DELETE')
}
}
return Array.from(permissions)
}
Let's breaking it down to understand what it does:
- if the user has an administrative account, grant all permissions (see rule #1);
const isAdmin = userGroups.includes(ADMINS_GROUP)
if (isAdmin) {
return ['VIEW', 'EDIT', 'DELETE']
}
- if the
participants
field is enabled, we rely on that to compute the permissions:- if the user is among the participants, but is not a doctor or patient, we grant only the
VIEW
permission; - if the user is a doctor involved in the appointment, we add the
EDIT
andDELETE
permissions; - if the user is a patient involved in the appointment, we add the
DELETE
permission.
- if the user is among the participants, but is not a doctor or patient, we grant only the
if (appointment.participants) {
const userParticipant = appointment.participants.find(participant => participant.id === userId)
const isDoctor = userParticipant?.type === DOCTOR_USER_FIELD
const isPatient = userParticipant?.type === PATIENT_USER_FIELD
if (userParticipant) {
permissions.add('VIEW')
if (isDoctor) {
permissions.add('EDIT')
permissions.add('DELETE')
} else if (isPatient) {
permissions.add('DELETE')
}
}
}
- if the
participants
field is not configured, we rely on the custom user fields; the code is semantically almost identical to the previous one, but it's worth emphasizing a couple of things:- having to manually check each user custom field, each one potentially being an array or single value, is definitely more cumbersone and error prone than working with a single field containing the full list of participants;
- under the assumption we are working with only two custom user fields (doctors and patients) we cannot have any generic participant, otherwise this code would need to also check if the user ID matches any other custom user field and, if it's the case, grant the
VIEW
permission according to rule #4.
if (appointment.participants) {
// Work with participants fields
} else {
const doctors = appointment[DOCTOR_USER_FIELD]
const isDoctor = Array.isArray(doctors) ? doctors.includes(userId) : doctors === userId
const patients = appointment[PATIENT_USER_FIELD]
const isPatient = Array.isArray(patients) ? patients.includes(userId) : patients === userId
if (isDoctor) {
permissions.add('VIEW')
permissions.add('EDIT')
permissions.add('DELETE')
} else if (isPatient) {
permissions.add('VIEW')
permissions.add('DELETE')
}
}
Enrich GET /calendar/ response
We are finally ready to wrap everything up in the handler which is going to process the request coming from the Appointment Manager.
If we assume the URL of the Appointment Manager is available thorugh an environment variable called APPOINTMENT_MANAGER_URL
, we could write a very simple handler:
async function handler(request, reply) {
const { query, log } = request
const { APPOINTMENT_MANAGER_URL } = this.config
const userId = request.getUserId()
const userGroups = request.getGroups()
const appointmentManagerClient = request.getHttpClient(APPOINTMENT_MANAGER_URL)
log.debug({ query }, 'Send request to AM')
const { statusCode, payload } = await appointmentManagerClient.get('/calendar/', { query })
log.debug({ statusCode }, 'Received request from AM')
payload.forEach(calendarEvent => {
const { eventType } = calendarEvent
switch (eventType) {
case 'Availability':
// Handle appointments with slot
calendarEvent.slots.forEach(slot => {
slot.permissions = getSlotPermissions(calendarEvent[RESOURCE_ID_FIELD_NAME], userId, userGroups)
slot.appointments.forEach(appointment => {
appointment.permissions = getAppointmentPermissions(appointment, userId, userGroups)
})
})
break
case 'Appointment':
// Handle appointments without slot
calendarEvent.permissions = getAppointmentPermissions(calendarEvent, userId, userGroups)
break
default:
log.debug({ eventType }, 'Returning event without changes')
}
})
return reply.code(statusCode).send(payload)
}
The handler is basically sending a request to the GET /calendar/
endpoint of the Appointment Manager, forwarding the query parameters, then it enriches the response with the slots and appointments permissions and returns the enriched response to the client.
Let's focus our attention to the enriching part:
payload.forEach(calendarEvent => {
const { eventType } = calendarEvent
switch (eventType) {
case 'Availability':
// Handle appointments with slot
calendarEvent.slots.forEach(slot => {
slot.permissions = getSlotPermissions(calendarEvent[RESOURCE_ID_FIELD_NAME], userId, userGroups)
slot.appointments.forEach(appointment => {
appointment.permissions = getAppointmentPermissions(appointment, userId, userGroups)
})
})
break
case 'Appointment':
// Handle appointments without slot
calendarEvent.permissions = getAppointmentPermissions(calendarEvent, userId, userGroups)
break
default:
log.debug({ eventType }, 'Returning event without changes')
}
})
As mentioned earlier, you need to process appointments appearing both inside a slot (the first switch case) and as standalone events (the second switch chase). In the first case, we have to navigate through the nested data and add the proper permissions to availability slots and slot appointments, while in the second case we simply need to set the proper appointment permissions using the functions we discussed earlier.