mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 19:44:52 +00:00
Remove service account JSON requirement from GCP Auth
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { JWTInput, OAuth2Client } from "google-auth-library";
|
||||
import { google } from "googleapis";
|
||||
import { OAuth2Client } from "google-auth-library";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
@@ -8,45 +7,13 @@ 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
|
||||
* Validates that the identity token [jwt] sent in from a client GCE instance as part of GCP ID Token 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;
|
||||
}) => {
|
||||
export const validateGceIdentity = async ({ identityId, jwt: identityToken }: { identityId: string; jwt: string }) => {
|
||||
const oAuth2Client = new OAuth2Client();
|
||||
const response = await oAuth2Client.getFederatedSignonCerts();
|
||||
const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync(
|
||||
@@ -57,9 +24,8 @@ export const validateGceIdentity = async ({
|
||||
);
|
||||
const payload = ticket.getPayload() as TGcpGceIdTokenPayload;
|
||||
if (!payload || !payload.email) throw new UnauthorizedError();
|
||||
const serviceAccountDetails = await getGcpServiceAccountDetails(payload.email, credentials);
|
||||
|
||||
return { serviceAccountDetails, gceInstanceDetails: payload };
|
||||
return { email: payload.email, computeEngineDetails: payload.google?.compute_engine };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -71,12 +37,10 @@ export const validateGceIdentity = async ({
|
||||
*/
|
||||
export const validateIamIdentity = async ({
|
||||
identityId,
|
||||
jwt: serviceAccountJwt,
|
||||
credentials
|
||||
jwt: serviceAccountJwt
|
||||
}: {
|
||||
identityId: string;
|
||||
jwt: string;
|
||||
credentials: string;
|
||||
}) => {
|
||||
const decodedJwt = jwt.decode(serviceAccountJwt, { complete: true }) as TDecodedGcpIamAuthJwt;
|
||||
const { sub, aud } = decodedJwt.payload;
|
||||
@@ -96,6 +60,5 @@ export const validateIamIdentity = async ({
|
||||
});
|
||||
|
||||
if (aud !== identityId) throw new UnauthorizedError();
|
||||
const serviceAccountDetails = await getGcpServiceAccountDetails(sub, credentials);
|
||||
return { serviceAccountDetails };
|
||||
return { email: sub };
|
||||
};
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { iam_v1 } from "googleapis";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod, SecretKeyEncoding, TIdentityGcpAuthsUpdate } from "@app/db/schemas";
|
||||
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 {
|
||||
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";
|
||||
@@ -28,7 +18,7 @@ import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal";
|
||||
import { validateGceIdentity, validateIamIdentity } from "./identity-gcp-auth-fns";
|
||||
import {
|
||||
TAttachGcpAuthDTO,
|
||||
TGcpGceIdTokenPayload,
|
||||
TGcpIdentityDetails,
|
||||
TGetGcpAuthDTO,
|
||||
TLoginGcpAuthDTO,
|
||||
TUpdateGcpAuthDTO
|
||||
@@ -39,7 +29,6 @@ type TIdentityGcpAuthServiceFactoryDep = {
|
||||
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">;
|
||||
};
|
||||
@@ -51,7 +40,6 @@ export const identityGcpAuthServiceFactory = ({
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
orgBotDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TIdentityGcpAuthServiceFactoryDep) => {
|
||||
@@ -62,47 +50,20 @@ export const identityGcpAuthServiceFactory = ({
|
||||
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;
|
||||
let gcpIdentityDetails: TGcpIdentityDetails;
|
||||
switch (identityGcpAuth.type) {
|
||||
case "gce": {
|
||||
const gceIdentity = await validateGceIdentity({
|
||||
gcpIdentityDetails = await validateGceIdentity({
|
||||
identityId,
|
||||
jwt: serviceAccountJwt,
|
||||
credentials
|
||||
jwt: serviceAccountJwt
|
||||
});
|
||||
serviceAccountDetails = gceIdentity.serviceAccountDetails;
|
||||
gceInstanceDetails = gceIdentity.gceInstanceDetails;
|
||||
break;
|
||||
}
|
||||
case "iam": {
|
||||
const iamIdentity = await validateIamIdentity({
|
||||
gcpIdentityDetails = await validateIamIdentity({
|
||||
identityId,
|
||||
jwt: serviceAccountJwt,
|
||||
credentials
|
||||
jwt: serviceAccountJwt
|
||||
});
|
||||
serviceAccountDetails = iamIdentity.serviceAccountDetails;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -116,30 +77,29 @@ export const identityGcpAuthServiceFactory = ({
|
||||
const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts
|
||||
.split(",")
|
||||
.map((serviceAccount) => serviceAccount.trim())
|
||||
.some(
|
||||
(serviceAccount) =>
|
||||
serviceAccount === serviceAccountDetails.email || serviceAccount === serviceAccountDetails.uniqueId
|
||||
);
|
||||
.some((serviceAccount) => serviceAccount === gcpIdentityDetails.email);
|
||||
|
||||
if (!isServiceAccountAllowed) throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
if (identityGcpAuth.allowedProjects) {
|
||||
// note that only possible to apply allowed projects/zones filters for GCE instances
|
||||
|
||||
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedProjects && gcpIdentityDetails.computeEngineDetails) {
|
||||
// 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);
|
||||
.some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id);
|
||||
|
||||
if (!isProjectAllowed) throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
if (identityGcpAuth.type === "gce" && gceInstanceDetails && identityGcpAuth.allowedZones) {
|
||||
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) {
|
||||
const isZoneAllowed = identityGcpAuth.allowedZones
|
||||
.split(",")
|
||||
.map((zone) => zone.trim())
|
||||
.some((zone) => zone === gceInstanceDetails!.google.compute_engine.zone);
|
||||
.some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone);
|
||||
|
||||
if (!isZoneAllowed) throw new UnauthorizedError();
|
||||
}
|
||||
@@ -180,7 +140,6 @@ export const identityGcpAuthServiceFactory = ({
|
||||
|
||||
const attachGcpAuth = async ({
|
||||
identityId,
|
||||
credentials,
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
@@ -232,60 +191,6 @@ export const identityGcpAuthServiceFactory = ({
|
||||
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(
|
||||
{
|
||||
@@ -294,9 +199,6 @@ export const identityGcpAuthServiceFactory = ({
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
encryptedCredentials,
|
||||
credentialsIV,
|
||||
credentialsTag,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
@@ -313,13 +215,12 @@ export const identityGcpAuthServiceFactory = ({
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityGcpAuth, credentials, orgId: identityMembershipOrg.orgId };
|
||||
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const updateGcpAuth = async ({
|
||||
identityId,
|
||||
type,
|
||||
credentials,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
@@ -375,7 +276,7 @@ export const identityGcpAuthServiceFactory = ({
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
|
||||
const updateQuery: TIdentityGcpAuthsUpdate = {
|
||||
const updatedGcpAuth = await identityGcpAuthDAL.updateById(identityGcpAuth.id, {
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
@@ -386,38 +287,10 @@ export const identityGcpAuthServiceFactory = ({
|
||||
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
|
||||
};
|
||||
};
|
||||
@@ -441,28 +314,7 @@ export const identityGcpAuthServiceFactory = ({
|
||||
);
|
||||
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 { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,6 @@ export type TLoginGcpAuthDTO = {
|
||||
|
||||
export type TAttachGcpAuthDTO = {
|
||||
identityId: string;
|
||||
credentials: string;
|
||||
type: "iam" | "gce";
|
||||
allowedServiceAccounts: string;
|
||||
allowedProjects: string;
|
||||
@@ -20,7 +19,6 @@ export type TAttachGcpAuthDTO = {
|
||||
|
||||
export type TUpdateGcpAuthDTO = {
|
||||
identityId: string;
|
||||
credentials?: string;
|
||||
type?: "iam" | "gce";
|
||||
allowedServiceAccounts?: string;
|
||||
allowedProjects?: string;
|
||||
@@ -35,21 +33,28 @@ export type TGetGcpAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
type TComputeEngineDetails = {
|
||||
instance_creation_timestamp: number;
|
||||
instance_id: string;
|
||||
instance_name: string;
|
||||
project_id: string;
|
||||
project_number: number;
|
||||
zone: string;
|
||||
};
|
||||
|
||||
export type TGcpIdentityDetails = {
|
||||
email: string;
|
||||
computeEngineDetails?: TComputeEngineDetails;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
google?: {
|
||||
compute_engine: TComputeEngineDetails;
|
||||
};
|
||||
iat: number;
|
||||
iss: string;
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user