Complete adding GCP GCE auth

This commit is contained in:
Tuan Dang
2024-05-08 15:51:09 -07:00
parent ac469dbe4f
commit e99182c141
32 changed files with 1178 additions and 545 deletions
+2 -2
View File
@@ -33,7 +33,7 @@ import { TGroupProjectServiceFactory } from "@app/services/group-project/group-p
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityAwsIamAuthServiceFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-service";
import { TIdentityGcpIamAuthServiceFactory } from "@app/services/identity-gcp-iam-auth/identity-gcp-iam-auth-service";
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
@@ -117,7 +117,7 @@ declare module "fastify" {
identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory;
identityUa: TIdentityUaServiceFactory;
identityGcpIamAuth: TIdentityGcpIamAuthServiceFactory;
identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAwsIamAuth: TIdentityAwsIamAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
+7 -7
View File
@@ -62,9 +62,9 @@ import {
TIdentityAwsIamAuths,
TIdentityAwsIamAuthsInsert,
TIdentityAwsIamAuthsUpdate,
TIdentityGcpIamAuths,
TIdentityGcpIamAuthsInsert,
TIdentityGcpIamAuthsUpdate,
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate,
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate,
@@ -332,10 +332,10 @@ declare module "knex/types/tables" {
TIdentityUniversalAuthsInsert,
TIdentityUniversalAuthsUpdate
>;
[TableName.IdentityGcpIamAuth]: Knex.CompositeTableType<
TIdentityGcpIamAuths,
TIdentityGcpIamAuthsInsert,
TIdentityGcpIamAuthsUpdate
[TableName.IdentityGcpAuth]: Knex.CompositeTableType<
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate
>;
[TableName.IdentityAwsIamAuth]: Knex.CompositeTableType<
TIdentityAwsIamAuths,
@@ -4,8 +4,8 @@ import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityGcpIamAuth))) {
await knex.schema.createTable(TableName.IdentityGcpIamAuth, (t) => {
if (!(await knex.schema.hasTable(TableName.IdentityGcpAuth))) {
await knex.schema.createTable(TableName.IdentityGcpAuth, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
@@ -14,15 +14,20 @@ export async function up(knex: Knex): Promise<void> {
t.timestamps(true, true, true);
t.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.text("encryptedCredentials").notNullable();
t.string("credentialsIV").notNullable();
t.string("credentialsTag").notNullable();
t.string("type").notNullable();
t.string("allowedServiceAccounts").notNullable();
t.string("allowedProjects").notNullable();
t.string("allowedZones").notNullable(); // GCE only (fully qualified zone names)
});
}
await createOnUpdateTrigger(knex, TableName.IdentityGcpIamAuth);
await createOnUpdateTrigger(knex, TableName.IdentityGcpAuth);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityGcpIamAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityGcpIamAuth);
await knex.schema.dropTableIfExists(TableName.IdentityGcpAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityGcpAuth);
}
@@ -11,8 +11,8 @@ export const AccessApprovalPoliciesSchema = z.object({
id: z.string().uuid(),
name: z.string(),
approvals: z.number().default(1),
envId: z.string().uuid(),
secretPath: z.string().nullable().optional(),
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
@@ -7,7 +7,7 @@ import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const IdentityGcpIamAuthsSchema = z.object({
export const IdentityGcpAuthsSchema = z.object({
id: z.string().uuid(),
accessTokenTTL: z.coerce.number().default(7200),
accessTokenMaxTTL: z.coerce.number().default(7200),
@@ -16,10 +16,15 @@ export const IdentityGcpIamAuthsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid(),
encryptedCredentials: z.string(),
credentialsIV: z.string(),
credentialsTag: z.string(),
type: z.string(),
allowedServiceAccounts: z.string(),
allowedProjects: z.string()
allowedProjects: z.string(),
allowedZones: z.string()
});
export type TIdentityGcpIamAuths = z.infer<typeof IdentityGcpIamAuthsSchema>;
export type TIdentityGcpIamAuthsInsert = Omit<z.input<typeof IdentityGcpIamAuthsSchema>, TImmutableDBKeys>;
export type TIdentityGcpIamAuthsUpdate = Partial<Omit<z.input<typeof IdentityGcpIamAuthsSchema>, TImmutableDBKeys>>;
export type TIdentityGcpAuths = z.infer<typeof IdentityGcpAuthsSchema>;
export type TIdentityGcpAuthsInsert = Omit<z.input<typeof IdentityGcpAuthsSchema>, TImmutableDBKeys>;
export type TIdentityGcpAuthsUpdate = Partial<Omit<z.input<typeof IdentityGcpAuthsSchema>, TImmutableDBKeys>>;
+1 -1
View File
@@ -18,7 +18,7 @@ export * from "./groups";
export * from "./identities";
export * from "./identity-access-tokens";
export * from "./identity-aws-iam-auths";
export * from "./identity-gcp-iam-auths";
export * from "./identity-gcp-auths";
export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege";
export * from "./identity-project-membership-role";
+2 -2
View File
@@ -44,7 +44,7 @@ export enum TableName {
Identity = "identities",
IdentityAccessToken = "identity_access_tokens",
IdentityUniversalAuth = "identity_universal_auths",
IdentityGcpIamAuth = "identity_gcp_iam_auths",
IdentityGcpAuth = "identity_gcp_auths",
IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsIamAuth = "identity_aws_iam_auths",
IdentityOrgMembership = "identity_org_memberships",
@@ -145,6 +145,6 @@ export enum ProjectUpgradeStatus {
export enum IdentityAuthMethod {
Univeral = "universal-auth",
GCP_IAM_AUTH = "gcp-iam-auth",
GCP_AUTH = "gcp-auth",
AWS_IAM_AUTH = "aws-iam-auth"
}
+1 -1
View File
@@ -22,7 +22,7 @@ export const UsersSchema = z.object({
updatedAt: z.date(),
isGhost: z.boolean().default(false),
username: z.string(),
isEmailVerified: z.boolean().nullable().optional()
isEmailVerified: z.boolean().default(false).nullable().optional()
});
export type TUsers = z.infer<typeof UsersSchema>;
@@ -66,10 +66,10 @@ export enum EventType {
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
LOGIN_IDENTITY_GCP_IAM_AUTH = "login-identity-gcp-iam-auth",
ADD_IDENTITY_GCP_IAM_AUTH = "add-identity-gcp-iam -auth",
UPDATE_IDENTITY_GCP_IAM_AUTH = "update-identity-gcp-iam-auth",
GET_IDENTITY_GCP_IAM_AUTH = "get-identity-gcp-iam-auth",
LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth",
ADD_IDENTITY_GCP_AUTH = "add-identity-gcp -auth",
UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth",
GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth",
LOGIN_IDENTITY_AWS_IAM_AUTH = "login-identity-aws-iam-auth",
ADD_IDENTITY_AWS_IAM_AUTH = "add-identity-aws-iam-auth",
UPDATE_IDENTITY_AWS_IAM_AUTH = "update-identity-aws-iam-auth",
@@ -414,21 +414,23 @@ interface RevokeIdentityUniversalAuthClientSecretEvent {
};
}
interface LoginIdentityGcpIamAuthEvent {
type: EventType.LOGIN_IDENTITY_GCP_IAM_AUTH;
interface LoginIdentityGcpAuthEvent {
type: EventType.LOGIN_IDENTITY_GCP_AUTH;
metadata: {
identityId: string;
identityGcpIamAuthId: string;
identityGcpAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityGcpIamAuthEvent {
type: EventType.ADD_IDENTITY_GCP_IAM_AUTH;
interface AddIdentityGcpAuthEvent {
type: EventType.ADD_IDENTITY_GCP_AUTH;
metadata: {
identityId: string;
type: string;
allowedServiceAccounts: string;
allowedProjects: string;
allowedZones: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
@@ -436,12 +438,14 @@ interface AddIdentityGcpIamAuthEvent {
};
}
interface UpdateIdentityGcpIamAuthEvent {
type: EventType.UPDATE_IDENTITY_GCP_IAM_AUTH;
interface UpdateIdentityGcpAuthEvent {
type: EventType.UPDATE_IDENTITY_GCP_AUTH;
metadata: {
identityId: string;
type?: string;
allowedServiceAccounts?: string;
allowedProjects?: string;
allowedZones?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
@@ -449,8 +453,8 @@ interface UpdateIdentityGcpIamAuthEvent {
};
}
interface GetIdentityGcpIamAuthEvent {
type: EventType.GET_IDENTITY_GCP_IAM_AUTH;
interface GetIdentityGcpAuthEvent {
type: EventType.GET_IDENTITY_GCP_AUTH;
metadata: {
identityId: string;
};
@@ -754,10 +758,10 @@ export type Event =
| CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent
| RevokeIdentityUniversalAuthClientSecretEvent
| LoginIdentityGcpIamAuthEvent
| AddIdentityGcpIamAuthEvent
| UpdateIdentityGcpIamAuthEvent
| GetIdentityGcpIamAuthEvent
| LoginIdentityGcpAuthEvent
| AddIdentityGcpAuthEvent
| UpdateIdentityGcpAuthEvent
| GetIdentityGcpAuthEvent
| LoginIdentityAwsIamAuthEvent
| AddIdentityAwsIamAuthEvent
| UpdateIdentityAwsIamAuthEvent
+7 -6
View File
@@ -80,8 +80,8 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { identityAwsIamAuthDALFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-dal";
import { identityAwsIamAuthServiceFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-service";
import { identityGcpIamAuthDALFactory } from "@app/services/identity-gcp-iam-auth/identity-gcp-iam-auth-dal";
import { identityGcpIamAuthServiceFactory } from "@app/services/identity-gcp-iam-auth/identity-gcp-iam-auth-service";
import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal";
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
@@ -207,7 +207,7 @@ export const registerRoutes = async (
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAwsIamAuthDAL = identityAwsIamAuthDALFactory(db);
const identityGcpIamAuthDAL = identityGcpIamAuthDALFactory(db);
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
const auditLogDAL = auditLogDALFactory(db);
const auditLogStreamDAL = auditLogStreamDALFactory(db);
@@ -706,11 +706,12 @@ export const registerRoutes = async (
identityUaDAL,
licenseService
});
const identityGcpIamAuthService = identityGcpIamAuthServiceFactory({
identityGcpIamAuthDAL,
const identityGcpAuthService = identityGcpAuthServiceFactory({
identityGcpAuthDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
orgBotDAL,
permissionService,
licenseService
});
@@ -792,7 +793,7 @@ export const registerRoutes = async (
identityAccessToken: identityAccessTokenService,
identityProject: identityProjectService,
identityUa: identityUaService,
identityGcpIamAuth: identityGcpIamAuthService,
identityGcpAuth: identityGcpAuthService,
identityAwsIamAuth: identityAWSIAMAuthService,
secretApprovalPolicy: sapService,
accessApprovalPolicy: accessApprovalPolicyService,
@@ -1,22 +1,24 @@
import { z } from "zod";
import { IdentityGcpIamAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateGcpIamAuthField } from "@app/services/identity-gcp-iam-auth/identity-gcp-iam-auth-validators";
import {
ModifiedIdentityGcpAuthsSchema,
validateGcpAuthField
} from "@app/services/identity-gcp-auth/identity-gcp-auth-validators";
export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvider) => {
export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/gcp-iam-auth/login",
url: "/gcp-auth/login",
config: {
rateLimit: writeLimit
},
schema: {
description: "Login with GCP IAM Auth",
description: "Login with GCP Auth",
body: z.object({
identityId: z.string(),
jwt: z.string()
@@ -31,18 +33,18 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide
}
},
handler: async (req) => {
const { identityGcpIamAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityGcpIamAuth.login(req.body);
const { identityGcpAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityGcpAuth.login(req.body);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: {
type: EventType.LOGIN_IDENTITY_GCP_IAM_AUTH,
type: EventType.LOGIN_IDENTITY_GCP_AUTH,
metadata: {
identityId: identityGcpIamAuth.identityId,
identityId: identityGcpAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
identityGcpIamAuthId: identityGcpIamAuth.id
identityGcpAuthId: identityGcpAuth.id
}
}
});
@@ -50,21 +52,21 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityGcpIamAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpIamAuth.accessTokenMaxTTL
expiresIn: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "POST",
url: "/gcp-iam-auth/identities/:identityId",
url: "/gcp-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Attach GCP IAM Auth configuration onto identity",
description: "Attach GCP Auth configuration onto identity",
security: [
{
bearerAuth: []
@@ -74,8 +76,11 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide
identityId: z.string().trim()
}),
body: z.object({
allowedServiceAccounts: validateGcpIamAuthField,
allowedProjects: validateGcpIamAuthField,
credentials: z.string().trim(),
type: z.enum(["iam", "gce"]),
allowedServiceAccounts: validateGcpAuthField,
allowedProjects: validateGcpAuthField,
allowedZones: validateGcpAuthField,
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
@@ -102,12 +107,12 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide
}),
response: {
200: z.object({
identityGcpIamAuth: IdentityGcpIamAuthsSchema
identityGcpAuth: ModifiedIdentityGcpAuthsSchema
})
}
},
handler: async (req) => {
const identityGcpIamAuth = await server.services.identityGcpIamAuth.attachGcpIamAuth({
const identityGcpAuth = await server.services.identityGcpAuth.attachGcpAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -118,34 +123,36 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityGcpIamAuth.orgId,
orgId: identityGcpAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_GCP_IAM_AUTH,
type: EventType.ADD_IDENTITY_GCP_AUTH,
metadata: {
identityId: identityGcpIamAuth.identityId,
allowedServiceAccounts: identityGcpIamAuth.allowedServiceAccounts,
allowedProjects: identityGcpIamAuth.allowedProjects,
accessTokenTTL: identityGcpIamAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpIamAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityGcpIamAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityGcpIamAuth.accessTokenNumUsesLimit
identityId: identityGcpAuth.identityId,
type: identityGcpAuth.type,
allowedServiceAccounts: identityGcpAuth.allowedServiceAccounts,
allowedProjects: identityGcpAuth.allowedProjects,
allowedZones: identityGcpAuth.allowedZones,
accessTokenTTL: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityGcpAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit
}
}
});
return { identityGcpIamAuth };
return { identityGcpAuth };
}
});
server.route({
method: "PATCH",
url: "/gcp-iam-auth/identities/:identityId",
url: "/gcp-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update GCP IAM Auth configuration on identity",
description: "Update GCP Auth configuration on identity",
security: [
{
bearerAuth: []
@@ -155,8 +162,11 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide
identityId: z.string().trim()
}),
body: z.object({
allowedServiceAccounts: z.string().trim().optional(),
allowedProjects: z.string().trim().optional(),
credentials: z.string().trim().optional(),
type: z.enum(["iam", "gce"]).optional(),
allowedServiceAccounts: validateGcpAuthField,
allowedProjects: validateGcpAuthField,
allowedZones: validateGcpAuthField,
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
@@ -176,12 +186,12 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide
}),
response: {
200: z.object({
identityGcpIamAuth: IdentityGcpIamAuthsSchema
identityGcpAuth: ModifiedIdentityGcpAuthsSchema
})
}
},
handler: async (req) => {
const identityGcpIamAuth = await server.services.identityGcpIamAuth.updateGcpIamAuth({
const identityGcpAuth = await server.services.identityGcpAuth.updateGcpAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
@@ -192,34 +202,36 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityGcpIamAuth.orgId,
orgId: identityGcpAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_GCP_IAM_AUTH,
type: EventType.UPDATE_IDENTITY_GCP_AUTH,
metadata: {
identityId: identityGcpIamAuth.identityId,
allowedServiceAccounts: identityGcpIamAuth.allowedServiceAccounts,
allowedProjects: identityGcpIamAuth.allowedProjects,
accessTokenTTL: identityGcpIamAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpIamAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityGcpIamAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityGcpIamAuth.accessTokenNumUsesLimit
identityId: identityGcpAuth.identityId,
type: identityGcpAuth.type,
allowedServiceAccounts: identityGcpAuth.allowedServiceAccounts,
allowedProjects: identityGcpAuth.allowedProjects,
allowedZones: identityGcpAuth.allowedZones,
accessTokenTTL: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityGcpAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit
}
}
});
return { identityGcpIamAuth };
return { identityGcpAuth };
}
});
server.route({
method: "GET",
url: "/gcp-iam-auth/identities/:identityId",
url: "/gcp-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Retrieve GCP IAM Auth configuration on identity",
description: "Retrieve GCP Auth configuration on identity",
security: [
{
bearerAuth: []
@@ -230,12 +242,12 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide
}),
response: {
200: z.object({
identityGcpIamAuth: IdentityGcpIamAuthsSchema
identityGcpAuth: ModifiedIdentityGcpAuthsSchema
})
}
},
handler: async (req) => {
const identityGcpIamAuth = await server.services.identityGcpIamAuth.getGcpIamAuth({
const identityGcpAuth = await server.services.identityGcpAuth.getGcpAuth({
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -245,16 +257,16 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityGcpIamAuth.orgId,
orgId: identityGcpAuth.orgId,
event: {
type: EventType.GET_IDENTITY_GCP_IAM_AUTH,
type: EventType.GET_IDENTITY_GCP_AUTH,
metadata: {
identityId: identityGcpIamAuth.identityId
identityId: identityGcpAuth.identityId
}
}
});
return { identityGcpIamAuth };
return { identityGcpAuth };
}
});
};
+2 -2
View File
@@ -3,7 +3,7 @@ import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAwsIamAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityGcpIamAuthRouter } from "./identity-gcp-iam-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityRouter } from "./identity-router";
import { registerIdentityUaRouter } from "./identity-ua";
import { registerIntegrationAuthRouter } from "./integration-auth-router";
@@ -29,7 +29,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
async (authRouter) => {
await authRouter.register(registerAuthRoutes);
await authRouter.register(registerIdentityUaRouter);
await authRouter.register(registerIdentityGcpIamAuthRouter);
await authRouter.register(registerIdentityGcpAuthRouter);
await authRouter.register(registerIdentityAwsIamAuthRouter);
await authRouter.register(registerIdentityAccessTokenRouter);
},
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityGcpAuthDALFactory = ReturnType<typeof identityGcpAuthDALFactory>;
export const identityGcpAuthDALFactory = (db: TDbClient) => {
const gcpAuthOrm = ormify(db, TableName.IdentityGcpAuth);
return gcpAuthOrm;
};
@@ -0,0 +1,101 @@
import axios from "axios";
import { JWTInput, OAuth2Client } from "google-auth-library";
import { google } from "googleapis";
import jwt from "jsonwebtoken";
import { UnauthorizedError } from "@app/lib/errors";
import { TDecodedGcpIamAuthJwt, TGcpGceIdTokenPayload } from "./identity-gcp-auth-types";
/**
* Return the full details of the service account corresponding to the service account email or unique ID [serviceAccount].
* @param {string} serviceAccount - The email or unique ID of the service account.
* @param {string} credentials - The credentials in the GCP Auth configuration for Infisical.
* @returns
*/
const getGcpServiceAccountDetails = async (serviceAccount: string, credentials: string) => {
const auth = new google.auth.GoogleAuth({
credentials: JSON.parse(credentials) as JWTInput,
scopes: ["https://www.googleapis.com/auth/iam"]
});
const iam = google.iam({
version: "v1",
auth
});
const { data } = await iam.projects.serviceAccounts.get({
name: `projects/-/serviceAccounts/${serviceAccount}`
});
return data;
};
/**
* Validates that the identity token [jwt] sent in from a client GCE instance as part of GCP GCE authentication
* is valid.
* @param {string} identityId - The ID of the identity in Infisical that is being authenticated against (used as audience).
* @param {string} jwt - The identity token to validate.
* @param {string} credentials - The credentials in the GCP Auth configuration for Infisical.
*/
export const validateGceIdentity = async ({
identityId,
jwt: identityToken,
credentials
}: {
identityId: string;
jwt: string;
credentials: string;
}) => {
const oAuth2Client = new OAuth2Client();
const response = await oAuth2Client.getFederatedSignonCerts();
const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync(
identityToken,
response.certs,
identityId, // audience
["https://accounts.google.com"]
);
const payload = ticket.getPayload() as TGcpGceIdTokenPayload;
if (!payload || !payload.email) throw new UnauthorizedError();
const serviceAccountDetails = await getGcpServiceAccountDetails(payload.email, credentials);
return { serviceAccountDetails, gceInstanceDetails: payload };
};
/**
* Validates that the signed JWT token for a GCP service account is valid as part of GCP IAM authentication.
* @param {string} identityId - The ID of the identity in Infisical that is being authenticated against (used as audience).
* @param {string} jwt - The signed JWT token to validate.
* @param {string} credentials - The credentials in the GCP Auth configuration for Infisical.
* @returns
*/
export const validateIamIdentity = async ({
identityId,
jwt: serviceAccountJwt,
credentials
}: {
identityId: string;
jwt: string;
credentials: string;
}) => {
const decodedJwt = jwt.decode(serviceAccountJwt, { complete: true }) as TDecodedGcpIamAuthJwt;
const { sub, aud } = decodedJwt.payload;
const {
data
}: {
data: {
[key: string]: string;
};
} = await axios.get(`https://www.googleapis.com/service_accounts/v1/metadata/x509/${sub}`);
const publicKey = data[decodedJwt.header.kid];
jwt.verify(serviceAccountJwt, publicKey, {
algorithms: ["RS256"]
});
if (aud !== identityId) throw new UnauthorizedError();
const serviceAccountDetails = await getGcpServiceAccountDetails(sub, credentials);
return { serviceAccountDetails };
};
@@ -0,0 +1,474 @@
import { ForbiddenError } from "@casl/ability";
import { iam_v1 } from "googleapis";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod, SecretKeyEncoding, TIdentityGcpAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import {
decryptSymmetric,
encryptSymmetric,
generateAsymmetricKeyPair,
generateSymmetricKey,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal";
import { validateGceIdentity, validateIamIdentity } from "./identity-gcp-auth-fns";
import {
TAttachGcpAuthDTO,
TGcpGceIdTokenPayload,
TGetGcpAuthDTO,
TLoginGcpAuthDTO,
TUpdateGcpAuthDTO
} from "./identity-gcp-auth-types";
type TIdentityGcpAuthServiceFactoryDep = {
identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TIdentityGcpAuthServiceFactory = ReturnType<typeof identityGcpAuthServiceFactory>;
export const identityGcpAuthServiceFactory = ({
identityGcpAuthDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
orgBotDAL,
permissionService,
licenseService
}: TIdentityGcpAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginGcpAuthDTO) => {
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
if (!identityGcpAuth) throw new UnauthorizedError();
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityGcpAuth.identityId });
if (!identityMembershipOrg) throw new UnauthorizedError();
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { encryptedCredentials, credentialsIV, credentialsTag } = identityGcpAuth;
let credentials = "";
if (encryptedCredentials && credentialsIV && credentialsTag) {
credentials = decryptSymmetric({
ciphertext: encryptedCredentials,
key,
tag: credentialsTag,
iv: credentialsIV
});
}
let serviceAccountDetails: iam_v1.Schema$ServiceAccount;
let gceInstanceDetails: TGcpGceIdTokenPayload | undefined;
switch (identityGcpAuth.type) {
case "gce": {
const gceIdentity = await validateGceIdentity({
identityId,
jwt: serviceAccountJwt,
credentials
});
serviceAccountDetails = gceIdentity.serviceAccountDetails;
gceInstanceDetails = gceIdentity.gceInstanceDetails;
break;
}
case "iam": {
const iamIdentity = await validateIamIdentity({
identityId,
jwt: serviceAccountJwt,
credentials
});
serviceAccountDetails = iamIdentity.serviceAccountDetails;
break;
}
default: {
throw new BadRequestError({ message: "Invalid GCP Auth type" });
}
}
if (identityGcpAuth.allowedServiceAccounts) {
// validate if the service account is in the list of allowed service accounts
const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts
.split(",")
.map((serviceAccount) => serviceAccount.trim())
.some(
(serviceAccount) =>
serviceAccount === serviceAccountDetails.email || serviceAccount === serviceAccountDetails.uniqueId
);
if (!isServiceAccountAllowed) throw new UnauthorizedError();
}
if (identityGcpAuth.allowedProjects) {
// validate if the project that the service account belongs to is in the list of allowed projects
const isProjectAllowed = identityGcpAuth.allowedProjects
.split(",")
.map((project) => project.trim())
.some((project) => project === serviceAccountDetails.projectId);
if (!isProjectAllowed) throw new UnauthorizedError();
}
if (identityGcpAuth.type === "gce" && gceInstanceDetails && identityGcpAuth.allowedZones) {
const isZoneAllowed = identityGcpAuth.allowedZones
.split(",")
.map((zone) => zone.trim())
.some((zone) => zone === gceInstanceDetails!.google.compute_engine.zone);
if (!isZoneAllowed) throw new UnauthorizedError();
}
const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityGcpAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityGcpAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
);
return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg };
};
const attachGcpAuth = async ({
identityId,
credentials,
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TAttachGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity.authMethod)
throw new BadRequestError({
message: "Failed to add GCP Auth to already configured identity"
});
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const orgBot = await orgBotDAL.transaction(async (tx) => {
const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx);
if (doc) return doc;
const { privateKey, publicKey } = generateAsymmetricKeyPair();
const key = generateSymmetricKey();
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag,
encoding: privateKeyKeyEncoding,
algorithm: privateKeyAlgorithm
} = infisicalSymmetricEncypt(privateKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag,
encoding: symmetricKeyKeyEncoding,
algorithm: symmetricKeyAlgorithm
} = infisicalSymmetricEncypt(key);
return orgBotDAL.create(
{
name: "Infisical org bot",
publicKey,
privateKeyIV,
encryptedPrivateKey,
symmetricKeyIV,
symmetricKeyTag,
encryptedSymmetricKey,
symmetricKeyAlgorithm,
orgId: identityMembershipOrg.orgId,
privateKeyTag,
privateKeyAlgorithm,
privateKeyKeyEncoding,
symmetricKeyKeyEncoding
},
tx
);
});
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const {
ciphertext: encryptedCredentials,
iv: credentialsIV,
tag: credentialsTag
} = encryptSymmetric(credentials, key);
const identityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
const doc = await identityGcpAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
encryptedCredentials,
credentialsIV,
credentialsTag,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.GCP_AUTH
},
tx
);
return doc;
});
return { ...identityGcpAuth, credentials, orgId: identityMembershipOrg.orgId };
};
const updateGcpAuth = async ({
identityId,
type,
credentials,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
throw new BadRequestError({
message: "Failed to update GCP Auth"
});
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityGcpAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityGcpAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityGcpAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updateQuery: TIdentityGcpAuthsUpdate = {
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
};
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
if (credentials !== undefined) {
const {
ciphertext: encryptedCredentials,
iv: credentialsIV,
tag: credentialsTag
} = encryptSymmetric(credentials, key);
updateQuery.encryptedCredentials = encryptedCredentials;
updateQuery.credentialsIV = credentialsIV;
updateQuery.credentialsTag = credentialsTag;
}
const updatedGcpAuth = await identityGcpAuthDAL.updateById(identityGcpAuth.id, updateQuery);
return {
...updatedGcpAuth,
credentials: decryptSymmetric({
ciphertext: updatedGcpAuth.encryptedCredentials,
iv: updatedGcpAuth.credentialsIV,
tag: updatedGcpAuth.credentialsTag,
key
}),
orgId: identityMembershipOrg.orgId
};
};
const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
throw new BadRequestError({
message: "The identity does not have GCP Auth attached"
});
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { encryptedCredentials, credentialsIV, credentialsTag } = identityGcpAuth;
let credentials = "";
if (encryptedCredentials && credentialsIV && credentialsTag) {
credentials = decryptSymmetric({
ciphertext: encryptedCredentials,
key,
tag: credentialsTag,
iv: credentialsIV
});
}
return { ...identityGcpAuth, credentials, orgId: identityMembershipOrg.orgId };
};
return {
login,
attachGcpAuth,
updateGcpAuth,
getGcpAuth
};
};
@@ -1,34 +1,61 @@
import { TProjectPermission } from "@app/lib/types";
export type TLoginGcpIamAuthDTO = {
export type TLoginGcpAuthDTO = {
identityId: string;
jwt: string;
};
export type TAttachGcpIamAuthDTO = {
export type TAttachGcpAuthDTO = {
identityId: string;
credentials: string;
type: "iam" | "gce";
allowedServiceAccounts: string;
allowedProjects: string;
allowedZones: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateGcpIamAuthDTO = {
export type TUpdateGcpAuthDTO = {
identityId: string;
credentials?: string;
type?: "iam" | "gce";
allowedServiceAccounts?: string;
allowedProjects?: string;
allowedZones?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetGcpIamAuthDTO = {
export type TGetGcpAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGcpGceIdTokenPayload = {
aud: string;
azp: string;
email: string;
email_verified: boolean;
exp: number;
google: {
compute_engine: {
instance_creation_timestamp: number;
instance_id: string;
instance_name: string;
project_id: string;
project_number: number;
zone: string;
};
};
iat: number;
iss: string;
sub: string;
};
export type TDecodedGcpIamAuthJwt = {
header: {
alg: string;
@@ -0,0 +1,24 @@
import { z } from "zod";
import { IdentityGcpAuthsSchema } from "@app/db/schemas";
export const ModifiedIdentityGcpAuthsSchema = IdentityGcpAuthsSchema.omit({
encryptedCredentials: true,
credentialsIV: true,
credentialsTag: true
}).extend({
credentials: z.string()
});
export const validateGcpAuthField = z
.string()
.trim()
.default("")
.transform((data) => {
if (data === "") return "";
// Trim each ID and join with ', ' to ensure formatting
return data
.split(",")
.map((id) => id.trim())
.join(", ");
});
@@ -1,10 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityGcpIamAuthDALFactory = ReturnType<typeof identityGcpIamAuthDALFactory>;
export const identityGcpIamAuthDALFactory = (db: TDbClient) => {
const gcpIamAuthOrm = ormify(db, TableName.IdentityGcpIamAuth);
return gcpIamAuthOrm;
};
@@ -1,17 +0,0 @@
/**
* Extracts the GCP service account email into the name and project ID parts where
* the email is in the format: <service-account-name>@<project-id>.iam.gserviceaccount.com
*/
export const extractGcpServiceAccountEmail = (email: string) => {
const regex = /^(.+)@(.+)\.iam\.gserviceaccount\.com$/;
const match = email.match(regex);
if (!match) {
throw new Error("Invalid GCP service account email format.");
}
const name = match[1];
const projectId = match[2];
return { name, projectId };
};
@@ -1,304 +0,0 @@
import { ForbiddenError } from "@casl/ability";
import axios from "axios";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TIdentityGcpIamAuthDALFactory } from "./identity-gcp-iam-auth-dal";
import { extractGcpServiceAccountEmail } from "./identity-gcp-iam-auth-fns";
import {
TAttachGcpIamAuthDTO,
TDecodedGcpIamAuthJwt,
TGetGcpIamAuthDTO,
TLoginGcpIamAuthDTO,
TUpdateGcpIamAuthDTO
} from "./identity-gcp-iam-auth-types";
type TIdentityGcpIamAuthServiceFactoryDep = {
identityGcpIamAuthDAL: Pick<TIdentityGcpIamAuthDALFactory, "findOne" | "transaction" | "create" | "updateById">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TIdentityGcpIamAuthServiceFactory = ReturnType<typeof identityGcpIamAuthServiceFactory>;
export const identityGcpIamAuthServiceFactory = ({
identityGcpIamAuthDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
permissionService,
licenseService
}: TIdentityGcpIamAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginGcpIamAuthDTO) => {
const identityGcpIamAuth = await identityGcpIamAuthDAL.findOne({ identityId });
if (!identityGcpIamAuth) throw new UnauthorizedError();
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityGcpIamAuth.identityId });
const decodedJwt = jwt.decode(serviceAccountJwt, { complete: true }) as TDecodedGcpIamAuthJwt;
const { sub, aud } = decodedJwt.payload;
const {
data
}: {
data: {
[key: string]: string;
};
} = await axios.get(`https://www.googleapis.com/service_accounts/v1/metadata/x509/${sub}`);
const publicKey = data[decodedJwt.header.kid];
jwt.verify(serviceAccountJwt, publicKey, {
algorithms: ["RS256"]
});
if (aud !== identityId) throw new UnauthorizedError();
const { name, projectId } = extractGcpServiceAccountEmail(sub);
if (identityGcpIamAuth.allowedServiceAccounts) {
// validate if the service account is in the list of allowed service accounts
const isServiceAccountAllowed = identityGcpIamAuth.allowedServiceAccounts
.split(",")
.map((serviceAccount) => serviceAccount.trim())
.some((serviceAccount) => serviceAccount === name);
if (!isServiceAccountAllowed) throw new UnauthorizedError();
}
if (identityGcpIamAuth.allowedProjects) {
// validate if the project that the service account belongs to is in the list of allowed projects
const isProjectAllowed = identityGcpIamAuth.allowedProjects
.split(",")
.map((project) => project.trim())
.some((project) => project === projectId);
if (!isProjectAllowed) throw new UnauthorizedError();
}
const identityAccessToken = await identityGcpIamAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityGcpIamAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityGcpIamAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpIamAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityGcpIamAuth.accessTokenNumUsesLimit
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityGcpIamAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
);
return { accessToken, identityGcpIamAuth, identityAccessToken, identityMembershipOrg };
};
const attachGcpIamAuth = async ({
identityId,
allowedServiceAccounts,
allowedProjects,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TAttachGcpIamAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity.authMethod)
throw new BadRequestError({
message: "Failed to add AWS IAM Auth to already configured identity"
});
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const identityAwsIamAuth = await identityGcpIamAuthDAL.transaction(async (tx) => {
const doc = await identityGcpIamAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
allowedServiceAccounts,
allowedProjects,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.GCP_IAM_AUTH
},
tx
);
return doc;
});
return { ...identityAwsIamAuth, orgId: identityMembershipOrg.orgId };
};
const updateGcpIamAuth = async ({
identityId,
allowedServiceAccounts,
allowedProjects,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateGcpIamAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_IAM_AUTH)
throw new BadRequestError({
message: "Failed to update GCP IAM Auth"
});
const identityGcpIamAuth = await identityGcpIamAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityGcpIamAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityGcpIamAuth.accessTokenMaxTTL) >
(accessTokenMaxTTL || identityGcpIamAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updatedGcpIamAuth = await identityGcpIamAuthDAL.updateById(identityGcpIamAuth.id, {
allowedServiceAccounts,
allowedProjects,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
});
return { ...updatedGcpIamAuth, orgId: identityMembershipOrg.orgId };
};
const getGcpIamAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpIamAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_IAM_AUTH)
throw new BadRequestError({
message: "The identity does not have GCP IAM Auth attached"
});
const gcpIamIdentityAuth = await identityGcpIamAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
return { ...gcpIamIdentityAuth, orgId: identityMembershipOrg.orgId };
};
return {
login,
attachGcpIamAuth,
updateGcpIamAuth,
getGcpIamAuth
};
};
@@ -1,14 +0,0 @@
import { z } from "zod";
export const validateGcpIamAuthField = z
.string()
.trim()
.default("")
.transform((data) => {
if (data === "") return "";
// Trim each ID and join with ', ' to ensure formatting
return data
.split(",")
.map((id) => id.trim())
.join(", ");
});