Security
By default, StartupJS allows full read/write/delete access to every document. You must define access rules to protect your data in production.
Document access control (@startupjs/sharedb-access)
This package adds per-document permission checks for create, read, update, and delete operations.
Enable it in server/index.js:
startupjsServer({
accessControl: true
})
Define access rules on your model:
export default class NewsModel {
static access = {
create: async (model, collection, docId, doc, session) => {
return hasPermission('global.admin', model, collection, docId, session)
},
read: async () => true,
update: async (model, collection, docId, oldDoc, session, ops, newDoc) => {
return hasPermission('global.admin', model, collection, docId, session)
},
delete: async (model, collection, docId, doc, session) => {
return hasPermission('global.admin', model, collection, docId, session)
}
}
}
hasPermission is your application-specific permission check. Each callback receives the server-side model and session, so you can look up roles or ownership.
Schema validation (@startupjs/sharedb-schema)
This package enforces JSON Schema rules on documents. Any write that violates the schema is rejected.
Enable it in server/index.js:
startupjsServer({
validateSchema: true
})
Define a schema on your model:
export default class UserModel {
static schema = {
type: 'object',
properties: {
username: {
type: 'string',
minLength: 1,
maxLength: 10
},
email: {
type: 'string',
format: 'email'
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 1
},
roleId: { type: 'string' },
hobbies: {
type: 'array',
maxItems: 3,
items: { type: 'string' },
uniqueItems: true
}
}
}
}
More details: https://github.com/startupjs/startupjs/tree/master/packages/sharedb-schema
Secure aggregations (@startupjs/server-aggregate)
Aggregations are unrestricted by default. Use server-defined aggregation templates to control what queries clients can run.
Enable it in server/index.js:
startupjsServer({
serverAggregate: true
})
Define aggregation templates:
static aggregations = {
example: async (model, params, session) => {
const { status } = params
const availableStatuses = getAvailableStatuses(model, session.userId)
if (!availableStatuses.includes(status)) {
throw Error("403: Can't query docs with status " + status)
}
return [
{ $match: { status } }
]
}
}
Use the aggregation in a query:
const $events = useSub($.events, {
$aggregationName: 'example',
$params: { status: 'published' }
})
REST API checks (server)
Always validate access for REST endpoints. Express makes this simple with middleware.
On the server, there are no private collections like $._session. Instead, the user ID is available on the request session object as req.session.userId. Use sub from 'startupjs' to subscribe to documents:
import express from 'express'
import { $, sub } from 'startupjs'
async function isLoggedIn (req, res, next) {
const userId = req.session.userId
if (!userId) return res.sendStatus(403)
next()
}
async function isAdmin (req, res, next) {
const userId = req.session.userId
const $user = await sub($.users[userId])
const role = $user.role.get()
if (role !== 'admin') return res.sendStatus(403)
next()
}
const adminRouter = express.Router()
adminRouter.post('/api/add-admin-task', [isLoggedIn, isAdmin, addAdminTask])
adminRouter.get('/api/get-admin-tasks', [isLoggedIn, isAdmin, getAdminTasks])
export default adminRouter