Skip to main content

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:

Backend code
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:

Backend code
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):

Backend code
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:

Backend code
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:

Backend code
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:

Backend code
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:

Backend code
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.

Backend code
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:

Backend code
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.

Backend code
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:

Backend code
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:

Backend code
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;
}
}