Compare commits

...

20 Commits

Author SHA1 Message Date
Sheen
2079913511 misc: final doc updates 2025-03-19 08:08:11 +00:00
Sheen Capadngan
049f0f56a0 misc: added env support for the other cli flags 2025-03-19 15:44:56 +08:00
Sheen Capadngan
4478dc8659 misc: minor header label updates 2025-03-19 03:05:50 +08:00
Sheen
510ddf2b1a misc: add machine identity server admin image 2025-03-18 19:05:23 +00:00
Sheen
5363f8c6ff misc: doc updates 2025-03-18 18:45:43 +00:00
Sheen
7d9de6acba Merge branch 'feat/automated-instance-bootstrapping' of https://github.com/Infisical/infisical into feat/automated-instance-bootstrapping 2025-03-18 18:37:42 +00:00
Sheen
bac944133a doc: finalized response schema 2025-03-18 18:36:42 +00:00
Sheen Capadngan
f059d65b45 misc: finalized response 2025-03-19 02:26:51 +08:00
Sheen Capadngan
015a193330 misc: added support for removing instance admin access of users 2025-03-19 02:13:24 +08:00
Sheen Capadngan
d91add2e7b misc: added return statements 2025-03-18 23:49:40 +08:00
Sheen Capadngan
6d72524896 misc: addressed typo 2025-03-18 23:15:48 +08:00
Sheen
1ec11d5963 doc: added docs 2025-03-18 14:43:19 +00:00
Sheen Capadngan
ed6306747a feat: add support for removing instance admin permission from identity 2025-03-18 03:00:54 +08:00
Sheen Capadngan
64569ab44b feat: added bootstrap to CLI 2025-03-18 02:27:42 +08:00
Sheen Capadngan
2d1d6f5ce8 misc: added privilege escalation checks 2025-03-18 01:20:01 +08:00
Sheen Capadngan
6ef358b172 feat: initialize base 2025-03-17 22:35:37 +08:00
Maidul Islam
8de7261c9a Update docs 2025-03-16 19:46:21 -04:00
Daniel Hougaard
67b1b79fe3 Merge pull request #3253 from Infisical/daniel/bump-helm
chore: bump helm
2025-03-17 00:28:19 +04:00
Daniel Hougaard
31477f4d2b chore: bump helm 2025-03-17 00:21:35 +04:00
Daniel Hougaard
f200372d74 Merge pull request #3252 from Infisical/daniel/patch-k8s-install
fix: k8s installation failing
2025-03-17 00:11:18 +04:00
53 changed files with 1204 additions and 131 deletions

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas/models";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds"))) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.specificType("adminIdentityIds", "text[]");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds")) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("adminIdentityIds");
});
}
}

View File

@@ -25,7 +25,8 @@ export const SuperAdminSchema = z.object({
encryptedSlackClientId: zodBuffer.nullable().optional(),
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
authConsentContent: z.string().nullable().optional(),
pageFrameContent: z.string().nullable().optional()
pageFrameContent: z.string().nullable().optional(),
adminIdentityIds: z.string().array().nullable().optional()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@@ -8,6 +8,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
export type TAuthMode =
| {
@@ -43,6 +44,7 @@ export type TAuthMode =
identityName: string;
orgId: string;
authMethod: null;
isInstanceAdmin?: boolean;
}
| {
authMode: AuthMode.SCIM_TOKEN;
@@ -129,13 +131,15 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
}
case AuthMode.IDENTITY_ACCESS_TOKEN: {
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp);
const serverCfg = await getServerCfg();
req.auth = {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
actor,
orgId: identity.orgId,
identityId: identity.identityId,
identityName: identity.name,
authMethod: null
authMethod: null,
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId)
};
break;
}

View File

@@ -1,16 +1,18 @@
import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
import { ForbiddenRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const verifySuperAdmin = <T extends FastifyRequest>(
req: T,
_res: FastifyReply,
done: HookHandlerDoneFunction
) => {
if (req.auth.actor !== ActorType.USER || !req.auth.user.superAdmin)
throw new ForbiddenRequestError({
message: "Requires elevated super admin privileges"
});
done();
if (isSuperAdmin(req.auth)) {
return done();
}
throw new ForbiddenRequestError({
message: "Requires elevated super admin privileges"
});
};

View File

@@ -637,6 +637,9 @@ export const registerRoutes = async (
userDAL,
identityDAL,
userAliasDAL,
identityTokenAuthDAL,
identityAccessTokenDAL,
identityOrgMembershipDAL,
authService: loginService,
serverCfgDAL: superAdminDAL,
kmsRootConfigDAL,

View File

@@ -98,7 +98,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.API_KEY])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@@ -139,7 +139,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@@ -171,12 +171,16 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
identities: IdentitiesSchema.pick({
name: true,
id: true
}).array()
})
.extend({
isInstanceAdmin: z.boolean()
})
.array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@@ -206,7 +210,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@@ -240,7 +244,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@@ -265,7 +269,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
})
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@@ -293,7 +297,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@@ -316,7 +320,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
})
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@@ -394,4 +398,141 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "DELETE",
url: "/identity-management/identities/:identityId/super-admin-access",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
identityId: z.string()
}),
response: {
200: z.object({
identity: IdentitiesSchema.pick({
name: true,
id: true
})
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const identity = await server.services.superAdmin.deleteIdentitySuperAdminAccess(
req.params.identityId,
req.permission.id
);
return {
identity
};
}
});
server.route({
method: "DELETE",
url: "/user-management/users/:userId/admin-access",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
userId: z.string()
}),
response: {
200: z.object({
user: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true
})
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const user = await server.services.superAdmin.deleteUserSuperAdminAccess(req.params.userId);
return {
user
};
}
});
server.route({
method: "POST",
url: "/bootstrap",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
email: z.string().email().trim().min(1),
password: z.string().trim().min(1),
organization: z.string().trim().min(1)
}),
response: {
200: z.object({
message: z.string(),
user: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true,
superAdmin: true
}),
organization: OrganizationsSchema.pick({
id: true,
name: true,
slug: true
}),
identity: IdentitiesSchema.pick({
id: true,
name: true
}).extend({
credentials: z.object({
token: z.string()
}) // would just be Token AUTH for now
})
})
}
},
handler: async (req) => {
const { user, organization, machineIdentity } = await server.services.superAdmin.bootstrapInstance({
...req.body,
organizationName: req.body.organization
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.AdminInit,
distinctId: user.user.username ?? "",
properties: {
username: user.user.username,
email: user.user.email ?? "",
lastName: user.user.lastName || "",
firstName: user.user.firstName || ""
}
});
return {
message: "Successfully bootstrapped instance",
user: user.user,
organization,
identity: machineIdentity
};
}
});
};

View File

@@ -11,6 +11,7 @@ import {
validateAccountIds,
validatePrincipalArns
} from "@app/services/identity-aws-auth/identity-aws-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -130,7 +131,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@@ -8,8 +8,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateAzureAuthField } from "@app/services/identity-azure-auth/identity-azure-auth-validators";
import {} from "../sanitizedSchemas";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -127,7 +126,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@@ -8,6 +8,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateGcpAuthField } from "@app/services/identity-gcp-auth/identity-gcp-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -121,7 +122,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@@ -12,6 +12,7 @@ import {
validateJwtAuthAudiencesField,
validateJwtBoundClaimsField
} from "@app/services/identity-jwt-auth/identity-jwt-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
const IdentityJwtAuthResponseSchema = IdentityJwtAuthsSchema.omit({
encryptedJwksCaCert: true,
@@ -169,7 +170,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick({
id: true,
@@ -147,7 +148,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@@ -11,6 +11,7 @@ import {
validateOidcAuthAudiencesField,
validateOidcBoundClaimsField
} from "@app/services/identity-oidc-auth/identity-oidc-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({
id: true,
@@ -146,7 +147,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
@@ -118,6 +119,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth),
...req.body
});
@@ -166,7 +168,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.identityId
id: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -74,7 +75,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
@@ -157,7 +159,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
@@ -257,7 +260,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
@@ -312,6 +316,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth),
...req.body
});
@@ -370,6 +375,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth),
...req.query
});
@@ -421,6 +427,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
tokenId: req.params.tokenId,
isActorSuperAdmin: isSuperAdmin(req.auth),
...req.body
});
@@ -470,7 +477,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
tokenId: req.params.tokenId
tokenId: req.params.tokenId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
return {

View File

@@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const sanitizedClientSecretSchema = IdentityUaClientSecretsSchema.pick({
id: true,
@@ -142,8 +143,10 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityUniversalAuth.orgId,

View File

@@ -16,6 +16,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
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 { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal";
import { extractPrincipalArn } from "./identity-aws-auth-fns";
import {
@@ -149,8 +150,11 @@ export const identityAwsAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachAwsAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@@ -16,6 +16,7 @@ export type TAttachAwsAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateAwsAuthDTO = {

View File

@@ -14,6 +14,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
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 { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityAzureAuthDALFactory } from "./identity-azure-auth-dal";
import { validateAzureIdentity } from "./identity-azure-auth-fns";
import {
@@ -122,8 +123,11 @@ export const identityAzureAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachAzureAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@@ -14,6 +14,7 @@ export type TAttachAzureAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateAzureAuthDTO = {

View File

@@ -14,6 +14,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
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 { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal";
import { validateIamIdentity, validateIdTokenIdentity } from "./identity-gcp-auth-fns";
import {
@@ -162,8 +163,11 @@ export const identityGcpAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachGcpAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@@ -15,6 +15,7 @@ export type TAttachGcpAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateGcpAuthDTO = {

View File

@@ -18,6 +18,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal";
import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns";
import {
@@ -248,8 +249,11 @@ export const identityJwtAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachJwtAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@@ -19,6 +19,7 @@ export type TAttachJwtAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateJwtAuthDTO = {

View File

@@ -18,6 +18,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal";
import { extractK8sUsername } from "./identity-kubernetes-auth-fns";
import {
@@ -227,8 +228,11 @@ export const identityKubernetesAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachKubernetesAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@@ -17,6 +17,7 @@ export type TAttachKubernetesAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateKubernetesAuthDTO = {

View File

@@ -19,6 +19,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns";
import {
@@ -196,8 +197,10 @@ export const identityOidcAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachOidcAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@@ -12,6 +12,7 @@ export type TAttachOidcAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateOidcAuthDTO = {

View File

@@ -14,6 +14,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
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 { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityTokenAuthDALFactory } from "./identity-token-auth-dal";
import {
TAttachTokenAuthDTO,
@@ -59,8 +60,11 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachTokenAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@@ -126,8 +130,11 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TUpdateTokenAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@@ -218,8 +225,11 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actor,
actorAuthMethod,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TRevokeTokenAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@@ -271,8 +281,11 @@ export const identityTokenAuthServiceFactory = ({
actor,
actorAuthMethod,
actorOrgId,
name
name,
isActorSuperAdmin
}: TCreateTokenAuthTokenDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@@ -350,8 +363,11 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actor,
actorAuthMethod,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TGetTokenAuthTokensDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@@ -386,7 +402,8 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actor,
actorAuthMethod,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TUpdateTokenAuthTokenDTO) => {
const foundToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
@@ -398,6 +415,8 @@ export const identityTokenAuthServiceFactory = ({
if (!identityMembershipOrg) {
throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` });
}
await validateIdentityUpdateForSuperAdminPrivileges(foundToken.identityId, isActorSuperAdmin);
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({
message: "The identity does not have Token Auth"
@@ -446,18 +465,22 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actor,
actorAuthMethod,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TRevokeTokenAuthTokenDTO) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
[`${TableName.IdentityAccessToken}.isAccessTokenRevoked` as "isAccessTokenRevoked"]: false,
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH
});
if (!identityAccessToken)
throw new NotFoundError({
message: `Token with ID ${tokenId} not found or already revoked`
});
await validateIdentityUpdateForSuperAdminPrivileges(identityAccessToken.identityId, isActorSuperAdmin);
const identityOrgMembership = await identityOrgMembershipDAL.findOne({
identityId: identityAccessToken.identityId
});

View File

@@ -6,6 +6,7 @@ export type TAttachTokenAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateTokenAuthDTO = {
@@ -14,6 +15,7 @@ export type TUpdateTokenAuthDTO = {
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetTokenAuthDTO = {
@@ -22,24 +24,29 @@ export type TGetTokenAuthDTO = {
export type TRevokeTokenAuthDTO = {
identityId: string;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TCreateTokenAuthTokenDTO = {
identityId: string;
name?: string;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetTokenAuthTokensDTO = {
identityId: string;
offset: number;
limit: number;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateTokenAuthTokenDTO = {
tokenId: string;
name?: string;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeTokenAuthTokenDTO = {
tokenId: string;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -17,6 +17,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
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 { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-dal";
import { TIdentityUaDALFactory } from "./identity-ua-dal";
import {
@@ -150,8 +151,11 @@ export const identityUaServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachUaDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@@ -7,6 +7,7 @@ export type TAttachUaDTO = {
accessTokenNumUsesLimit: number;
clientSecretTrustedIps: { ipAddress: string }[];
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateUaDTO = {

View File

@@ -9,6 +9,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { ActorType } from "../auth/auth-type";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityDALFactory } from "./identity-dal";
import { TIdentityMetadataDALFactory } from "./identity-metadata-dal";
import { TIdentityOrgDALFactory } from "./identity-org-dal";
@@ -112,8 +113,11 @@ export const identityServiceFactory = ({
actorId,
actorAuthMethod,
actorOrgId,
metadata
metadata,
isActorSuperAdmin
}: TUpdateIdentityDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin);
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });
@@ -209,7 +213,16 @@ export const identityServiceFactory = ({
return identity;
};
const deleteIdentity = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteIdentityDTO) => {
const deleteIdentity = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id,
isActorSuperAdmin
}: TDeleteIdentityDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin);
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });

View File

@@ -12,10 +12,12 @@ export type TUpdateIdentityDTO = {
role?: string;
name?: string;
metadata?: { key: string; value: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TOrgPermission, "orgId">;
export type TDeleteIdentityDTO = {
id: string;
isActorSuperAdmin?: boolean;
} & Omit<TOrgPermission, "orgId">;
export type TGetIdentityByIdDTO = {

View File

@@ -0,0 +1,30 @@
import { ForbiddenRequestError } from "@app/lib/errors";
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { ActorType } from "../auth/auth-type";
import { getServerCfg } from "./super-admin-service";
export const isSuperAdmin = (auth: TAuthMode) => {
if (auth.actor === ActorType.USER && auth.user.superAdmin) {
return true;
}
if (auth.actor === ActorType.IDENTITY && auth.isInstanceAdmin) {
return true;
}
return false;
};
export const validateIdentityUpdateForSuperAdminPrivileges = async (
identityId: string,
isActorSuperAdmin?: boolean
) => {
const serverCfg = await getServerCfg();
if (serverCfg.adminIdentityIds?.includes(identityId) && !isActorSuperAdmin) {
throw new ForbiddenRequestError({
message:
"You are attempting to modify an instance admin identity. This requires elevated instance admin privileges"
});
}
};

View File

@@ -1,16 +1,21 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type";
import { AuthMethod, AuthTokenType } from "../auth/auth-type";
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 { TIdentityTokenAuthDALFactory } from "../identity-token-auth/identity-token-auth-dal";
import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns";
import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
@@ -20,10 +25,19 @@ import { TUserDALFactory } from "../user/user-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
import { UserAliasType } from "../user-alias/user-alias-types";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import { LoginMethod, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
import {
LoginMethod,
TAdminBootstrapInstanceDTO,
TAdminGetIdentitiesDTO,
TAdminGetUsersDTO,
TAdminSignUpDTO
} from "./super-admin-types";
type TSuperAdminServiceFactoryDep = {
identityDAL: Pick<TIdentityDALFactory, "getIdentitiesByFilter">;
identityDAL: TIdentityDALFactory;
identityTokenAuthDAL: TIdentityTokenAuthDALFactory;
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
serverCfgDAL: TSuperAdminDALFactory;
userDAL: TUserDALFactory;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne">;
@@ -60,7 +74,10 @@ export const superAdminServiceFactory = ({
keyStore,
kmsRootConfigDAL,
kmsService,
licenseService
licenseService,
identityAccessTokenDAL,
identityTokenAuthDAL,
identityOrgMembershipDAL
}: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => {
// TODO(akhilmhdh): bad pattern time less change this later to me itself
@@ -274,6 +291,137 @@ export const superAdminServiceFactory = ({
return { token, user: userInfo, organization };
};
const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => {
const appCfg = getConfig();
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg?.initialized) {
throw new BadRequestError({ message: "Instance has already been set up" });
}
const existingUser = await userDAL.findOne({ email });
if (existingUser) throw new BadRequestError({ name: "Instance initialization", message: "User already exists" });
const userInfo = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create(
{
firstName: "Admin",
lastName: "User",
username: email,
email,
superAdmin: true,
isGhost: false,
isAccepted: true,
authMethods: [AuthMethod.EMAIL],
isEmailVerified: true
},
tx
);
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(password);
const encKeys = await generateUserSrpKeys(email, password);
const userEnc = await userDAL.createUserEncryption(
{
userId: newUser.id,
encryptionVersion: 2,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
return { user: newUser, enc: userEnc };
});
const initialOrganizationName = organizationName ?? "Admin Org";
const organization = await orgService.createOrganization({
userId: userInfo.user.id,
userEmail: userInfo.user.email,
orgName: initialOrganizationName
});
const { identity, credentials } = await identityDAL.transaction(async (tx) => {
const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx);
await identityOrgMembershipDAL.create(
{
identityId: newIdentity.id,
orgId: organization.id,
role: OrgMembershipRole.Admin
},
tx
);
const tokenAuth = await identityTokenAuthDAL.create(
{
identityId: newIdentity.id,
accessTokenMaxTTL: 0,
accessTokenTTL: 0,
accessTokenNumUsesLimit: 0,
accessTokenTrustedIps: JSON.stringify([
{
type: "ipv4",
prefix: 0,
ipAddress: "0.0.0.0"
},
{
type: "ipv6",
prefix: 0,
ipAddress: "::"
}
])
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: newIdentity.id,
isAccessTokenRevoked: false,
accessTokenTTL: tokenAuth.accessTokenTTL,
accessTokenMaxTTL: tokenAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: tokenAuth.accessTokenNumUsesLimit,
name: "Instance Admin Token",
authMethod: IdentityAuthMethod.TOKEN_AUTH
},
tx
);
const generatedAccessToken = jwt.sign(
{
identityId: newIdentity.id,
identityAccessTokenId: newToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET
);
return { identity: newIdentity, auth: tokenAuth, credentials: { token: generatedAccessToken } };
});
await updateServerCfg({ initialized: true, adminIdentityIds: [identity.id] }, userInfo.user.id);
return {
user: userInfo,
organization,
machineIdentity: {
...identity,
credentials
}
};
};
const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => {
return userDAL.getUsersByFilter({
limit,
@@ -289,13 +437,46 @@ export const superAdminServiceFactory = ({
return user;
};
const getIdentities = ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
return identityDAL.getIdentitiesByFilter({
const deleteIdentitySuperAdminAccess = async (identityId: string, actorId: string) => {
const identity = await identityDAL.findById(identityId);
if (!identity) {
throw new NotFoundError({ name: "Identity", message: "Identity not found" });
}
const currentAdminIdentityIds = (await getServerCfg()).adminIdentityIds ?? [];
if (!currentAdminIdentityIds?.includes(identityId)) {
throw new BadRequestError({ name: "Identity", message: "Identity does not have super admin access" });
}
await updateServerCfg({ adminIdentityIds: currentAdminIdentityIds.filter((id) => id !== identityId) }, actorId);
return identity;
};
const deleteUserSuperAdminAccess = async (userId: string) => {
const user = await userDAL.findById(userId);
if (!user) {
throw new NotFoundError({ name: "User", message: "User not found" });
}
const updatedUser = userDAL.updateById(userId, { superAdmin: false });
return updatedUser;
};
const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
const identities = await identityDAL.getIdentitiesByFilter({
limit,
offset,
searchTerm,
sortBy: "name"
});
const serverCfg = await getServerCfg();
return identities.map((identity) => ({
...identity,
isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id))
}));
};
const grantServerAdminAccessToUser = async (userId: string) => {
@@ -393,12 +574,15 @@ export const superAdminServiceFactory = ({
initServerCfg,
updateServerCfg,
adminSignUp,
bootstrapInstance,
getUsers,
deleteUser,
getIdentities,
getAdminSlackConfig,
updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies,
grantServerAdminAccessToUser
grantServerAdminAccessToUser,
deleteIdentitySuperAdminAccess,
deleteUserSuperAdminAccess
};
};

View File

@@ -16,6 +16,12 @@ export type TAdminSignUpDTO = {
userAgent: string;
};
export type TAdminBootstrapInstanceDTO = {
email: string;
password: string;
organizationName: string;
};
export type TAdminGetUsersDTO = {
offset: number;
limit: number;

View File

@@ -600,3 +600,23 @@ func CallGatewayHeartBeatV1(httpClient *resty.Client) error {
return nil
}
func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (map[string]interface{}, error) {
var resBody map[string]interface{}
response, err := httpClient.
R().
SetResult(&resBody).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v1/admin/bootstrap", request.Domain))
if err != nil {
return nil, fmt.Errorf("CallBootstrapInstance: Unable to complete api request [err=%w]", err)
}
if response.IsError() {
return nil, fmt.Errorf("CallBootstrapInstance: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
}
return resBody, nil
}

View File

@@ -654,3 +654,10 @@ type ExchangeRelayCertResponseV1 struct {
Certificate string `json:"certificate"`
CertificateChain string `json:"certificateChain"`
}
type BootstrapInstanceRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Organization string `json:"organization"`
Domain string `json:"domain"`
}

View File

@@ -0,0 +1,104 @@
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var bootstrapCmd = &cobra.Command{
Use: "bootstrap",
Short: "Used to bootstrap your Infisical instance",
DisableFlagsInUseLine: true,
Example: "infisical bootstrap",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
email, _ := cmd.Flags().GetString("email")
if email == "" {
if envEmail, ok := os.LookupEnv("INFISICAL_ADMIN_EMAIL"); ok {
email = envEmail
}
}
if email == "" {
log.Error().Msg("email is required")
return
}
password, _ := cmd.Flags().GetString("password")
if password == "" {
if envPassword, ok := os.LookupEnv("INFISICAL_ADMIN_PASSWORD"); ok {
password = envPassword
}
}
if password == "" {
log.Error().Msg("password is required")
return
}
organization, _ := cmd.Flags().GetString("organization")
if organization == "" {
if envOrganization, ok := os.LookupEnv("INFISICAL_ADMIN_ORGANIZATION"); ok {
organization = envOrganization
}
}
if organization == "" {
log.Error().Msg("organization is required")
return
}
domain, _ := cmd.Flags().GetString("domain")
if domain == "" {
if envDomain, ok := os.LookupEnv("INFISICAL_API_URL"); ok {
domain = envDomain
}
}
if domain == "" {
log.Error().Msg("domain is required")
return
}
httpClient := resty.New().
SetHeader("Accept", "application/json")
bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{
Domain: util.AppendAPIEndpoint(domain),
Email: email,
Password: password,
Organization: organization,
})
if err != nil {
log.Error().Msgf("Failed to bootstrap instance: %v", err)
return
}
responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ")
if err != nil {
log.Fatal().Msgf("Failed to convert response to JSON: %v", err)
return
}
fmt.Println(string(responseJSON))
},
}
func init() {
bootstrapCmd.Flags().String("domain", "", "The domain of your self-hosted Infisical instance")
bootstrapCmd.Flags().String("email", "", "The desired email address of the instance admin")
bootstrapCmd.Flags().String("password", "", "The desired password of the instance admin")
bootstrapCmd.Flags().String("organization", "", "The name of the organization to create for the instance")
rootCmd.AddCommand(bootstrapCmd)
}

View File

@@ -15,15 +15,3 @@ Since Infisical's team is globally distributed, it is hard for us to keep track
## Winter break
Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla).
## Parental leave
At Infisical, we recognize that parental leave is a special and important time, significantly different from a typical vacation. Were proud to offer parental leave to everyone, regardless of gender, and whether youve become a parent through childbirth or adoption.
For team members who have been with Infisical for over a year by the time of your childs birth or adoption, you are eligible for up to 12 weeks of paid parental leave. This leave will be provided in one continuous block to allow you uninterrupted time with your family. If you have been with Infisical for less than a year, we will follow the parental leave provisions required by your local jurisdiction.
While we trust your judgment, parental leave is intended to be a distinct benefit and is not designed to be combined with our unlimited PTO policy. To ensure fairness and balance, we generally discourage combining parental leave with an extended vacation.
When youre ready, please notify Maidul about your plans for parental leave, ideally at least four months in advance. This allows us to support you fully and arrange any necessary logistics, including salary adjustments and statutory paperwork.
Were here to support you as you embark on this exciting new chapter in your life!

View File

@@ -0,0 +1,132 @@
---
title: "infisical bootstrap"
description: "Automate the initial setup of a new Infisical instance for headless deployment and infrastructure-as-code workflows"
---
```bash
infisical bootstrap --domain=<domain> --email=<email> --password=<password> --organization=<organization>
```
## Description
The `infisical bootstrap` command is used when deploying Infisical in automated environments where manual UI setup is not feasible. It's ideal for:
- Containerized deployments in Kubernetes or Docker environments
- Infrastructure-as-code pipelines with Terraform or similar tools
- Continuous deployment workflows
- DevOps automation scenarios
The command initializes a fresh Infisical instance by creating an admin user, organization, and instance admin machine identity, enabling subsequent programmatic configuration without human intervention.
<Warning>
This command creates an instance admin machine identity with the highest level
of privileges. The returned token should be treated with the utmost security,
similar to a root credential. Unauthorized access to this token could
compromise your entire Infisical instance.
</Warning>
## Flags
<Accordion title="--domain" defaultOpen="true">
The URL of your Infisical instance. This can be set using the `INFISICAL_API_URL` environment variable.
```bash
# Example
infisical bootstrap --domain=https://your-infisical-instance.com
```
This flag is required.
</Accordion>
<Accordion title="--email">
Email address for the admin user account that will be created. This can be set using the `INFISICAL_ADMIN_EMAIL` environment variable.
```bash
# Example
infisical bootstrap --email=admin@example.com
```
This flag is required.
</Accordion>
<Accordion title="--password">
Password for the admin user account. This can be set using the `INFISICAL_ADMIN_PASSWORD` environment variable.
```bash
# Example
infisical bootstrap --password=your-secure-password
```
This flag is required.
</Accordion>
<Accordion title="--organization">
Name of the organization that will be created within the instance. This can be set using the `INFISICAL_ADMIN_ORGANIZATION` environment variable.
```bash
# Example
infisical bootstrap --organization=your-org-name
```
This flag is required.
</Accordion>
## Response
The command returns a JSON response with details about the created user, organization, and machine identity:
```json
{
"identity": {
"credentials": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y"
},
"id": "db224792-ed18-4277-9dae-57e752315854",
"name": "Instance Admin Identity"
},
"message": "Successfully bootstrapped instance",
"organization": {
"id": "b56bece0-42f5-4262-b25e-be7bf5f84957",
"name": "dog",
"slug": "dog-v-e5l"
},
"user": {
"email": "admin@example.com",
"firstName": "Admin",
"id": "a418f355-c8da-453c-bbc8-6c07208eeb3c",
"lastName": "User",
"superAdmin": true,
"username": "admin@example.com"
}
}
```
## Usage with Automation
For automation purposes, you can extract just the machine identity token from the response:
```bash
infisical bootstrap --domain=https://your-infisical-instance.com --email=admin@example.com --password=your-secure-password --organization=your-org-name | jq ".identity.credentials.token"
```
This extracts only the token, which can be captured in a variable or piped to other commands.
## Example: Capture Token in a Variable
```bash
TOKEN=$(infisical bootstrap --domain=https://your-infisical-instance.com --email=admin@example.com --password=your-secure-password --organization=your-org-name | jq -r ".identity.credentials.token")
# Now use the token for further automation
echo "Token has been captured and can be used for authentication"
```
## Notes
- The bootstrap process can only be performed once on a fresh Infisical instance
- All flags are required for the bootstrap process to complete successfully
- Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities
- The generated admin user account can be used to log in via the UI if needed

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

View File

@@ -311,7 +311,8 @@
"group": "Guides",
"pages": [
"self-hosting/guides/mongo-to-postgres",
"self-hosting/guides/custom-certificates"
"self-hosting/guides/custom-certificates",
"self-hosting/guides/automated-bootstrapping"
]
},
{
@@ -341,6 +342,7 @@
"cli/commands/dynamic-secrets",
"cli/commands/ssh",
"cli/commands/gateway",
"cli/commands/bootstrap",
"cli/commands/export",
"cli/commands/token",
"cli/commands/service-token",

View File

@@ -0,0 +1,150 @@
---
title: "Programmatic Provisioning"
description: "Learn how to provision and configure Infisical instances programmatically without UI interaction"
---
Infisical's Automated Bootstrapping feature enables you to provision and configure an Infisical instance without using the UI, allowing for complete automation through static configuration files, API calls, or CLI commands. This is especially valuable for enterprise environments where automated deployment and infrastructure-as-code practices are essential.
## Overview
The Automated Bootstrapping workflow automates the following processes:
- Creating an admin user account
- Initializing an organization for the entire instance
- Establishing an **instance admin machine identity** with full administrative permissions
- Returning the machine identity credentials for further automation
## Key Concepts
- **Instance Initialization**: Infisical requires [configuration variables](/self-hosting/configuration/envars) to be set during launch, after which the bootstrap process can be triggered.
- **Instance Admin Machine Identity**: The bootstrapping process creates a machine identity with instance-level admin privileges, which can be used to programmatically manage all aspects of the Infisical instance.
![Instance Admin Identity](/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png)
- **Token Auth**: The instance admin machine identity uses [Token Auth](/documentation/platform/identities/token-auth), providing a JWT token that can be used directly to make authenticated requests to the Infisical API.
## Prerequisites
- An Infisical instance launched with all required configuration variables
- Access to the Infisical CLI or the ability to make API calls to the instance
- Network connectivity to the Infisical instance
## Bootstrap Methods
You can bootstrap an Infisical instance using either the API or the CLI.
<Tabs>
<Tab title="Using the API">
Make a POST request to the bootstrap endpoint:
```
POST: http://your-infisical-instance.com/api/v1/admin/bootstrap
{
"email": "admin@example.com",
"password": "your-secure-password",
"organization": "your-org-name"
}
```
Example using curl:
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"your-secure-password","organization":"your-org-name"}' \
http://your-infisical-instance.com/api/v1/admin/bootstrap
```
</Tab>
<Tab title="Using the CLI">
Use the [Infisical CLI](/cli/commands/bootstrap) to bootstrap the instance and extract the token for immediate use in automation:
```bash
infisical bootstrap --domain="http://localhost:8080" --email="admin@example.com" --password="your-secure-password" --organization="your-org-name" | jq ".identity.credentials.token"
```
This example command pipes the output through `jq` to extract only the machine identity token, making it easy to capture and use directly in automation scripts or export as an environment variable for tools like Terraform.
</Tab>
</Tabs>
## API Response Structure
The bootstrap process returns a JSON response with details about the created user, organization, and machine identity:
```json
{
"identity": {
"credentials": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y"
},
"id": "db224792-ed18-4277-9dae-57e752315854",
"name": "Instance Admin Identity"
},
"message": "Successfully bootstrapped instance",
"organization": {
"id": "b56bece0-42f5-4262-b25e-be7bf5f84957",
"name": "dog",
"slug": "dog-v-e5l"
},
"user": {
"email": "admin@example.com",
"firstName": "Admin",
"id": "a418f355-c8da-453c-bbc8-6c07208eeb3c",
"lastName": "User",
"superAdmin": true,
"username": "admin@example.com"
}
}
```
## Using the Instance Admin Machine Identity Token
The bootstrap process automatically creates a machine identity with Token Auth configured. The returned token has instance-level admin privileges (the highest level of access) and should be treated with the same security considerations as a root credential.
The token enables full programmatic control of your Infisical instance and can be used in the following ways:
### 1. Infrastructure Automation
Store the token securely for use with infrastructure automation tools. Due to the sensitive nature of this token, ensure it's protected using appropriate secret management practices:
#### Kubernetes Secret (with appropriate RBAC restrictions)
```yaml
apiVersion: v1
kind: Secret
metadata:
name: infisical-admin-credentials
type: Opaque
data:
token: <base64-encoded-token>
```
#### Environment Variable for Terraform
```bash
export INFISICAL_TOKEN=your-access-token
terraform apply
```
### 2. Programmatic Resource Management
Use the token to authenticate API calls for creating and managing Infisical resources. The token works exactly like any other Token Auth access token in the Infisical API:
```bash
curl -X POST \
-H "Authorization: Bearer ${INFISICAL_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"projectName": "New Project",
"projectDescription": "A project created via API",
"slug": "new-project-slug",
"template": "default",
"type": "SECRET_MANAGER"
}' \
https://your-infisical-instance.com/api/v2/projects
```
## Important Notes
- **Security Warning**: The instance admin machine identity has the highest level of privileges in your Infisical deployment. The token should be treated with the utmost security and handled like a root credential. Unauthorized access to this token could compromise your entire Infisical instance.
- Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities
- The instance admin permission of the generated identity can be revoked later in the server admin panel if needed
- The generated admin user account can still be used for UI access if needed, or can be removed if you prefer to manage everything through the machine identity
- This process is designed to work with future Crossplane providers and the existing Terraform provider for full infrastructure-as-code capabilities
- All necessary configuration variables should be set during the initial launch of the Infisical instance

View File

@@ -1,7 +1,9 @@
export {
useAdminDeleteUser,
useAdminGrantServerAdminAccess,
useAdminRemoveIdentitySuperAdminAccess,
useCreateAdminUser,
useRemoveUserServerAdminAccess,
useUpdateAdminSlackConfig,
useUpdateServerConfig,
useUpdateServerEncryptionStrategy

View File

@@ -70,6 +70,40 @@ export const useAdminDeleteUser = () => {
});
};
export const useAdminRemoveIdentitySuperAdminAccess = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (identityId: string) => {
await apiRequest.delete(
`/api/v1/admin/identity-management/identities/${identityId}/super-admin-access`
);
return {};
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getIdentities]
});
}
});
};
export const useRemoveUserServerAdminAccess = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userId: string) => {
await apiRequest.delete(`/api/v1/admin/user-management/users/${userId}/admin-access`);
return {};
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getUsers]
});
}
});
};
export const useAdminGrantServerAdminAccess = () => {
const queryClient = useQueryClient();
return useMutation({

View File

@@ -1,6 +1,7 @@
import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { Identity } from "@app/hooks/api/identities/types";
import { User } from "../types";
import {
@@ -10,7 +11,6 @@ import {
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
import { Identity } from "@app/hooks/api/identities/types";
export const adminStandaloneKeys = {
getUsers: "get-users",

View File

@@ -15,6 +15,7 @@ export type Identity = {
authMethods: IdentityAuthMethod[];
createdAt: string;
updatedAt: string;
isInstanceAdmin?: boolean;
};
export type IdentityAccessToken = {

View File

@@ -28,13 +28,13 @@ import {
useGetServerRootKmsEncryptionDetails,
useUpdateServerConfig
} from "@app/hooks/api";
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
import { AuthPanel } from "./components/AuthPanel";
import { EncryptionPanel } from "./components/EncryptionPanel";
import { IntegrationPanel } from "./components/IntegrationPanel";
import { RateLimitPanel } from "./components/RateLimitPanel";
import { UserPanel } from "./components/UserPanel";
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
enum TabSections {
Settings = "settings",
@@ -59,8 +59,8 @@ const formSchema = z.object({
trustLdapEmails: z.boolean(),
trustOidcEmails: z.boolean(),
defaultAuthOrgId: z.string(),
authConsentContent: z.string().optional(),
pageFrameContent: z.string().optional()
authConsentContent: z.string().optional().default(""),
pageFrameContent: z.string().optional().default("")
});
type TDashboardForm = z.infer<typeof formSchema>;
@@ -86,8 +86,8 @@ export const OverviewPage = () => {
trustLdapEmails: config.trustLdapEmails,
trustOidcEmails: config.trustOidcEmails,
defaultAuthOrgId: config.defaultAuthOrgId ?? "",
authConsentContent: config.authConsentContent,
pageFrameContent: config.pageFrameContent
authConsentContent: config.authConsentContent ?? "",
pageFrameContent: config.pageFrameContent ?? ""
}
});
@@ -165,8 +165,8 @@ export const OverviewPage = () => {
<Tab value={TabSections.Auth}>Authentication</Tab>
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
<Tab value={TabSections.Integrations}>Integrations</Tab>
<Tab value={TabSections.Users}>Users</Tab>
<Tab value={TabSections.Identities}>Identities</Tab>
<Tab value={TabSections.Users}>User Identities</Tab>
<Tab value={TabSections.Identities}>Machine Identities</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Settings}>

View File

@@ -1,9 +1,16 @@
import { useState } from "react";
import { faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons";
import { faEllipsis, faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Input,
Table,
@@ -15,10 +22,22 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useDebounce } from "@app/hooks";
import { useDebounce, usePopUp } from "@app/hooks";
import { useAdminRemoveIdentitySuperAdminAccess } from "@app/hooks/api/admin";
import { useAdminGetIdentities } from "@app/hooks/api/admin/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
const IdentityPanelTable = () => {
const IdentityPanelTable = ({
handlePopUpOpen
}: {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeServerAdmin"]>,
data?: {
name: string;
id: string;
}
) => void;
}) => {
const [searchIdentityFilter, setSearchIdentityFilter] = useState("");
const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500);
@@ -48,15 +67,48 @@ const IdentityPanelTable = () => {
<THead>
<Tr>
<Th>Name</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={2} innerKey="identities" />}
{!isPending &&
data?.pages?.map((identities) =>
identities.map(({ name, id }) => (
identities.map(({ name, id, isInstanceAdmin }) => (
<Tr key={`identity-${id}`} className="w-full">
<Td>{name}</Td>
<Td>
{name}
{isInstanceAdmin && (
<Badge variant="primary" className="ml-2">
Server Admin
</Badge>
)}
</Td>
<Td>
{isInstanceAdmin && (
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{isInstanceAdmin && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeServerAdmin", { name, id });
}}
>
Remove Server Admin
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</Td>
</Tr>
))
)}
@@ -81,11 +133,49 @@ const IdentityPanelTable = () => {
);
};
export const IdentityPanel = () => (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
export const IdentityPanel = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeServerAdmin"
] as const);
const { mutate: deleteIdentitySuperAdminAccess } = useAdminRemoveIdentitySuperAdminAccess();
const handleRemoveServerAdmin = async () => {
const { id } = popUp?.removeServerAdmin?.data as { id: string; name: string };
try {
await deleteIdentitySuperAdminAccess(id);
createNotification({
type: "success",
text: "Successfully removed server admin permissions"
});
} catch {
createNotification({
type: "error",
text: "Error removing server admin permissions"
});
}
handlePopUpClose("removeServerAdmin");
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
</div>
<IdentityPanelTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.removeServerAdmin.isOpen}
title={`Are you sure want to remove Server Admin permissions from ${
(popUp?.removeServerAdmin?.data as { name: string })?.name || ""
}?`}
subTitle=""
onChange={(isOpen) => handlePopUpToggle("removeServerAdmin", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleRemoveServerAdmin}
buttonText="Remove Access"
/>
</div>
<IdentityPanelTable />
</div>
);
);
};

View File

@@ -33,22 +33,26 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useSubscription, useUser } from "@app/context";
import { useSubscription } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks";
import {
useAdminDeleteUser,
useAdminGetUsers,
useAdminGrantServerAdminAccess
useAdminGrantServerAdminAccess,
useRemoveUserServerAdminAccess
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions";
const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user";
const UserPanelTable = ({
handlePopUpOpen
}: {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeUser", "upgradePlan", "upgradeToServerAdmin"]>,
popUpName: keyof UsePopUpState<
["removeUser", "upgradePlan", "upgradeToServerAdmin", "removeServerAdmin"]
>,
data?: {
username: string;
id: string;
@@ -58,8 +62,6 @@ const UserPanelTable = ({
}) => {
const [searchUserFilter, setSearchUserFilter] = useState("");
const [adminsOnly, setAdminsOnly] = useState(false);
const { user } = useUser();
const userId = user?.id || "";
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const { subscription } = useSubscription();
@@ -143,45 +145,61 @@ const UserPanelTable = ({
</Td>
<Td className="w-5/12">{email}</Td>
<Td>
{userId !== id && (
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeUser", { username, id });
}}
>
Remove User
</DropdownMenuItem>
{!superAdmin && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeUser", { username, id });
if (!subscription?.instanceUserManagement) {
handlePopUpOpen("upgradePlan", {
username,
id,
message: addServerAdminUpgradePlanMessage
});
return;
}
handlePopUpOpen("upgradeToServerAdmin", { username, id });
}}
>
Remove User
Make User Server Admin
</DropdownMenuItem>
{!superAdmin && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
if (!subscription?.instanceUserManagement) {
handlePopUpOpen("upgradePlan", {
username,
id,
message: addServerAdminUpgradePlanMessage
});
return;
}
handlePopUpOpen("upgradeToServerAdmin", { username, id });
}}
>
Make User Server Admin
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
)}
{superAdmin && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
if (!subscription?.instanceUserManagement) {
handlePopUpOpen("upgradePlan", {
username,
id,
message: removeServerAdminUpgradePlanMessage
});
return;
}
handlePopUpOpen("removeServerAdmin", { username, id });
}}
>
Remove Server Admin
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
);
@@ -212,11 +230,13 @@ export const UserPanel = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeUser",
"upgradePlan",
"upgradeToServerAdmin"
"upgradeToServerAdmin",
"removeServerAdmin"
] as const);
const { mutateAsync: deleteUser } = useAdminDeleteUser();
const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess();
const { mutateAsync: removeAdminAccess } = useRemoveUserServerAdminAccess();
const handleRemoveUser = async () => {
const { id } = popUp?.removeUser?.data as { id: string; username: string };
@@ -256,6 +276,25 @@ export const UserPanel = () => {
handlePopUpClose("upgradeToServerAdmin");
};
const handleRemoveServerAdminAccess = async () => {
const { id } = popUp?.removeServerAdmin?.data as { id: string; username: string };
try {
await removeAdminAccess(id);
createNotification({
type: "success",
text: "Successfully removed server admin access from user"
});
} catch {
createNotification({
type: "error",
text: "Error removing server admin access from user"
});
}
handlePopUpClose("removeServerAdmin");
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4">
@@ -282,6 +321,17 @@ export const UserPanel = () => {
onDeleteApproved={handleGrantServerAdminAccess}
buttonText="Grant Access"
/>
<DeleteActionModal
isOpen={popUp.removeServerAdmin.isOpen}
title={`Are you sure want to remove Server Admin permissions from ${
(popUp?.removeServerAdmin?.data as { id: string; username: string })?.username || ""
}?`}
subTitle=""
onChange={(isOpen) => handlePopUpToggle("removeServerAdmin", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleRemoveServerAdminAccess}
buttonText="Remove Access"
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}

View File

@@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: v0.8.13
version: v0.8.14
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v0.8.13"
appVersion: "v0.8.14"

View File

@@ -32,7 +32,7 @@ controllerManager:
- ALL
image:
repository: infisical/kubernetes-operator
tag: v0.8.13
tag: v0.8.14
resources:
limits:
cpu: 500m