mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 21:28:18 +00:00
Finish preliminary backend endpoints / db structure for k8s auth
This commit is contained in:
Vendored
+2
@@ -30,6 +30,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
|
|||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
|
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
|
||||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||||
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||||
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||||
@@ -113,6 +114,7 @@ declare module "fastify" {
|
|||||||
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
||||||
identityProject: TIdentityProjectServiceFactory;
|
identityProject: TIdentityProjectServiceFactory;
|
||||||
identityUa: TIdentityUaServiceFactory;
|
identityUa: TIdentityUaServiceFactory;
|
||||||
|
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
|
||||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||||
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
|
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
|
||||||
secretRotation: TSecretRotationServiceFactory;
|
secretRotation: TSecretRotationServiceFactory;
|
||||||
|
|||||||
Vendored
+8
@@ -47,6 +47,9 @@ import {
|
|||||||
TIdentityAccessTokens,
|
TIdentityAccessTokens,
|
||||||
TIdentityAccessTokensInsert,
|
TIdentityAccessTokensInsert,
|
||||||
TIdentityAccessTokensUpdate,
|
TIdentityAccessTokensUpdate,
|
||||||
|
TIdentityKubernetesAuths,
|
||||||
|
TIdentityKubernetesAuthsInsert,
|
||||||
|
TIdentityKubernetesAuthsUpdate,
|
||||||
TIdentityOrgMemberships,
|
TIdentityOrgMemberships,
|
||||||
TIdentityOrgMembershipsInsert,
|
TIdentityOrgMembershipsInsert,
|
||||||
TIdentityOrgMembershipsUpdate,
|
TIdentityOrgMembershipsUpdate,
|
||||||
@@ -314,6 +317,11 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityUniversalAuthsInsert,
|
TIdentityUniversalAuthsInsert,
|
||||||
TIdentityUniversalAuthsUpdate
|
TIdentityUniversalAuthsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.IdentityKubernetesAuth]: Knex.CompositeTableType<
|
||||||
|
TIdentityKubernetesAuths,
|
||||||
|
TIdentityKubernetesAuthsInsert,
|
||||||
|
TIdentityKubernetesAuthsUpdate
|
||||||
|
>;
|
||||||
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
|
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
|
||||||
TIdentityUaClientSecrets,
|
TIdentityUaClientSecrets,
|
||||||
TIdentityUaClientSecretsInsert,
|
TIdentityUaClientSecretsInsert,
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.IdentityKubernetesAuth))) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityKubernetesAuth, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
|
||||||
|
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
|
||||||
|
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
|
||||||
|
t.jsonb("accessTokenTrustedIps").notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("identityId").notNullable().unique();
|
||||||
|
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
t.string("kubernetesHost").notNullable();
|
||||||
|
t.string("encryptedCaCert").notNullable();
|
||||||
|
t.string("caCertIV").notNullable();
|
||||||
|
t.string("caCertTag").notNullable();
|
||||||
|
t.string("encryptedTokenReviewerJwt").notNullable();
|
||||||
|
t.string("tokenReviewerJwtIV").notNullable();
|
||||||
|
t.string("tokenReviewerJwtTag").notNullable();
|
||||||
|
t.string("allowedNamespaces").notNullable();
|
||||||
|
t.string("allowedNames").notNullable();
|
||||||
|
t.string("allowedAudience").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.IdentityKubernetesAuth);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityKubernetesAuth);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.IdentityKubernetesAuth);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IdentityKubernetesAuthsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
accessTokenTTL: z.coerce.number().default(7200),
|
||||||
|
accessTokenMaxTTL: z.coerce.number().default(7200),
|
||||||
|
accessTokenNumUsesLimit: z.coerce.number().default(0),
|
||||||
|
accessTokenTrustedIps: z.unknown(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
identityId: z.string().uuid(),
|
||||||
|
kubernetesHost: z.string(),
|
||||||
|
encryptedCaCert: z.string(),
|
||||||
|
caCertIV: z.string(),
|
||||||
|
caCertTag: z.string(),
|
||||||
|
encryptedTokenReviewerJwt: z.string(),
|
||||||
|
tokenReviewerJwtIV: z.string(),
|
||||||
|
tokenReviewerJwtTag: z.string(),
|
||||||
|
allowedNamespaces: z.string(),
|
||||||
|
allowedNames: z.string(),
|
||||||
|
allowedAudience: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;
|
||||||
|
export type TIdentityKubernetesAuthsInsert = Omit<z.input<typeof IdentityKubernetesAuthsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TIdentityKubernetesAuthsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof IdentityKubernetesAuthsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -13,6 +13,7 @@ export * from "./group-project-memberships";
|
|||||||
export * from "./groups";
|
export * from "./groups";
|
||||||
export * from "./identities";
|
export * from "./identities";
|
||||||
export * from "./identity-access-tokens";
|
export * from "./identity-access-tokens";
|
||||||
|
export * from "./identity-kubernetes-auths";
|
||||||
export * from "./identity-org-memberships";
|
export * from "./identity-org-memberships";
|
||||||
export * from "./identity-project-additional-privilege";
|
export * from "./identity-project-additional-privilege";
|
||||||
export * from "./identity-project-membership-role";
|
export * from "./identity-project-membership-role";
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export enum TableName {
|
|||||||
Identity = "identities",
|
Identity = "identities",
|
||||||
IdentityAccessToken = "identity_access_tokens",
|
IdentityAccessToken = "identity_access_tokens",
|
||||||
IdentityUniversalAuth = "identity_universal_auths",
|
IdentityUniversalAuth = "identity_universal_auths",
|
||||||
|
IdentityKubernetesAuth = "identity_kubernetes_auths",
|
||||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||||
IdentityOrgMembership = "identity_org_memberships",
|
IdentityOrgMembership = "identity_org_memberships",
|
||||||
IdentityProjectMembership = "identity_project_memberships",
|
IdentityProjectMembership = "identity_project_memberships",
|
||||||
@@ -138,5 +139,6 @@ export enum ProjectUpgradeStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum IdentityAuthMethod {
|
export enum IdentityAuthMethod {
|
||||||
Univeral = "universal-auth"
|
Univeral = "universal-auth",
|
||||||
|
Kubernetes_Auth = "kubernetes-auth"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ export enum EventType {
|
|||||||
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
|
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
|
||||||
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
|
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
|
||||||
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
|
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
|
||||||
|
LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth",
|
||||||
|
ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth",
|
||||||
|
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
|
||||||
|
GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth",
|
||||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-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",
|
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
||||||
@@ -383,6 +387,50 @@ interface GetIdentityUniversalAuthEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LoginIdentityKubernetesAuthEvent {
|
||||||
|
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
identityKubernetesAuthId: string;
|
||||||
|
identityAccessTokenId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddIdentityKubernetesAuthEvent {
|
||||||
|
type: EventType.ADD_IDENTITY_KUBERNETES_AUTH;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
kubernetesHost: string;
|
||||||
|
allowedNamespaces: string;
|
||||||
|
allowedNames: string;
|
||||||
|
accessTokenTTL: number;
|
||||||
|
accessTokenMaxTTL: number;
|
||||||
|
accessTokenNumUsesLimit: number;
|
||||||
|
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateIdentityKubernetesAuthEvent {
|
||||||
|
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
kubernetesHost?: string;
|
||||||
|
allowedNamespaces?: string;
|
||||||
|
allowedNames?: string;
|
||||||
|
accessTokenTTL?: number;
|
||||||
|
accessTokenMaxTTL?: number;
|
||||||
|
accessTokenNumUsesLimit?: number;
|
||||||
|
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetIdentityKubernetesAuthEvent {
|
||||||
|
type: EventType.GET_IDENTITY_KUBERNETES_AUTH;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateIdentityUniversalAuthClientSecretEvent {
|
interface CreateIdentityUniversalAuthClientSecretEvent {
|
||||||
type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET;
|
type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -657,6 +705,10 @@ export type Event =
|
|||||||
| AddIdentityUniversalAuthEvent
|
| AddIdentityUniversalAuthEvent
|
||||||
| UpdateIdentityUniversalAuthEvent
|
| UpdateIdentityUniversalAuthEvent
|
||||||
| GetIdentityUniversalAuthEvent
|
| GetIdentityUniversalAuthEvent
|
||||||
|
| LoginIdentityKubernetesAuthEvent
|
||||||
|
| AddIdentityKubernetesAuthEvent
|
||||||
|
| UpdateIdentityKubernetesAuthEvent
|
||||||
|
| GetIdentityKubernetesAuthEvent
|
||||||
| CreateIdentityUniversalAuthClientSecretEvent
|
| CreateIdentityUniversalAuthClientSecretEvent
|
||||||
| GetIdentityUniversalAuthClientSecretsEvent
|
| GetIdentityUniversalAuthClientSecretsEvent
|
||||||
| RevokeIdentityUniversalAuthClientSecretEvent
|
| RevokeIdentityUniversalAuthClientSecretEvent
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
|||||||
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
|
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
|
||||||
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
|
import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal";
|
||||||
|
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
|
||||||
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
|
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
|
||||||
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||||
@@ -192,6 +194,7 @@ export const registerRoutes = async (
|
|||||||
const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
|
const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
|
||||||
|
|
||||||
const identityUaDAL = identityUaDALFactory(db);
|
const identityUaDAL = identityUaDALFactory(db);
|
||||||
|
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
|
||||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||||
|
|
||||||
const auditLogDAL = auditLogDALFactory(db);
|
const auditLogDAL = auditLogDALFactory(db);
|
||||||
@@ -646,6 +649,15 @@ export const registerRoutes = async (
|
|||||||
identityUaDAL,
|
identityUaDAL,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
|
||||||
|
identityKubernetesAuthDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
identityAccessTokenDAL,
|
||||||
|
identityDAL,
|
||||||
|
orgBotDAL,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
const dynamicSecretProviders = buildDynamicSecretProviders();
|
const dynamicSecretProviders = buildDynamicSecretProviders();
|
||||||
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
||||||
@@ -715,6 +727,7 @@ export const registerRoutes = async (
|
|||||||
identityAccessToken: identityAccessTokenService,
|
identityAccessToken: identityAccessTokenService,
|
||||||
identityProject: identityProjectService,
|
identityProject: identityProjectService,
|
||||||
identityUa: identityUaService,
|
identityUa: identityUaService,
|
||||||
|
identityKubernetesAuth: identityKubernetesAuthService,
|
||||||
secretApprovalPolicy: sapService,
|
secretApprovalPolicy: sapService,
|
||||||
secretApprovalRequest: sarService,
|
secretApprovalRequest: sarService,
|
||||||
secretRotation: secretRotationService,
|
secretRotation: secretRotationService,
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { IdentityKubernetesAuthsSchema } 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";
|
||||||
|
|
||||||
|
const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.omit({
|
||||||
|
encryptedCaCert: true,
|
||||||
|
caCertIV: true,
|
||||||
|
caCertTag: true,
|
||||||
|
encryptedTokenReviewerJwt: true,
|
||||||
|
tokenReviewerJwtIV: true,
|
||||||
|
tokenReviewerJwtTag: true
|
||||||
|
}).extend({
|
||||||
|
caCert: z.string(),
|
||||||
|
tokenReviewerJwt: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerIdentityKubernetesRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/kubernetes/login",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Login with Kubernetes Auth",
|
||||||
|
body: z.object({
|
||||||
|
identityId: z.string().trim(),
|
||||||
|
jwt: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
expiresIn: z.coerce.number(),
|
||||||
|
accessTokenMaxTTL: z.coerce.number(),
|
||||||
|
tokenType: z.literal("Bearer")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { identityKubernetesAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||||
|
await server.services.identityKubernetesAuth.login({
|
||||||
|
identityId: req.body.identityId,
|
||||||
|
jwt: req.body.jwt
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityMembershipOrg?.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityKubernetesAuth.identityId,
|
||||||
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
|
identityKubernetesAuthId: identityKubernetesAuth.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
tokenType: "Bearer" as const,
|
||||||
|
expiresIn: identityKubernetesAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/kubernetes-auth/identities/:identityId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Attach Kubernetes Auth configuration onto identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
kubernetesHost: z.string().trim().min(1),
|
||||||
|
caCert: z.string().trim().min(1),
|
||||||
|
tokenReviewerJwt: z.string().trim().min(1),
|
||||||
|
allowedNamespaces: z.string(), // TODO: validation
|
||||||
|
allowedNames: z.string(),
|
||||||
|
allowedAudience: z.string(),
|
||||||
|
accessTokenTrustedIps: z
|
||||||
|
.object({
|
||||||
|
ipAddress: z.string().trim()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
|
||||||
|
accessTokenTTL: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.refine((value) => value !== 0, {
|
||||||
|
message: "accessTokenTTL must have a non zero number"
|
||||||
|
})
|
||||||
|
.default(2592000),
|
||||||
|
accessTokenMaxTTL: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.refine((value) => value !== 0, {
|
||||||
|
message: "accessTokenMaxTTL must have a non zero number"
|
||||||
|
})
|
||||||
|
.default(2592000),
|
||||||
|
accessTokenNumUsesLimit: z.number().int().min(0).default(0)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identityKubernetesAuth = await server.services.identityKubernetesAuth.attachKubernetesAuth({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body,
|
||||||
|
identityId: req.params.identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityKubernetesAuth.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ADD_IDENTITY_KUBERNETES_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityKubernetesAuth.identityId,
|
||||||
|
kubernetesHost: identityKubernetesAuth.kubernetesHost,
|
||||||
|
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
||||||
|
allowedNames: identityKubernetesAuth.allowedNames,
|
||||||
|
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
|
||||||
|
accessTokenTrustedIps: identityKubernetesAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||||
|
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityKubernetesAuth: IdentityKubernetesAuthResponseSchema.parse(identityKubernetesAuth) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/kubernetes-auth/identities/:identityId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Update Kubernetes Auth configuration on identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
kubernetesHost: z.string().trim().min(1).optional(),
|
||||||
|
kubernetesCaCert: z.string().trim().min(1).optional(),
|
||||||
|
tokenReviewerJwt: z.string().trim().min(1).optional(),
|
||||||
|
allowedNamespaces: z.string().optional(), // TODO: validation
|
||||||
|
allowedNames: z.string().optional(),
|
||||||
|
allowedAudience: z.string().optional(),
|
||||||
|
accessTokenTrustedIps: z
|
||||||
|
.object({
|
||||||
|
ipAddress: z.string().trim()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.optional(),
|
||||||
|
accessTokenTTL: z.number().int().min(0).optional(),
|
||||||
|
accessTokenNumUsesLimit: z.number().int().min(0).optional(),
|
||||||
|
accessTokenMaxTTL: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.refine((value) => value !== 0, {
|
||||||
|
message: "accessTokenMaxTTL must have a non zero number"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identityKubernetesAuth: IdentityKubernetesAuthsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identityKubernetesAuth = await server.services.identityKubernetesAuth.updateKubernetesAuth({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body,
|
||||||
|
identityId: req.params.identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityKubernetesAuth.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityKubernetesAuth.identityId,
|
||||||
|
kubernetesHost: identityKubernetesAuth.kubernetesHost,
|
||||||
|
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
||||||
|
allowedNames: identityKubernetesAuth.allowedNames,
|
||||||
|
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
|
||||||
|
accessTokenTrustedIps: identityKubernetesAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||||
|
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityKubernetesAuth };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/kubernetes-auth/identities/:identityId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Retrieve Kubernetes Auth configuration on identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identityKubernetesAuth = await server.services.identityKubernetesAuth.getKubernetesAuth({
|
||||||
|
identityId: req.params.identityId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityKubernetesAuth.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_IDENTITY_KUBERNETES_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityKubernetesAuth.identityId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityKubernetesAuth: IdentityKubernetesAuthResponseSchema.parse(identityKubernetesAuth) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { registerAdminRouter } from "./admin-router";
|
|||||||
import { registerAuthRoutes } from "./auth-router";
|
import { registerAuthRoutes } from "./auth-router";
|
||||||
import { registerProjectBotRouter } from "./bot-router";
|
import { registerProjectBotRouter } from "./bot-router";
|
||||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||||
|
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
|
||||||
import { registerIdentityRouter } from "./identity-router";
|
import { registerIdentityRouter } from "./identity-router";
|
||||||
import { registerIdentityUaRouter } from "./identity-ua";
|
import { registerIdentityUaRouter } from "./identity-ua";
|
||||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||||
@@ -27,6 +28,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
async (authRouter) => {
|
async (authRouter) => {
|
||||||
await authRouter.register(registerAuthRoutes);
|
await authRouter.register(registerAuthRoutes);
|
||||||
await authRouter.register(registerIdentityUaRouter);
|
await authRouter.register(registerIdentityUaRouter);
|
||||||
|
await authRouter.register(registerIdentityKubernetesRouter);
|
||||||
await authRouter.register(registerIdentityAccessTokenRouter);
|
await authRouter.register(registerIdentityAccessTokenRouter);
|
||||||
},
|
},
|
||||||
{ prefix: "/auth" }
|
{ prefix: "/auth" }
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TIdentityKubernetesAuthDALFactory = ReturnType<typeof identityKubernetesAuthDALFactory>;
|
||||||
|
|
||||||
|
export const identityKubernetesAuthDALFactory = (db: TDbClient) => {
|
||||||
|
const kubernetesAuthOrm = ormify(db, TableName.IdentityKubernetesAuth);
|
||||||
|
return kubernetesAuthOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Extracts the K8s service account name and namespace
|
||||||
|
* from the username in this format: system:serviceaccount:default:infisical-auth
|
||||||
|
*/
|
||||||
|
export const extractK8sUsername = (username: string) => {
|
||||||
|
const parts = username.split(":");
|
||||||
|
// Ensure that the username format is correct
|
||||||
|
if (parts.length === 4 && parts[0] === "system" && parts[1] === "serviceaccount") {
|
||||||
|
return {
|
||||||
|
namespace: parts[2],
|
||||||
|
name: parts[3]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error("Invalid username format");
|
||||||
|
};
|
||||||
@@ -0,0 +1,517 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import axios from "axios";
|
||||||
|
import https from "https";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate } 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 { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal";
|
||||||
|
import { extractK8sUsername } from "./identity-kubernetes-auth-fns";
|
||||||
|
import {
|
||||||
|
TAttachKubernetesAuthDTO,
|
||||||
|
TCreateTokenReviewResponse,
|
||||||
|
TGetKubernetesAuthDTO,
|
||||||
|
TLoginKubernetesAuthDTO,
|
||||||
|
TUpdateKubernetesAuthDTO
|
||||||
|
} from "./identity-kubernetes-auth-types";
|
||||||
|
|
||||||
|
type TIdentityKubernetesAuthServiceFactoryDep = {
|
||||||
|
identityKubernetesAuthDAL: Pick<
|
||||||
|
TIdentityKubernetesAuthDALFactory,
|
||||||
|
"create" | "findOne" | "transaction" | "updateById"
|
||||||
|
>;
|
||||||
|
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||||
|
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">;
|
||||||
|
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||||
|
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
|
||||||
|
|
||||||
|
export const identityKubernetesAuthServiceFactory = ({
|
||||||
|
identityKubernetesAuthDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
identityAccessTokenDAL,
|
||||||
|
identityDAL,
|
||||||
|
orgBotDAL,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
}: TIdentityKubernetesAuthServiceFactoryDep) => {
|
||||||
|
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
|
||||||
|
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
||||||
|
if (!identityKubernetesAuth) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
|
||||||
|
identityId: identityKubernetesAuth.identityId
|
||||||
|
});
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find 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 { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } =
|
||||||
|
identityKubernetesAuth;
|
||||||
|
|
||||||
|
let caCert = "";
|
||||||
|
if (encryptedCaCert && caCertIV && caCertTag) {
|
||||||
|
caCert = decryptSymmetric({
|
||||||
|
ciphertext: encryptedCaCert,
|
||||||
|
iv: caCertIV,
|
||||||
|
tag: caCertTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenReviewerJwt = "";
|
||||||
|
if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) {
|
||||||
|
tokenReviewerJwt = decryptSymmetric({
|
||||||
|
ciphertext: encryptedTokenReviewerJwt,
|
||||||
|
iv: tokenReviewerJwtIV,
|
||||||
|
tag: tokenReviewerJwtTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data }: { data: TCreateTokenReviewResponse } = await axios.post(
|
||||||
|
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
|
||||||
|
{
|
||||||
|
apiVersion: "authentication.k8s.io/v1",
|
||||||
|
kind: "TokenReview",
|
||||||
|
spec: {
|
||||||
|
token: serviceAccountJwt
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${tokenReviewerJwt}`
|
||||||
|
},
|
||||||
|
...(caCert && {
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
ca: caCert,
|
||||||
|
rejectUnauthorized: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if ("error" in data.status) throw new UnauthorizedError({ message: data.status.error });
|
||||||
|
|
||||||
|
// check the response to determine if the token is valid
|
||||||
|
if (!(data.status && data.status.authenticated)) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);
|
||||||
|
|
||||||
|
if (identityKubernetesAuth.allowedNamespaces) {
|
||||||
|
// validate if [targetNamespace] is in the list of allowed namespaces
|
||||||
|
|
||||||
|
const isNamespaceAllowed = identityKubernetesAuth.allowedNamespaces
|
||||||
|
.split(",")
|
||||||
|
.map((namespace) => namespace.trim())
|
||||||
|
.some((namespace) => namespace === targetNamespace);
|
||||||
|
|
||||||
|
if (!isNamespaceAllowed) throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityKubernetesAuth.allowedNames) {
|
||||||
|
// validate if [targetName] is in the list of allowed names
|
||||||
|
|
||||||
|
const isNameAllowed = identityKubernetesAuth.allowedNames
|
||||||
|
.split(",")
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.some((name) => name === targetName);
|
||||||
|
|
||||||
|
if (!isNameAllowed) throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityKubernetesAuth.allowedAudience) {
|
||||||
|
// validate if [audience] is in the list of allowed audiences
|
||||||
|
const isAudienceAllowed = data.status.audiences.some(
|
||||||
|
(audience) => audience === identityKubernetesAuth.allowedAudience
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAudienceAllowed) throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => {
|
||||||
|
const newToken = await identityAccessTokenDAL.create(
|
||||||
|
{
|
||||||
|
identityId: identityKubernetesAuth.identityId,
|
||||||
|
isAccessTokenRevoked: false,
|
||||||
|
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
|
||||||
|
accessTokenNumUses: 0,
|
||||||
|
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return newToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const accessToken = jwt.sign(
|
||||||
|
{
|
||||||
|
identityId: identityKubernetesAuth.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, identityKubernetesAuth, identityAccessToken, identityMembershipOrg };
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachKubernetesAuth = async ({
|
||||||
|
identityId,
|
||||||
|
kubernetesHost,
|
||||||
|
caCert,
|
||||||
|
tokenReviewerJwt,
|
||||||
|
allowedNamespaces,
|
||||||
|
allowedNames,
|
||||||
|
allowedAudience,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TAttachKubernetesAuthDTO) => {
|
||||||
|
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 Kubernetes 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: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedTokenReviewerJwt,
|
||||||
|
iv: tokenReviewerJwtIV,
|
||||||
|
tag: tokenReviewerJwtTag
|
||||||
|
} = encryptSymmetric(tokenReviewerJwt, key);
|
||||||
|
|
||||||
|
const identityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
|
||||||
|
const doc = await identityKubernetesAuthDAL.create(
|
||||||
|
{
|
||||||
|
identityId: identityMembershipOrg.identityId,
|
||||||
|
kubernetesHost,
|
||||||
|
encryptedCaCert,
|
||||||
|
caCertIV,
|
||||||
|
caCertTag,
|
||||||
|
encryptedTokenReviewerJwt,
|
||||||
|
tokenReviewerJwtIV,
|
||||||
|
tokenReviewerJwtTag,
|
||||||
|
allowedNamespaces,
|
||||||
|
allowedNames,
|
||||||
|
allowedAudience,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await identityDAL.updateById(
|
||||||
|
identityMembershipOrg.identityId,
|
||||||
|
{
|
||||||
|
authMethod: IdentityAuthMethod.Kubernetes_Auth
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateKubernetesAuth = async ({
|
||||||
|
identityId,
|
||||||
|
kubernetesHost,
|
||||||
|
caCert,
|
||||||
|
tokenReviewerJwt,
|
||||||
|
allowedNamespaces,
|
||||||
|
allowedNames,
|
||||||
|
allowedAudience,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateKubernetesAuthDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Kubernetes_Auth)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to update Kubernetes Auth"
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
||||||
|
|
||||||
|
if (
|
||||||
|
(accessTokenMaxTTL || identityKubernetesAuth.accessTokenMaxTTL) > 0 &&
|
||||||
|
(accessTokenTTL || identityKubernetesAuth.accessTokenMaxTTL) >
|
||||||
|
(accessTokenMaxTTL || identityKubernetesAuth.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: TIdentityKubernetesAuthsUpdate = {
|
||||||
|
kubernetesHost,
|
||||||
|
allowedNamespaces,
|
||||||
|
allowedNames,
|
||||||
|
allowedAudience,
|
||||||
|
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 (caCert !== undefined) {
|
||||||
|
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
|
||||||
|
updateQuery.encryptedCaCert = encryptedCACert;
|
||||||
|
updateQuery.caCertIV = caCertIV;
|
||||||
|
updateQuery.caCertTag = caCertTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenReviewerJwt !== undefined) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedTokenReviewerJwt,
|
||||||
|
iv: tokenReviewerJwtIV,
|
||||||
|
tag: tokenReviewerJwtTag
|
||||||
|
} = encryptSymmetric(tokenReviewerJwt, key);
|
||||||
|
updateQuery.encryptedTokenReviewerJwt = encryptedTokenReviewerJwt;
|
||||||
|
updateQuery.tokenReviewerJwtIV = tokenReviewerJwtIV;
|
||||||
|
updateQuery.tokenReviewerJwtTag = tokenReviewerJwtTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery);
|
||||||
|
|
||||||
|
return { ...updatedKubernetesAuth, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getKubernetesAuth = async ({
|
||||||
|
identityId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TGetKubernetesAuthDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Kubernetes_Auth)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have Kubernetes Auth attached"
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityKubernetesAuth = await identityKubernetesAuthDAL.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 { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } =
|
||||||
|
identityKubernetesAuth;
|
||||||
|
|
||||||
|
let caCert = "";
|
||||||
|
if (encryptedCaCert && caCertIV && caCertTag) {
|
||||||
|
caCert = decryptSymmetric({
|
||||||
|
ciphertext: encryptedCaCert,
|
||||||
|
iv: caCertIV,
|
||||||
|
tag: caCertTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenReviewerJwt = "";
|
||||||
|
if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) {
|
||||||
|
tokenReviewerJwt = decryptSymmetric({
|
||||||
|
ciphertext: encryptedTokenReviewerJwt,
|
||||||
|
iv: tokenReviewerJwtIV,
|
||||||
|
tag: tokenReviewerJwtTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
login,
|
||||||
|
attachKubernetesAuth,
|
||||||
|
updateKubernetesAuth,
|
||||||
|
getKubernetesAuth
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TLoginKubernetesAuthDTO = {
|
||||||
|
identityId: string;
|
||||||
|
jwt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAttachKubernetesAuthDTO = {
|
||||||
|
identityId: string;
|
||||||
|
kubernetesHost: string;
|
||||||
|
caCert: string;
|
||||||
|
tokenReviewerJwt: string;
|
||||||
|
allowedNamespaces: string;
|
||||||
|
allowedNames: string;
|
||||||
|
allowedAudience: string;
|
||||||
|
accessTokenTTL: number;
|
||||||
|
accessTokenMaxTTL: number;
|
||||||
|
accessTokenNumUsesLimit: number;
|
||||||
|
accessTokenTrustedIps: { ipAddress: string }[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateKubernetesAuthDTO = {
|
||||||
|
identityId: string;
|
||||||
|
kubernetesHost?: string;
|
||||||
|
caCert?: string;
|
||||||
|
tokenReviewerJwt?: string;
|
||||||
|
allowedNamespaces?: string;
|
||||||
|
allowedNames?: string;
|
||||||
|
allowedAudience?: string;
|
||||||
|
accessTokenTTL?: number;
|
||||||
|
accessTokenMaxTTL?: number;
|
||||||
|
accessTokenNumUsesLimit?: number;
|
||||||
|
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetKubernetesAuthDTO = {
|
||||||
|
identityId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
type TCreateTokenReviewSuccessResponse = {
|
||||||
|
authenticated: true;
|
||||||
|
user: {
|
||||||
|
username: string;
|
||||||
|
uid: string;
|
||||||
|
groups: string[];
|
||||||
|
};
|
||||||
|
audiences: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TCreateTokenReviewErrorResponse = {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateTokenReviewResponse = {
|
||||||
|
apiVersion: "authentication.k8s.io/v1";
|
||||||
|
kind: "TokenReview";
|
||||||
|
spec: {
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
status: TCreateTokenReviewSuccessResponse | TCreateTokenReviewErrorResponse;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user