This commit is contained in:
Matthew Bessette 2025-01-09 20:33:57 -05:00
parent 1dc45267ac
commit da84b6b085
19 changed files with 757 additions and 1 deletions

Binary file not shown.

View File

@ -24,6 +24,39 @@ model Audit {
response String? response String?
} }
model IamRole {
id String @id
path String?
name String
assumeRolePolicy String?
description String?
maxSessionDuration Int?
permissionBoundaryArn String?
lastUsedDate DateTime?
lastUsedRegion String?
accountId String
createdAt DateTime @default(now())
@@unique([accountId, name])
}
model IamPolicy {
id String
version Int @default(1)
isDefault Boolean
path String?
name String
description String?
policy String
isAttachable Boolean @default(false)
accountId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@id([id, version])
@@unique([accountId, path, name])
}
model KmsAlias { model KmsAlias {
name String name String
accountId String accountId String

View File

@ -17,6 +17,8 @@ import { SqsModule } from './sqs/sqs.module';
import { PrismaModule } from './_prisma/prisma.module'; import { PrismaModule } from './_prisma/prisma.module';
import { StsModule } from './sts/sts.module'; import { StsModule } from './sts/sts.module';
import { StsHandlers } from './sts/sts.constants'; import { StsHandlers } from './sts/sts.constants';
import { IamModule } from './iam/iam.module';
import { IAMHandlers } from './iam/iam.constants';
@Module({ @Module({
imports: [ imports: [
@ -26,6 +28,7 @@ import { StsHandlers } from './sts/sts.constants';
}), }),
PrismaModule, PrismaModule,
AwsSharedEntitiesModule, AwsSharedEntitiesModule,
IamModule,
KmsModule, KmsModule,
SecretsManagerModule, SecretsManagerModule,
SnsModule, SnsModule,
@ -41,6 +44,7 @@ import { StsHandlers } from './sts/sts.constants';
provide: ActionHandlers, provide: ActionHandlers,
useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}), useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}),
inject: [ inject: [
IAMHandlers,
KMSHandlers, KMSHandlers,
SecretsManagerHandlers, SecretsManagerHandlers,
SnsHandlers, SnsHandlers,

View File

@ -162,4 +162,13 @@ export class UnsupportedOperationException extends AwsException {
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
) )
} }
} }
export class EntityAlreadyExists extends AwsException {
constructor(message: string) {
super(
message,
EntityAlreadyExists.name,
HttpStatus.CONFLICT,
)
}
}

View File

@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
type QueryParams = {
PolicyArn: string;
RoleName: string;
}
@Injectable()
export class AttachRolePolicyHandler extends AbstractActionHandler<QueryParams> {
constructor(
) {
super();
}
format = Format.Xml;
action = Action.IamAttachRolePolicy;
validator = Joi.object<QueryParams, true>({
PolicyArn: Joi.string().required(),
RoleName: Joi.string().required(),
});
protected async handle({ PolicyArn, RoleName }: QueryParams, awsProperties: AwsProperties) {
}
}

View File

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamPolicy } from './iam-policy.entity';
import { breakdownArn } from '../util/breakdown-arn';
type QueryParams = {
PolicyArn: string;
PolicyDocument: string;
SetAsDefault: boolean;
}
@Injectable()
export class CreatePolicyVersionHandler extends AbstractActionHandler<QueryParams> {
constructor(
) {
super();
}
format = Format.Xml;
action = Action.IamCreatePolicyVersion;
validator = Joi.object<QueryParams, true>({
PolicyArn: Joi.string().required(),
PolicyDocument: Joi.string().required(),
SetAsDefault: Joi.boolean().required(),
});
protected async handle({ PolicyArn, PolicyDocument, SetAsDefault }: QueryParams, awsProperties: AwsProperties) {
}
}

View File

@ -0,0 +1,53 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamPolicy } from './iam-policy.entity';
import { IamService } from './iam.service';
import { TagsService } from '../aws-shared-entities/tags.service';
import { randomUUID } from 'crypto';
type QueryParams = {
Description: string;
Path: string;
PolicyDocument: string;
PolicyName: string;
} & Record<string, string>;
@Injectable()
export class CreatePolicyHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly iamService: IamService,
private readonly tagsService: TagsService,
) {
super();
}
format = Format.Xml;
action = Action.IamCreatePolicy;
validator = Joi.object<QueryParams, true>({
Description: Joi.string().max(1000).allow(null, '').default(null),
Path: Joi.string().min(1).max(512).default(null).regex(new RegExp(`((/[A-Za-z0-9\.,\+@=_-]+)*)/`)),
PolicyDocument: Joi.string().min(1).max(131072).required(),
PolicyName: Joi.string().min(1).max(128).required(),
});
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
const { Description, Path, PolicyName, PolicyDocument } = params;
const policy = await this.iamService.createPolicy({
id: randomUUID(),
version: 1,
isDefault: true,
name: PolicyName,
path: Path,
description: Description,
policy: PolicyDocument,
accountId: awsProperties.accountId,
});
return policy.metadata;
}
}

View File

@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamService } from './iam.service';
import { randomUUID } from 'crypto';
type QueryParams = {
RoleName: string;
Path: string;
AssumeRolePolicyDocument: string;
MaxSessionDuration: number;
Description: string;
}
@Injectable()
export class CreateRoleHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly iamService: IamService,
) {
super();
}
format = Format.Xml;
action = Action.IamCreateRole;
validator = Joi.object<QueryParams, true>({
AssumeRolePolicyDocument: Joi.string().min(1).max(131072).required(),
Description: Joi.string().max(1000).allow(null, '').default(null),
MaxSessionDuration: Joi.number().min(3600).max(43200).default(3600),
Path: Joi.string().min(1).max(512).required(),
RoleName: Joi.string().min(1).max(64).required(),
});
protected async handle({ RoleName, Path, AssumeRolePolicyDocument, MaxSessionDuration, Description }: QueryParams, awsProperties: AwsProperties) {
const role = await this.iamService.createRole({
id: randomUUID(),
accountId: awsProperties.accountId,
name: RoleName,
path: Path,
assumeRolePolicy: AssumeRolePolicyDocument,
maxSessionDuration: MaxSessionDuration,
description: Description,
});
return {
Role: role.metadata,
}
}
}

View File

@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamService } from './iam.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
type QueryParams = {
RoleName: string;
}
@Injectable()
export class DeleteRoleHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly iamService: IamService,
) {
super();
}
format = Format.Xml;
action = Action.IamDeleteRole;
validator = Joi.object<QueryParams, true>({
RoleName: Joi.string().min(1).max(64).required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
await this.iamService.deleteRoleByName(awsProperties.accountId, RoleName);
}
}

View File

@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
type QueryParams = {
PolicyArn: string;
VersionId: string;
}
@Injectable()
export class GetPolicyVersionHandler extends AbstractActionHandler<QueryParams> {
constructor(
) {
super();
}
format = Format.Xml;
action = Action.IamGetPolicyVersion;
validator = Joi.object<QueryParams, true>({
PolicyArn: Joi.string().required(),
VersionId: Joi.string().required(),
});
protected async handle({ PolicyArn, VersionId }: QueryParams, awsProperties: AwsProperties) {
}
}

View File

@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
type QueryParams = {
PolicyArn: string;
}
@Injectable()
export class GetPolicyHandler extends AbstractActionHandler<QueryParams> {
constructor(
) {
super();
}
format = Format.Xml;
action = Action.IamGetPolicy;
validator = Joi.object<QueryParams, true>({
PolicyArn: Joi.string().required(),
});
protected async handle({ PolicyArn }: QueryParams, awsProperties: AwsProperties) {
}
}

View File

@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamService } from './iam.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
type QueryParams = {
RoleName: string;
}
@Injectable()
export class GetRoleHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly iamService: IamService,
) {
super();
}
format = Format.Xml;
action = Action.IamGetRole;
validator = Joi.object<QueryParams, true>({
RoleName: Joi.string().min(1).max(64).required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
const role = await this.iamService.findOneRoleByName(awsProperties.accountId, RoleName);
if (!role) {
throw new NotFoundException();
}
return {
Role: role.metadata,
}
}
}

View File

@ -0,0 +1,54 @@
import { IamPolicy as PrismaIamPolicy } from "@prisma/client";
export class IamPolicy implements PrismaIamPolicy {
id: string;
accountId: string;
name: string;
version: number;
isDefault: boolean;
policy: string;
path: string | null;
description: string | null;
isAttachable: boolean;
createdAt: Date;
updatedAt: Date;
constructor(p: PrismaIamPolicy) {
this.id = p.id;
this.accountId = p.accountId;
this.name = p.name;
this.version = p.version;
this.isDefault = p.isDefault;
this.policy = p.policy;
this.path = p.path;
this.description = p.description;
this.isAttachable = p.isAttachable;
this.createdAt = p.createdAt;
this.updatedAt = p.updatedAt;
}
get arn() {
const parts = ['policy'];
if (this.path && this.path !== '/') {
parts.push(this.path);
}
parts.push(this.name);
return `arn:aws:iam::${this.accountId}:${parts.join('/')}`;
}
get metadata() {
return {
Arn: this.arn,
AttachmentCount: 0,
CreateDate: this.createdAt.toISOString(),
DefaultVersionId: `v${this.version}`,
Description: this.description,
IsAttachable: this.isAttachable,
Path: this.path,
PolicyId: this.id,
PolicyName: this.name,
UpdateDate: this.updatedAt.toISOString(),
}
}
}

View File

@ -0,0 +1,52 @@
import { IamRole as PrismaIamRole } from '@prisma/client';
export class IamRole implements PrismaIamRole {
accountId: string;
path: string | null;
name: string;
createdAt: Date;
id: string;
maxSessionDuration: number | null;
assumeRolePolicy: string | null;
description: string | null;
permissionBoundaryArn: string | null;
lastUsedDate: Date | null;
lastUsedRegion: string | null;
constructor(p: PrismaIamRole) {
this.accountId = p.accountId;
this.path = p.path;
this.name = p.name;
this.createdAt = p.createdAt;
this.id = p.id;
this.maxSessionDuration = p.maxSessionDuration;
this.assumeRolePolicy = p.assumeRolePolicy;
this.description = p.description;
this.permissionBoundaryArn = p.permissionBoundaryArn;
this.lastUsedDate = p.lastUsedDate;
this.lastUsedRegion = p.lastUsedRegion;
}
get arn() {
const parts = ['role'];
if (this.path && this.path !== '/') {
parts.push(this.path);
}
parts.push(this.name);
return `arn:aws:iam::${this.accountId}:${parts.join('/')}`;
}
get metadata() {
return {
Path: this.path,
Arn: this.arn,
RoleName: this.name,
AssumeRolePolicyDocument: this.assumeRolePolicy,
CreateDate: this.createdAt.toISOString(),
RoleId: this.id,
MaxSessionDuration: this.maxSessionDuration,
}
}
}

5
src/iam/iam.constants.ts Normal file
View File

@ -0,0 +1,5 @@
import { AbstractActionHandler } from '../abstract-action.handler';
import { Action } from '../action.enum';
export type IAMHandlers = Record<Action, AbstractActionHandler>;
export const IAMHandlers = Symbol.for('IAMHandlers');

194
src/iam/iam.module.ts Normal file
View File

@ -0,0 +1,194 @@
import { Module } from '@nestjs/common';
import { Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
import { CreatePolicyHandler } from './create-policy.handler';
import { CreateRoleHandler } from './create-role.handler';
import { IAMHandlers } from './iam.constants';
import { PrismaModule } from '../_prisma/prisma.module';
import { IamService } from './iam.service';
import { GetRoleHandler } from './get-role.handler';
const handlers = [
CreatePolicyHandler,
CreateRoleHandler,
GetRoleHandler,
]
const actions = [
Action.IamAddClientIDToOpenIDConnectProvider,
Action.IamAddRoleToInstanceProfile,
Action.IamAddUserToGroup,
Action.IamAttachGroupPolicy,
Action.IamAttachRolePolicy,
Action.IamAttachUserPolicy,
Action.IamChangePassword,
Action.IamCreateAccessKey,
Action.IamCreateAccountAlias,
Action.IamCreateGroup,
Action.IamCreateInstanceProfile,
Action.IamCreateLoginProfile,
Action.IamCreateOpenIDConnectProvider,
Action.IamCreatePolicy,
Action.IamCreatePolicyVersion,
Action.IamCreateRole,
Action.IamCreateSAMLProvider,
Action.IamCreateServiceLinkedRole,
Action.IamCreateServiceSpecificCredential,
Action.IamCreateUser,
Action.IamCreateVirtualMFADevice,
Action.IamDeactivateMFADevice,
Action.IamDeleteAccessKey,
Action.IamDeleteAccountAlias,
Action.IamDeleteAccountPasswordPolicy,
Action.IamDeleteGroup,
Action.IamDeleteGroupPolicy,
Action.IamDeleteInstanceProfile,
Action.IamDeleteLoginProfile,
Action.IamDeleteOpenIDConnectProvider,
Action.IamDeletePolicy,
Action.IamDeletePolicyVersion,
Action.IamDeleteRole,
Action.IamDeleteRolePermissionsBoundary,
Action.IamDeleteRolePolicy,
Action.IamDeleteSAMLProvider,
Action.IamDeleteServerCertificate,
Action.IamDeleteServiceLinkedRole,
Action.IamDeleteServiceSpecificCredential,
Action.IamDeleteSigningCertificate,
Action.IamDeleteSSHPublicKey,
Action.IamDeleteUser,
Action.IamDeleteUserPermissionsBoundary,
Action.IamDeleteUserPolicy,
Action.IamDeleteVirtualMFADevice,
Action.IamDetachGroupPolicy,
Action.IamDetachRolePolicy,
Action.IamDetachUserPolicy,
Action.IamEnableMFADevice,
Action.IamGenerateCredentialReport,
Action.IamGenerateOrganizationsAccessReport,
Action.IamGenerateServiceLastAccessedDetails,
Action.IamGetAccessKeyLastUsed,
Action.IamGetAccountAuthorizationDetails,
Action.IamGetAccountPasswordPolicy,
Action.IamGetAccountSummary,
Action.IamGetContextKeysForCustomPolicy,
Action.IamGetContextKeysForPrincipalPolicy,
Action.IamGetCredentialReport,
Action.IamGetGroup,
Action.IamGetGroupPolicy,
Action.IamGetInstanceProfile,
Action.IamGetLoginProfile,
Action.IamGetOpenIDConnectProvider,
Action.IamGetOrganizationsAccessReport,
Action.IamGetPolicy,
Action.IamGetPolicyVersion,
Action.IamGetRole,
Action.IamGetRolePolicy,
Action.IamGetSAMLProvider,
Action.IamGetServerCertificate,
Action.IamGetServiceLastAccessedDetails,
Action.IamGetServiceLastAccessedDetailsWithEntities,
Action.IamGetServiceLinkedRoleDeletionStatus,
Action.IamGetSSHPublicKey,
Action.IamGetUser,
Action.IamGetUserPolicy,
Action.IamListAccessKeys,
Action.IamListAccountAliases,
Action.IamListAttachedGroupPolicies,
Action.IamListAttachedRolePolicies,
Action.IamListAttachedUserPolicies,
Action.IamListEntitiesForPolicy,
Action.IamListGroupPolicies,
Action.IamListGroups,
Action.IamListGroupsForUser,
Action.IamListInstanceProfiles,
Action.IamListInstanceProfilesForRole,
Action.IamListInstanceProfileTags,
Action.IamListMFADevices,
Action.IamListMFADeviceTags,
Action.IamListOpenIDConnectProviders,
Action.IamListOpenIDConnectProviderTags,
Action.IamListPolicies,
Action.IamListPoliciesGrantingServiceAccess,
Action.IamListPolicyTags,
Action.IamListPolicyVersions,
Action.IamListRolePolicies,
Action.IamListRoles,
Action.IamListRoleTags,
Action.IamListSAMLProviders,
Action.IamListSAMLProviderTags,
Action.IamListServerCertificates,
Action.IamListServerCertificateTags,
Action.IamListServiceSpecificCredentials,
Action.IamListSigningCertificates,
Action.IamListSSHPublicKeys,
Action.IamListUserPolicies,
Action.IamListUsers,
Action.IamListUserTags,
Action.IamListVirtualMFADevices,
Action.IamPutGroupPolicy,
Action.IamPutRolePermissionsBoundary,
Action.IamPutRolePolicy,
Action.IamPutUserPermissionsBoundary,
Action.IamPutUserPolicy,
Action.IamRemoveClientIDFromOpenIDConnectProvider,
Action.IamRemoveRoleFromInstanceProfile,
Action.IamRemoveUserFromGroup,
Action.IamResetServiceSpecificCredential,
Action.IamResyncMFADevice,
Action.IamSetDefaultPolicyVersion,
Action.IamSetSecurityTokenServicePreferences,
Action.IamSimulateCustomPolicy,
Action.IamSimulatePrincipalPolicy,
Action.IamTagInstanceProfile,
Action.IamTagMFADevice,
Action.IamTagOpenIDConnectProvider,
Action.IamTagPolicy,
Action.IamTagRole,
Action.IamTagSAMLProvider,
Action.IamTagServerCertificate,
Action.IamTagUser,
Action.IamUntagInstanceProfile,
Action.IamUntagMFADevice,
Action.IamUntagOpenIDConnectProvider,
Action.IamUntagPolicy,
Action.IamUntagRole,
Action.IamUntagSAMLProvider,
Action.IamUntagServerCertificate,
Action.IamUntagUser,
Action.IamUpdateAccessKey,
Action.IamUpdateAccountPasswordPolicy,
Action.IamUpdateAssumeRolePolicy,
Action.IamUpdateGroup,
Action.IamUpdateLoginProfile,
Action.IamUpdateOpenIDConnectProviderThumbprint,
Action.IamUpdateRole,
Action.IamUpdateRoleDescription,
Action.IamUpdateSAMLProvider,
Action.IamUpdateServerCertificate,
Action.IamUpdateServiceSpecificCredential,
Action.IamUpdateSigningCertificate,
Action.IamUpdateSSHPublicKey,
Action.IamUpdateUser,
Action.IamUploadServerCertificate,
Action.IamUploadSigningCertificate,
Action.IamUploadSSHPublicKey,
]
@Module({
imports: [
AwsSharedEntitiesModule,
PrismaModule,
],
providers: [
...handlers,
IamService,
ExistingActionHandlersProvider(handlers),
DefaultActionHandlerProvider(IAMHandlers, Format.Xml, actions),
],
exports: [IAMHandlers],
})
export class IamModule {}

49
src/iam/iam.service.ts Normal file
View File

@ -0,0 +1,49 @@
import { Injectable } from "@nestjs/common";
import { PrismaService } from "../_prisma/prisma.service";
import { Prisma } from "@prisma/client";
import { IamPolicy } from "./iam-policy.entity";
import { IamRole } from "./iam-role.entity";
import { EntityAlreadyExists } from "../aws-shared-entities/aws-exceptions";
@Injectable()
export class IamService {
constructor(
private readonly prismaService: PrismaService,
) {}
async createRole(data: Prisma.IamRoleCreateInput): Promise<IamRole> {
try {
const record = await this.prismaService.iamRole.create({ data });
return new IamRole(record);
} catch (err) {
throw new EntityAlreadyExists(`RoleName ${data.name} already exists`);
}
}
async findOneRoleByName(accountId: string, name: string): Promise<IamRole | null> {
const record = await this.prismaService.iamRole.findFirst({
where: {
name,
accountId,
}
});
return record ? new IamRole(record) : null;
}
async deleteRoleByName(accountId: string, name: string) {
await this.prismaService.iamRole.deleteMany({
where: {
name,
accountId,
}
});
}
async createPolicy(data: Prisma.IamPolicyCreateInput): Promise<IamPolicy> {
const record = await this.prismaService.iamPolicy.create({ data });
return new IamPolicy(record);
}
}

View File

@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
type QueryParams = {
RoleName: string;
}
@Injectable()
export class ListAttachedRolePoliciesHandler extends AbstractActionHandler<QueryParams> {
constructor(
) {
super();
}
format = Format.Xml;
action = Action.IamListAttachedRolePolicies;
validator = Joi.object<QueryParams, true>({
RoleName: Joi.string().required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
}
}

View File

@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
type QueryParams = {
Marker: string;
MaxItems: number;
RoleName: string;
}
@Injectable()
export class ListRolePoliciesHandler extends AbstractActionHandler<QueryParams> {
constructor(
) {
super();
}
format = Format.Xml;
action = Action.IamListRolePolicies;
validator = Joi.object<QueryParams, true>({
Marker: Joi.string().allow(null),
MaxItems: Joi.number().min(1).max(1000).default(100),
RoleName: Joi.string().required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
}
}