Compare commits

...

39 Commits

Author SHA1 Message Date
Sheen Capadngan
08145f9b96 misc: made audit log endpoint mi accessible 2024-10-14 17:09:49 +08:00
Daniel Hougaard
1f4db2bd80 Merge pull request #2582 from Infisical/daniel/stream-upload
fix: env-key large file uploads
2024-10-14 12:11:17 +04:00
Daniel Hougaard
d8d784a0bc Update external-migration-router.ts 2024-10-14 12:04:41 +04:00
Daniel Hougaard
2dc1416f30 fix: envkey upload timeout 2024-10-14 11:49:26 +04:00
Maidul Islam
7fdcb29bab Merge pull request #2573 from Infisical/daniel/envkey-import-bug
feat: Process Envkey import in queue
2024-10-13 22:48:59 -07:00
BlackMagiq
6a89e3527c Merge pull request #2579 from Infisical/vmatsiiako-changelog-patch-1-1
Update overview.mdx
2024-10-13 14:34:37 -07:00
Vlad Matsiiako
d1d0667cd5 Update overview.mdx 2024-10-12 22:03:08 -07:00
Daniel Hougaard
865db5a9b3 removed redundancies 2024-10-12 07:54:21 +04:00
Daniel Hougaard
ad2f19658b requested changes 2024-10-12 07:40:14 +04:00
Daniel Hougaard
162005d72f feat: redis-based external imports 2024-10-11 11:15:56 +04:00
Maidul Islam
09d28156f8 Merge pull request #2572 from Infisical/vmatsiiako-readme-patch-1
Update README.md
2024-10-10 19:40:45 -07:00
Vlad Matsiiako
fc67c496c5 Update README.md 2024-10-10 19:39:51 -07:00
Maidul Islam
540a1a29b1 Merge pull request #2570 from akhilmhdh/fix/scim-error-response
Resolved response schema mismatch for scim
2024-10-10 13:53:33 -07:00
Maidul Islam
3163adf486 increase depth count 2024-10-10 13:50:03 -07:00
=
e042f9b5e2 feat: made missing errors as internal server error and added depth in scim knex 2024-10-11 01:42:38 +05:30
Daniel Hougaard
05a1b5397b Merge pull request #2571 from Infisical/daniel/envkey-import-bug
fix: handle undefined variable values
2024-10-10 21:23:08 +04:00
Daniel Hougaard
19776df46c fix: handle undefined variable values 2024-10-10 21:13:17 +04:00
Maidul Islam
64fd65aa52 Update requirements.mdx 2024-10-10 08:58:35 -07:00
=
3d58eba78c fix: resolved response schema mismatch for scim 2024-10-10 18:38:29 +05:30
Maidul Islam
565884d089 Merge pull request #2569 from Infisical/maidul-helm-static-dynamic
Make helm chart more dynamic
2024-10-10 00:05:04 -07:00
Maidul Islam
2a83da1cb6 update helm chart version 2024-10-10 00:00:56 -07:00
Maidul Islam
f186ce9649 Add support for existing pg secret 2024-10-09 23:43:37 -07:00
Maidul Islam
6ecfee5faf Merge pull request #2568 from Infisical/daniel/envvar-fix
fix: allow 25MB uploads for migrations
2024-10-09 17:21:09 -07:00
Daniel Hougaard
662f1a31f6 fix: allow 25MB uploads for migrations 2024-10-10 03:37:08 +04:00
Maidul Islam
06f9a1484b Merge pull request #2567 from scott-ray-wilson/fix-unintentional-project-creation
Fix: Prevent Example Project Creation on SSO Signup When Joining Org
2024-10-09 15:01:44 -07:00
Scott Wilson
c90e8ca715 chore: revert prem features 2024-10-09 14:01:16 -07:00
Scott Wilson
6ddc4ce4b1 fix: prevent example project from being created when joining existing org SSO 2024-10-09 13:58:22 -07:00
Maidul Islam
4fffac07fd Merge pull request #2559 from akhilmhdh/fix/ssm-integration-1-1
fix: resolved ssm failing for empty secret in 1-1 mapping
2024-10-09 13:19:22 -07:00
Scott Wilson
75d71d4208 Merge pull request #2549 from scott-ray-wilson/org-default-role
Feat: Default Org Membership Role
2024-10-09 11:55:47 -07:00
Scott Wilson
e38628509d improvement: address more feedback 2024-10-09 11:52:02 -07:00
Scott Wilson
0b247176bb improvements: address feedback 2024-10-09 11:52:02 -07:00
Daniel Hougaard
faad09961d Update OrgRoleTable.tsx 2024-10-09 22:47:14 +04:00
Scott Wilson
98d4f808e5 improvement: set intial org role value in dropdown on add user to default org membership value 2024-10-09 11:04:47 -07:00
Scott Wilson
2ae91db65d Merge pull request #2565 from scott-ray-wilson/add-project-users-multi-select
Feature: Multi-Select Component and Improve Adding Users to Project
2024-10-09 10:45:59 -07:00
Scott Wilson
529328f0ae chore: revert package-lock name 2024-10-09 10:02:42 -07:00
Scott Wilson
e59d9ff3c6 chore: revert prem features 2024-10-09 10:00:38 -07:00
Scott Wilson
4aad36601c feature: add multiselect component and improve adding users to project 2024-10-09 09:58:00 -07:00
=
4aaba3ef9f fix: resolved ssm failing for empty secret in 1-1 mapping 2024-10-09 16:06:48 +05:30
Scott Wilson
eae5e57346 feat: default org membership role 2024-10-07 15:02:14 -07:00
60 changed files with 1528 additions and 586 deletions

View File

@@ -135,9 +135,7 @@ Lean about Infisical's code scanning feature [here](https://infisical.com/docs/c
This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo):
<a href="[https://infisical.cal.com/vlad/infisical-demo](https://infisical.cal.com/vlad/infisical-demo)"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo).
## Security
@@ -163,4 +161,3 @@ Not sure where to get started? You can:
- [Twitter](https://twitter.com/infisical) for fast news
- [YouTube](https://www.youtube.com/@infisical_os) for videos on secret management
- [Blog](https://infisical.com/blog) for secret management insights, articles, tutorials, and updates
- [Roadmap](https://www.notion.so/infisical/be2d2585a6694e40889b03aef96ea36b?v=5b19a8127d1a4060b54769567a8785fa) for planned features

View File

@@ -21,6 +21,7 @@
"@fastify/etag": "^5.1.0",
"@fastify/formbody": "^7.4.0",
"@fastify/helmet": "^11.1.1",
"@fastify/multipart": "8.3.0",
"@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0",
"@fastify/session": "^10.7.0",
@@ -4311,6 +4312,15 @@
"fast-uri": "^2.0.0"
}
},
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@fastify/cookie": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-9.3.1.tgz",
@@ -4381,6 +4391,20 @@
"helmet": "^7.0.0"
}
},
"node_modules/@fastify/multipart": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.3.0.tgz",
"integrity": "sha512-A8h80TTyqUzaMVH0Cr9Qcm6RxSkVqmhK/MVBYHYeRRSUbUYv08WecjWKSlG2aSnD4aGI841pVxAjC+G1GafUeQ==",
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.1.0",
"@fastify/deepmerge": "^1.0.0",
"@fastify/error": "^3.0.0",
"fastify-plugin": "^4.0.0",
"secure-json-parse": "^2.4.0",
"stream-wormhole": "^1.1.0"
}
},
"node_modules/@fastify/passport": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@fastify/passport/-/passport-2.4.0.tgz",
@@ -16604,6 +16628,15 @@
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="
},
"node_modules/stream-wormhole": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz",
"integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",

View File

@@ -125,6 +125,7 @@
"@fastify/etag": "^5.1.0",
"@fastify/formbody": "^7.4.0",
"@fastify/helmet": "^11.1.1",
"@fastify/multipart": "8.3.0",
"@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0",
"@fastify/session": "^10.7.0",

View File

@@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
// org default role
if (await knex.schema.hasTable(TableName.Organization)) {
const hasDefaultRoleCol = await knex.schema.hasColumn(TableName.Organization, "defaultMembershipRole");
if (!hasDefaultRoleCol) {
await knex.schema.alterTable(TableName.Organization, (tb) => {
tb.string("defaultMembershipRole").notNullable().defaultTo("member");
});
}
}
}
export async function down(knex: Knex): Promise<void> {
// org default role
if (await knex.schema.hasTable(TableName.Organization)) {
const hasDefaultRoleCol = await knex.schema.hasColumn(TableName.Organization, "defaultMembershipRole");
if (hasDefaultRoleCol) {
await knex.schema.alterTable(TableName.Organization, (tb) => {
tb.dropColumn("defaultMembershipRole");
});
}
}
}

View File

@@ -19,7 +19,8 @@ export const OrganizationsSchema = z.object({
authEnforced: z.boolean().default(false).nullable().optional(),
scimEnabled: z.boolean().default(false).nullable().optional(),
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
kmsEncryptedDataKey: zodBuffer.nullable().optional()
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
defaultMembershipRole: z.string().default("member")
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;

View File

@@ -1,14 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import {
OrgMembershipRole,
OrgMembershipStatus,
SecretKeyEncoding,
TableName,
TLdapConfigsUpdate,
TUsers
} from "@app/db/schemas";
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -28,6 +21,7 @@ import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -444,11 +438,14 @@ export const ldapConfigServiceFactory = ({
{ tx }
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgDAL.createMembership(
{
userId: userAlias.userId,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: OrgMembershipStatus.Accepted,
isActive: true
},
@@ -529,12 +526,15 @@ export const ldapConfigServiceFactory = ({
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},

View File

@@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
@@ -23,6 +23,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -187,12 +188,15 @@ export const oidcConfigServiceFactory = ({
{ tx }
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},
@@ -261,12 +265,15 @@ export const oidcConfigServiceFactory = ({
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},

View File

@@ -2,7 +2,6 @@ import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import {
OrgMembershipRole,
OrgMembershipStatus,
SecretKeyEncoding,
TableName,
@@ -26,6 +25,7 @@ import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -369,12 +369,15 @@ export const samlConfigServiceFactory = ({
{ tx }
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},
@@ -472,12 +475,15 @@ export const samlConfigServiceFactory = ({
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},

View File

@@ -16,6 +16,7 @@ import { AuthTokenType } from "@app/services/auth/auth-type";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -318,12 +319,15 @@ export const scimServiceFactory = ({
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
orgMembership = await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.NoAccess,
role,
roleId,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},
@@ -391,12 +395,15 @@ export const scimServiceFactory = ({
orgMembership = foundOrgMembership;
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
orgMembership = await orgMembershipDAL.create(
{
userId: user.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},

View File

@@ -240,7 +240,8 @@ export const secretSnapshotServiceFactory = ({
},
tx
);
const snapshotSecrets = await snapshotSecretV2BridgeDAL.insertMany(
const snapshotSecrets = await snapshotSecretV2BridgeDAL.batchInsert(
secretVersions.map(({ id }) => ({
secretVersionId: id,
envId: folder.environment.envId,
@@ -248,7 +249,8 @@ export const secretSnapshotServiceFactory = ({
})),
tx
);
const snapshotFolders = await snapshotFolderDAL.insertMany(
const snapshotFolders = await snapshotFolderDAL.batchInsert(
folderVersions.map(({ id }) => ({
folderVersionId: id,
envId: folder.environment.envId,

View File

@@ -70,3 +70,14 @@ export const objectify = <T, Key extends string | number | symbol, Value = T>(
{} as Record<Key, Value>
);
};
/**
* Chunks an array into smaller arrays of the given size.
*/
export const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
};

View File

@@ -8,12 +8,14 @@ const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
return filter;
};
export const generateKnexQueryFromScim = (
const processDynamicQuery = (
rootQuery: Knex.QueryBuilder,
rootScimFilter: string,
getAttributeField: (attr: string) => string | null
scimRootFilterAst: Filter,
getAttributeField: (attr: string) => string | null,
depth = 0
) => {
const scimRootFilterAst = parse(rootScimFilter);
if (depth > 20) return;
const stack = [
{
scimFilterAst: scimRootFilterAst,
@@ -75,42 +77,35 @@ export const generateKnexQueryFromScim = (
break;
}
case "and": {
void query.andWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
scimFilterAst.filters.forEach((el) => {
void query.andWhere((subQueryBuilder) => {
processDynamicQuery(subQueryBuilder, el, getAttributeField, depth + 1);
});
});
break;
}
case "or": {
void query.orWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
scimFilterAst.filters.forEach((el) => {
void query.orWhere((subQueryBuilder) => {
processDynamicQuery(subQueryBuilder, el, getAttributeField, depth + 1);
});
});
break;
}
case "not": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: scimFilterAst.filter
});
processDynamicQuery(subQueryBuilder, scimFilterAst.filter, getAttributeField, depth + 1);
});
break;
}
case "[]": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter)
});
void query.where((subQueryBuilder) => {
processDynamicQuery(
subQueryBuilder,
appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter),
getAttributeField,
depth + 1
);
});
break;
}
@@ -119,3 +114,12 @@ export const generateKnexQueryFromScim = (
}
}
};
export const generateKnexQueryFromScim = (
rootQuery: Knex.QueryBuilder,
rootScimFilter: string,
getAttributeField: (attr: string) => string | null
) => {
const scimRootFilterAst = parse(rootScimFilter);
return processDynamicQuery(rootQuery, scimRootFilterAst, getAttributeField);
};

View File

@@ -1,7 +1,7 @@
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
import Redis from "ioredis";
import { SecretKeyEncoding } from "@app/db/schemas";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import {
TScanFullRepoEventPayload,
@@ -32,7 +32,8 @@ export enum QueueName {
SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration",
AccessTokenStatusUpdate = "access-token-status-update"
AccessTokenStatusUpdate = "access-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
}
export enum QueueJobs {
@@ -56,7 +57,8 @@ export enum QueueJobs {
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration",
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
ServiceTokenStatusUpdate = "service-token-status-update"
ServiceTokenStatusUpdate = "service-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
}
export type TQueueJobTypes = {
@@ -166,6 +168,19 @@ export type TQueueJobTypes = {
name: QueueJobs.ProjectV3Migration;
payload: { projectId: string };
};
[QueueName.ImportSecretsFromExternalSource]: {
name: QueueJobs.ImportSecretsFromExternalSource;
payload: {
actorEmail: string;
data: {
iv: string;
tag: string;
ciphertext: string;
algorithm: SecretEncryptionAlgo;
encoding: SecretKeyEncoding;
};
};
};
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@@ -3,9 +3,12 @@ import fp from "fastify-plugin";
import { DefaultResponseErrorsSchema } from "../routes/sanitizedSchemas";
const isScimRoutes = (pathname: string) =>
pathname.startsWith("/api/v1/scim/Users") || pathname.startsWith("/api/v1/scim/Groups");
export const addErrorsToResponseSchemas = fp(async (server) => {
server.addHook("onRoute", (routeOptions) => {
if (routeOptions.schema && routeOptions.schema.response) {
if (routeOptions.schema && routeOptions.schema.response && !isScimRoutes(routeOptions.path)) {
routeOptions.schema.response = {
...DefaultResponseErrorsSchema,
...routeOptions.schema.response

View File

@@ -97,7 +97,11 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
message
});
} else {
void res.send(error);
void res.status(HttpStatusCodes.InternalServerError).send({
statusCode: HttpStatusCodes.InternalServerError,
error: "InternalServerError",
message: "Something went wrong"
});
}
});
});

View File

@@ -97,6 +97,7 @@ import { certificateTemplateDALFactory } from "@app/services/certificate-templat
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
@@ -532,7 +533,7 @@ export const registerRoutes = async (
orgService,
licenseService
});
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL });
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL, orgDAL });
const superAdminService = superAdminServiceFactory({
userDAL,
authService: loginService,
@@ -1202,12 +1203,26 @@ export const registerRoutes = async (
permissionService
});
const migrationService = externalMigrationServiceFactory({
projectService,
orgService,
const externalMigrationQueue = externalMigrationQueueFactory({
projectEnvService,
permissionService,
secretService
projectDAL,
projectService,
smtpService,
kmsService,
projectEnvDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
folderDAL,
secretDAL: secretV2BridgeDAL,
queueService,
secretV2BridgeService
});
const migrationService = externalMigrationServiceFactory({
externalMigrationQueue,
userDAL,
permissionService
});
await superAdminService.initServerCfg();

View File

@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import {
@@ -137,7 +138,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const auditLogs = await server.services.auditLog.listAuditLogs({
filter: {
@@ -217,7 +218,15 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
.regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens")
.optional(),
authEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional()
scimEnabled: z.boolean().optional(),
defaultMembershipRoleSlug: z
.string()
.min(1)
.trim()
.refine((v) => slugify(v) === v, {
message: "Membership role must be a valid slug"
})
.optional()
}),
response: {
200: z.object({

View File

@@ -1,30 +1,50 @@
import { z } from "zod";
import fastifyMultipart from "@fastify/multipart";
import { BadRequestError } from "@app/lib/errors";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const MB25_IN_BYTES = 26214400;
export const registerExternalMigrationRouter = async (server: FastifyZodProvider) => {
await server.register(fastifyMultipart);
server.route({
method: "POST",
bodyLimit: MB25_IN_BYTES,
url: "/env-key",
config: {
rateLimit: readLimit
},
schema: {
body: z.object({
decryptionKey: z.string().trim().min(1),
encryptedJson: z.object({
nonce: z.string().trim().min(1),
data: z.string().trim().min(1)
})
})
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await req.file({
limits: {
fileSize: MB25_IN_BYTES
}
});
if (!data) {
throw new BadRequestError({ message: "No file provided" });
}
const fullFile = Buffer.from(await data.toBuffer()).toString("utf8");
const parsedJsonFile = JSON.parse(fullFile) as { nonce: string; data: string };
const decryptionKey = (data.fields.decryptionKey as { value: string }).value;
if (!parsedJsonFile.nonce || !parsedJsonFile.data) {
throw new BadRequestError({ message: "Invalid file format. Nonce or data missing." });
}
if (!decryptionKey) {
throw new BadRequestError({ message: "Decryption key is required" });
}
await server.services.migration.importEnvKeyData({
decryptionKey: req.body.decryptionKey,
encryptedJson: req.body.encryptedJson,
decryptionKey,
encryptedJson: parsedJsonFile,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,

View File

@@ -4,22 +4,41 @@ import sjcl from "sjcl";
import tweetnacl from "tweetnacl";
import tweetnaclUtil from "tweetnacl-util";
import { OrgMembershipRole, ProjectMembershipRole, SecretType } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { SecretType } from "@app/db/schemas";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { chunkArray } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgServiceFactory } from "../org/org-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretServiceFactory } from "../secret/secret-service";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { fnSecretBulkInsert, getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types";
export type TImportDataIntoInfisicalDTO = {
projectService: TProjectServiceFactory;
orgService: TOrgServiceFactory;
projectEnvService: TProjectEnvServiceFactory;
secretService: TSecretServiceFactory;
projectDAL: Pick<TProjectDALFactory, "transaction">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath">;
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
input: TImportInfisicalDataCreate;
};
@@ -46,13 +65,13 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
const parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON;
const infisicalImportData: InfisicalImportData = {
projects: new Map<string, { name: string; id: string }>(),
environments: new Map<string, { name: string; id: string; projectId: string }>(),
secrets: new Map<string, { name: string; id: string; projectId: string; environmentId: string; value: string }>()
projects: [],
environments: [],
secrets: []
};
parsedJson.apps.forEach((app: { name: string; id: string }) => {
infisicalImportData.projects.set(app.id, { name: app.name, id: app.id });
infisicalImportData.projects.push({ name: app.name, id: app.id });
});
// string to string map for env templates
@@ -63,7 +82,7 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
// environments
for (const env of parsedJson.baseEnvironments) {
infisicalImportData.environments?.set(env.id, {
infisicalImportData.environments.push({
id: env.id,
name: envTemplates.get(env.environmentRoleId)!,
projectId: env.envParentId
@@ -75,9 +94,8 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
if (!env.includes("|")) {
const envData = parsedJson.envs[env];
for (const secret of Object.keys(envData.variables)) {
const id = randomUUID();
infisicalImportData.secrets?.set(id, {
id,
infisicalImportData.secrets.push({
id: randomUUID(),
name: secret,
environmentId: env,
value: envData.variables[secret].val
@@ -91,9 +109,14 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
export const importDataIntoInfisicalFn = async ({
projectService,
orgService,
projectEnvService,
secretService,
projectEnvDAL,
projectDAL,
secretDAL,
kmsService,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
}: TImportDataIntoInfisicalDTO) => {
// Import data to infisical
@@ -104,94 +127,132 @@ export const importDataIntoInfisicalFn = async ({
const originalToNewProjectId = new Map<string, string>();
const originalToNewEnvironmentId = new Map<string, string>();
for await (const [id, project] of data.projects) {
const newProject = await projectService
.createProject({
actor,
actorId,
actorOrgId,
actorAuthMethod,
workspaceName: project.name,
createDefaultEnvs: false
})
.catch(() => {
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}] [id:${id}]` });
});
originalToNewProjectId.set(project.id, newProject.id);
}
// Invite user importing projects
const invites = await orgService.inviteUserToOrganization({
actorAuthMethod,
actorId,
actorOrgId,
actor,
inviteeEmails: [],
orgId: actorOrgId,
organizationRoleSlug: OrgMembershipRole.NoAccess,
projects: Array.from(originalToNewProjectId.values()).map((project) => ({
id: project,
projectRoleSlug: [ProjectMembershipRole.Member]
}))
});
if (!invites) {
throw new BadRequestError({ message: `Failed to invite user to projects: [userId:${actorId}]` });
}
// Import environments
if (data.environments) {
for await (const [id, environment] of data.environments) {
try {
const newEnvironment = await projectEnvService.createEnvironment({
await projectDAL.transaction(async (tx) => {
for await (const project of data.projects) {
const newProject = await projectService
.createProject({
actor,
actorId,
actorOrgId,
actorAuthMethod,
name: environment.name,
projectId: originalToNewProjectId.get(environment.projectId)!,
slug: slugify(`${environment.name}-${alphaNumericNanoId(4)}`)
workspaceName: project.name,
createDefaultEnvs: false,
tx
})
.catch((e) => {
logger.error(e, `Failed to import to project [name:${project.name}]`);
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}]` });
});
if (!newEnvironment) {
logger.error(`Failed to import environment: [name:${environment.name}] [id:${id}]`);
originalToNewProjectId.set(project.id, newProject.id);
}
// Import environments
if (data.environments) {
for await (const environment of data.environments) {
const projectId = originalToNewProjectId.get(environment.projectId)!;
const slug = slugify(`${environment.name}-${alphaNumericNanoId(4)}`);
const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx);
if (existingEnv) {
throw new BadRequestError({
message: `Failed to import environment: [name:${environment.name}] [id:${id}]`
message: `Environment with slug '${slug}' already exist`,
name: "CreateEnvironment"
});
}
originalToNewEnvironmentId.set(id, newEnvironment.slug);
} catch (error) {
throw new BadRequestError({
message: `Failed to import environment: ${environment.name}]`,
name: "EnvKeyMigrationImportEnvironment"
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx);
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
originalToNewEnvironmentId.set(environment.id, doc.slug);
}
}
if (data.secrets && data.secrets.length > 0) {
const mappedToEnvironmentId = new Map<
string,
{
secretKey: string;
secretValue: string;
}[]
>();
for (const secret of data.secrets) {
if (!mappedToEnvironmentId.has(secret.environmentId)) {
mappedToEnvironmentId.set(secret.environmentId, []);
}
mappedToEnvironmentId.get(secret.environmentId)!.push({
secretKey: secret.name,
secretValue: secret.value || ""
});
}
}
}
// Import secrets
if (data.secrets) {
for await (const [id, secret] of data.secrets) {
const dataProjectId = data.environments?.get(secret.environmentId)?.projectId;
if (!dataProjectId) {
throw new BadRequestError({ message: `Failed to import secret "${secret.name}", project not found` });
}
const projectId = originalToNewProjectId.get(dataProjectId);
const newSecret = await secretService.createSecretRaw({
actorId,
actor,
actorOrgId,
environment: originalToNewEnvironmentId.get(secret.environmentId)!,
actorAuthMethod,
projectId: projectId!,
secretPath: "/",
secretName: secret.name,
type: SecretType.Shared,
secretValue: secret.value
});
if (!newSecret) {
throw new BadRequestError({ message: `Failed to import secret: [name:${secret.name}] [id:${id}]` });
// for each of the mappedEnvironmentId
for await (const [envId, secrets] of mappedToEnvironmentId) {
const environment = data.environments.find((env) => env.id === envId);
const projectId = originalToNewProjectId.get(environment?.projectId as string)!;
if (!projectId) {
throw new BadRequestError({ message: `Failed to import secret, project not found` });
}
const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey(
{
type: KmsDataKey.SecretManager,
projectId
},
tx
);
const envSlug = originalToNewEnvironmentId.get(envId)!;
const folder = await folderDAL.findBySecretPath(projectId, envSlug, "/", tx);
if (!folder)
throw new NotFoundError({
message: `Folder not found for the given environment slug (${envSlug}) & secret path (/)`,
name: "Create secret"
});
const secretsByKeys = await secretDAL.findBySecretKeys(
folder.id,
secrets.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
})),
tx
);
if (secretsByKeys.length) {
throw new BadRequestError({
message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}`
});
}
const secretBatches = chunkArray(secrets, 2500);
for await (const secretBatch of secretBatches) {
await fnSecretBulkInsert({
inputSecrets: secretBatch.map((el) => {
const references = getAllNestedSecretReferences(el.secretValue);
return {
version: 1,
encryptedValue: el.secretValue
? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob
: undefined,
key: el.secretKey,
references,
type: SecretType.Shared
};
}),
folderId: folder.id,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
});
}
}
}
}
});
};

View File

@@ -0,0 +1,141 @@
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { importDataIntoInfisicalFn } from "./external-migration-fns";
import { ExternalPlatforms, TImportInfisicalDataCreate } from "./external-migration-types";
export type TExternalMigrationQueueFactoryDep = {
smtpService: TSmtpService;
queueService: TQueueServiceFactory;
projectDAL: Pick<TProjectDALFactory, "transaction">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath">;
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
};
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
export const externalMigrationQueueFactory = ({
queueService,
projectService,
smtpService,
projectDAL,
projectEnvService,
secretV2BridgeService,
kmsService,
projectEnvDAL,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
}: TExternalMigrationQueueFactoryDep) => {
const startImport = async (dto: {
actorEmail: string;
data: {
iv: string;
tag: string;
ciphertext: string;
algorithm: SecretEncryptionAlgo;
encoding: SecretKeyEncoding;
};
}) => {
await queueService.queue(
QueueName.ImportSecretsFromExternalSource,
QueueJobs.ImportSecretsFromExternalSource,
dto,
{
removeOnComplete: true,
removeOnFail: true
}
);
};
queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => {
try {
const { data, actorEmail } = job.data;
await smtpService.sendMail({
recipients: [actorEmail],
subjectLine: "Infisical import started",
substitutions: {
provider: ExternalPlatforms.EnvKey
},
template: SmtpTemplates.ExternalImportStarted
});
const decrypted = infisicalSymmetricDecrypt({
ciphertext: data.ciphertext,
iv: data.iv,
keyEncoding: data.encoding,
tag: data.tag
});
const decryptedJson = JSON.parse(decrypted) as TImportInfisicalDataCreate;
await importDataIntoInfisicalFn({
input: decryptedJson,
projectDAL,
projectEnvDAL,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
kmsService,
projectService,
projectEnvService,
secretV2BridgeService
});
await smtpService.sendMail({
recipients: [actorEmail],
subjectLine: "Infisical import successful",
substitutions: {
provider: ExternalPlatforms.EnvKey
},
template: SmtpTemplates.ExternalImportSuccessful
});
} catch (err) {
await smtpService.sendMail({
recipients: [job.data.actorEmail],
subjectLine: "Infisical import failed",
substitutions: {
provider: ExternalPlatforms.EnvKey,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
error: (err as any)?.message || "Unknown error"
},
template: SmtpTemplates.ExternalImportFailed
});
logger.error(err, "Failed to import data from external source");
}
});
return {
startImport
};
};

View File

@@ -1,30 +1,25 @@
import { OrgMembershipRole } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { ForbiddenRequestError } from "@app/lib/errors";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretServiceFactory } from "../secret/secret-service";
import { decryptEnvKeyDataFn, importDataIntoInfisicalFn, parseEnvKeyDataFn } from "./external-migration-fns";
import { TUserDALFactory } from "../user/user-dal";
import { decryptEnvKeyDataFn, parseEnvKeyDataFn } from "./external-migration-fns";
import { TExternalMigrationQueueFactory } from "./external-migration-queue";
import { TImportEnvKeyDataCreate } from "./external-migration-types";
type TExternalMigrationServiceFactoryDep = {
projectService: TProjectServiceFactory;
orgService: TOrgServiceFactory;
projectEnvService: TProjectEnvServiceFactory;
secretService: TSecretServiceFactory;
permissionService: TPermissionServiceFactory;
externalMigrationQueue: TExternalMigrationQueueFactory;
userDAL: Pick<TUserDALFactory, "findById">;
};
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
export const externalMigrationServiceFactory = ({
projectService,
orgService,
projectEnvService,
permissionService,
secretService
externalMigrationQueue,
userDAL
}: TExternalMigrationServiceFactoryDep) => {
const importEnvKeyData = async ({
decryptionKey,
@@ -41,21 +36,28 @@ export const externalMigrationServiceFactory = ({
actorAuthMethod,
actorOrgId
);
if (membership.role !== OrgMembershipRole.Admin) {
throw new ForbiddenRequestError({ message: "Only admins can import data" });
}
const user = await userDAL.findById(actorId);
const json = await decryptEnvKeyDataFn(decryptionKey, encryptedJson);
const envKeyData = await parseEnvKeyDataFn(json);
const response = await importDataIntoInfisicalFn({
input: { data: envKeyData, actor, actorId, actorOrgId, actorAuthMethod },
projectService,
orgService,
projectEnvService,
secretService
const stringifiedJson = JSON.stringify({
data: envKeyData,
actor,
actorId,
actorOrgId,
actorAuthMethod
});
const encrypted = infisicalSymmetricEncypt(stringifiedJson);
await externalMigrationQueue.startImport({
actorEmail: user.email!,
data: encrypted
});
return response;
};
return {

View File

@@ -1,26 +1,9 @@
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export type InfisicalImportData = {
projects: Map<string, { name: string; id: string }>;
environments?: Map<
string,
{
name: string;
id: string;
projectId: string;
}
>;
secrets?: Map<
string,
{
name: string;
id: string;
environmentId: string;
value: string;
}
>;
projects: Array<{ name: string; id: string }>;
environments: Array<{ name: string; id: string; projectId: string }>;
secrets: Array<{ name: string; id: string; environmentId: string; value: string }>;
};
export type TImportEnvKeyDataCreate = {
@@ -104,3 +87,7 @@ export type TEnvKeyExportJSON = {
}
>;
};
export enum ExternalPlatforms {
EnvKey = "EnvKey"
}

View File

@@ -9,6 +9,7 @@
import {
CreateSecretCommand,
DeleteSecretCommand,
DescribeSecretCommand,
GetSecretValueCommand,
ResourceNotFoundException,
@@ -899,12 +900,21 @@ const syncSecretsAWSSecretManager = async ({
}
if (!isEqual(secretToCompare, secretValue)) {
await secretsManager.send(
new UpdateSecretCommand({
SecretId: secretId,
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue)
})
);
if (secretValue) {
await secretsManager.send(
new UpdateSecretCommand({
SecretId: secretId,
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue)
})
);
// delete it
} else {
await secretsManager.send(
new DeleteSecretCommand({
SecretId: secretId
})
);
}
}
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
@@ -989,16 +999,21 @@ const syncSecretsAWSSecretManager = async ({
} catch (err) {
// case 1: when AWS manager can't find the specified secret
if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send(
new CreateSecretCommand({
Name: secretId,
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue),
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
: []
})
);
if (secretValue) {
await secretsManager.send(
new CreateSecretCommand({
Name: secretId,
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue),
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
: []
})
);
}
// case 2: something unexpected went wrong, so we'll throw the error to reflect the error in the integration sync status
} else {
throw err;

View File

@@ -160,8 +160,8 @@ export const kmsServiceFactory = ({
* In mean time the rest of the request will wait until creation is finished followed by getting the created on
* In real time this would be milliseconds
*/
const getOrgKmsKeyId = async (orgId: string) => {
let org = await orgDAL.findById(orgId);
const getOrgKmsKeyId = async (orgId: string, trx?: Knex) => {
let org = await orgDAL.findById(orgId, trx);
if (!org) {
throw new NotFoundError({ message: "Org not found" });
@@ -180,9 +180,9 @@ export const kmsServiceFactory = ({
waitingCb: () => logger.info("KMS. Waiting for org key to be created")
});
org = await orgDAL.findById(orgId);
org = await orgDAL.findById(orgId, trx);
} else {
const keyId = await orgDAL.transaction(async (tx) => {
const keyId = await (trx || orgDAL).transaction(async (tx) => {
org = await orgDAL.findById(orgId, tx);
if (org.kmsDefaultKeyId) {
return org.kmsDefaultKeyId;
@@ -240,11 +240,12 @@ export const kmsServiceFactory = ({
const decryptWithKmsKey = async ({
kmsId,
depth = 0
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number }) => {
depth = 0,
tx
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number; tx?: Knex }) => {
if (depth > 2) throw new BadRequestError({ message: "KMS depth max limit" });
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
if (!kmsDoc) {
throw new NotFoundError({ message: "KMS ID not found" });
}
@@ -261,7 +262,8 @@ export const kmsServiceFactory = ({
// we put a limit of depth to avoid too many cycles
const orgKmsDecryptor = await decryptWithKmsKey({
kmsId: kmsDoc.orgKms.id,
depth: depth + 1
depth: depth + 1,
tx
});
const orgKmsDataKey = await orgKmsDecryptor({
@@ -375,9 +377,9 @@ export const kmsServiceFactory = ({
};
};
const $getOrgKmsDataKey = async (orgId: string) => {
const kmsKeyId = await getOrgKmsKeyId(orgId);
let org = await orgDAL.findById(orgId);
const $getOrgKmsDataKey = async (orgId: string, trx?: Knex) => {
const kmsKeyId = await getOrgKmsKeyId(orgId, trx);
let org = await orgDAL.findById(orgId, trx);
if (!org) {
throw new NotFoundError({ message: "Org not found" });
@@ -396,9 +398,9 @@ export const kmsServiceFactory = ({
waitingCb: () => logger.info("KMS. Waiting for org data key to be created")
});
org = await orgDAL.findById(orgId);
org = await orgDAL.findById(orgId, trx);
} else {
const orgDataKey = await orgDAL.transaction(async (tx) => {
const orgDataKey = await (trx || orgDAL).transaction(async (tx) => {
org = await orgDAL.findById(orgId, tx);
if (org.kmsEncryptedDataKey) {
return;
@@ -455,8 +457,8 @@ export const kmsServiceFactory = ({
});
};
const getProjectSecretManagerKmsKeyId = async (projectId: string) => {
let project = await projectDAL.findById(projectId);
const getProjectSecretManagerKmsKeyId = async (projectId: string, trx?: Knex) => {
let project = await projectDAL.findById(projectId, trx);
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
@@ -477,7 +479,7 @@ export const kmsServiceFactory = ({
project = await projectDAL.findById(projectId);
} else {
const kmsKeyId = await projectDAL.transaction(async (tx) => {
const kmsKeyId = await (trx || projectDAL).transaction(async (tx) => {
project = await projectDAL.findById(projectId, tx);
if (project.kmsSecretManagerKeyId) {
return project.kmsSecretManagerKeyId;
@@ -520,9 +522,9 @@ export const kmsServiceFactory = ({
return project.kmsSecretManagerKeyId;
};
const $getProjectSecretManagerKmsDataKey = async (projectId: string) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
let project = await projectDAL.findById(projectId);
const $getProjectSecretManagerKmsDataKey = async (projectId: string, trx?: Knex) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId, trx);
let project = await projectDAL.findById(projectId, trx);
if (!project.kmsSecretManagerEncryptedDataKey) {
const lock = await keyStore
@@ -538,18 +540,21 @@ export const kmsServiceFactory = ({
delay: 500
});
project = await projectDAL.findById(projectId);
project = await projectDAL.findById(projectId, trx);
} else {
const projectDataKey = await projectDAL.transaction(async (tx) => {
const projectDataKey = await (trx || projectDAL).transaction(async (tx) => {
project = await projectDAL.findById(projectId, tx);
if (project.kmsSecretManagerEncryptedDataKey) {
return;
}
const dataKey = randomSecureBytes();
const kmsEncryptor = await encryptWithKmsKey({
kmsId: kmsKeyId
});
const kmsEncryptor = await encryptWithKmsKey(
{
kmsId: kmsKeyId
},
tx
);
const { cipherTextBlob } = await kmsEncryptor({
plainText: dataKey
@@ -585,7 +590,8 @@ export const kmsServiceFactory = ({
}
const kmsDecryptor = await decryptWithKmsKey({
kmsId: kmsKeyId
kmsId: kmsKeyId,
tx: trx
});
return kmsDecryptor({
@@ -593,13 +599,13 @@ export const kmsServiceFactory = ({
});
};
const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO) => {
const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
switch (dto.type) {
case KmsDataKey.SecretManager: {
return $getProjectSecretManagerKmsDataKey(dto.projectId);
return $getProjectSecretManagerKmsDataKey(dto.projectId, trx);
}
default: {
return $getOrgKmsDataKey(dto.orgId);
return $getOrgKmsDataKey(dto.orgId, trx);
}
}
};
@@ -607,8 +613,9 @@ export const kmsServiceFactory = ({
// by keeping the decrypted data key in inner scope
// none of the entities outside can interact directly or expose the data key
// NOTICE: If changing here update migrations/utils/kms
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => {
const dataKey = await $getDataKey(encryptionContext);
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
const dataKey = await $getDataKey(encryptionContext, trx);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return {

View File

@@ -108,7 +108,9 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
)
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
.where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
.orderBy("firstName")
.orderBy("lastName");
return members.map(({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
@@ -370,6 +372,7 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization),
db.ref("defaultMembershipRole").withSchema(TableName.Organization),
db.ref("externalId").withSchema(TableName.UserAliases)
)
.where({ isGhost: false });

View File

@@ -0,0 +1,54 @@
import { OrgMembershipRole } from "@app/db/schemas";
import { TFeatureSet } from "@app/ee/services/license/license-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
const RESERVED_ORG_ROLE_SLUGS = Object.values(OrgMembershipRole).filter((role) => role !== "custom");
// this is only for updating an org
export const getDefaultOrgMembershipRoleForUpdateOrg = async ({
membershipRoleSlug,
orgRoleDAL,
plan,
orgId
}: {
orgId: string;
membershipRoleSlug: string;
orgRoleDAL: TOrgRoleDALFactory;
plan: TFeatureSet;
}) => {
const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(membershipRoleSlug as OrgMembershipRole);
if (isCustomRole) {
if (!plan?.rbac)
throw new BadRequestError({
message:
"Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role."
});
const customRole = await orgRoleDAL.findOne({ slug: membershipRoleSlug, orgId });
if (!customRole) throw new NotFoundError({ name: "UpdateOrg", message: "Organization role not found" });
// use ID for default role
return customRole.id;
}
// not custom, use reserved slug
return membershipRoleSlug;
};
// this is only for creating an org membership
export const getDefaultOrgMembershipRole = async (
defaultOrgMembershipRole: string // can either be ID or reserved slug
) => {
const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(defaultOrgMembershipRole as OrgMembershipRole);
if (isCustomRole)
return {
roleId: defaultOrgMembershipRole,
role: OrgMembershipRole.Custom
};
// will be reserved slug
return { roleId: undefined, role: defaultOrgMembershipRole as OrgMembershipRole };
};

View File

@@ -11,6 +11,7 @@ import {
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { ActorAuthMethod } from "../auth/auth-type";
import { TOrgRoleDALFactory } from "./org-role-dal";
@@ -18,11 +19,12 @@ import { TOrgRoleDALFactory } from "./org-role-dal";
type TOrgRoleServiceFactoryDep = {
orgRoleDAL: TOrgRoleDALFactory;
permissionService: TPermissionServiceFactory;
orgDAL: TOrgDALFactory;
};
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRoleServiceFactoryDep) => {
export const orgRoleServiceFactory = ({ orgRoleDAL, orgDAL, permissionService }: TOrgRoleServiceFactoryDep) => {
const createRole = async (
userId: string,
orgId: string,
@@ -129,6 +131,19 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
const org = await orgDAL.findOrgById(orgId);
if (!org)
throw new NotFoundError({
message: "Failed to find organization"
});
if (org.defaultMembershipRole === roleId)
throw new BadRequestError({
message: "Cannot delete default org membership role. Please re-assign and try again."
});
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
if (!deletedRole) throw new NotFoundError({ message: "Organization role not found", name: "Update role" });

View File

@@ -32,6 +32,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@@ -264,7 +265,7 @@ export const orgServiceFactory = ({
actorOrgId,
actorAuthMethod,
orgId,
data: { name, slug, authEnforced, scimEnabled }
data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug }
}: TUpdateOrgDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
@@ -298,11 +299,22 @@ export const orgServiceFactory = ({
});
}
let defaultMembershipRole: string | undefined;
if (defaultMembershipRoleSlug) {
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
membershipRoleSlug: defaultMembershipRoleSlug,
orgId,
orgRoleDAL,
plan
});
}
const org = await orgDAL.updateById(orgId, {
name,
slug: slug ? slugify(slug) : undefined,
authEnforced,
scimEnabled
scimEnabled,
defaultMembershipRole
});
if (!org) throw new NotFoundError({ message: "Organization not found" });
return org;

View File

@@ -26,18 +26,13 @@ export type TDeleteOrgMembershipDTO = {
};
export type TInviteUserToOrgDTO = {
actorId: string;
actor: ActorType;
orgId: string;
actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod;
inviteeEmails: string[];
organizationRoleSlug: string;
projects?: {
id: string;
projectRoleSlug?: string[];
}[];
};
} & TOrgPermission;
export type TVerifyUserToOrgDTO = {
email: string;
@@ -63,7 +58,13 @@ export type TFindAllWorkspacesDTO = {
};
export type TUpdateOrgDTO = {
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>;
data: Partial<{
name: string;
slug: string;
authEnforced: boolean;
scimEnabled: boolean;
defaultMembershipRoleSlug: string;
}>;
} & TOrgPermission;
export type TGetOrgGroupsDTO = TOrgPermission;

View File

@@ -147,6 +147,7 @@ export const projectServiceFactory = ({
workspaceName,
slug: projectSlug,
kmsKeyId,
tx: trx,
createDefaultEnvs = true
}: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId });
@@ -169,7 +170,7 @@ export const projectServiceFactory = ({
});
}
const results = await projectDAL.transaction(async (tx) => {
const results = await (trx || projectDAL).transaction(async (tx) => {
const ghostUser = await orgService.addGhostUser(organization.id, tx);
if (kmsKeyId) {

View File

@@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TProjectKeys } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
@@ -30,6 +32,7 @@ export type TCreateProjectDTO = {
slug?: string;
kmsKeyId?: string;
createDefaultEnvs?: boolean;
tx?: Knex;
};
export type TDeleteProjectBySlugDTO = {

View File

@@ -82,7 +82,10 @@ export const fnSecretBulkInsert = async ({
})
);
const newSecrets = await secretDAL.insertMany(sanitizedInputSecrets.map((el) => ({ ...el, folderId })));
const newSecrets = await secretDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
tx
);
const newSecretGroupedByKeyName = groupBy(newSecrets, (item) => item.key);
const newSecretTags = inputSecrets.flatMap(({ tagIds: secretTags = [], key }) =>
secretTags.map((tag) => ({

View File

@@ -85,7 +85,7 @@ type TSecretQueueFactoryDep = {
secretTagDAL: TSecretTagDALFactory;
userDAL: Pick<TUserDALFactory, "findById">;
secretVersionTagDAL: TSecretVersionTagDALFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
kmsService: TKmsServiceFactory;
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "batchInsert" | "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "batchInsert">;

View File

@@ -34,7 +34,10 @@ export enum SmtpTemplates {
WorkspaceInvite = "workspaceInvitation.handlebars",
ScimUserProvisioned = "scimUserProvisioned.handlebars",
PkiExpirationAlert = "pkiExpirationAlert.handlebars",
IntegrationSyncFailed = "integrationSyncFailed.handlebars"
IntegrationSyncFailed = "integrationSyncFailed.handlebars",
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
ExternalImportFailed = "externalImportFailed.handlebars",
ExternalImportStarted = "externalImportStarted.handlebars"
}
export enum SmtpHost {

View File

@@ -0,0 +1,21 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Import failed</title>
</head>
<body>
<h2>An import from {{provider}} to Infisical has failed</h2>
<p>An import from
{{provider}}
to Infisical has failed due to unforeseen circumstances. Please re-try your import, and if the issue persists, you
can contact the Infisical team at team@infisical.com.
</p>
<p>Error: {{error}}</p>
</body>
</html>

View File

@@ -0,0 +1,17 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Import in progress</title>
</head>
<body>
<h2>An import from {{provider}} to Infisical is in progress</h2>
<p>An import from
{{provider}}
to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import
has finished or if it fails.</p>
</body>
</html>

View File

@@ -0,0 +1,14 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Import successful</title>
</head>
<body>
<h2>An import from {{provider}} to Infisical was successful</h2>
<p>An import from {{provider}} was successful. Your data is now available in Infisical.</p>
</body>
</html>

View File

@@ -4,6 +4,27 @@ title: "Changelog"
The changelog below reflects new product developments and updates on a monthly basis.
## September 2024
- Improved paginations for identities and secrets.
- Significant improvements to the [Infisical Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs).
- Created [Slack Integration](https://infisical.com/docs/documentation/platform/workflow-integrations/slack-integration#slack-integration) for Access Requests and Approval Workflows.
- Added Dynamic Secrets for [Elaticsearch](https://infisical.com/docs/documentation/platform/dynamic-secrets/elastic-search) and [MongoDB](https://infisical.com/docs/documentation/platform/dynamic-secrets/mongo-db).
- More authentication methods are now supported by Infisical SDKs and Agent.
- Integrations now have dedicated audit logs and an overview screen.
- Added support for secret referencing in the Terraform Provider.
- Released support for [older versions of .NET](https://www.nuget.org/packages/Infisical.Sdk#supportedframeworks-body-tab) via SDK.
- Released Infisical PKI Issuer which works alongside `cert-manager` to manage certificates in Kubernetes.
## August 2024
- Added [Azure DevOps integration](https://infisical.com/docs/integrations/cloud/azure-devops).
- Released ability to hot-reload variables in CLI ([--watch flag](https://infisical.com/docs/cli/commands/run#infisical-run:watch)).
- Added Dynamic Secrets for [Redis](https://infisical.com/docs/documentation/platform/dynamic-secrets/redis).
- Added [Alerting](https://infisical.com/docs/documentation/platform/pki/alerting) for Certificate Management.
- You can now specify roles and project memberships when adding new users.
- Approval workflows now have email notifications.
- Access requests are now integrated with User Groups.
- Released ability to use IAM Roles for AWS Integrations.
## July 2024
- Released the official [Ruby SDK](https://infisical.com/docs/sdks/languages/ruby).
- Increased the speed and efficiency of secret operations.

View File

@@ -59,6 +59,7 @@ Redis requirements:
- Use Redis versions 6.x or 7.x. We advise upgrading to at least Redis 6.2.
- Redis Cluster mode is currently not supported; use Redis Standalone, with or without High Availability (HA).
- Redis storage needs are minimal: a setup with 2 vCPU, 4 GB RAM, and 30GB SSD will be sufficient for small deployments.
- Set cache eviction policy to `noeviction`.
## Supported Web Browsers

View File

@@ -88,6 +88,7 @@
"react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-select": "^5.8.1",
"react-table": "^7.8.0",
"react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1",
@@ -2505,15 +2506,16 @@
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
"integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz",
"integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.1",
"@emotion/memoize": "^0.8.1",
"@emotion/serialize": "^1.1.2",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.2.0",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
@@ -2522,18 +2524,31 @@
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/cache": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
"integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
"version": "11.13.1",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz",
"integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.8.1",
"@emotion/sheet": "^1.2.2",
"@emotion/utils": "^1.2.1",
"@emotion/weak-memoize": "^0.3.1",
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.0",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/cache/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/css": {
"version": "11.11.2",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz",
@@ -2547,9 +2562,10 @@
}
},
"node_modules/@emotion/hash": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
"integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
@@ -2571,18 +2587,49 @@
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
},
"node_modules/@emotion/serialize": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz",
"integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==",
"node_modules/@emotion/react": {
"version": "11.13.3",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz",
"integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.1",
"@emotion/memoize": "^0.8.1",
"@emotion/unitless": "^0.8.1",
"@emotion/utils": "^1.2.1",
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.12.0",
"@emotion/cache": "^11.13.0",
"@emotion/serialize": "^1.3.1",
"@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
"@emotion/utils": "^1.4.0",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz",
"integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.1",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/serialize/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/server": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/server/-/server-11.11.0.tgz",
@@ -2603,9 +2650,10 @@
}
},
"node_modules/@emotion/sheet": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
"integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/stylis": {
"version": "0.8.5",
@@ -2613,28 +2661,31 @@
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
},
"node_modules/@emotion/unitless": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
"dev": true,
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz",
"integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
"integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz",
"integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
@@ -8844,6 +8895,15 @@
"redux": "^4.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.11",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz",
"integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==",
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.20.6",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
@@ -12668,6 +12728,16 @@
"utila": "~0.4"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -21113,6 +21183,33 @@
"react": ">= 16.3"
}
},
"node_modules/react-select": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz",
"integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.8.1",
"@floating-ui/dom": "^1.0.1",
"@types/react-transition-group": "^4.4.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.6.0",
"react-transition-group": "^4.3.0",
"use-isomorphic-layout-effect": "^1.1.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-select/node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -21167,6 +21264,22 @@
"node": ">=6"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -24242,6 +24355,20 @@
}
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-memo-one": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",

View File

@@ -101,6 +101,7 @@
"react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-select": "^5.8.1",
"react-table": "^7.8.0",
"react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1",

View File

@@ -0,0 +1,104 @@
import Select, {
ClearIndicatorProps,
components,
DropdownIndicatorProps,
MultiValueRemoveProps,
OptionProps,
Props
} from "react-select";
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
import { faChevronDown, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
const DropdownIndicator = (props: DropdownIndicatorProps) => {
return (
<components.DropdownIndicator {...props}>
<FontAwesomeIcon icon={faChevronDown} size="xs" />
</components.DropdownIndicator>
);
};
const ClearIndicator = (props: ClearIndicatorProps) => {
return (
<components.ClearIndicator {...props}>
<FontAwesomeIcon icon={faCircleXmark} />
</components.ClearIndicator>
);
};
const MultiValueRemove = (props: MultiValueRemoveProps) => {
return (
<components.MultiValueRemove {...props}>
<FontAwesomeIcon icon={faX} size="xs" />
</components.MultiValueRemove>
);
};
const Option = ({ isSelected, children, ...props }: OptionProps) => {
return (
<components.Option isSelected={isSelected} {...props}>
{children}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</components.Option>
);
};
export const MultiSelect = (props: Props) => (
<Select
isMulti
closeMenuOnSelect={false}
hideSelectedOptions={false}
unstyled
styles={{
input: (base) => ({
...base,
"input:focus": {
boxShadow: "none"
}
}),
multiValueLabel: (base) => ({
...base,
whiteSpace: "normal",
overflow: "visible"
}),
control: (base) => ({
...base,
transition: "none"
})
}}
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
classNames={{
container: () => "w-full font-inter",
control: ({ isFocused }) =>
twMerge(
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
"border w-full p-0.5 rounded-md font-inter bg-mineshaft-900 hover:cursor-pointer"
),
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
input: () => "pl-1 py-0.5",
valueContainer: () => "p-1 max-h-[14rem] !overflow-y-scroll gap-1",
singleValue: () => "leading-7 ml-1",
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
multiValueLabel: () => "leading-6 text-sm",
multiValueRemove: () => "hover:text-red text-bunker-400",
indicatorsContainer: () => "p-1 gap-1",
clearIndicator: () => "p-1 hover:text-red text-bunker-400",
indicatorSeparator: () => "bg-bunker-400",
dropdownIndicator: () => "text-bunker-200 p-1",
menu: () =>
"mt-2 border text-sm text-mineshaft-200 bg-mineshaft-900 border-mineshaft-600 rounded-md",
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
option: ({ isFocused, isSelected }) =>
twMerge(
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
isSelected && "text-mineshaft-400",
"hover:cursor-pointer text-xs px-3 py-2"
),
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
}}
{...props}
/>
);

View File

@@ -0,0 +1 @@
export * from "./MultiSelect";

View File

@@ -18,6 +18,7 @@ export * from "./IconButton";
export * from "./Input";
export * from "./Menu";
export * from "./Modal";
export * from "./MultiSelect";
export * from "./NoticeBanner";
export * from "./Pagination";
export * from "./Popoverv2";

View File

@@ -8,21 +8,27 @@ export const useImportEnvKey = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
encryptedJson,
decryptionKey
}: {
encryptedJson: {
nonce: string;
data: string;
};
decryptionKey: string;
}) => {
const { data } = await apiRequest.post("/api/v3/migrate/env-key/", {
encryptedJson,
decryptionKey
});
return data;
mutationFn: async ({ file, decryptionKey }: { file: File; decryptionKey: string }) => {
const formData = new FormData();
formData.append("decryptionKey", decryptionKey);
formData.append("file", file);
try {
const response = await apiRequest.post("/api/v3/migrate/env-key/", formData, {
headers: {
"Content-Type": "multipart/form-data"
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
console.log(`Upload Progress: ${percentCompleted}%`);
}
});
console.log("Upload successful:", response.data);
} catch (error) {
console.error("Upload failed:", error);
}
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);

View File

@@ -82,12 +82,13 @@ export const useCreateOrg = (options: { invalidate: boolean } = { invalidate: tr
export const useUpdateOrg = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateOrgDTO>({
mutationFn: ({ name, authEnforced, scimEnabled, slug, orgId }) => {
mutationFn: ({ name, authEnforced, scimEnabled, slug, orgId, defaultMembershipRoleSlug }) => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name,
authEnforced,
scimEnabled,
slug
slug,
defaultMembershipRoleSlug
});
},
onSuccess: () => {

View File

@@ -10,6 +10,7 @@ export type Organization = {
orgAuthMethod: string;
scimEnabled: boolean;
slug: string;
defaultMembershipRole: string;
};
export type UpdateOrgDTO = {
@@ -18,6 +19,7 @@ export type UpdateOrgDTO = {
authEnforced?: boolean;
scimEnabled?: boolean;
slug?: string;
defaultMembershipRoleSlug?: string;
};
export type BillingDetails = {

View File

@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import {
faCheckCircle,
@@ -33,6 +34,7 @@ import {
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { isCustomOrgRole } from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable";
import { OrgInviteLink } from "./OrgInviteLink";
@@ -78,7 +80,20 @@ export const AddOrgMemberModal = ({
watch,
reset,
formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
} = useForm<TAddMemberForm>({
resolver: zodResolver(addMemberFormSchema)
});
// set initial form role based off org default role
useEffect(() => {
if (organizationRoles) {
reset({
organizationRoleSlug: isCustomOrgRole(currentOrg?.defaultMembershipRole!)
? organizationRoles?.find((role) => role.id === currentOrg?.defaultMembershipRole)?.slug!
: currentOrg?.defaultMembershipRole
});
}
}, [organizationRoles]);
const selectedProjectIds = watch("projectIds", []);
@@ -207,7 +222,6 @@ export const AddOrgMemberModal = ({
<div>
<Select
className="w-full"
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
{...field}
onValueChange={(val) => field.onChange(val)}
>

View File

@@ -6,6 +6,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
@@ -19,14 +20,30 @@ import {
Td,
Th,
THead,
Tr
Tooltip,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteOrgRole, useGetOrgRoles } from "@app/hooks/api";
import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
import { TOrgRole } from "@app/hooks/api/roles/types";
import { RoleModal } from "@app/views/Org/RolePage/components";
enum OrgMembershipRole {
Admin = "admin",
Member = "member",
NoAccess = "no-access"
}
export const isCustomOrgRole = (slug: string) =>
!Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole);
export const OrgRoleTable = () => {
const router = useRouter();
const { currentOrg } = useOrganization();
@@ -34,12 +51,14 @@ export const OrgRoleTable = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"role",
"deleteRole"
"deleteRole",
"upgradePlan"
] as const);
const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(orgId);
const { mutateAsync: deleteRole } = useDeleteOrgRole();
const { mutateAsync: updateOrg } = useUpdateOrg();
const { subscription } = useSubscription();
const handleRoleDelete = async () => {
const { id } = popUp?.deleteRole?.data as TOrgRole;
@@ -56,6 +75,30 @@ export const OrgRoleTable = () => {
}
};
const handleSetRoleAsDefault = async (defaultMembershipRoleSlug: string) => {
const isCustomRole = isCustomOrgRole(defaultMembershipRoleSlug);
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
description:
"You can set the default org role to a custom role if you upgrade your Infisical plan."
});
return;
}
try {
await updateOrg({
orgId,
defaultMembershipRoleSlug
});
createNotification({ type: "success", text: "Successfully updated default membership role" });
handlePopUpClose("deleteRole");
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to update default membership role" });
}
};
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
@@ -90,14 +133,30 @@ export const OrgRoleTable = () => {
{roles?.map((role) => {
const { id, name, slug } = role;
const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
const isDefaultOrgRole = isCustomOrgRole(slug)
? id === currentOrg?.defaultMembershipRole
: slug === currentOrg?.defaultMembershipRole;
return (
<Tr
key={`role-list-${id}`}
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => router.push(`/org/${orgId}/roles/${id}`)}
>
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
{name}
<Td className="max-w-md">
<div className="flex">
<p className="overflow-hidden text-ellipsis whitespace-nowrap">{name}</p>
{isDefaultOrgRole && (
<Tooltip
content={`Members joining your organization will be assigned the ${name} role unless otherwise specified.`}
>
<div>
<Badge variant="success" className="ml-1">
Default
</Badge>
</div>
</Tooltip>
)}
</div>
</Td>
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
{slug}
@@ -129,29 +188,61 @@ export const OrgRoleTable = () => {
</DropdownMenuItem>
)}
</OrgPermissionCan>
{!isNonMutatable && (
{!isDefaultOrgRole && (
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Role}
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
disabled={!isAllowed}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteRole", role);
handleSetRoleAsDefault(slug);
}}
disabled={!isAllowed}
>
Delete Role
Set as Default Role
</DropdownMenuItem>
)}
</OrgPermissionCan>
)}
{!isNonMutatable && (
<Tooltip
position="left"
content={
isDefaultOrgRole
? "Cannot delete default organization membership role. Re-assign default to allow deletion."
: ""
}
>
<div>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Role}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed && !isDefaultOrgRole
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteRole", role);
}}
disabled={!isAllowed || isDefaultOrgRole}
>
Delete Role
</DropdownMenuItem>
)}
</OrgPermissionCan>
</div>
</Tooltip>
)}
</DropdownMenuContent>
</DropdownMenu>
</Td>
@@ -172,6 +263,11 @@ export const OrgRoleTable = () => {
onClose={() => handlePopUpClose("deleteRole")}
onDeleteApproved={handleRoleDelete}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};

View File

@@ -17,7 +17,8 @@ import {
DropdownMenuTrigger,
FormControl,
Modal,
ModalContent
ModalContent,
MultiSelect
} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
@@ -31,7 +32,7 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = z.object({
orgMembershipIds: z.array(z.string().trim()).min(1),
orgMemberships: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).min(1),
projectRoleSlugs: z.array(z.string().trim().min(1)).min(1)
});
@@ -60,20 +61,20 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
handleSubmit,
reset,
watch,
formState: { isSubmitting }
formState: { isSubmitting, errors }
} = useForm<TAddMemberForm>({
resolver: zodResolver(addMemberFormSchema),
defaultValues: { orgMembershipIds: [], projectRoleSlugs: [ProjectMembershipRole.Member] }
defaultValues: { orgMemberships: [], projectRoleSlugs: [ProjectMembershipRole.Member] }
});
const { mutateAsync: addMembersToProject } = useAddUsersToOrg();
const onAddMember = async ({ orgMembershipIds, projectRoleSlugs }: TAddMemberForm) => {
const onAddMembers = async ({ orgMemberships, projectRoleSlugs }: TAddMemberForm) => {
if (!currentWorkspace) return;
if (!currentOrg?.id) return;
const selectedMembers = orgMembershipIds.map((orgMembershipId) =>
orgUsers?.find((orgUser) => orgUser.id === orgMembershipId)
const selectedMembers = orgMemberships.map((orgMembership) =>
orgUsers?.find((orgUser) => orgUser.id === orgMembership.value)
);
if (!selectedMembers) return;
@@ -118,10 +119,15 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
members?.forEach((member) => {
wsUserUsernames.set(member.user.username, true);
});
return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username));
return (orgUsers || [])
.filter(({ user: u }) => !wsUserUsernames.has(u.username))
.map((member) => ({
value: member.id,
label: `${member.user.firstName} ${member.user.lastName}`
}));
}, [orgUsers, members]);
const selectedOrgMembershipIds = watch("orgMembershipIds");
const selectedOrgMemberships = watch("orgMemberships");
const selectedRoleSlugs = watch("projectRoleSlugs");
return (
@@ -130,175 +136,115 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
onOpenChange={(isOpen) => handlePopUpToggle("addMember", isOpen)}
>
<ModalContent
bodyClassName="overflow-visible"
title={t("section.members.add-dialog.add-member-to-project") as string}
subTitle={t("section.members.add-dialog.user-will-email")}
>
{filteredOrgUsers.length ? (
<form onSubmit={handleSubmit(onAddMember)}>
<div className="flex w-full items-center gap-2">
<div className="w-full">
<Controller
control={control}
name="orgMembershipIds"
render={({ field }) => (
<FormControl label="Invite users to project">
<DropdownMenu>
<DropdownMenuTrigger asChild>
{filteredOrgUsers && filteredOrgUsers.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedOrgMembershipIds.length === 1
? filteredOrgUsers.find(
(orgUser) => orgUser.id === selectedOrgMembershipIds[0]
)?.user.username
: selectedOrgMembershipIds.length === 0
? "No users selected"
: `${selectedOrgMembershipIds.length} users selected`}
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No users found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{filteredOrgUsers && filteredOrgUsers.length > 0 ? (
filteredOrgUsers.map((member) => {
const isSelected = selectedOrgMembershipIds.includes(member.id);
<form onSubmit={handleSubmit(onAddMembers)}>
<div className="flex w-full flex-col items-start gap-2">
<Controller
control={control}
name="orgMemberships"
render={({ field }) => (
<FormControl
className="w-full"
isError={!!errors.orgMemberships?.length}
errorText={errors.orgMemberships?.[0]?.message}
label="Invite users to project"
>
<MultiSelect
className="w-full"
placeholder="Add one or more users..."
isMulti
name="members"
options={filteredOrgUsers}
value={field.value}
onChange={field.onChange}
/>
</FormControl>
)}
/>
return (
<DropdownMenuItem
onSelect={(event) =>
filteredOrgUsers.length > 1 && event.preventDefault()
}
onClick={() => {
if (selectedOrgMembershipIds.includes(String(member.id))) {
field.onChange(
selectedOrgMembershipIds.filter(
(membershipId: string) =>
membershipId !== String(member.id)
)
);
} else {
field.onChange([
...selectedOrgMembershipIds,
String(member.id)
]);
}
}}
key={`membership-id-${member.id}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{member.user.username}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
<Controller
control={control}
name="projectRoleSlugs"
render={({ field }) => (
<FormControl
className="w-full"
label="Select roles"
tooltipText="Select the roles that you wish to assign to the users"
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{roles && roles.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedRoleSlugs.length === 1
? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name
: selectedRoleSlugs.length === 0
? "Select at least one role"
: `${selectedRoleSlugs.length} roles selected`}
<FontAwesomeIcon
icon={faChevronDown}
className={twMerge("ml-2 text-xs")}
/>
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No roles found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{roles && roles.length > 0 ? (
roles.map((role) => {
const isSelected = selectedRoleSlugs.includes(role.slug);
<div className="flex min-w-fit justify-end">
<Controller
control={control}
name="projectRoleSlugs"
render={({ field }) => (
<FormControl
label="Select roles"
tooltipText="Select the roles that you wish to assign to the users"
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{roles && roles.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedRoleSlugs.length === 1
? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name
: selectedRoleSlugs.length === 0
? "Select at least one role"
: `${selectedRoleSlugs.length} roles selected`}
<FontAwesomeIcon
icon={faChevronDown}
className={twMerge("ml-2 text-xs")}
/>
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No roles found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{roles && roles.length > 0 ? (
roles.map((role) => {
const isSelected = selectedRoleSlugs.includes(role.slug);
return (
<DropdownMenuItem
onSelect={(event) => roles.length > 1 && event.preventDefault()}
onClick={() => {
if (selectedRoleSlugs.includes(String(role.slug))) {
field.onChange(
selectedRoleSlugs.filter(
(roleSlug: string) => roleSlug !== String(role.slug)
)
);
} else {
field.onChange([...selectedRoleSlugs, role.slug]);
}
}}
key={`role-slug-${role.slug}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
return (
<DropdownMenuItem
onSelect={(event) => roles.length > 1 && event.preventDefault()}
onClick={() => {
if (selectedRoleSlugs.includes(String(role.slug))) {
field.onChange(
selectedRoleSlugs.filter(
(roleSlug: string) => roleSlug !== String(role.slug)
)
);
} else {
field.onChange([...selectedRoleSlugs, role.slug]);
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{role.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
}}
key={`role-slug-${role.slug}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{role.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
@@ -307,7 +253,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
isLoading={isSubmitting}
isDisabled={
isSubmitting ||
selectedOrgMembershipIds.length === 0 ||
selectedOrgMemberships.length === 0 ||
selectedRoleSlugs.length === 0
}
>

View File

@@ -13,17 +13,13 @@ type Props = {
onClose: () => void;
};
const formSchema = z.object({
encryptionKey: z.string().min(1),
encryptedJson: z.object({
nonce: z.string().min(1),
data: z.string().min(1)
})
});
type TFormData = z.infer<typeof formSchema>;
export const EnvKeyPlatformModal = ({ onClose }: Props) => {
const formSchema = z.object({
encryptionKey: z.string().min(1),
file: z.instanceof(File)
});
type TFormData = z.infer<typeof formSchema>;
const fileUploadRef = useRef<HTMLInputElement>(null);
const { mutateAsync: importEnvKey } = useImportEnvKey();
@@ -40,8 +36,8 @@ export const EnvKeyPlatformModal = ({ onClose }: Props) => {
});
const onSubmit = async (data: TFormData) => {
if (!data.encryptedJson) {
setError("encryptedJson", {
if (!data.file) {
setError("file", {
type: "required",
message: "File is required"
});
@@ -50,12 +46,13 @@ export const EnvKeyPlatformModal = ({ onClose }: Props) => {
try {
await importEnvKey({
encryptedJson: data.encryptedJson,
file: data.file,
decryptionKey: data.encryptionKey
});
createNotification({
text: "Data imported successfully.",
type: "success"
title: "Import started",
text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.",
type: "info"
});
onClose();
@@ -70,7 +67,6 @@ export const EnvKeyPlatformModal = ({ onClose }: Props) => {
};
const onImportFileDrop = (file?: File) => {
const reader = new FileReader();
if (!file) {
createNotification({
text: "No file selected.",
@@ -78,40 +74,8 @@ export const EnvKeyPlatformModal = ({ onClose }: Props) => {
});
return;
}
reader.onload = (event) => {
if (!event?.target?.result) return;
const droppedFile = event.target.result.toString();
const formattedData: Record<string, string> = JSON.parse(droppedFile);
if (
Object.keys(formattedData).includes("nonce") &&
Object.keys(formattedData).includes("data")
) {
const data = {
nonce: formattedData.nonce,
data: formattedData.data
};
setValue("encryptedJson", data, { shouldDirty: true, shouldValidate: true });
} else {
setValue(
"encryptedJson",
{
nonce: "",
data: ""
},
{ shouldDirty: true, shouldValidate: true }
);
if (fileUploadRef.current) {
fileUploadRef.current.value = "";
}
createNotification({
text: "Improper file format, please upload the EnvKey export.",
type: "error"
});
}
};
reader.readAsText(file);
setValue("file", file, { shouldDirty: true, shouldValidate: true });
};
return (

View File

@@ -1,13 +1,19 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api";
import { Button, FormControl, Input, Select, SelectItem, Spinner } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import { useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
import { isCustomOrgRole } from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable";
const formSchema = yup.object({
name: yup
@@ -19,36 +25,55 @@ const formSchema = yup.object({
.string()
.matches(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
.required()
.label("Organization Slug")
.label("Organization Slug"),
defaultMembershipRole: yup.string().required().label("Default Membership Role")
});
type FormData = yup.InferType<typeof formSchema>;
export const OrgNameChangeSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { permission } = useOrgPermission();
const { handleSubmit, control, reset } = useForm<FormData>({
resolver: yupResolver(formSchema)
});
const { mutateAsync, isLoading } = useUpdateOrg();
const canReadOrgRoles = permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(
currentOrg?.id!,
canReadOrgRoles
);
const [isFormInitialized, setIsFormInitialized] = useState(false);
useEffect(() => {
if (currentOrg) {
reset({
name: currentOrg.name,
slug: currentOrg.slug
slug: currentOrg.slug,
...(canReadOrgRoles &&
roles?.length && {
// will always be present, can't remove role if default
defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole)
? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug!
: currentOrg.defaultMembershipRole
})
});
setIsFormInitialized(true);
}
}, [currentOrg]);
}, [currentOrg, roles]);
const onFormSubmit = async ({ name, slug }: FormData) => {
const onFormSubmit = async ({ name, slug, defaultMembershipRole }: FormData) => {
try {
if (!currentOrg?.id) return;
if (!currentOrg?.id || !roles?.length) return;
await mutateAsync({
orgId: currentOrg?.id,
name,
slug
slug,
defaultMembershipRoleSlug: defaultMembershipRole
});
createNotification({
@@ -64,6 +89,14 @@ export const OrgNameChangeSection = (): JSX.Element => {
}
};
if (!isFormInitialized) {
return (
<div className="flex h-[25.25rem] w-full items-center justify-center">
<Spinner size="lg" />
</div>
);
}
return (
<form onSubmit={handleSubmit(onFormSubmit)} className="py-4">
<div className="">
@@ -92,6 +125,40 @@ export const OrgNameChangeSection = (): JSX.Element => {
name="slug"
/>
</div>
{canReadOrgRoles && (
<div className="pb-4">
<h2 className="text-md mb-2 text-mineshaft-100">Default Organization Member Role</h2>
<p className="text-mineshaft-400" />
<Controller
defaultValue=""
control={control}
name="defaultMembershipRole"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
helperText="Users joining your organization will be assigned this role unless otherwise specified."
isError={Boolean(error)}
errorText={error?.message}
className="max-w-md"
>
<Select
isDisabled={isRolesLoading}
className="w-full capitalize"
value={value}
onValueChange={!roles?.length ? undefined : onChange}
>
{roles?.map((role) => {
return (
<SelectItem key={role.id} value={role.slug}>
{role.name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
</div>
)}
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button

View File

@@ -176,12 +176,16 @@ export const UserInfoSSOStep = ({
organizationId: orgId
});
const project = await ProjectService.initProject({
projectName: "Example Project"
});
// only create example project if not joining existing org
if (!providerOrganizationName) {
const project = await ProjectService.initProject({
projectName: "Example Project"
});
localStorage.setItem("projectData.id", project.id);
}
localStorage.setItem("orgData.id", orgId);
localStorage.setItem("projectData.id", project.id);
setStep(2);
} catch (error) {

View File

@@ -7,7 +7,7 @@ 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: 1.0.8
version: 1.2.0
# 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

View File

@@ -55,6 +55,13 @@ spec:
ports:
- containerPort: 8080
env:
{{- if .Values.postgresql.useExistingPostgresSecret.enabled }}
- name: DB_CONNECTION_URI
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.useExistingPostgresSecret.existingConnectionStringSecret.name }}
key: {{ .Values.postgresql.useExistingPostgresSecret.existingConnectionStringSecret.key }}
{{- end }}
{{- if .Values.postgresql.enabled }}
- name: DB_CONNECTION_URI
value: {{ include "infisical.postgresDBConnectionString" . }}

View File

@@ -2,6 +2,7 @@ apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: k8s-wait-for-infisical-schema-migration
namespace: {{ .Release.Namespace }}
rules:
- apiGroups: ["batch"]
resources: ["jobs"]
@@ -10,11 +11,12 @@ rules:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: default
name: infisical-database-schema-migration
namespace: {{ .Release.Namespace }}
subjects:
- kind: ServiceAccount
name: default
namespace: {{ .Release.Namespace }}
name: {{ .Values.infisical.databaseSchemaMigrationJob.serviceAccountName | default "default" }}
namespace: {{ .Values.infisical.databaseSchemaMigrationJob.serviceAccountNamespace | default .Release.Namespace }}
roleRef:
kind: Role
name: k8s-wait-for-infisical-schema-migration

View File

@@ -16,6 +16,7 @@ spec:
app.kubernetes.io/instance: {{ .Release.Name | quote }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
serviceAccountName: {{ .Values.infisical.databaseSchemaMigrationJob.serviceAccountName | default "default" }}
{{- if $infisicalValues.image.imagePullSecrets }}
imagePullSecrets:
{{- toYaml $infisicalValues.image.imagePullSecrets | nindent 6 }}
@@ -26,6 +27,13 @@ spec:
image: "{{ $infisicalValues.image.repository }}:{{ $infisicalValues.image.tag }}"
command: ["npm", "run", "migration:latest"]
env:
{{- if .Values.postgresql.useExistingPostgresSecret.enabled }}
- name: DB_CONNECTION_URI
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.useExistingPostgresSecret.existingConnectionStringSecret.name }}
key: {{ .Values.postgresql.useExistingPostgresSecret.existingConnectionStringSecret.key }}
{{- end }}
{{- if .Values.postgresql.enabled }}
- name: DB_CONNECTION_URI
value: {{ include "infisical.postgresDBConnectionString" . }}

View File

@@ -5,6 +5,10 @@ infisical:
enabled: true
name: infisical
autoDatabaseSchemaMigration: true
databaseSchemaMigrationJob:
serviceAccountNamespace: default
serviceAccountName: default
fullnameOverride: ""
podAnnotations: {}
deploymentAnnotations: {}
@@ -18,6 +22,7 @@ infisical:
affinity: {}
kubeSecretRef: "infisical-secrets"
service:
annotations: {}
type: ClusterIP
@@ -43,6 +48,7 @@ ingress:
# - some.domain.com
postgresql:
# -- When enabled, this will start up a in cluster Postgres
enabled: true
name: "postgresql"
fullnameOverride: "postgresql"
@@ -50,6 +56,15 @@ postgresql:
username: infisical
password: root
database: infisicalDB
useExistingPostgresSecret:
# -- When this is enabled, postgresql.enabled needs to be false
enabled: false
# -- The name from where to get the existing postgresql connection string
existingConnectionStringSecret:
# -- The name of the secret that contains the postgres connection string
name: ""
# -- Secret key name that contains the postgres connection string
key: ""
redis:
enabled: true

View File

@@ -15,6 +15,21 @@ server {
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /api/v3/migrate {
client_max_body_size 25M;
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://backend:4000;
proxy_redirect off;
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /.well-known/est {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -17,6 +17,21 @@ server {
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /api/v3/migrate {
client_max_body_size 25M;
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://backend:4000;
proxy_redirect off;
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /.well-known/est {
proxy_set_header X-Real-RIP $remote_addr;