mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 13:22:08 +00:00
Complete adding GCP GCE auth
This commit is contained in:
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TLoginGcpAuthDTO = {
|
||||
identityId: string;
|
||||
jwt: string;
|
||||
};
|
||||
|
||||
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 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 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;
|
||||
kid: string;
|
||||
typ: string;
|
||||
};
|
||||
payload: {
|
||||
sub: string;
|
||||
aud: string;
|
||||
};
|
||||
signature: string;
|
||||
metadata: {
|
||||
[key: string]: 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(", ");
|
||||
});
|
||||
Reference in New Issue
Block a user