Securing data access
Use a range of decorators provided by Squid to protect any database that is connected to Squid, including the built-in internal database.
Each decorator is designed to protect a different part of a database.
To secure a specific type of action, a different context object will be passed to the function. For example, if you want
to secure read
operations, the context object will be of type QueryContext
, while write
operations will use the
MutationContext
object.
You can secure any of the following types of data access using the appropriate decorator:
read
insert
update
delete
write
all
Note that the write
decorator includes insert
, update
, and delete
operations, so it provides comprehensive
protection for all types of write operations on the database.
@secureDatabase
The @secureDatabase
decorator provided by Squid can be used to protect all database access, regardless of which
table or collection is being accessed. This decorator enforces authorization checks on all actions that access the database.
For instance, developers can use the @secureDatabase
decorator to allow only authenticated users to access the database.
To do so, they can define a function that checks whether the user is authenticated:
import { secureDatabase, SquidService } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureDatabase('all', 'usersDatabase')
verifyUserAuthenticated(): boolean {
return this.isAuthenticated();
}
}
To allow only users with an admin
property to modify the usersDatabase
, developers can use the @secureDatabase
decorator in combination with the verifyUserAuthenticated
function that checks whether the user is authenticated and has
an admin
property:
import {
secureDatabase,
SquidService,
MutationContext,
} from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureDatabase('write', 'usersDatabase')
verifyUserAuthenticated(context: MutationContext): boolean {
const userAuth = this.getUserAuth();
if (!userAuth) return false;
return !!userAuth.attributes['admin'];
}
}
In some scenarios, you need to access the context of an action to determine whether the client is authorized. For example, the following function checks whether the user is authenticated and whether the write they are attempting is an insert
(and not an update
):
import {
secureDatabase,
SquidService,
MutationContext,
} from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureDatabase('write', 'usersDatabase')
allowOnlyInsertsAndAuthenticated(context: MutationContext): boolean {
return this.isAuthenticated() && context.getMutationType() === 'insert';
}
}
@secureCollection
Developers can protect specific collections within the database using the @secureCollection
decorator. This ensures that only authorized users are able to access and modify the data within a specific collection.
For instance, developers can use the @secureCollection
decorator to allow a user to read only documents with their
userId in the owner
column. Here's an example code snippet:
import {
secureCollection,
SquidService,
QueryContext,
} from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureCollection('Items', 'read')
secureItemsRead(context: QueryContext<Item>): boolean {
const userId = this.getUserAuth()?.userId;
if (!userId) return false;
return context.isSubqueryOf('owner', '==', userId);
}
}
You may also want to make sure a user can only update, delete, or insert Items they own:
import {
secureCollection,
SquidService,
MutationContext,
} from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureCollection('Items', 'write')
secureItemWrite(context: MutationContext<Item>): boolean {
const userId = this.getUserAuth()?.userId;
if (!userId) return false;
const { before, after } = context.beforeAndAfterDocs;
if (before && before.owner !== userId) return false;
if (after && after.owner !== userId) return false;
return true;
}
}
Securing native queries
To secure a native query, use the @secureNativeQuery()
decorator, passing the integration ID of the database. The following example defines a function that checks whether the user is authenticated:
import { secureNativeQuery, SquidService } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureNativeQuery('YOUR_INTEGRATION_ID')
verifyUserAuthenticated(): boolean {
return this.isAuthenticated();
}
}
You can add more fine-grained access using auth token attributes as shown in this example:
import { secureNativeQuery, SquidService } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureNativeQuery('YOUR_INTEGRATION_ID')
verifyAdminUser(): boolean {
// Get the authenticated user's details
const userAuth = this.getUserAuth();
if (!userAuth) {
return false;
}
// Check for the admin attribute
return !!userAuth.attributes['admin'];
}
}
You can also use a collection to store auth permissions. Ensure that the collection is secured with a Security Service function.
import { secureNativeQuery, SquidService } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureNativeQuery('YOUR_INTEGRATION_ID')
verifyNativeQueryAccess(): Promise<boolean> {
// Get the authenticated user's ID
const userId = this.getUserAuth()?.userId;
if (!userId) {
return false;
}
// Check if the user's ID is in the collection listing users permitted to access native query
const userTableAccess = await this.squid
.collection('table_access')
.doc(userId)
.snapshot();
return !!userTableAccess;
}
}
Native query context
When executing a native relational or MongoDB query, a context is available in the Squid backend indicating what type of native query the client wants to execute ('relational'
or 'mongo'
) and other attributes based on the type of query.
The following context type is provided when executing a native relational query:
RelationalNativeQueryContext {
type: 'relational';
query: string;
params: Record<string, any>;
}
Use this context to limit what types of native queries the client can make. For example, the following code lets the client one run type of native query where they select the documents in a collection called SQUIDS
(or rows in a table) where the value of the YEAR
field is greater than 1980. If the client tries to condut a different query or query the table for values earlier than 1980, then the query will fail.
import {
secureNativeQuery,
SquidService,
RelationalNativeQueryContext,
} from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureNativeQuery('YOUR_INTEGRATION_ID')
verifyNativeQueryAccess(context: RelationalNativeQueryContext): boolean {
if (
context.query !== 'SELECT * FROM SQUIDS WHERE YEAR = ${year}' ||
context.params.year < 1980
) {
return false;
}
return true;
}
}
The following context type is provided when executing a native MongoDB query:
MongoNativeQueryContext {
type: 'mongo';
collectionName: string;
aggregationPipeline: Array<any | undefined>;
}
Use this context to limit the types of aggregation pipeline queries a client can make to your Mongo aggregation pipeline. The following example allows the user to execute a MongoDB aggregation pipeline only in the ORDERS
collection:
import {
secureNativeQuery,
SquidService,
MongoNativeQueryContext,
} from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureNativeQuery('YOUR_INTEGRATION_ID')
verifyNativeMongoQueryAccess(context: MongoNativeQueryContext): boolean {
if (context.collectionName !== 'ORDERS') {
return false;
}
return true;
}
}