Compare commits

...

113 Commits

Author SHA1 Message Date
carlosmonastyrski
52bd1afb0a Move booleanSchema to sanitizedSchema - fix default value 2025-03-21 18:35:32 -03:00
carlosmonastyrski
d918dd8967 Move booleanSchema to sanitizedSchema 2025-03-21 18:29:55 -03:00
carlosmonastyrski
e2e0f6a346 Add recursive flag to folders get endpoint to retrieve all nested folders 2025-03-21 18:08:22 -03:00
carlosmonastyrski
3ffee049ee Merge pull request #3268 from Infisical/feat/addCustomDomainToWindmillIntegration
Add Windmill custom api url domain
2025-03-21 14:19:39 -03:00
Maidul Islam
524462d7bc Merge pull request #3276 from akhilmhdh/feat/folder-sql-improvement
Indexed and optimized folder queries
2025-03-21 09:55:10 -04:00
Vlad Matsiiako
351e573fea Merge pull request #3288 from Infisical/0xArshdeep-patch-1
Update terraform.mdx
2025-03-20 16:10:00 -07:00
ArshBallagan
f1bc26e2e5 Update terraform.mdx 2025-03-20 16:09:01 -07:00
Maidul Islam
8aeb607f6e Merge pull request #3287 from akhilmhdh/fix/patch-4
Fix/patch 4
2025-03-20 17:32:32 -04:00
=
e530b7a788 feat: reduced to two 2025-03-21 02:59:44 +05:30
=
bf61090b5a feat: added cache clear and refresh with session limit 2025-03-21 02:58:59 +05:30
Maidul Islam
106b068a51 Merge pull request #3286 from akhilmhdh/fix/patch-4
feat: removed refresh
2025-03-20 17:16:01 -04:00
=
6f0a97a2fa feat: removed refresh 2025-03-21 02:43:10 +05:30
Maidul Islam
5d604be091 Merge pull request #3285 from akhilmhdh/fix/patch-4
feat: return fastify res and making it async
2025-03-20 17:01:03 -04:00
=
905cf47d90 feat: return fastify res and making it async 2025-03-21 02:26:33 +05:30
Sheen
2c40d316f4 Merge pull request #3284 from Infisical/misc/add-flag-for-disabling-worker-queue
misc: add flag for disabling workers
2025-03-21 03:49:57 +08:00
Sheen Capadngan
32521523c1 misc: add flag for disabling workers 2025-03-21 03:46:17 +08:00
Sheen
3a2e8939b1 Merge pull request #3282 from Infisical/misc/add-event-loop-stats
misc: add event loop stats
2025-03-21 01:54:12 +08:00
Sheen Capadngan
e5947fcab9 misc: add event loop stats 2025-03-21 01:48:11 +08:00
Maidul Islam
a6d9c74054 Merge pull request #3272 from akhilmhdh/feat/metadata-audit-log
Permission metadata data in audit log
2025-03-20 13:47:52 -04:00
Daniel Hougaard
f7cf2bb78f Merge pull request #3278 from Infisical/daniel/kubernetes-hsm-docs
docs(hsm): kubernetes deployment docs
2025-03-20 03:25:48 +04:00
Daniel Hougaard
ff24e76a32 docs(hsm): kubernetes deployment docs, requested changes 2025-03-20 02:59:07 +04:00
Maidul Islam
6ac802b6c9 Merge pull request #3280 from akhilmhdh/fix/patch-4
feat: added k8s abort
2025-03-19 18:01:18 -04:00
=
ff92e00503 feat: added k8s abort 2025-03-20 03:29:40 +05:30
Maidul Islam
b20474c505 Merge pull request #3279 from akhilmhdh/fix/patch-4
feat: added log and updated license ttl to 5min
2025-03-19 17:11:12 -04:00
=
e19ffc91c6 feat: added more log 2025-03-20 02:40:37 +05:30
=
61eb66efca feat: added log and updated license ttl to 5min 2025-03-20 02:36:26 +05:30
Daniel Hougaard
15999daa24 docs(hsm): kubernetes deployment docs 2025-03-19 23:02:39 +04:00
carlosmonastyrski
82520a7f0a Check local urls for cloud instances on windmill custom domain input 2025-03-19 15:54:07 -03:00
Daniel Hougaard
ec31211bca Merge pull request #3277 from Infisical/daniel/fix-helm-required-field
fix(k8s): remove required field from helm
2025-03-19 21:31:45 +04:00
Daniel Hougaard
0ecf6044d9 fix(k8s): remove required field from helm 2025-03-19 20:50:28 +04:00
carlosmonastyrski
6c512f47bf Merge pull request #3274 from Infisical/fix/bulkDeleteSecretIncorrectPermissions
Bulk Delete Secret Incorrectly says lacking permission
2025-03-19 12:30:19 -03:00
=
c4b7d4618d feat: updated ui 2025-03-19 20:33:31 +05:30
=
003f2b003d feat: indexed and optimized folder queries 2025-03-19 19:46:05 +05:30
carlosmonastyrski
33b135f02c Merge pull request #3275 from Infisical/fix/universalAuthDocApiUrlImprovement
Universal Auth doc improvement to show US/EU API URLs
2025-03-19 11:10:23 -03:00
carlosmonastyrski
eed7cc6408 Add tip to universal auth doc to show US/EU API URLs 2025-03-19 10:49:33 -03:00
Sheen
440ada464f Merge pull request #3259 from Infisical/feat/automated-instance-bootstrapping
feat: automated bootstrapping
2025-03-19 21:40:55 +08:00
carlosmonastyrski
6b7abbbeb9 Return accum if no entry is found for env.slug on secretsToDelete to avoid losing reducer process due to permission.can 2025-03-19 09:10:15 -03:00
carlosmonastyrski
3944e20a5b Merge pull request #3257 from Infisical/feat/addFileImportToSecretSetCLI
Add file option to secret set CLI command to retrieve secrets from .env or .yaml files
2025-03-19 08:01:11 -03:00
=
747b5ec68d feat: updated doc 2025-03-19 15:00:36 +05:30
Sheen
2079913511 misc: final doc updates 2025-03-19 08:08:11 +00:00
=
ed0dc324a3 feat: updated audit log ui 2025-03-19 13:28:26 +05:30
=
1c13ed54af feat: updated audit log to permission metadataw 2025-03-19 13:28:26 +05:30
Sheen Capadngan
049f0f56a0 misc: added env support for the other cli flags 2025-03-19 15:44:56 +08:00
BlackMagiq
9ad725fd6c Merge pull request #3271 from Infisical/vmatsiiako-patch-cure53-1
Update security.mdx
2025-03-18 19:37:29 -07:00
Vlad Matsiiako
9a954c8f15 Update security.mdx 2025-03-18 19:32:26 -07:00
Daniel Hougaard
81a64d081c Merge pull request #3270 from Infisical/daniel/helm-custom-volume-support
feat(helm): custom volume support
2025-03-19 06:22:59 +04:00
Daniel Hougaard
43804f62e6 Update CHANGELOG.md 2025-03-19 06:18:48 +04:00
Daniel Hougaard
67089af17a feat(helm): custom volume support 2025-03-19 05:39:00 +04:00
carlosmonastyrski
8abfea0409 Fix getAppsWindmill url field type 2025-03-18 19:14:49 -03:00
carlosmonastyrski
ce4adccc80 Add Windmill custom api url domain to connection details page 2025-03-18 19:07:11 -03:00
carlosmonastyrski
dcd3b5df56 Add Windmill custom api url domain 2025-03-18 19:01:52 -03:00
Maidul Islam
d83240749f Merge pull request #3265 from Infisical/minor-changes
Minor changes to oidc claims and mappings
2025-03-18 17:22:51 -04:00
Maidul Islam
36144d8c42 add proper docs 2025-03-18 17:21:48 -04:00
Sheen Capadngan
4478dc8659 misc: minor header label updates 2025-03-19 03:05:50 +08:00
Sheen
510ddf2b1a misc: add machine identity server admin image 2025-03-18 19:05:23 +00:00
Sheen
5363f8c6ff misc: doc updates 2025-03-18 18:45:43 +00:00
Sheen
7d9de6acba Merge branch 'feat/automated-instance-bootstrapping' of https://github.com/Infisical/infisical into feat/automated-instance-bootstrapping 2025-03-18 18:37:42 +00:00
Sheen
bac944133a doc: finalized response schema 2025-03-18 18:36:42 +00:00
Sheen Capadngan
f059d65b45 misc: finalized response 2025-03-19 02:26:51 +08:00
=
c487b2b34a feat: updated doc 2025-03-18 23:44:50 +05:30
Sheen Capadngan
015a193330 misc: added support for removing instance admin access of users 2025-03-19 02:13:24 +08:00
=
8e20531b40 feat: changed key to be required 2025-03-18 23:05:31 +05:30
Sheen Capadngan
d91add2e7b misc: added return statements 2025-03-18 23:49:40 +08:00
=
8ead2aa774 feat: updated documentation on identity oidc auth permission 2025-03-18 20:54:12 +05:30
=
1b2128e3cc feat: updated code to auth field in permission for identity 2025-03-18 20:53:51 +05:30
Sheen Capadngan
6d72524896 misc: addressed typo 2025-03-18 23:15:48 +08:00
Sheen
1ec11d5963 doc: added docs 2025-03-18 14:43:19 +00:00
carlosmonastyrski
ad6f285b59 Throw if file and args secrets are used on secret set command 2025-03-18 09:58:10 -03:00
carlosmonastyrski
d4842dd273 Improve secret set --file message and make it mutually exclusive with manual secrets args 2025-03-18 08:32:31 -03:00
Maidul Islam
78f83cb478 remove default open 2025-03-17 21:51:47 -04:00
Maidul Islam
c8a871de7c fix lint 2025-03-17 19:47:08 -04:00
Maidul Islam
64c0951df3 add new line so there is no change 2025-03-17 19:38:50 -04:00
Maidul Islam
c185414a3c bring back .env example 2025-03-17 19:38:05 -04:00
Maidul Islam
f9695741f1 Minor changes to oidc claims and mappings
- Made the claims expanded by default (it looked off when they were closed)
- Moved claims from advanced to geneal tab and kept the mapping in the advanced tab
- Added better description for the tooltip

question: i feel like it would be better to access metadata like: `{{identity.auth.oidc.claim.<...>}}` instead of like how it is now: `{{identity.metadata.auth.oidc.claim.<...>}}`? What do you think
2025-03-17 19:36:40 -04:00
Scott Wilson
b7c4b11260 Merge pull request #3246 from Infisical/disable-delete-sync-option
Feature: Disable Secret Deletion Sync Option
2025-03-17 15:16:55 -07:00
Maidul Islam
a7fe79c046 Merge pull request #3242 from akhilmhdh/feat/metadata-oidc
Feat/metadata OIDC
2025-03-17 16:55:50 -04:00
Sheen Capadngan
ed6306747a feat: add support for removing instance admin permission from identity 2025-03-18 03:00:54 +08:00
Sheen Capadngan
64569ab44b feat: added bootstrap to CLI 2025-03-18 02:27:42 +08:00
=
9eb89bb46d fix: null causing ui error 2025-03-17 23:52:11 +05:30
=
c4da1ce32d feat: resolved PR feedbacks 2025-03-17 23:38:24 +05:30
Sheen Capadngan
2d1d6f5ce8 misc: added privilege escalation checks 2025-03-18 01:20:01 +08:00
carlosmonastyrski
add97c9b38 Merge pull request #3241 from Infisical/feat/addDynamicSecretsToOverview
Add dynamic secrets modal form on secrets overview page
2025-03-17 13:14:22 -03:00
Maidul Islam
768ba4f4dc Merge pull request #3261 from Infisical/revert-3238-feat/ENG-2320-echo-environment-being-used-in-cli
Revert "feat: confirm environment exists when running `run` command"
2025-03-17 12:09:39 -04:00
Maidul Islam
18c32d872c Revert "feat: confirm environment exists when running run command" 2025-03-17 12:06:35 -04:00
Akhil Mohan
1fd40ab6ab Merge pull request #3260 from akhilmhdh/fix/gateway-migration
fix: corrected table name check in migration
2025-03-17 21:31:00 +05:30
=
9d258f57ce fix: corrected table name check in migration 2025-03-17 21:28:50 +05:30
Scott Wilson
45ccbaf4c9 Merge pull request #3243 from Infisical/gcp-sync-handle-destroyed-values
Fix: Handle Disabled/Destroyed Values in GCP Sync
2025-03-17 08:41:52 -07:00
Sheen Capadngan
6ef358b172 feat: initialize base 2025-03-17 22:35:37 +08:00
carlosmonastyrski
838c1af448 Add file option to secret set CLI command to retrieve secrets from .env or .yaml files 2025-03-17 09:51:21 -03:00
Maidul Islam
8de7261c9a Update docs 2025-03-16 19:46:21 -04:00
Daniel Hougaard
67b1b79fe3 Merge pull request #3253 from Infisical/daniel/bump-helm
chore: bump helm
2025-03-17 00:28:19 +04:00
Daniel Hougaard
31477f4d2b chore: bump helm 2025-03-17 00:21:35 +04:00
Daniel Hougaard
f200372d74 Merge pull request #3252 from Infisical/daniel/patch-k8s-install
fix: k8s installation failing
2025-03-17 00:11:18 +04:00
Daniel Hougaard
f955b68519 Update infisicalsecret-crd.yaml 2025-03-17 00:03:53 +04:00
Maidul Islam
9269b63943 Merge pull request #3248 from kanad13/patch-2
Grammar fixes to local-development.mdx
2025-03-15 12:09:07 -04:00
Maidul Islam
8f96653273 Merge pull request #3247 from Infisical/address-saml-cve
Upgrade passport/saml to 5.0
2025-03-15 12:07:33 -04:00
=
7dffc08eba feat: resolved type error 2025-03-15 21:32:52 +05:30
Kunal Pathak
126b0ce7e7 Grammar fixes to local-development.mdx 2025-03-15 13:15:40 +01:00
=
0b71f7f297 fix: resolved idpCert rename 2025-03-15 12:57:45 +05:30
carlosmonastyrski
e53439d586 Improvements on dynamic secrets overview 2025-03-14 23:18:57 -03:00
Maidul Islam
c86e508817 upgrade to saml 5.0 2025-03-14 21:17:57 -04:00
Maidul Islam
6426b85c1e Upgrade passport/saml to 5.0
This addresses the breaking changes in 5.0 listed here https://github.com/node-saml/node-saml/blob/v5.0.0/CHANGELOG.md#-major-changes

Todo: test with existing saml workflow
2025-03-14 21:16:42 -04:00
Maidul Islam
3d6da1e548 Merge pull request #3245 from Infisical/revert-3244-fix-saml-cve
Revert "Address SAML CVE"
2025-03-14 17:58:06 -04:00
Maidul Islam
7e46fe8148 Revert "Address SAML CVE" 2025-03-14 17:57:48 -04:00
Scott Wilson
48943b4d78 improvement: refine status check 2025-03-14 11:26:59 -07:00
Scott Wilson
fd1afc2cbe fix: handle disabled/destroyed values in gcp sync 2025-03-14 11:04:49 -07:00
carlosmonastyrski
6905029455 Change overview dynamic secret creation modal to set only one env 2025-03-14 14:59:19 -03:00
=
2ef77c737a feat: added a simple oidc server 2025-03-14 22:11:23 +05:30
=
0f31fa3128 feat: updated form for oidc auth 2025-03-14 22:11:23 +05:30
=
1da5a5f417 feat: completed backend code for oidc permission inject 2025-03-14 22:11:22 +05:30
carlosmonastyrski
94d7d2b029 Fix call of onCompleted after all promises are resolved 2025-03-14 12:44:49 -03:00
carlosmonastyrski
e39d1a0530 Fix call of onCompleted after all promises are resolved 2025-03-14 12:26:20 -03:00
carlosmonastyrski
4c5f3859d6 Add dynamic secrets modal form on secrets overview page 2025-03-14 11:59:13 -03:00
180 changed files with 5675 additions and 1208 deletions

8
.gitignore vendored
View File

@@ -1,5 +1,3 @@
.direnv/
# backend
node_modules
.env
@@ -28,6 +26,8 @@ node_modules
/.pnp
.pnp.js
.env
# testing
coverage
reports
@@ -63,12 +63,10 @@ yarn-error.log*
# Editor specific
.vscode/*
**/.idea/*
.idea/*
frontend-build
# cli
.go/
*.tgz
cli/infisical-merge
cli/test/infisical-merge

View File

@@ -1,7 +0,0 @@
import "@fastify/request-context";
declare module "@fastify/request-context" {
interface RequestContextData {
reqId: string;
}
}

View File

@@ -100,6 +100,13 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
declare module "@fastify/request-context" {
interface RequestContextData {
reqId: string;
identityAuthInfo?: {
identityId: string;
oidc?: {
claims: Record<string, string>;
};
};
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
}
}

View File

@@ -85,7 +85,7 @@ export async function up(knex: Knex): Promise<void> {
}
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
// not setting a foreign constraint so that cascade effects are not triggered
if (!doesGatewayColExist) {

View File

@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
if (!hasMappingField) {
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
t.jsonb("claimMetadataMapping");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
if (hasMappingField) {
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
t.dropColumn("claimMetadataMapping");
});
}
}

View File

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

View File

@@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const doesParentColumExist = await knex.schema.hasColumn(TableName.SecretFolder, "parentId");
const doesNameColumnExist = await knex.schema.hasColumn(TableName.SecretFolder, "name");
if (doesParentColumExist && doesNameColumnExist) {
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
t.index(["parentId", "name"]);
});
}
}
export async function down(knex: Knex): Promise<void> {
const doesParentColumExist = await knex.schema.hasColumn(TableName.SecretFolder, "parentId");
const doesNameColumnExist = await knex.schema.hasColumn(TableName.SecretFolder, "name");
if (doesParentColumExist && doesNameColumnExist) {
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
t.dropIndex(["parentId", "name"]);
});
}
}

View File

@@ -26,7 +26,8 @@ export const IdentityOidcAuthsSchema = z.object({
boundSubject: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
encryptedCaCertificate: zodBuffer.nullable().optional()
encryptedCaCertificate: zodBuffer.nullable().optional(),
claimMetadataMapping: z.unknown().nullable().optional()
});
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;

View File

@@ -12,7 +12,6 @@ import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({
id: z.string().uuid(),
encryptedValue: z.string().nullable().optional(),
type: z.string(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
hashedHex: z.string().nullable().optional(),
@@ -27,7 +26,8 @@ export const SecretSharingSchema = z.object({
lastViewedAt: z.date().nullable().optional(),
password: z.string().nullable().optional(),
encryptedSecret: zodBuffer.nullable().optional(),
identifier: z.string().nullable().optional()
identifier: z.string().nullable().optional(),
type: z.string().default("share")
});
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;

View File

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

View File

@@ -1,10 +1,10 @@
import ms from "ms";
import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";

View File

@@ -1,4 +1,3 @@
import ms from "ms";
import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
@@ -6,6 +5,7 @@ import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/pro
import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";

View File

@@ -1,11 +1,11 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { UnauthorizedError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";

View File

@@ -1,10 +1,10 @@
import ms from "ms";
import { z } from "zod";
import { KmipClientsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { KmipPermission } from "@app/ee/services/kmip/kmip-enum";
import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types";
import { ms } from "@app/lib/ms";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";

View File

@@ -25,7 +25,7 @@ type TSAMLConfig = {
callbackUrl: string;
entryPoint: string;
issuer: string;
cert: string;
idpCert: string;
audience: string;
wantAuthnResponseSigned?: boolean;
wantAssertionsSigned?: boolean;
@@ -72,7 +72,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoConfig.id}`,
entryPoint: ssoConfig.entryPoint,
issuer: ssoConfig.issuer,
cert: ssoConfig.cert,
idpCert: ssoConfig.cert,
audience: appCfg.SITE_URL || ""
};
if (ssoConfig.authProvider === SamlProviders.JUMPCLOUD_SAML) {
@@ -302,15 +302,21 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const saml = await server.services.saml.createSamlCfg({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId,
...req.body
const { isActive, authProvider, issuer, entryPoint, cert } = req.body;
const { permission } = req;
return server.services.saml.createSamlCfg({
isActive,
authProvider,
issuer,
entryPoint,
idpCert: cert,
actor: permission.type,
actorId: permission.id,
actorAuthMethod: permission.authMethod,
actorOrgId: permission.orgId,
orgId: req.body.organizationId
});
return saml;
}
});
@@ -337,15 +343,21 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const saml = await server.services.saml.updateSamlCfg({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId,
...req.body
const { isActive, authProvider, issuer, entryPoint, cert } = req.body;
const { permission } = req;
return server.services.saml.updateSamlCfg({
isActive,
authProvider,
issuer,
entryPoint,
idpCert: cert,
actor: permission.type,
actorId: permission.id,
actorAuthMethod: permission.authMethod,
actorOrgId: permission.orgId,
orgId: req.body.organizationId
});
return saml;
}
});
};

View File

@@ -1,9 +1,9 @@
import ms from "ms";
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

View File

@@ -1,5 +1,4 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
@@ -10,6 +9,7 @@ import {
isValidUserPattern
} from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators";
import { SSH_CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

View File

@@ -1,11 +1,11 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";

View File

@@ -1,11 +1,11 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";

View File

@@ -1,9 +1,10 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import msFn from "ms";
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -246,7 +247,7 @@ export const accessApprovalRequestServiceFactory = ({
requesterEmail: requestedByUser.email,
isTemporary,
...(isTemporary && {
expiresIn: ms(ms(temporaryRange || ""), { long: true })
expiresIn: msFn(ms(temporaryRange || ""), { long: true })
}),
secretPath,
environment: envSlug,

View File

@@ -1,8 +1,10 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { ActionProjectType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
@@ -81,8 +83,12 @@ export const auditLogServiceFactory = ({
if (!data.projectId && !data.orgId)
throw new BadRequestError({ message: "Must specify either project id or org id" });
}
return auditLogQueue.pushToLog(data);
const el = { ...data };
if (el.actor.type === ActorType.USER || el.actor.type === ActorType.IDENTITY) {
const permissionMetadata = requestContext.get("identityPermissionMetadata");
el.actor.metadata.permission = permissionMetadata;
}
return auditLogQueue.pushToLog(el);
};
return {

View File

@@ -290,6 +290,7 @@ interface UserActorMetadata {
userId: string;
email?: string | null;
username: string;
permission?: Record<string, unknown>;
}
interface ServiceActorMetadata {
@@ -300,6 +301,7 @@ interface ServiceActorMetadata {
interface IdentityActorMetadata {
identityId: string;
name: string;
permission?: Record<string, unknown>;
}
interface ScimClientActorMetadata {}
@@ -978,6 +980,7 @@ interface AddIdentityOidcAuthEvent {
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
claimMetadataMapping: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
@@ -1002,6 +1005,7 @@ interface UpdateIdentityOidcAuthEvent {
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
claimMetadataMapping?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;

View File

@@ -1,5 +1,4 @@
import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -11,6 +10,7 @@ import {
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { ms } from "@app/lib/ms";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";

View File

@@ -1,10 +1,10 @@
import { ForbiddenError, subject } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import ms from "ms";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";

View File

@@ -1,10 +1,10 @@
import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";

View File

@@ -1,11 +1,11 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { isValidHostname, isValidIp } from "@app/lib/ip";
import { ms } from "@app/lib/ms";
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import {

View File

@@ -50,7 +50,7 @@ export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
const LICENSE_SERVER_CLOUD_PLAN_TTL = 30; // 30 second
const LICENSE_SERVER_CLOUD_PLAN_TTL = 5 * 60; // 5 mins
const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`;
export const licenseServiceFactory = ({
@@ -142,7 +142,10 @@ export const licenseServiceFactory = ({
try {
if (instanceType === InstanceType.Cloud) {
const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId));
if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
if (cachedPlan) {
logger.info(`getPlan: plan fetched from cache [orgId=${orgId}] [projectId=${projectId}]`);
return JSON.parse(cachedPlan) as TFeatureSet;
}
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
@@ -170,6 +173,8 @@ export const licenseServiceFactory = ({
JSON.stringify(onPremFeatures)
);
return onPremFeatures;
} finally {
logger.info(`getPlan: Process done for [orgId=${orgId}] [projectId=${projectId}]`);
}
return onPremFeatures;
};

View File

@@ -131,12 +131,12 @@ function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrg
}
}
const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
const escapeHandlebarsMissingDict = (obj: Record<string, string>, key: string) => {
const handler = {
get(target: Record<string, string>, prop: string) {
if (!(prop in target)) {
if (!Object.hasOwn(target, prop)) {
// eslint-disable-next-line no-param-reassign
target[prop] = `{{identity.metadata.${prop}}}`; // Add missing key as an "own" property
target[prop] = `{{${key}.${prop}}}`; // Add missing key as an "own" property
}
return target[prop];
}
@@ -145,4 +145,4 @@ const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
return new Proxy(obj, handler);
};
export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO };
export { escapeHandlebarsMissingDict, isAuthMethodSaml, validateOrgSSO };

View File

@@ -1,5 +1,6 @@
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra";
import { requestContext } from "@fastify/request-context";
import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars";
@@ -22,7 +23,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal";
import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns";
import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns";
import {
TBuildOrgPermissionDTO,
TBuildProjectPermissionDTO,
@@ -243,13 +244,13 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
const unescapedMetadata = objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
);
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata });
const interpolateRules = templatedRules(
{
identity: {
@@ -317,20 +318,26 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo");
const unescapedMetadata = objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
);
const identityAuthInfo =
unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo
? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
: {};
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata, auth: unescapedIdentityAuthInfo });
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
metadata: metadataKeyValuePair,
auth: identityAuthInfo
}
},
{ data: false }
@@ -424,12 +431,13 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
),
"identity.metadata"
);
const interpolateRules = templatedRules(
{
@@ -469,14 +477,14 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
),
"identity.metadata"
);
const interpolateRules = templatedRules(
{
identity: {

View File

@@ -1,10 +1,10 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";

View File

@@ -63,7 +63,7 @@ export const samlConfigServiceFactory = ({
kmsService
}: TSamlConfigServiceFactoryDep) => {
const createSamlCfg = async ({
cert,
idpCert,
actor,
actorAuthMethod,
actorOrgId,
@@ -93,9 +93,9 @@ export const samlConfigServiceFactory = ({
orgId,
authProvider,
isActive,
encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob,
encryptedSamlCertificate: encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob,
encryptedSamlEntryPoint: encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob,
encryptedSamlCertificate: encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob
encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob
});
return samlConfig;
@@ -106,7 +106,7 @@ export const samlConfigServiceFactory = ({
actor,
actorOrgId,
actorAuthMethod,
cert,
idpCert,
actorId,
issuer,
isActive,
@@ -136,8 +136,8 @@ export const samlConfigServiceFactory = ({
updateQuery.encryptedSamlIssuer = encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob;
}
if (cert !== undefined) {
updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob;
if (idpCert !== undefined) {
updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob;
}
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);

View File

@@ -15,7 +15,7 @@ export type TCreateSamlCfgDTO = {
isActive: boolean;
entryPoint: string;
issuer: string;
cert: string;
idpCert: string;
} & TOrgPermission;
export type TUpdateSamlCfgDTO = Partial<{
@@ -23,7 +23,7 @@ export type TUpdateSamlCfgDTO = Partial<{
isActive: boolean;
entryPoint: string;
issuer: string;
cert: string;
idpCert: string;
}> &
TOrgPermission;

View File

@@ -1,10 +1,10 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";

View File

@@ -1,13 +1,13 @@
import { execFile } from "child_process";
import crypto from "crypto";
import { promises as fs } from "fs";
import ms from "ms";
import os from "os";
import path from "path";
import { promisify } from "util";
import { TSshCertificateTemplates } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import {

View File

@@ -329,6 +329,7 @@ export const OIDC_AUTH = {
boundIssuer: "The unique identifier of the identity provider issuing the JWT.",
boundAudiences: "The list of intended recipients.",
boundClaims: "The attributes that should be present in the JWT for it to be valid.",
claimMetadataMapping: "The attributes that should be present in the permission metadata from the JWT.",
boundSubject: "The expected principal that is the subject of the JWT.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an access token in seconds.",
@@ -342,6 +343,7 @@ export const OIDC_AUTH = {
boundIssuer: "The new unique identifier of the identity provider issuing the JWT.",
boundAudiences: "The new list of intended recipients.",
boundClaims: "The new attributes that should be present in the JWT for it to be valid.",
claimMetadataMapping: "The new attributes that should be present in the permission metadata from the JWT.",
boundSubject: "The new expected principal that is the subject of the JWT.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
@@ -629,7 +631,8 @@ export const FOLDERS = {
workspaceId: "The ID of the project to list folders from.",
environment: "The slug of the environment to list folders from.",
path: "The path to list folders from.",
directory: "The directory to list folders from. (Deprecated in favor of path)"
directory: "The directory to list folders from. (Deprecated in favor of path)",
recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories."
},
GET_BY_ID: {
folderId: "The ID of the folder to get details."

View File

@@ -56,6 +56,7 @@ const envSchema = z
// TODO(akhilmhdh): will be changed to one
ENCRYPTION_KEY: zpStr(z.string().optional()),
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
QUEUE_WORKERS_ENABLED: zodStrBool.default("true"),
HTTPS_ENABLED: zodStrBool,
// smtp options
SMTP_HOST: zpStr(z.string().optional()),

View File

@@ -0,0 +1,15 @@
import msFn, { StringValue } from "ms";
import { BadRequestError } from "../errors";
export const ms = (val: string) => {
if (typeof val !== "string") {
throw new BadRequestError({ message: `Date must be string` });
}
try {
return msFn(val as StringValue);
} catch {
throw new BadRequestError({ message: `Invalid date format string: ${val}` });
}
};

View File

@@ -0,0 +1,34 @@
/**
* Safely retrieves a value from a nested object using dot notation path
*/
export const getStringValueByDot = (
obj: Record<string, unknown> | null | undefined,
path: string,
defaultValue?: string
): string | undefined => {
// Handle null or undefined input
if (!obj) {
return defaultValue;
}
const parts = path.split(".");
let current: unknown = obj;
for (const part of parts) {
const isObject = typeof current === "object" && !Array.isArray(current) && current !== null;
if (!isObject) {
return defaultValue;
}
if (!Object.hasOwn(current as object, part)) {
// Check if the property exists as an own property
return defaultValue;
}
current = (current as Record<string, unknown>)[part];
}
if (typeof current !== "string") {
return defaultValue;
}
return current;
};

View File

@@ -272,10 +272,13 @@ export const queueServiceFactory = (
connection
});
workerContainer[name] = new Worker<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name, jobFn, {
...queueSettings,
connection
});
const appCfg = getConfig();
if (appCfg.QUEUE_WORKERS_ENABLED) {
workerContainer[name] = new Worker<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name, jobFn, {
...queueSettings,
connection
});
}
};
const startPg = async <T extends QueueName>(
@@ -307,6 +310,11 @@ export const queueServiceFactory = (
event: U,
listener: WorkerListener<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>[U]
) => {
const appCfg = getConfig();
if (!appCfg.QUEUE_WORKERS_ENABLED) {
return;
}
const worker = workerContainer[name];
worker.on(event, listener);
};

View File

@@ -1,3 +1,4 @@
import { requestContext } from "@fastify/request-context";
import { FastifyRequest } from "fastify";
import fp from "fastify-plugin";
import jwt, { JwtPayload } from "jsonwebtoken";
@@ -8,6 +9,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
export type TAuthMode =
| {
@@ -43,6 +45,7 @@ export type TAuthMode =
identityName: string;
orgId: string;
authMethod: null;
isInstanceAdmin?: boolean;
}
| {
authMode: AuthMode.SCIM_TOKEN;
@@ -129,14 +132,22 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
}
case AuthMode.IDENTITY_ACCESS_TOKEN: {
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp);
const serverCfg = await getServerCfg();
req.auth = {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
actor,
orgId: identity.orgId,
identityId: identity.identityId,
identityName: identity.name,
authMethod: null
authMethod: null,
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId)
};
if (token?.identityAuth?.oidc) {
requestContext.set("identityAuthInfo", {
identityId: identity.identityId,
oidc: token?.identityAuth?.oidc
});
}
break;
}
case AuthMode.SERVICE_TOKEN: {

View File

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

View File

@@ -65,7 +65,7 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) =>
payload: JSON.stringify(req.body),
signature: signatureSHA256
});
void res.send("ok");
return res.send("ok");
}
});
}

View File

@@ -34,7 +34,7 @@ export const registerServeUI = async (
TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED
};
const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`;
void res.send(js);
return res.send(js);
}
});
@@ -57,7 +57,7 @@ export const registerServeUI = async (
reply.callNotFound();
return;
}
void reply.sendFile("index.html");
return reply.sendFile("index.html");
}
});
}

View File

@@ -1,5 +1,6 @@
import { CronJob } from "cron";
import { Knex } from "knex";
import { monitorEventLoopDelay } from "perf_hooks";
import { z } from "zod";
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
@@ -96,6 +97,7 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig, TEnvConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { TQueueServiceFactory } from "@app/queue";
import { readLimit } from "@app/server/config/rateLimiter";
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
@@ -246,6 +248,9 @@ import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
export const registerRoutes = async (
server: FastifyZodProvider,
{
@@ -637,6 +642,9 @@ export const registerRoutes = async (
userDAL,
identityDAL,
userAliasDAL,
identityTokenAuthDAL,
identityAccessTokenDAL,
identityOrgMembershipDAL,
authService: loginService,
serverCfgDAL: superAdminDAL,
kmsRootConfigDAL,
@@ -1627,6 +1635,18 @@ export const registerRoutes = async (
const cfg = getConfig();
const serverCfg = await getServerCfg();
const meanLagMs = histogram.mean / 1e6;
const maxLagMs = histogram.max / 1e6;
const p99LagMs = histogram.percentile(99) / 1e6;
logger.info(
`Event loop stats - Mean: ${meanLagMs.toFixed(2)}ms, Max: ${maxLagMs.toFixed(2)}ms, p99: ${p99LagMs.toFixed(
2
)}ms`
);
logger.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`);
// try {
// await db.raw("SELECT NOW()");
// } catch (err) {

View File

@@ -70,6 +70,19 @@ export const DefaultResponseErrorsSchema = {
})
};
export const booleanSchema = z
.union([z.boolean(), z.string().trim()])
.transform((value) => {
if (typeof value === "string") {
// ie if not empty, 0 or false, return true
return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false";
}
return value;
})
.optional()
.default(true);
export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
z.object({
environment: z.object({

View File

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

View File

@@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import ms from "ms";
import { z } from "zod";
import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

View File

@@ -1,9 +1,9 @@
import ms from "ms";
import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

View File

@@ -1,9 +1,9 @@
import ms from "ms";
import { z } from "zod";
import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

View File

@@ -16,7 +16,12 @@ import { secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import {
booleanSchema,
SanitizedDynamicSecretSchema,
SanitizedTagSchema,
secretRawSchema
} from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
@@ -24,20 +29,6 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
const MAX_DEEP_SEARCH_LIMIT = 500; // arbitrary limit to prevent excessive results
// handle querystring boolean values
const booleanSchema = z
.union([z.boolean(), z.string().trim()])
.transform((value) => {
if (typeof value === "string") {
// ie if not empty, 0 or false, return true
return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false";
}
return value;
})
.optional()
.default(true);
const parseSecretPathSearch = (search?: string) => {
if (!search)
return {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import {
validateOidcAuthAudiencesField,
validateOidcBoundClaimsField
} from "@app/services/identity-oidc-auth/identity-oidc-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({
id: true,
@@ -23,6 +24,7 @@ const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({
boundIssuer: true,
boundAudiences: true,
boundClaims: true,
claimMetadataMapping: true,
boundSubject: true,
createdAt: true,
updatedAt: true
@@ -104,6 +106,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer),
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences),
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims),
claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.claimMetadataMapping).optional(),
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject),
accessTokenTrustedIps: z
.object({
@@ -146,7 +149,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
@@ -161,6 +165,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
boundIssuer: identityOidcAuth.boundIssuer,
boundAudiences: identityOidcAuth.boundAudiences,
boundClaims: identityOidcAuth.boundClaims as Record<string, string>,
claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record<string, string>,
boundSubject: identityOidcAuth.boundSubject as string,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
@@ -200,6 +205,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
boundIssuer: z.string().min(1).describe(OIDC_AUTH.UPDATE.boundIssuer),
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.UPDATE.boundAudiences),
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.boundClaims),
claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.claimMetadataMapping).optional(),
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.UPDATE.boundSubject),
accessTokenTrustedIps: z
.object({
@@ -258,6 +264,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
boundIssuer: identityOidcAuth.boundIssuer,
boundAudiences: identityOidcAuth.boundAudiences,
boundClaims: identityOidcAuth.boundClaims as Record<string, string>,
claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record<string, string>,
boundSubject: identityOidcAuth.boundSubject as string,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import ms from "ms";
import { z } from "zod";
import {
@@ -10,6 +9,7 @@ import {
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECT_USERS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

View File

@@ -9,6 +9,8 @@ import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { booleanSchema } from "../sanitizedSchemas";
export const registerSecretFolderRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
@@ -347,11 +349,14 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.LIST.directory)
.describe(FOLDERS.LIST.directory),
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
}),
response: {
200: z.object({
folders: SecretFoldersSchema.array()
folders: SecretFoldersSchema.extend({
relativePath: z.string().optional()
}).array()
})
}
},

View File

@@ -1,4 +1,3 @@
import ms from "ms";
import { z } from "zod";
import {
@@ -8,6 +7,7 @@ import {
ProjectUserMembershipRolesSchema
} from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

View File

@@ -1,4 +1,3 @@
import ms from "ms";
import { z } from "zod";
import {
@@ -9,6 +8,7 @@ import {
} from "@app/db/schemas";
import { ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";

View File

@@ -2,7 +2,6 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import ms from "ms";
import { z } from "zod";
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
@@ -10,6 +9,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";

View File

@@ -1,7 +1,6 @@
import ms from "ms";
import { TCertificateTemplates } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
export const validateCertificateDetailsAgainstTemplate = (
cert: {

View File

@@ -1,5 +1,4 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -9,6 +8,7 @@ import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { isUuidV4 } from "@app/lib/validator";
import { TGroupDALFactory } from "../../ee/services/group/group-dal";

View File

@@ -7,4 +7,9 @@ export type TIdentityAccessTokenJwtPayload = {
clientSecretId: string;
identityAccessTokenId: string;
authTokenType: string;
identityAuth: {
oidc?: {
claims: Record<string, string>;
};
};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { getStringValueByDot } from "@app/lib/template/dot-access";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
@@ -18,6 +19,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal";
import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns";
import {
@@ -177,8 +179,9 @@ export const identityJwtAuthServiceFactory = ({
if (identityJwtAuth.boundClaims) {
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
const value = getStringValueByDot(tokenData, claimKey) || "";
if (!tokenData[claimKey]) {
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
@@ -248,8 +251,11 @@ export const identityJwtAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachJwtAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

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

View File

@@ -18,6 +18,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal";
import { extractK8sUsername } from "./identity-kubernetes-auth-fns";
import {
@@ -101,7 +102,8 @@ export const identityKubernetesAuthServiceFactory = ({
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
@@ -227,8 +229,11 @@ export const identityKubernetesAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachKubernetesAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

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

View File

@@ -12,6 +12,7 @@ import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { getStringValueByDot } from "@app/lib/template/dot-access";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
@@ -19,6 +20,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns";
import {
@@ -77,7 +79,7 @@ export const identityOidcAuthServiceFactory = ({
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
{
httpsAgent: requestAgent
httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
}
);
const jwksUri = discoveryDoc.jwks_uri;
@@ -91,7 +93,7 @@ export const identityOidcAuthServiceFactory = ({
const client = new JwksClient({
jwksUri,
requestAgent
requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
});
const { kid } = decodedToken.header;
@@ -108,7 +110,6 @@ export const identityOidcAuthServiceFactory = ({
message: `Access denied: ${error.message}`
});
}
throw error;
}
@@ -135,10 +136,16 @@ export const identityOidcAuthServiceFactory = ({
if (identityOidcAuth.boundClaims) {
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
const value = getStringValueByDot(tokenData, claimKey) || "";
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
}
// handle both single and multi-valued claims
if (
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
) {
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(value, claimEntry))) {
throw new UnauthorizedError({
message: "Access denied: OIDC claim not allowed."
});
@@ -146,6 +153,20 @@ export const identityOidcAuthServiceFactory = ({
});
}
const filteredClaims: Record<string, string> = {};
if (identityOidcAuth.claimMetadataMapping) {
Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
const claimKey = (identityOidcAuth.claimMetadataMapping as Record<string, string>)[permissionKey];
const value = getStringValueByDot(tokenData, claimKey) || "";
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
}
filteredClaims[permissionKey] = value;
});
}
const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
@@ -167,7 +188,12 @@ export const identityOidcAuthServiceFactory = ({
{
identityId: identityOidcAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
identityAuth: {
oidc: {
claims: filteredClaims
}
}
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
@@ -188,6 +214,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
claimMetadataMapping,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
@@ -196,8 +223,10 @@ export const identityOidcAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachOidcAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@@ -254,6 +283,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
claimMetadataMapping,
boundSubject,
accessTokenMaxTTL,
accessTokenTTL,
@@ -274,6 +304,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
claimMetadataMapping,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
@@ -335,6 +366,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
claimMetadataMapping,
boundSubject,
accessTokenMaxTTL,
accessTokenTTL,

View File

@@ -7,11 +7,13 @@ export type TAttachOidcAuthDTO = {
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
claimMetadataMapping?: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateOidcAuthDTO = {
@@ -21,6 +23,7 @@ export type TUpdateOidcAuthDTO = {
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
claimMetadataMapping?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;

View File

@@ -1,5 +1,4 @@
import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms";
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -7,6 +6,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { ActorType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-dal";
import { TIdentityUaDALFactory } from "./identity-ua-dal";
import {
@@ -68,9 +69,15 @@ export const identityUaServiceFactory = ({
isClientSecretRevoked: false
});
const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) =>
bcrypt.compareSync(clientSecret, clientSecretHash)
);
let validClientSecretInfo: (typeof clientSecrtInfo)[0] | null = null;
for await (const info of clientSecrtInfo) {
const isMatch = await bcrypt.compare(clientSecret, info.clientSecretHash);
if (isMatch) {
validClientSecretInfo = info;
break;
}
}
if (!validClientSecretInfo) throw new UnauthorizedError({ message: "Invalid credentials" });
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
@@ -103,7 +110,7 @@ export const identityUaServiceFactory = ({
}
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo.id, tx);
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityUa.identityId,
@@ -150,8 +157,11 @@ export const identityUaServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachUaDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

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

View File

@@ -1,7 +1,7 @@
import { TDbClient } from "@app/db";
import { TableName, TIdentities } from "@app/db/schemas";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TIdentityDALFactory = ReturnType<typeof identityDALFactory>;

View File

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

View File

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

View File

@@ -923,16 +923,14 @@ const getAppsCodefresh = async ({ accessToken }: { accessToken: string }) => {
/**
* Return list of projects for Windmill integration
*/
const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
const { data } = await request.get<{ id: string; name: string }[]>(
`${IntegrationUrls.WINDMILL_API_URL}/workspaces/list`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
const getAppsWindmill = async ({ accessToken, url }: { accessToken: string; url?: string | null }) => {
const apiUrl = url ? `${url}/api` : IntegrationUrls.WINDMILL_API_URL;
const { data } = await request.get<{ id: string; name: string }[]>(`${apiUrl}/workspaces/list`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
);
});
// check for write access of secrets in windmill workspaces
const writeAccessCheck = data.map(async (app) => {
@@ -941,7 +939,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
const folderPath = "f/folder/variable";
const { data: writeUser } = await request.post<object>(
`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`,
`${apiUrl}/w/${app.id}/variables/create`,
{
path: userPath,
value: "variable",
@@ -957,7 +955,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
);
const { data: writeFolder } = await request.post<object>(
`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`,
`${apiUrl}/w/${app.id}/variables/create`,
{
path: folderPath,
value: "variable",
@@ -974,14 +972,14 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
// is write access is allowed then delete the created secrets from workspace
if (writeUser && writeFolder) {
await request.delete(`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, {
await request.delete(`${apiUrl}/w/${app.id}/variables/delete/${userPath}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
});
await request.delete(`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, {
await request.delete(`${apiUrl}/w/${app.id}/variables/delete/${folderPath}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
@@ -1316,7 +1314,8 @@ export const getApps = async ({
case Integrations.WINDMILL:
return getAppsWindmill({
accessToken
accessToken,
url
});
case Integrations.DIGITAL_OCEAN_APP_PLATFORM:

View File

@@ -4127,10 +4127,10 @@ const syncSecretsWindmill = async ({
is_secret: boolean;
description?: string;
}
const apiUrl = integration.url ? `${integration.url}/api` : IntegrationUrls.WINDMILL_API_URL;
// get secrets stored in windmill workspace
const res = (
await request.get<WindmillSecret[]>(`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/list`, {
await request.get<WindmillSecret[]>(`${apiUrl}/w/${integration.appId}/variables/list`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
@@ -4146,7 +4146,6 @@ const syncSecretsWindmill = async ({
// eslint-disable-next-line
const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$");
for await (const key of Object.keys(secrets)) {
if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) {
if (!(key in res)) {
@@ -4154,7 +4153,7 @@ const syncSecretsWindmill = async ({
// -> create secret
await request.post(
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/create`,
`${apiUrl}/w/${integration.appId}/variables/create`,
{
path: key,
value: secrets[key].value,
@@ -4171,7 +4170,7 @@ const syncSecretsWindmill = async ({
} else {
// -> update secret
await request.post(
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/update/${res[key].path}`,
`${apiUrl}/w/${integration.appId}/variables/update/${res[key].path}`,
{
path: key,
value: secrets[key].value,
@@ -4192,16 +4191,13 @@ const syncSecretsWindmill = async ({
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// -> delete secret
await request.delete(
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/delete/${res[key].path}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json"
}
await request.delete(`${apiUrl}/w/${integration.appId}/variables/delete/${res[key].path}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json"
}
);
});
}
}
};

View File

@@ -1,6 +1,5 @@
/* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -11,6 +10,7 @@ import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { ActorType } from "../auth/auth-type";

View File

@@ -1,7 +1,7 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TProjectEnvironments, TSecretFolders, TSecretFoldersUpdate } from "@app/db/schemas";
import { TableName, TSecretFolders, TSecretFoldersUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { groupBy, removeTrailingSlash } from "@app/lib/fn";
import { ormify, selectAllTableCols } from "@app/lib/knex";
@@ -41,12 +41,12 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str
void baseQb
.select({
depth: 1,
// latestFolderVerId: db.raw("NULL::uuid"),
path: db.raw("'/'")
})
.from(TableName.SecretFolder)
.where({
parentId: null
parentId: null,
name: "root"
})
.whereIn(
"envId",
@@ -69,9 +69,7 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str
.where((wb) =>
formatedQuery.map(({ secretPath }) =>
wb.orWhereRaw(
`depth = array_position(ARRAY[${secretPath.map(() => "?").join(",")}]::varchar[], ${
TableName.SecretFolder
}.name,depth)`,
`secret_folders.name = (ARRAY[${secretPath.map(() => "?").join(",")}]::varchar[])[depth]`,
[...secretPath]
)
)
@@ -107,7 +105,6 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
void baseQb
.select({
depth: 1,
// latestFolderVerId: db.raw("NULL::uuid"),
path: db.raw("'/'")
})
.from(TableName.SecretFolder)
@@ -117,6 +114,11 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
parentId: null
})
.whereIn(`${TableName.Environment}.slug`, environments)
.select(
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("projectId").withSchema(TableName.Environment)
)
.select(selectAllTableCols(TableName.SecretFolder))
.union(
(qb) =>
@@ -128,21 +130,20 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
depth: db.raw("parent.depth + 1"),
path: db.raw(
"CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)"
)
),
envSlug: db.ref("envSlug").withSchema("parent"),
envName: db.ref("envName").withSchema("parent"),
projectId: db.ref("projectId").withSchema("parent")
})
.select(selectAllTableCols(TableName.SecretFolder))
.whereRaw(
`depth = array_position(ARRAY[${pathSegments
.map(() => "?")
.join(",")}]::varchar[], secret_folders.name,depth)`,
[...pathSegments]
)
.whereRaw(`secret_folders.name = (ARRAY[${pathSegments.map(() => "?").join(",")}]::varchar[])[depth]`, [
...pathSegments
])
.from(TableName.SecretFolder)
.join("parent", "parent.id", `${TableName.SecretFolder}.parentId`)
);
})
.from<TSecretFolders & { depth: number; path: string }>("parent")
.leftJoin<TProjectEnvironments>(TableName.Environment, `${TableName.Environment}.id`, "parent.envId")
.select<
(TSecretFolders & {
depth: number;
@@ -152,13 +153,7 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
envName: string;
projectId: string;
})[]
>(
selectAllTableCols("parent" as TableName.SecretFolder),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("projectId").withSchema(TableName.Environment)
);
>(selectAllTableCols("parent" as TableName.SecretFolder));
};
const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: string[]) =>
@@ -220,19 +215,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
throw new BadRequestError({
message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed."
});
const formatedPath = removeTrailingSlash(path);
try {
const folder = await sqlFindFolderByPathQuery(
tx || db.replicaNode(),
projectId,
[environment],
removeTrailingSlash(path)
)
.orderBy("depth", "desc")
const query = sqlFindFolderByPathQuery(tx || db.replicaNode(), projectId, [environment], formatedPath)
.where("path", formatedPath)
.first();
if (folder && folder.path !== removeTrailingSlash(path)) {
return;
}
const folder = await query;
if (!folder) return;
const { envId: id, envName: name, envSlug: slug, ...el } = folder;
return { ...el, envId: id, environment: { id, name, slug } };
@@ -250,22 +238,13 @@ export const secretFolderDALFactory = (db: TDbClient) => {
});
try {
const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1;
const formatedPath = removeTrailingSlash(path);
const folders = await sqlFindFolderByPathQuery(
tx || db.replicaNode(),
projectId,
environments,
removeTrailingSlash(path)
)
.orderBy("depth", "desc")
.where("depth", pathDepth);
const firstFolder = folders[0];
if (firstFolder && firstFolder.path !== removeTrailingSlash(path)) {
return [];
}
formatedPath
).where("path", removeTrailingSlash(path));
return folders.map((folder) => {
const { envId: id, envName: name, envSlug: slug, ...el } = folder;
@@ -323,7 +302,6 @@ export const secretFolderDALFactory = (db: TDbClient) => {
const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => {
try {
const folders = await sqlFindSecretPathByFolderId(tx || db.replicaNode(), projectId, folderIds);
// travelling all the way from leaf node to root contains real path
const rootFolders = groupBy(
folders.filter(({ parentId }) => parentId === null),

View File

@@ -401,7 +401,8 @@ export const secretFolderServiceFactory = ({
orderBy,
orderDirection,
limit,
offset
offset,
recursive
}: TGetFolderDTO) => {
// folder list is allowed to be read by anyone
// permission to check does user has access
@@ -420,6 +421,17 @@ export const secretFolderServiceFactory = ({
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!parentFolder) return [];
if (recursive) {
const recursiveFolders = await folderDAL.findByEnvsDeep({ parentIds: [parentFolder.id] });
// remove the parent folder
return recursiveFolders
.filter((folder) => folder.id !== parentFolder.id)
.map((folder) => ({
...folder,
relativePath: folder.path
}));
}
const folders = await folderDAL.find(
{
envId: env.id,

View File

@@ -45,6 +45,7 @@ export type TGetFolderDTO = {
orderDirection?: OrderByDirection;
limit?: number;
offset?: number;
recursive?: boolean;
} & TProjectPermission;
export type TGetFolderByIdDTO = {

View File

@@ -71,8 +71,16 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
} catch (error) {
// when a secret in GCP has no versions, we treat it as if it's a blank value
if (error instanceof AxiosError && error.response?.status === 404) {
// when a secret in GCP has no versions, or is disabled/destroyed, we treat it as if it's a blank value
if (
error instanceof AxiosError &&
(error.response?.status === 404 ||
(error.response?.status === 400 &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
error.response.data.error.status === "FAILED_PRECONDITION" &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
error.response.data.error.message.match(/(?:disabled|destroyed)/i)))
) {
res[key] = "";
} else {
throw new SecretSyncError({

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,6 @@ package api
import "time"
type Environment struct {
Name string `json:"name"`
Slug string `json:"slug"`
ID string `json:"id"`
}
// Stores info for login one
type LoginOneRequest struct {
Email string `json:"email"`
@@ -20,6 +14,7 @@ type LoginOneResponse struct {
}
// Stores info for login two
type LoginTwoRequest struct {
Email string `json:"email"`
ClientProof string `json:"clientProof"`
@@ -173,10 +168,9 @@ type Secret struct {
}
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Environments []Environment `json:"environments"`
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
type RawSecret struct {
@@ -654,3 +648,10 @@ type ExchangeRelayCertResponseV1 struct {
Certificate string `json:"certificate"`
CertificateChain string `json:"certificateChain"`
}
type BootstrapInstanceRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Organization string `json:"organization"`
Domain string `json:"domain"`
}

View File

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

Some files were not shown because too many files have changed in this diff Show More