@mondart/nestjs-common-module-iam
v3.2.10
Published
Reusable IAM (Identity and Access Management) utilities for NestJS.
Downloads
1,480
Readme
@mondart/nestjs-common-module-iam
Request-scoped IAM (Identity and Access Management) utilities for NestJS: a
global guard that resolves the caller's user type (AGENT / USER /
GUEST) from the request's IAM context, decorators to declare which user
types and entity-access rules a route requires, and pipes that turn those
rules into a TypeORM where clause so list/find-one queries are
automatically scoped to what the caller is allowed to see.
Registration
import { IamContextModule, IamUserTypeEnum } from '@mondart/nestjs-common-module-iam';
@Module({
imports: [
IamContextModule.forRoot({
defaultAllowedUserTypes: [IamUserTypeEnum.USER],
allowRoutesWithoutIamDecorator: false, // recommended
controllers: {
ignore: ['HealthController'], // by class name or class reference
},
entityAccess: {
presets: [
{
entity: CartsEntity,
resourceName: 'Cart',
access: { agent: { bypass: true } },
},
],
},
}),
],
})
export class AppModule {}IamContextModule is @Global() and registers IamContextGuard as the
APP_GUARD, so every route in the application is checked unless the
controller is excluded via controllers.ignore/controllers.select, or the
route is decorated with @PublicIam(). With
allowRoutesWithoutIamDecorator: false (the default), any route that isn't
@PublicIam() and doesn't declare @AllowIam(...) throws instead of
silently allowing everyone through.
Allowing user types on a route
import { AllowIam, IamUserTypeEnum, PublicIam } from '@mondart/nestjs-common-module-iam';
@AllowIam(IamUserTypeEnum.USER, IamUserTypeEnum.GUEST)
@Get()
findAll() { ... }
@PublicIam()
@Get('health')
health() { ... } // skips the IAM guard entirelyIamContextGuard resolves the current user type from the IAM context
attached to the request (agentId/isAgent -> AGENT, userId/user ->
USER, guestId/guest.id -> GUEST) and rejects the request with
ForbiddenException if that type isn't in the route's allowed list.
Entity access rules
Access rules describe, per user type, which field(s) on the entity must
match a value pulled from the IAM context or the request. Build them with
the helpers in user-owner.helper:
import {
ApplyIamEntityScope,
RequireIamEntityAccess,
allOf,
byIam,
matchIam,
matchReq,
unowned,
userAuditAccess,
} from '@mondart/nestjs-common-module-iam';
// GET /carts/:uuid — the caller must own the row identified by `uuid`
@RequireIamEntityAccess<CartsEntity>({
entity: CartsEntity,
lookup: { field: 'uuid', valueFrom: 'req.params.uuid' },
access: {
agent: { bypass: true },
user: { anyOf: [...userAuditAccess<CartsEntity>(), unowned('createdByUser', 'updatedByUser')] },
guest: { anyOf: [unowned('createdByUser', 'updatedByUser')] },
},
resourceName: 'Cart',
})
@Get(':uuid')
findOne(@Param('uuid') uuid: string) { ... }
// GET /carts — every row returned must belong to the caller's contact+store
@ApplyIamEntityScope<CartsEntity>({
access: {
agent: { bypass: true },
user: { anyOf: [allOf(matchIam('contactId', 'user.contactId', 'number'), matchIam('storeId', 'user.storeId', 'number'))] },
guest: { anyOf: [unowned('contactId')] },
},
})
@Get()
findAll(@Paginate() query: PaginateQuery) { ... }@RequireIamEntityAccess()makesIamContextGuardcheck, before the handler runs, that a row matchinglookupexists and that it also matches the caller's access rule — throwingNotFoundExceptionif the row doesn't exist at all, orForbiddenExceptionif it exists but the caller isn't allowed to see it.@ApplyIamEntityScope()doesn't check anything itself; it stores the resolvedwhereclause on the request for the query-scope pipes (below) to pick up.access.agent.bypass(default whenagentaccess is unset) skips scoping entirely for agents; setbypass: falsewith ananyOfto scope agents too.byIam(field, path, cast)/byReq(field, path, cast)build a singlefield = <value at path>condition read from the IAM context or the request;matchIam/matchReqare the same but for combining withallOfinto an AND group.nullFields/unownedrequire the given fields to beNULL(an "unowned" row).userAuditAccess()/agentAuditAccess()are shortcuts for the commoncreatedByUser = iam.userId OR updatedByUser = iam.userIdownership pattern.- Rules declared directly on the decorator override an entity preset from
IamContextModule.forRoot(), which in turn overrides the module-wideentityAccess.defaultAccess/defaultScope.
Scoping list and find-one queries
IamPaginateQueryScopePipe and IamFindOneQueryScopePipe read the where
clause IamContextGuard resolved from @ApplyIamEntityScope() and attach
it to the query object under the iamWhere key
(IAM_QUERY_SCOPE_WHERE_KEY), so the controller/service can merge it into
the query it passes to nestjs-paginate/core-crud:
import { IamPaginateQueryScopePipe, IamScopedPaginateQuery } from '@mondart/nestjs-common-module-iam';
@ApplyIamEntityScope<CartsEntity>({ access: { user: { anyOf: [byIam('contactId', 'user.contactId', 'number')] } } })
@Get()
findAll(@Paginate(undefined, IamPaginateQueryScopePipe) query: IamScopedPaginateQuery<CartsEntity>) {
return this.cartsService.findAllWithPagination(query, paginateConfig, {
selectQueryBuilder: query.iamWhere
? this.cartsRepository.createQueryBuilder('cart').andWhere(query.iamWhere)
: undefined,
});
}Both pipes are request-scoped and a no-op (return the query unchanged) when
the route has no @ApplyIamEntityScope() metadata. withDeleted on the
scope options also flows onto the paginate query when set.
User types
export enum IamUserTypeEnum {
AGENT = 'AGENT',
USER = 'USER',
GUEST = 'GUEST',
}