Compare commits
2 Commits
daniel/fix
...
testing-me
Author | SHA1 | Date | |
---|---|---|---|
|
0b1f4f0e2a | ||
|
b4485a2a57 |
@@ -74,8 +74,8 @@ CAPTCHA_SECRET=
|
||||
|
||||
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=false
|
||||
OTEL_EXPORT_TYPE=prometheus
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=
|
||||
OTEL_EXPORT_TYPE=
|
||||
OTEL_EXPORT_OTLP_ENDPOINT=
|
||||
OTEL_OTLP_PUSH_INTERVAL=
|
||||
|
||||
|
@@ -1,86 +0,0 @@
|
||||
import { createFolder, deleteFolder } from "e2e-test/testUtils/folders";
|
||||
import { createSecretV2, deleteSecretV2, getSecretsV2 } from "e2e-test/testUtils/secrets";
|
||||
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
|
||||
describe("Secret Recursive Testing", async () => {
|
||||
const projectId = seedData1.projectV3.id;
|
||||
const folderAndSecretNames = [
|
||||
{ name: "deep1", path: "/", expectedSecretCount: 4 },
|
||||
{ name: "deep21", path: "/deep1", expectedSecretCount: 2 },
|
||||
{ name: "deep3", path: "/deep1/deep2", expectedSecretCount: 1 },
|
||||
{ name: "deep22", path: "/deep2", expectedSecretCount: 1 }
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
const rootFolderIds: string[] = [];
|
||||
for (const folder of folderAndSecretNames) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const createdFolder = await createFolder({
|
||||
authToken: jwtAuthToken,
|
||||
environmentSlug: "prod",
|
||||
workspaceId: projectId,
|
||||
secretPath: folder.path,
|
||||
name: folder.name
|
||||
});
|
||||
|
||||
if (folder.path === "/") {
|
||||
rootFolderIds.push(createdFolder.id);
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await createSecretV2({
|
||||
secretPath: folder.path,
|
||||
authToken: jwtAuthToken,
|
||||
environmentSlug: "prod",
|
||||
workspaceId: projectId,
|
||||
key: folder.name,
|
||||
value: folder.name
|
||||
});
|
||||
}
|
||||
|
||||
return async () => {
|
||||
await Promise.all(
|
||||
rootFolderIds.map((id) =>
|
||||
deleteFolder({
|
||||
authToken: jwtAuthToken,
|
||||
secretPath: "/",
|
||||
id,
|
||||
workspaceId: projectId,
|
||||
environmentSlug: "prod"
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await deleteSecretV2({
|
||||
authToken: jwtAuthToken,
|
||||
secretPath: "/",
|
||||
workspaceId: projectId,
|
||||
environmentSlug: "prod",
|
||||
key: folderAndSecretNames[0].name
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
test.each(folderAndSecretNames)("$path recursive secret fetching", async ({ path, expectedSecretCount }) => {
|
||||
const secrets = await getSecretsV2({
|
||||
authToken: jwtAuthToken,
|
||||
secretPath: path,
|
||||
workspaceId: projectId,
|
||||
environmentSlug: "prod",
|
||||
recursive: true
|
||||
});
|
||||
|
||||
expect(secrets.secrets.length).toEqual(expectedSecretCount);
|
||||
expect(secrets.secrets.sort((a, b) => a.secretKey.localeCompare(b.secretKey))).toEqual(
|
||||
folderAndSecretNames
|
||||
.filter((el) => el.path.startsWith(path))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((el) =>
|
||||
expect.objectContaining({
|
||||
secretKey: el.name,
|
||||
secretValue: el.name
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
@@ -97,7 +97,6 @@ export const getSecretsV2 = async (dto: {
|
||||
environmentSlug: string;
|
||||
secretPath: string;
|
||||
authToken: string;
|
||||
recursive?: boolean;
|
||||
}) => {
|
||||
const getSecretsResponse = await testServer.inject({
|
||||
method: "GET",
|
||||
@@ -110,8 +109,7 @@ export const getSecretsV2 = async (dto: {
|
||||
environment: dto.environmentSlug,
|
||||
secretPath: dto.secretPath,
|
||||
expandSecretReferences: "true",
|
||||
include_imports: "true",
|
||||
recursive: String(dto.recursive || false)
|
||||
include_imports: "true"
|
||||
}
|
||||
});
|
||||
expect(getSecretsResponse.statusCode).toBe(200);
|
||||
|
22
backend/package-lock.json
generated
@@ -33,7 +33,6 @@
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@octopusdeploy/api-client": "^3.4.1",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
|
||||
@@ -6956,21 +6955,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz",
|
||||
"integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w=="
|
||||
},
|
||||
"node_modules/@octopusdeploy/api-client": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@octopusdeploy/api-client/-/api-client-3.4.1.tgz",
|
||||
"integrity": "sha512-j6FRgDNzc6AQoT3CAguYLWxoMR4W5TKCT1BCPpqjEN9mknmdMSKfYORs3djn/Yj/BhqtITTydDpBoREbzKY5+g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "^1.2.1",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^8.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"semver": "^7.3.8",
|
||||
"urijs": "^1.19.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
@@ -22413,12 +22397,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/urijs": {
|
||||
"version": "1.19.11",
|
||||
"resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz",
|
||||
"integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/url": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
|
||||
|
@@ -141,7 +141,6 @@
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@octopusdeploy/api-client": "^3.4.1",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -8,6 +7,7 @@ import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -48,15 +48,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
.nullable(),
|
||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
||||
name: z
|
||||
.string()
|
||||
.describe(DYNAMIC_SECRETS.CREATE.name)
|
||||
.min(1)
|
||||
.toLowerCase()
|
||||
.max(64)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid"
|
||||
})
|
||||
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
|
||||
import { GROUPS } from "@app/lib/api-docs";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -14,15 +14,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
||||
slug: z
|
||||
.string()
|
||||
.min(5)
|
||||
.max(36)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(GROUPS.CREATE.slug),
|
||||
slug: slugSchema({ min: 5, max: 36 }).optional().describe(GROUPS.CREATE.slug),
|
||||
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role)
|
||||
}),
|
||||
response: {
|
||||
@@ -100,14 +92,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().trim().min(1).describe(GROUPS.UPDATE.name),
|
||||
slug: z
|
||||
.string()
|
||||
.min(5)
|
||||
.max(36)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(GROUPS.UPDATE.slug),
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe(GROUPS.UPDATE.slug),
|
||||
role: z.string().trim().min(1).describe(GROUPS.UPDATE.role)
|
||||
})
|
||||
.partial(),
|
||||
|
@@ -8,6 +8,7 @@ import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import {
|
||||
ProjectPermissionSchema,
|
||||
@@ -33,17 +34,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
body: z.object({
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: ProjectPermissionSchema.array()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||
.optional(),
|
||||
@@ -77,7 +68,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body,
|
||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||
slug: req.body.slug ?? slugify(alphaNumericNanoId(12)),
|
||||
isTemporary: false,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error this is valid ts
|
||||
@@ -103,17 +94,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
body: z.object({
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: ProjectPermissionSchema.array()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||
.optional(),
|
||||
@@ -159,7 +140,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body,
|
||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||
slug: req.body.slug ?? slugify(alphaNumericNanoId(12)),
|
||||
isTemporary: true,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error this is valid ts
|
||||
@@ -189,16 +170,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug),
|
||||
privilegeDetails: z
|
||||
.object({
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
|
||||
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
|
||||
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
|
||||
IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -18,17 +18,10 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
organizationId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.trim()
|
||||
.refine(
|
||||
(val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid"
|
||||
}),
|
||||
slug: slugSchema({ min: 1, max: 64 }).refine(
|
||||
(val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
),
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
permissions: z.any().array()
|
||||
@@ -94,17 +87,13 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
roleId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
// TODO: Switch to slugSchema after verifying correct methods with Akhil - Omar 11/24
|
||||
slug: slugSchema({ min: 1, max: 64 })
|
||||
.refine(
|
||||
(val) => typeof val !== "undefined" && !Object.keys(OrgMembershipRole).includes(val),
|
||||
(val) => !Object.keys(OrgMembershipRole).includes(val),
|
||||
"Please choose a different slug, the slug you have entered is reserved."
|
||||
)
|
||||
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
|
||||
message: "Slug must be a valid"
|
||||
}),
|
||||
.optional(),
|
||||
name: z.string().trim().optional(),
|
||||
description: z.string().trim().optional(),
|
||||
permissions: z.any().array().optional()
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -32,18 +32,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.min(1)
|
||||
slug: slugSchema({ max: 64 })
|
||||
.refine(
|
||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid"
|
||||
})
|
||||
.describe(PROJECT_ROLE.CREATE.slug),
|
||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
||||
@@ -94,21 +87,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.optional()
|
||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||
slug: slugSchema()
|
||||
.refine(
|
||||
(val) =>
|
||||
typeof val === "undefined" ||
|
||||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
)
|
||||
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
|
||||
message: "Slug must be a valid"
|
||||
}),
|
||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||
.optional(),
|
||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
|
||||
@@ -8,22 +7,13 @@ import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-tem
|
||||
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
||||
import { ProjectTemplates } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768;
|
||||
|
||||
const SlugSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(32)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Must be valid slug format"
|
||||
});
|
||||
|
||||
const isReservedRoleSlug = (slug: string) =>
|
||||
Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
||||
|
||||
@@ -34,14 +24,14 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
|
||||
roles: z
|
||||
.object({
|
||||
name: z.string().trim().min(1),
|
||||
slug: SlugSchema,
|
||||
slug: slugSchema(),
|
||||
permissions: UnpackedPermissionSchema.array()
|
||||
})
|
||||
.array(),
|
||||
environments: z
|
||||
.object({
|
||||
name: z.string().trim().min(1),
|
||||
slug: SlugSchema,
|
||||
slug: slugSchema(),
|
||||
position: z.number().min(1)
|
||||
})
|
||||
.array()
|
||||
@@ -50,7 +40,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
|
||||
const ProjectTemplateRolesSchema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1),
|
||||
slug: SlugSchema,
|
||||
slug: slugSchema(),
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
})
|
||||
.array()
|
||||
@@ -78,7 +68,7 @@ const ProjectTemplateRolesSchema = z
|
||||
const ProjectTemplateEnvironmentsSchema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1),
|
||||
slug: SlugSchema,
|
||||
slug: slugSchema(),
|
||||
position: z.number().min(1)
|
||||
})
|
||||
.array()
|
||||
@@ -188,9 +178,11 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
||||
schema: {
|
||||
description: "Create a project template.",
|
||||
body: z.object({
|
||||
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
|
||||
message: `The requested project template name is reserved.`
|
||||
}).describe(ProjectTemplates.CREATE.name),
|
||||
name: slugSchema({ field: "name" })
|
||||
.refine((val) => !isInfisicalProjectTemplate(val), {
|
||||
message: `The requested project template name is reserved.`
|
||||
})
|
||||
.describe(ProjectTemplates.CREATE.name),
|
||||
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
|
||||
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
|
||||
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
|
||||
@@ -230,9 +222,10 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
||||
description: "Update a project template.",
|
||||
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }),
|
||||
body: z.object({
|
||||
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
|
||||
message: `The requested project template name is reserved.`
|
||||
})
|
||||
name: slugSchema({ field: "name" })
|
||||
.refine((val) => !isInfisicalProjectTemplate(val), {
|
||||
message: `The requested project template name is reserved.`
|
||||
})
|
||||
.optional()
|
||||
.describe(ProjectTemplates.UPDATE.name),
|
||||
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),
|
||||
|
@@ -7,6 +7,7 @@ import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/pr
|
||||
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -21,17 +22,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
||||
schema: {
|
||||
body: z.object({
|
||||
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
||||
type: z.discriminatedUnion("isTemporary", [
|
||||
z.object({
|
||||
@@ -87,15 +78,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
slug: z
|
||||
.string()
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
||||
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
.optional()
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||
|
@@ -7,6 +7,7 @@ import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-p
|
||||
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -28,17 +29,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
body: z.object({
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
|
||||
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
|
||||
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
|
||||
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
|
||||
type: z.discriminatedUnion("isTemporary", [
|
||||
z.object({
|
||||
@@ -100,16 +91,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.id)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
|
||||
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -29,18 +29,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.min(1)
|
||||
slug: slugSchema({ min: 1, max: 64 })
|
||||
.refine(
|
||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid"
|
||||
})
|
||||
.describe(PROJECT_ROLE.CREATE.slug),
|
||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
||||
@@ -90,21 +83,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.optional()
|
||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||
slug: slugSchema({ min: 1, max: 64 })
|
||||
.refine(
|
||||
(val) =>
|
||||
typeof val === "undefined" ||
|
||||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
)
|
||||
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
|
||||
message: "Slug must be a valid"
|
||||
}),
|
||||
.optional()
|
||||
.describe(PROJECT_ROLE.UPDATE.slug),
|
||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||
|
@@ -112,7 +112,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
})
|
||||
) as object;
|
||||
|
||||
const selectedTTL = ttl || dynamicSecretCfg.defaultTTL;
|
||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||
const { maxTTL } = dynamicSecretCfg;
|
||||
const expireAt = new Date(new Date().getTime() + ms(selectedTTL));
|
||||
if (maxTTL) {
|
||||
@@ -187,7 +187,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
})
|
||||
) as object;
|
||||
|
||||
const selectedTTL = ttl || dynamicSecretCfg.defaultTTL;
|
||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||
const { maxTTL } = dynamicSecretCfg;
|
||||
const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL));
|
||||
if (maxTTL) {
|
||||
|
@@ -31,6 +31,7 @@ export enum OrgPermissionSubjects {
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
| [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace]
|
||||
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Role]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Member]
|
||||
@@ -51,6 +52,7 @@ export type OrgPermissionSet =
|
||||
const buildAdminPermission = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
// ws permissions
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
// role permission
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
@@ -133,6 +135,7 @@ export const orgAdminPermissions = buildAdminPermission();
|
||||
const buildMemberPermission = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
|
@@ -18,7 +18,6 @@ import { TGroupProjectDALFactory } from "@app/services/group-project/group-proje
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
|
||||
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
||||
import { OrgAuthMethod } from "@app/services/org/org-types";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
@@ -72,7 +71,6 @@ type TScimServiceFactoryDep = {
|
||||
| "deleteMembershipById"
|
||||
| "transaction"
|
||||
| "updateMembershipById"
|
||||
| "findOrgById"
|
||||
>;
|
||||
orgMembershipDAL: Pick<
|
||||
TOrgMembershipDALFactory,
|
||||
@@ -290,7 +288,8 @@ export const scimServiceFactory = ({
|
||||
const createScimUser = async ({ externalId, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
|
||||
if (!email) throw new ScimRequestError({ detail: "Invalid request. Missing email.", status: 400 });
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
const org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org)
|
||||
throw new ScimRequestError({
|
||||
detail: "Organization not found",
|
||||
@@ -303,24 +302,13 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
if (!org.orgAuthMethod) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Neither SAML or OIDC SSO is configured",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
const aliasType = org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML;
|
||||
const trustScimEmails =
|
||||
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails;
|
||||
|
||||
const userAlias = await userAliasDAL.findOne({
|
||||
externalId,
|
||||
orgId,
|
||||
aliasType
|
||||
aliasType: UserAliasType.SAML
|
||||
});
|
||||
|
||||
const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => {
|
||||
@@ -361,7 +349,7 @@ export const scimServiceFactory = ({
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (trustScimEmails) {
|
||||
if (serverCfg.trustSamlEmails) {
|
||||
user = await userDAL.findOne(
|
||||
{
|
||||
email,
|
||||
@@ -379,9 +367,9 @@ export const scimServiceFactory = ({
|
||||
);
|
||||
user = await userDAL.create(
|
||||
{
|
||||
username: trustScimEmails ? email : uniqueUsername,
|
||||
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
|
||||
email,
|
||||
isEmailVerified: trustScimEmails,
|
||||
isEmailVerified: serverCfg.trustSamlEmails,
|
||||
firstName,
|
||||
lastName,
|
||||
authMethods: [],
|
||||
@@ -394,7 +382,7 @@ export const scimServiceFactory = ({
|
||||
await userAliasDAL.create(
|
||||
{
|
||||
userId: user.id,
|
||||
aliasType,
|
||||
aliasType: UserAliasType.SAML,
|
||||
externalId,
|
||||
emails: email ? [email] : [],
|
||||
orgId
|
||||
@@ -449,7 +437,7 @@ export const scimServiceFactory = ({
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
organizationName: org.name,
|
||||
callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/organizations/${org.slug}`
|
||||
callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}`
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -468,14 +456,6 @@ export const scimServiceFactory = ({
|
||||
|
||||
// partial
|
||||
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org.orgAuthMethod) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Neither SAML or OIDC SSO is configured",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
const [membership] = await orgDAL
|
||||
.findMembership({
|
||||
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
||||
@@ -513,9 +493,6 @@ export const scimServiceFactory = ({
|
||||
scimPatch(scimUser, operations);
|
||||
|
||||
const serverCfg = await getServerCfg();
|
||||
const trustScimEmails =
|
||||
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails;
|
||||
|
||||
await userDAL.transaction(async (tx) => {
|
||||
await orgMembershipDAL.updateById(
|
||||
membership.id,
|
||||
@@ -531,7 +508,7 @@ export const scimServiceFactory = ({
|
||||
firstName: scimUser.name.givenName,
|
||||
email: scimUser.emails[0].value,
|
||||
lastName: scimUser.name.familyName,
|
||||
isEmailVerified: hasEmailChanged ? trustScimEmails : true
|
||||
isEmailVerified: hasEmailChanged ? serverCfg.trustSamlEmails : true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -549,14 +526,6 @@ export const scimServiceFactory = ({
|
||||
email,
|
||||
externalId
|
||||
}: TReplaceScimUserDTO) => {
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org.orgAuthMethod) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Neither SAML or OIDC SSO is configured",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
const [membership] = await orgDAL
|
||||
.findMembership({
|
||||
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
||||
@@ -586,7 +555,7 @@ export const scimServiceFactory = ({
|
||||
await userAliasDAL.update(
|
||||
{
|
||||
orgId,
|
||||
aliasType: org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML,
|
||||
aliasType: UserAliasType.SAML,
|
||||
userId: membership.userId
|
||||
},
|
||||
{
|
||||
@@ -607,8 +576,7 @@ export const scimServiceFactory = ({
|
||||
firstName,
|
||||
email,
|
||||
lastName,
|
||||
isEmailVerified:
|
||||
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails
|
||||
isEmailVerified: serverCfg.trustSamlEmails
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -1082,8 +1082,7 @@ export const INTEGRATION = {
|
||||
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
|
||||
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
|
||||
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
|
||||
shouldEnableDelete: "The flag to enable deletion of secrets.",
|
||||
octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy."
|
||||
shouldEnableDelete: "The flag to enable deletion of secrets."
|
||||
}
|
||||
},
|
||||
UPDATE: {
|
||||
|
@@ -10,7 +10,7 @@ export const GITLAB_URL = "https://gitlab.com";
|
||||
export const IS_PACKAGED = (process as any)?.pkg !== undefined;
|
||||
|
||||
const zodStrBool = z
|
||||
.string()
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.transform((val) => val === "true");
|
||||
|
||||
|
24
backend/src/server/lib/schemas.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
interface SlugSchemaInputs {
|
||||
min?: number;
|
||||
max?: number;
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInputs = {}) => {
|
||||
return z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.min(min, {
|
||||
message: `${field} field must be at least ${min} character${min === 1 ? "" : "s"}`
|
||||
})
|
||||
.max(max, {
|
||||
message: `${field} field must be at most ${max} character${max === 1 ? "" : "s"}`
|
||||
})
|
||||
.refine((v) => slugify(v, { lowercase: true, separator: "-" }) === v, {
|
||||
message: `${field} field can only contain letters, numbers, and hyphens`
|
||||
});
|
||||
};
|
@@ -1,4 +1,3 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||
@@ -8,19 +7,12 @@ import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
|
||||
|
||||
const keyNameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(32)
|
||||
.toLowerCase()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Name must be slug friendly"
|
||||
});
|
||||
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
|
||||
const keyDescriptionSchema = z.string().trim().max(500).optional();
|
||||
|
||||
const base64Schema = z.string().superRefine((val, ctx) => {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ExternalGroupOrgRoleMappingsSchema } from "@app/db/schemas/external-group-org-role-mappings";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -48,13 +48,7 @@ export const registerExternalGroupOrgRoleMappingRouter = async (server: FastifyZ
|
||||
mappings: z
|
||||
.object({
|
||||
groupName: z.string().trim().min(1),
|
||||
roleSlug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.toLowerCase()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Role must be a valid slug"
|
||||
})
|
||||
roleSlug: slugSchema({ max: 64 })
|
||||
})
|
||||
.array()
|
||||
}),
|
||||
|
@@ -5,7 +5,6 @@ import { INTEGRATION_AUTH } from "@app/lib/api-docs";
|
||||
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 { OctopusDeployScope } from "@app/services/integration-auth/integration-auth-types";
|
||||
|
||||
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
||||
|
||||
@@ -1009,118 +1008,4 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
||||
return { buildConfigs };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:integrationAuthId/octopus-deploy/scope-values",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
scope: z.nativeEnum(OctopusDeployScope),
|
||||
spaceId: z.string().trim(),
|
||||
resourceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
Environments: z
|
||||
.object({
|
||||
Name: z.string(),
|
||||
Id: z.string()
|
||||
})
|
||||
.array(),
|
||||
Machines: z
|
||||
.object({
|
||||
Name: z.string(),
|
||||
Id: z.string()
|
||||
})
|
||||
.array(),
|
||||
Actions: z
|
||||
.object({
|
||||
Name: z.string(),
|
||||
Id: z.string()
|
||||
})
|
||||
.array(),
|
||||
Roles: z
|
||||
.object({
|
||||
Name: z.string(),
|
||||
Id: z.string()
|
||||
})
|
||||
.array(),
|
||||
Channels: z
|
||||
.object({
|
||||
Name: z.string(),
|
||||
Id: z.string()
|
||||
})
|
||||
.array(),
|
||||
TenantTags: z
|
||||
.object({
|
||||
Name: z.string(),
|
||||
Id: z.string()
|
||||
})
|
||||
.array(),
|
||||
Processes: z
|
||||
.object({
|
||||
ProcessType: z.string(),
|
||||
Name: z.string(),
|
||||
Id: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const scopeValues = await server.services.integrationAuth.getOctopusDeployScopeValues({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.integrationAuthId,
|
||||
scope: req.query.scope,
|
||||
spaceId: req.query.spaceId,
|
||||
resourceId: req.query.resourceId
|
||||
});
|
||||
return scopeValues;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:integrationAuthId/octopus-deploy/spaces",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
spaces: z
|
||||
.object({
|
||||
Name: z.string(),
|
||||
Id: z.string(),
|
||||
IsDefault: z.boolean()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const spaces = await server.services.integrationAuth.getOctopusDeploySpaces({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.integrationAuthId
|
||||
});
|
||||
return { spaces };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
@@ -14,6 +13,7 @@ import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-t
|
||||
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
||||
import { getLastMidnightDateISO } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -243,22 +243,10 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
body: z.object({
|
||||
name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(64, { message: "Slug must be 64 or fewer characters" })
|
||||
.regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens")
|
||||
.optional(),
|
||||
slug: slugSchema({ max: 64 }).optional(),
|
||||
authEnforced: z.boolean().optional(),
|
||||
scimEnabled: z.boolean().optional(),
|
||||
defaultMembershipRoleSlug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.trim()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Membership role must be a valid slug"
|
||||
})
|
||||
.optional(),
|
||||
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
|
||||
enforceMfa: z.boolean().optional(),
|
||||
selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
|
||||
}),
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectEnvironmentsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ENVIRONMENTS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -124,13 +124,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
|
||||
position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(ENVIRONMENTS.CREATE.slug)
|
||||
slug: slugSchema({ max: 64 }).describe(ENVIRONMENTS.CREATE.slug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -188,14 +182,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
||||
id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.refine((v) => !v || slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(ENVIRONMENTS.UPDATE.slug),
|
||||
slug: slugSchema({ max: 64 }).optional().describe(ENVIRONMENTS.UPDATE.slug),
|
||||
name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name),
|
||||
position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position)
|
||||
}),
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretTagsSchema } from "@app/db/schemas";
|
||||
import { SECRET_TAGS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -111,14 +111,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.describe(SECRET_TAGS.CREATE.slug)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
|
||||
}),
|
||||
slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.CREATE.slug),
|
||||
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
|
||||
}),
|
||||
response: {
|
||||
@@ -153,14 +146,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.describe(SECRET_TAGS.UPDATE.slug)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
|
||||
}),
|
||||
slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.UPDATE.slug),
|
||||
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
|
||||
}),
|
||||
response: {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SlackIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -35,12 +35,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
}),
|
||||
slug: slugSchema({ max: 64 }),
|
||||
description: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
@@ -288,13 +283,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional(),
|
||||
slug: slugSchema({ max: 64 }).optional(),
|
||||
description: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
|
@@ -14,12 +14,10 @@ import { Strategy as GoogleStrategy } from "passport-google-oauth20";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { fetchGithubEmails } from "@app/lib/requests/github";
|
||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||
import { OrgAuthMethod } from "@app/services/org/org-types";
|
||||
|
||||
export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -198,44 +196,6 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
handler: () => {}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/redirect/organizations/:orgSlug",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
orgSlug: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
callback_port: z.string().optional()
|
||||
})
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const org = await server.services.org.findOrgBySlug(req.params.orgSlug);
|
||||
if (org.orgAuthMethod === OrgAuthMethod.SAML) {
|
||||
return res.redirect(
|
||||
`${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}?${
|
||||
req.query.callback_port ? `callback_port=${req.query.callback_port}` : ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
if (org.orgAuthMethod === OrgAuthMethod.OIDC) {
|
||||
return res.redirect(
|
||||
`${appCfg.SITE_URL}/api/v1/sso/oidc/login?orgSlug=${org.slug}${
|
||||
req.query.callback_port ? `&callbackPort=${req.query.callback_port}` : ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "The organization does not have any SSO configured."
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/github",
|
||||
method: "GET",
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
@@ -12,6 +11,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -27,14 +27,6 @@ const projectWithEnv = SanitizedProjectSchema.extend({
|
||||
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
|
||||
});
|
||||
|
||||
const slugSchema = z
|
||||
.string()
|
||||
.min(5)
|
||||
.max(36)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be at least 5 character but no more than 36"
|
||||
});
|
||||
|
||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
/* Get project key */
|
||||
server.route({
|
||||
@@ -162,21 +154,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
|
||||
projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription),
|
||||
slug: z
|
||||
.string()
|
||||
.min(5)
|
||||
.max(36)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(PROJECTS.CREATE.slug),
|
||||
slug: slugSchema({ min: 5, max: 36 }).optional().describe(PROJECTS.CREATE.slug),
|
||||
kmsKeyId: z.string().optional(),
|
||||
template: z
|
||||
.string()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Template name must be in slug format"
|
||||
})
|
||||
template: slugSchema({ field: "Template Name", max: 64 })
|
||||
.optional()
|
||||
.default(InfisicalProjectTemplate.Default)
|
||||
.describe(PROJECTS.CREATE.template)
|
||||
@@ -244,7 +224,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
slug: slugSchema.describe("The slug of the project to delete.")
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to delete.")
|
||||
}),
|
||||
response: {
|
||||
200: SanitizedProjectSchema
|
||||
@@ -278,7 +258,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema.describe("The slug of the project to get.")
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.")
|
||||
}),
|
||||
response: {
|
||||
200: projectWithEnv
|
||||
@@ -311,7 +291,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema.describe("The slug of the project to update.")
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to update.")
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
|
||||
@@ -354,7 +334,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema.describe(PROJECTS.LIST_CAS.slug)
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CAS.slug)
|
||||
}),
|
||||
querystring: z.object({
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status),
|
||||
@@ -395,7 +375,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema.describe(PROJECTS.LIST_CERTIFICATES.slug)
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CERTIFICATES.slug)
|
||||
}),
|
||||
querystring: z.object({
|
||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
||||
|
@@ -120,8 +120,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
apiVersion: "authentication.k8s.io/v1",
|
||||
kind: "TokenReview",
|
||||
spec: {
|
||||
token: serviceAccountJwt,
|
||||
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
|
||||
token: serviceAccountJwt
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { Client as OctopusDeployClient, ProjectRepository as OctopusDeployRepository } from "@octopusdeploy/api-client";
|
||||
|
||||
import { TIntegrationAuths } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -1088,33 +1087,6 @@ const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: strin
|
||||
return apps;
|
||||
};
|
||||
|
||||
const getAppsOctopusDeploy = async ({
|
||||
apiKey,
|
||||
instanceURL,
|
||||
spaceName = "Default"
|
||||
}: {
|
||||
apiKey: string;
|
||||
instanceURL: string;
|
||||
spaceName?: string;
|
||||
}) => {
|
||||
const client = await OctopusDeployClient.create({
|
||||
instanceURL,
|
||||
apiKey,
|
||||
userAgentApp: "Infisical Integration"
|
||||
});
|
||||
|
||||
const repository = new OctopusDeployRepository(client, spaceName);
|
||||
|
||||
const projects = await repository.list({
|
||||
take: 1000
|
||||
});
|
||||
|
||||
return projects.Items.map((project) => ({
|
||||
name: project.Name,
|
||||
appId: project.Id
|
||||
}));
|
||||
};
|
||||
|
||||
export const getApps = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
@@ -1288,13 +1260,6 @@ export const getApps = async ({
|
||||
orgName: azureDevOpsOrgName as string
|
||||
});
|
||||
|
||||
case Integrations.OCTOPUS_DEPLOY:
|
||||
return getAppsOctopusDeploy({
|
||||
apiKey: accessToken,
|
||||
instanceURL: url!,
|
||||
spaceName: workspaceSlug
|
||||
});
|
||||
|
||||
default:
|
||||
throw new NotFoundError({ message: `Integration '${integration}' not found` });
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client";
|
||||
import AWS from "aws-sdk";
|
||||
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
|
||||
@@ -10,7 +9,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
@@ -21,7 +20,6 @@ import { getApps } from "./integration-app-list";
|
||||
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||
import {
|
||||
OctopusDeployScope,
|
||||
TBitbucketEnvironment,
|
||||
TBitbucketWorkspace,
|
||||
TChecklyGroups,
|
||||
@@ -40,8 +38,6 @@ import {
|
||||
TIntegrationAuthGithubOrgsDTO,
|
||||
TIntegrationAuthHerokuPipelinesDTO,
|
||||
TIntegrationAuthNorthflankSecretGroupDTO,
|
||||
TIntegrationAuthOctopusDeployProjectScopeValuesDTO,
|
||||
TIntegrationAuthOctopusDeploySpacesDTO,
|
||||
TIntegrationAuthQoveryEnvironmentsDTO,
|
||||
TIntegrationAuthQoveryOrgsDTO,
|
||||
TIntegrationAuthQoveryProjectDTO,
|
||||
@@ -52,7 +48,6 @@ import {
|
||||
TIntegrationAuthVercelBranchesDTO,
|
||||
TNorthflankSecretGroup,
|
||||
TOauthExchangeDTO,
|
||||
TOctopusDeployVariableSet,
|
||||
TSaveIntegrationAccessTokenDTO,
|
||||
TTeamCityBuildConfig,
|
||||
TVercelBranches
|
||||
@@ -1526,88 +1521,6 @@ export const integrationAuthServiceFactory = ({
|
||||
return integrationAuthDAL.create(newIntegrationAuth);
|
||||
};
|
||||
|
||||
const getOctopusDeploySpaces = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TIntegrationAuthOctopusDeploySpacesDTO) => {
|
||||
const integrationAuth = await integrationAuthDAL.findById(id);
|
||||
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integrationAuth.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
|
||||
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||
|
||||
const client = await OctopusClient.create({
|
||||
apiKey: accessToken,
|
||||
instanceURL: integrationAuth.url!,
|
||||
userAgentApp: "Infisical Integration"
|
||||
});
|
||||
|
||||
const spaceRepository = new OctopusSpaceRepository(client);
|
||||
|
||||
const spaces = await spaceRepository.list({
|
||||
partialName: "", // throws error if no string is present...
|
||||
take: 1000
|
||||
});
|
||||
|
||||
return spaces.Items;
|
||||
};
|
||||
|
||||
const getOctopusDeployScopeValues = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id,
|
||||
scope,
|
||||
spaceId,
|
||||
resourceId
|
||||
}: TIntegrationAuthOctopusDeployProjectScopeValuesDTO) => {
|
||||
const integrationAuth = await integrationAuthDAL.findById(id);
|
||||
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integrationAuth.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
|
||||
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||
|
||||
let url: string;
|
||||
switch (scope) {
|
||||
case OctopusDeployScope.Project:
|
||||
url = `${integrationAuth.url}/api/${spaceId}/projects/${resourceId}/variables`;
|
||||
break;
|
||||
// future support tenant, variable set etc.
|
||||
default:
|
||||
throw new InternalServerError({ message: `Unhandled Octopus Deploy scope` });
|
||||
}
|
||||
|
||||
// SDK doesn't support variable set...
|
||||
const { data: variableSet } = await request.get<TOctopusDeployVariableSet>(url, {
|
||||
headers: {
|
||||
"X-NuGet-ApiKey": accessToken,
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
return variableSet.ScopeValues;
|
||||
};
|
||||
|
||||
return {
|
||||
listIntegrationAuthByProjectId,
|
||||
listOrgIntegrationAuth,
|
||||
@@ -1639,8 +1552,6 @@ export const integrationAuthServiceFactory = ({
|
||||
getBitbucketWorkspaces,
|
||||
getBitbucketEnvironments,
|
||||
getIntegrationAccessToken,
|
||||
duplicateIntegrationAuth,
|
||||
getOctopusDeploySpaces,
|
||||
getOctopusDeployScopeValues
|
||||
duplicateIntegrationAuth
|
||||
};
|
||||
};
|
||||
|
@@ -193,72 +193,3 @@ export type TIntegrationsWithEnvironment = TIntegrations & {
|
||||
| null
|
||||
| undefined;
|
||||
};
|
||||
|
||||
export type TIntegrationAuthOctopusDeploySpacesDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthOctopusDeployProjectScopeValuesDTO = {
|
||||
id: string;
|
||||
spaceId: string;
|
||||
resourceId: string;
|
||||
scope: OctopusDeployScope;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export enum OctopusDeployScope {
|
||||
Project = "project"
|
||||
// add tenant, variable set, etc.
|
||||
}
|
||||
|
||||
export type TOctopusDeployVariableSet = {
|
||||
Id: string;
|
||||
OwnerId: string;
|
||||
Version: number;
|
||||
Variables: {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Value: string;
|
||||
Description: string;
|
||||
Scope: {
|
||||
Environment?: string[];
|
||||
Machine?: string[];
|
||||
Role?: string[];
|
||||
TargetRole?: string[];
|
||||
Action?: string[];
|
||||
User?: string[];
|
||||
Trigger?: string[];
|
||||
ParentDeployment?: string[];
|
||||
Private?: string[];
|
||||
Channel?: string[];
|
||||
TenantTag?: string[];
|
||||
Tenant?: string[];
|
||||
ProcessOwner?: string[];
|
||||
};
|
||||
IsEditable: boolean;
|
||||
Prompt: {
|
||||
Description: string;
|
||||
DisplaySettings: Record<string, string>;
|
||||
Label: string;
|
||||
Required: boolean;
|
||||
} | null;
|
||||
Type: "String";
|
||||
IsSensitive: boolean;
|
||||
}[];
|
||||
ScopeValues: {
|
||||
Environments: { Id: string; Name: string }[];
|
||||
Machines: { Id: string; Name: string }[];
|
||||
Actions: { Id: string; Name: string }[];
|
||||
Roles: { Id: string; Name: string }[];
|
||||
Channels: { Id: string; Name: string }[];
|
||||
TenantTags: { Id: string; Name: string }[];
|
||||
Processes: {
|
||||
ProcessType: string;
|
||||
Id: string;
|
||||
Name: string;
|
||||
}[];
|
||||
};
|
||||
SpaceId: string;
|
||||
Links: {
|
||||
Self: string;
|
||||
};
|
||||
};
|
||||
|
@@ -34,8 +34,7 @@ export enum Integrations {
|
||||
HASURA_CLOUD = "hasura-cloud",
|
||||
RUNDECK = "rundeck",
|
||||
AZURE_DEVOPS = "azure-devops",
|
||||
AZURE_APP_CONFIGURATION = "azure-app-configuration",
|
||||
OCTOPUS_DEPLOY = "octopus-deploy"
|
||||
AZURE_APP_CONFIGURATION = "azure-app-configuration"
|
||||
}
|
||||
|
||||
export enum IntegrationType {
|
||||
@@ -414,15 +413,6 @@ export const getIntegrationOptions = async () => {
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Octopus Deploy",
|
||||
slug: "octopus-deploy",
|
||||
image: "Octopus Deploy.png",
|
||||
isAvailable: true,
|
||||
type: "sat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
}
|
||||
];
|
||||
|
||||
|
@@ -32,14 +32,14 @@ import { z } from "zod";
|
||||
import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
|
||||
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
|
||||
import { OctopusDeployScope, TIntegrationsWithEnvironment, TOctopusDeployVariableSet } from "./integration-auth-types";
|
||||
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
|
||||
import {
|
||||
IntegrationInitialSyncBehavior,
|
||||
IntegrationMappingBehavior,
|
||||
@@ -473,7 +473,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
id: string; // secret URI
|
||||
value: string;
|
||||
attributes: {
|
||||
enabled: boolean;
|
||||
enabled: true;
|
||||
created: number;
|
||||
updated: number;
|
||||
recoveryLevel: string;
|
||||
@@ -509,19 +509,10 @@ const syncSecretsAzureKeyVault = async ({
|
||||
|
||||
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`);
|
||||
|
||||
const enabledAzureKeyVaultSecrets = getAzureKeyVaultSecrets.filter((secret) => secret.attributes.enabled);
|
||||
|
||||
// disabled keys to skip sending updates to
|
||||
const disabledAzureKeyVaultSecretKeys = getAzureKeyVaultSecrets
|
||||
.filter(({ attributes }) => !attributes.enabled)
|
||||
.map((getAzureKeyVaultSecret) => {
|
||||
return getAzureKeyVaultSecret.id.substring(getAzureKeyVaultSecret.id.lastIndexOf("/") + 1);
|
||||
});
|
||||
|
||||
let lastSlashIndex: number;
|
||||
const res = (
|
||||
await Promise.all(
|
||||
enabledAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
|
||||
getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
|
||||
if (!lastSlashIndex) {
|
||||
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/");
|
||||
}
|
||||
@@ -667,7 +658,6 @@ const syncSecretsAzureKeyVault = async ({
|
||||
}) => {
|
||||
let isSecretSet = false;
|
||||
let maxTries = 6;
|
||||
if (disabledAzureKeyVaultSecretKeys.includes(key)) return;
|
||||
|
||||
while (!isSecretSet && maxTries > 0) {
|
||||
// try to set secret
|
||||
@@ -4211,61 +4201,6 @@ const syncSecretsRundeck = async ({
|
||||
}
|
||||
};
|
||||
|
||||
const syncSecretsOctopusDeploy = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: TIntegrations;
|
||||
integrationAuth: TIntegrationAuths;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let url: string;
|
||||
switch (integration.scope) {
|
||||
case OctopusDeployScope.Project:
|
||||
url = `${integrationAuth.url}/api/${integration.targetEnvironmentId}/projects/${integration.appId}/variables`;
|
||||
break;
|
||||
// future support tenant, variable set, etc.
|
||||
default:
|
||||
throw new InternalServerError({ message: `Unhandled Octopus Deploy scope: ${integration.scope}` });
|
||||
}
|
||||
|
||||
// SDK doesn't support variable set...
|
||||
const { data: variableSet } = await request.get<TOctopusDeployVariableSet>(url, {
|
||||
headers: {
|
||||
"X-NuGet-ApiKey": accessToken,
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
await request.put(
|
||||
url,
|
||||
{
|
||||
...variableSet,
|
||||
Variables: Object.entries(secrets).map(([key, value]) => ({
|
||||
Name: key,
|
||||
Value: value.value,
|
||||
Description: value.comment ?? "",
|
||||
Scope:
|
||||
(integration.metadata as { octopusDeployScopeValues: TOctopusDeployVariableSet["ScopeValues"] })
|
||||
?.octopusDeployScopeValues ?? {},
|
||||
IsEditable: false,
|
||||
Prompt: null,
|
||||
Type: "String",
|
||||
IsSensitive: true
|
||||
}))
|
||||
} as unknown as TOctopusDeployVariableSet,
|
||||
{
|
||||
headers: {
|
||||
"X-NuGet-ApiKey": accessToken,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to [app] in integration named [integration]
|
||||
*
|
||||
@@ -4578,14 +4513,6 @@ export const syncIntegrationSecrets = async ({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case Integrations.OCTOPUS_DEPLOY:
|
||||
await syncSecretsOctopusDeploy({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({ message: "Invalid integration" });
|
||||
}
|
||||
|
@@ -46,18 +46,5 @@ export const IntegrationMetadataSchema = z.object({
|
||||
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
|
||||
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete),
|
||||
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
|
||||
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets),
|
||||
|
||||
octopusDeployScopeValues: z
|
||||
.object({
|
||||
// in Octopus Deploy Scope Value Format
|
||||
Environment: z.string().array().optional(),
|
||||
Action: z.string().array().optional(),
|
||||
Channel: z.string().array().optional(),
|
||||
Machine: z.string().array().optional(),
|
||||
ProcessOwner: z.string().array().optional(),
|
||||
Role: z.string().array().optional()
|
||||
})
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.octopusDeployScopeValues)
|
||||
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets)
|
||||
});
|
||||
|
@@ -14,8 +14,6 @@ import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, withTransaction } from "@app/lib/knex";
|
||||
import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
|
||||
|
||||
import { OrgAuthMethod } from "./org-types";
|
||||
|
||||
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
|
||||
|
||||
export const orgDALFactory = (db: TDbClient) => {
|
||||
@@ -23,78 +21,13 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findOrgById = async (orgId: string) => {
|
||||
try {
|
||||
const org = (await db
|
||||
.replicaNode()(TableName.Organization)
|
||||
.where({ [`${TableName.Organization}.id` as "id"]: orgId })
|
||||
.leftJoin(TableName.SamlConfig, (qb) => {
|
||||
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||
`${TableName.SamlConfig}.isActive`,
|
||||
"=",
|
||||
db.raw("true")
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.OidcConfig, (qb) => {
|
||||
qb.on(`${TableName.OidcConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||
`${TableName.OidcConfig}.isActive`,
|
||||
"=",
|
||||
db.raw("true")
|
||||
);
|
||||
})
|
||||
.select(selectAllTableCols(TableName.Organization))
|
||||
.select(
|
||||
db.raw(`
|
||||
CASE
|
||||
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.SAML}'
|
||||
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.OIDC}'
|
||||
ELSE ''
|
||||
END as "orgAuthMethod"
|
||||
`)
|
||||
)
|
||||
.first()) as TOrganizations & { orgAuthMethod?: string };
|
||||
|
||||
const org = await db.replicaNode()(TableName.Organization).where({ id: orgId }).first();
|
||||
return org;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find org by id" });
|
||||
}
|
||||
};
|
||||
|
||||
const findOrgBySlug = async (orgSlug: string) => {
|
||||
try {
|
||||
const org = (await db
|
||||
.replicaNode()(TableName.Organization)
|
||||
.where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug })
|
||||
.leftJoin(TableName.SamlConfig, (qb) => {
|
||||
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||
`${TableName.SamlConfig}.isActive`,
|
||||
"=",
|
||||
db.raw("true")
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.OidcConfig, (qb) => {
|
||||
qb.on(`${TableName.OidcConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||
`${TableName.OidcConfig}.isActive`,
|
||||
"=",
|
||||
db.raw("true")
|
||||
);
|
||||
})
|
||||
.select(selectAllTableCols(TableName.Organization))
|
||||
.select(
|
||||
db.raw(`
|
||||
CASE
|
||||
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.SAML}'
|
||||
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.OIDC}'
|
||||
ELSE ''
|
||||
END as "orgAuthMethod"
|
||||
`)
|
||||
)
|
||||
.first()) as TOrganizations & { orgAuthMethod?: string };
|
||||
|
||||
return org;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find org by slug" });
|
||||
}
|
||||
};
|
||||
|
||||
// special query
|
||||
const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => {
|
||||
try {
|
||||
@@ -465,7 +398,6 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
findAllOrgMembers,
|
||||
countAllOrgMembers,
|
||||
findOrgById,
|
||||
findOrgBySlug,
|
||||
findAllOrgsByUserId,
|
||||
ghostUserExists,
|
||||
findOrgMembersByUsername,
|
||||
|
@@ -187,15 +187,6 @@ export const orgServiceFactory = ({
|
||||
return members;
|
||||
};
|
||||
|
||||
const findOrgBySlug = async (slug: string) => {
|
||||
const org = await orgDAL.findOrgBySlug(slug);
|
||||
if (!org) {
|
||||
throw new NotFoundError({ message: `Organization with slug '${slug}' not found` });
|
||||
}
|
||||
|
||||
return org;
|
||||
};
|
||||
|
||||
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
|
||||
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
|
||||
|
||||
@@ -284,7 +275,6 @@ export const orgServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
const currentOrg = await orgDAL.findOrgById(actorOrgId);
|
||||
|
||||
if (enforceMfa !== undefined) {
|
||||
if (!plan.enforceMfa) {
|
||||
@@ -315,11 +305,6 @@ export const orgServiceFactory = ({
|
||||
"Failed to enable/disable SCIM provisioning due to plan restriction. Upgrade plan to enable/disable SCIM provisioning."
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
||||
if (scimEnabled && !currentOrg.orgAuthMethod) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot enable SCIM when neither SAML or OIDC is configured."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (authEnforced) {
|
||||
@@ -1147,7 +1132,6 @@ export const orgServiceFactory = ({
|
||||
createIncidentContact,
|
||||
deleteIncidentContact,
|
||||
getOrgGroups,
|
||||
listProjectMembershipsByOrgMembershipId,
|
||||
findOrgBySlug
|
||||
listProjectMembershipsByOrgMembershipId
|
||||
};
|
||||
};
|
||||
|
@@ -74,8 +74,3 @@ export type TGetOrgGroupsDTO = TOrgPermission;
|
||||
export type TListProjectMembershipsByOrgMembershipIdDTO = {
|
||||
orgMembershipId: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export enum OrgAuthMethod {
|
||||
OIDC = "oidc",
|
||||
SAML = "saml"
|
||||
}
|
||||
|
@@ -365,8 +365,9 @@ export const recursivelyGetSecretPaths = async ({
|
||||
folderId: p.folderId
|
||||
}));
|
||||
|
||||
// path relative will start with ../ if its outside directory
|
||||
const pathsInCurrentDirectory = paths.filter((folder) => !path.relative(currentPath, folder.path).startsWith(".."));
|
||||
const pathsInCurrentDirectory = paths.filter((folder) =>
|
||||
folder.path.startsWith(currentPath === "/" ? "" : currentPath)
|
||||
);
|
||||
|
||||
return pathsInCurrentDirectory;
|
||||
};
|
||||
|
@@ -10,7 +10,7 @@ require (
|
||||
github.com/fatih/semgroup v1.2.0
|
||||
github.com/gitleaks/go-gitdiff v0.8.0
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/infisical/go-sdk v0.4.3
|
||||
github.com/infisical/go-sdk v0.3.8
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
||||
github.com/muesli/mango-cobra v1.2.0
|
||||
|
@@ -265,8 +265,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/infisical/go-sdk v0.4.3 h1:O5ZJ2eCBAZDE9PIAfBPq9Utb2CgQKrhmj9R0oFTRu4U=
|
||||
github.com/infisical/go-sdk v0.4.3/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M=
|
||||
github.com/infisical/go-sdk v0.3.8 h1:0dGOhF3cwt0q5QzpnUs4lxwBiEza+DQYOyvEn7AfrM0=
|
||||
github.com/infisical/go-sdk v0.3.8/go.mod h1:HHW7DgUqoolyQIUw/9HdpkZ3bDLwWyZ0HEtYiVaDKQw=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
|
@@ -205,25 +205,6 @@ func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesR
|
||||
return workSpacesResponse, nil
|
||||
}
|
||||
|
||||
func CallGetProjectById(httpClient *resty.Client, id string) (Project, error) {
|
||||
var projectResponse GetProjectByIdResponse
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&projectResponse).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Get(fmt.Sprintf("%v/v1/workspace/%s", config.INFISICAL_URL, id))
|
||||
|
||||
if err != nil {
|
||||
return Project{}, err
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return Project{}, fmt.Errorf("CallGetProjectById: Unsuccessful response: [response=%v]", response)
|
||||
}
|
||||
|
||||
return projectResponse.Project, nil
|
||||
}
|
||||
|
||||
func CallIsAuthenticated(httpClient *resty.Client) bool {
|
||||
var workSpacesResponse GetWorkSpacesResponse
|
||||
response, err := httpClient.
|
||||
|
@@ -128,10 +128,6 @@ type GetWorkSpacesResponse struct {
|
||||
} `json:"workspaces"`
|
||||
}
|
||||
|
||||
type GetProjectByIdResponse struct {
|
||||
Project Project `json:"workspace"`
|
||||
}
|
||||
|
||||
type GetOrganizationsResponse struct {
|
||||
Organizations []struct {
|
||||
ID string `json:"id"`
|
||||
@@ -167,12 +163,6 @@ type Secret struct {
|
||||
PlainTextKey string `json:"plainTextKey"`
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
type RawSecret struct {
|
||||
SecretKey string `json:"secretKey,omitempty"`
|
||||
SecretValue string `json:"secretValue,omitempty"`
|
||||
|
@@ -1,571 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/Infisical/infisical-merge/packages/visualize"
|
||||
|
||||
// "github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
// "github.com/Infisical/infisical-merge/packages/visualize"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/posthog/posthog-go"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
infisicalSdkModels "github.com/infisical/go-sdk/packages/models"
|
||||
)
|
||||
|
||||
var dynamicSecretCmd = &cobra.Command{
|
||||
Example: `infisical dynamic-secrets`,
|
||||
Short: "Used to list dynamic secrets",
|
||||
Use: "dynamic-secrets",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: getDynamicSecretList,
|
||||
}
|
||||
|
||||
func getDynamicSecretList(cmd *cobra.Command, args []string) {
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
if !cmd.Flags().Changed("env") {
|
||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||
if environmentFromWorkspace != "" {
|
||||
environmentName = environmentFromWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secretsPath, err := cmd.Flags().GetString("path")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse path flag")
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
httpClient := resty.New()
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get local project details")
|
||||
}
|
||||
projectId = workspaceFile.WorkspaceId
|
||||
}
|
||||
|
||||
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||
infisicalToken = token.Token
|
||||
} else {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to authenticate")
|
||||
}
|
||||
|
||||
if loggedInUserDetails.LoginExpired {
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(infisicalToken)
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch project details")
|
||||
}
|
||||
|
||||
dynamicSecretRootCredentials, err := infisicalClient.DynamicSecrets().List(infisicalSdk.ListDynamicSecretsRootCredentialsOptions{
|
||||
ProjectSlug: projectDetails.Slug,
|
||||
SecretPath: secretsPath,
|
||||
EnvironmentSlug: environmentName,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
||||
}
|
||||
|
||||
visualize.PrintAllDynamicRootCredentials(dynamicSecretRootCredentials)
|
||||
Telemetry.CaptureEvent("cli-command:dynamic-secrets", posthog.NewProperties().Set("count", len(dynamicSecretRootCredentials)).Set("version", util.CLI_VERSION))
|
||||
}
|
||||
|
||||
var dynamicSecretLeaseCmd = &cobra.Command{
|
||||
Example: `lease`,
|
||||
Short: "Manage leases for dynamic secrets",
|
||||
Use: "lease",
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
var dynamicSecretLeaseCreateCmd = &cobra.Command{
|
||||
Example: `lease create <dynamic secret name>"`,
|
||||
Short: "Used to lease dynamic secret by name",
|
||||
Use: "create [dynamic-secret]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: createDynamicSecretLeaseByName,
|
||||
}
|
||||
|
||||
func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
dynamicSecretRootCredentialName := args[0]
|
||||
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
if !cmd.Flags().Changed("env") {
|
||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||
if environmentFromWorkspace != "" {
|
||||
environmentName = environmentFromWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
ttl, err := cmd.Flags().GetString("ttl")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secretsPath, err := cmd.Flags().GetString("path")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse path flag")
|
||||
}
|
||||
|
||||
plainOutput, err := cmd.Flags().GetBool("plain")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
httpClient := resty.New()
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get local project details")
|
||||
}
|
||||
projectId = workspaceFile.WorkspaceId
|
||||
}
|
||||
|
||||
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||
infisicalToken = token.Token
|
||||
} else {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to authenticate")
|
||||
}
|
||||
|
||||
if loggedInUserDetails.LoginExpired {
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(infisicalToken)
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch project details")
|
||||
}
|
||||
|
||||
dynamicSecretRootCredential, err := infisicalClient.DynamicSecrets().GetByName(infisicalSdk.GetDynamicSecretRootCredentialByNameOptions{
|
||||
DynamicSecretName: dynamicSecretRootCredentialName,
|
||||
ProjectSlug: projectDetails.Slug,
|
||||
SecretPath: secretsPath,
|
||||
EnvironmentSlug: environmentName,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
||||
}
|
||||
|
||||
leaseCredentials, _, leaseDetails, err := infisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{
|
||||
DynamicSecretName: dynamicSecretRootCredential.Name,
|
||||
ProjectSlug: projectDetails.Slug,
|
||||
TTL: ttl,
|
||||
SecretPath: secretsPath,
|
||||
EnvironmentSlug: environmentName,
|
||||
})
|
||||
if err != nil {
|
||||
util.HandleError(err, "To lease dynamic secret")
|
||||
}
|
||||
|
||||
if plainOutput {
|
||||
for key, value := range leaseCredentials {
|
||||
if cred, ok := value.(string); ok {
|
||||
fmt.Printf("%s=%s\n", key, cred)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Dynamic Secret Leasing")
|
||||
fmt.Printf("Name: %s\n", dynamicSecretRootCredential.Name)
|
||||
fmt.Printf("Provider: %s\n", dynamicSecretRootCredential.Type)
|
||||
fmt.Printf("Lease ID: %s\n", leaseDetails.Id)
|
||||
fmt.Printf("Expire At: %s\n", leaseDetails.ExpireAt.Local().Format("02-Jan-2006 03:04:05 PM"))
|
||||
visualize.PrintAllDyamicSecretLeaseCredentials(leaseCredentials)
|
||||
}
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:dynamic-secrets lease", posthog.NewProperties().Set("type", dynamicSecretRootCredential.Type).Set("version", util.CLI_VERSION))
|
||||
}
|
||||
|
||||
var dynamicSecretLeaseRenewCmd = &cobra.Command{
|
||||
Example: `lease renew <dynamic secret name>"`,
|
||||
Short: "Used to renew dynamic secret lease by name",
|
||||
Use: "renew [lease-id]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: renewDynamicSecretLeaseByName,
|
||||
}
|
||||
|
||||
func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
dynamicSecretLeaseId := args[0]
|
||||
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
if !cmd.Flags().Changed("env") {
|
||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||
if environmentFromWorkspace != "" {
|
||||
environmentName = environmentFromWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
ttl, err := cmd.Flags().GetString("ttl")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secretsPath, err := cmd.Flags().GetString("path")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse path flag")
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
httpClient := resty.New()
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get local project details")
|
||||
}
|
||||
projectId = workspaceFile.WorkspaceId
|
||||
}
|
||||
|
||||
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||
infisicalToken = token.Token
|
||||
} else {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to authenticate")
|
||||
}
|
||||
|
||||
if loggedInUserDetails.LoginExpired {
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(infisicalToken)
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch project details")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
||||
}
|
||||
|
||||
leaseDetails, err := infisicalClient.DynamicSecrets().Leases().RenewById(infisicalSdk.RenewDynamicSecretLeaseOptions{
|
||||
ProjectSlug: projectDetails.Slug,
|
||||
TTL: ttl,
|
||||
SecretPath: secretsPath,
|
||||
EnvironmentSlug: environmentName,
|
||||
LeaseId: dynamicSecretLeaseId,
|
||||
})
|
||||
if err != nil {
|
||||
util.HandleError(err, "To renew dynamic secret lease")
|
||||
}
|
||||
|
||||
fmt.Println("Successfully renewed dynamic secret lease")
|
||||
visualize.PrintAllDynamicSecretLeases([]infisicalSdkModels.DynamicSecretLease{leaseDetails})
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:dynamic-secrets lease renew", posthog.NewProperties().Set("version", util.CLI_VERSION))
|
||||
}
|
||||
|
||||
var dynamicSecretLeaseRevokeCmd = &cobra.Command{
|
||||
Example: `lease delete <dynamic secret name>"`,
|
||||
Short: "Used to delete dynamic secret lease by name",
|
||||
Use: "delete [lease-id]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: revokeDynamicSecretLeaseByName,
|
||||
}
|
||||
|
||||
func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
dynamicSecretLeaseId := args[0]
|
||||
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
if !cmd.Flags().Changed("env") {
|
||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||
if environmentFromWorkspace != "" {
|
||||
environmentName = environmentFromWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secretsPath, err := cmd.Flags().GetString("path")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse path flag")
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
httpClient := resty.New()
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get local project details")
|
||||
}
|
||||
projectId = workspaceFile.WorkspaceId
|
||||
}
|
||||
|
||||
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||
infisicalToken = token.Token
|
||||
} else {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to authenticate")
|
||||
}
|
||||
|
||||
if loggedInUserDetails.LoginExpired {
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(infisicalToken)
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch project details")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
||||
}
|
||||
|
||||
leaseDetails, err := infisicalClient.DynamicSecrets().Leases().DeleteById(infisicalSdk.DeleteDynamicSecretLeaseOptions{
|
||||
ProjectSlug: projectDetails.Slug,
|
||||
SecretPath: secretsPath,
|
||||
EnvironmentSlug: environmentName,
|
||||
LeaseId: dynamicSecretLeaseId,
|
||||
})
|
||||
if err != nil {
|
||||
util.HandleError(err, "To revoke dynamic secret lease")
|
||||
}
|
||||
|
||||
fmt.Println("Successfully revoked dynamic secret lease")
|
||||
visualize.PrintAllDynamicSecretLeases([]infisicalSdkModels.DynamicSecretLease{leaseDetails})
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:dynamic-secrets lease revoke", posthog.NewProperties().Set("version", util.CLI_VERSION))
|
||||
}
|
||||
|
||||
var dynamicSecretLeaseListCmd = &cobra.Command{
|
||||
Example: `lease list <dynamic secret name>"`,
|
||||
Short: "Used to list leases of a dynamic secret by name",
|
||||
Use: "list [dynamic-secret]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: listDynamicSecretLeaseByName,
|
||||
}
|
||||
|
||||
func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
dynamicSecretRootCredentialName := args[0]
|
||||
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
if !cmd.Flags().Changed("env") {
|
||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||
if environmentFromWorkspace != "" {
|
||||
environmentName = environmentFromWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secretsPath, err := cmd.Flags().GetString("path")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse path flag")
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
httpClient := resty.New()
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get local project details")
|
||||
}
|
||||
projectId = workspaceFile.WorkspaceId
|
||||
}
|
||||
|
||||
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||
infisicalToken = token.Token
|
||||
} else {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to authenticate")
|
||||
}
|
||||
|
||||
if loggedInUserDetails.LoginExpired {
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(infisicalToken)
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch project details")
|
||||
}
|
||||
|
||||
dynamicSecretLeases, err := infisicalClient.DynamicSecrets().Leases().List(infisicalSdk.ListDynamicSecretLeasesOptions{
|
||||
DynamicSecretName: dynamicSecretRootCredentialName,
|
||||
ProjectSlug: projectDetails.Slug,
|
||||
SecretPath: secretsPath,
|
||||
EnvironmentSlug: environmentName,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch dynamic secret leases list")
|
||||
}
|
||||
|
||||
visualize.PrintAllDynamicSecretLeases(dynamicSecretLeases)
|
||||
Telemetry.CaptureEvent("cli-command:dynamic-secrets lease list", posthog.NewProperties().Set("lease-count", len(dynamicSecretLeases)).Set("version", util.CLI_VERSION))
|
||||
}
|
||||
|
||||
func init() {
|
||||
dynamicSecretLeaseCreateCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||
dynamicSecretLeaseCreateCmd.Flags().String("token", "", "Create dynamic secret leases using machine identity access token")
|
||||
dynamicSecretLeaseCreateCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||
dynamicSecretLeaseCreateCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
|
||||
dynamicSecretLeaseCreateCmd.Flags().Bool("plain", false, "Print leased credentials without formatting, one per line")
|
||||
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseCreateCmd)
|
||||
|
||||
dynamicSecretLeaseListCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||
dynamicSecretLeaseListCmd.Flags().String("token", "", "Fetch dynamic secret leases machine identity access token")
|
||||
dynamicSecretLeaseListCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseListCmd)
|
||||
|
||||
dynamicSecretLeaseRenewCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||
dynamicSecretLeaseRenewCmd.Flags().String("token", "", "Renew dynamic secrets machine identity access token")
|
||||
dynamicSecretLeaseRenewCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||
dynamicSecretLeaseRenewCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
|
||||
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRenewCmd)
|
||||
|
||||
dynamicSecretLeaseRevokeCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||
dynamicSecretLeaseRevokeCmd.Flags().String("token", "", "Delete dynamic secrets using machine identity access token")
|
||||
dynamicSecretLeaseRevokeCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRevokeCmd)
|
||||
|
||||
dynamicSecretCmd.AddCommand(dynamicSecretLeaseCmd)
|
||||
|
||||
dynamicSecretCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
|
||||
dynamicSecretCmd.Flags().String("projectId", "", "Manually set the projectId to fetch dynamic-secret when using machine identity based auth")
|
||||
dynamicSecretCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
||||
dynamicSecretCmd.Flags().String("path", "/", "get dynamic secret within a folder path")
|
||||
rootCmd.AddCommand(dynamicSecretCmd)
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
package visualize
|
||||
|
||||
import infisicalModels "github.com/infisical/go-sdk/packages/models"
|
||||
|
||||
func PrintAllDyamicSecretLeaseCredentials(leaseCredentials map[string]any) {
|
||||
rows := [][]string{}
|
||||
for key, value := range leaseCredentials {
|
||||
if cred, ok := value.(string); ok {
|
||||
rows = append(rows, []string{key, cred})
|
||||
}
|
||||
}
|
||||
|
||||
headers := []string{"Key", "Value"}
|
||||
|
||||
GenericTable(headers, rows)
|
||||
}
|
||||
|
||||
func PrintAllDynamicRootCredentials(dynamicRootCredentials []infisicalModels.DynamicSecret) {
|
||||
rows := [][]string{}
|
||||
for _, el := range dynamicRootCredentials {
|
||||
rows = append(rows, []string{el.Name, el.Type, el.DefaultTTL, el.MaxTTL})
|
||||
}
|
||||
|
||||
headers := []string{"Name", "Provider", "Default TTL", "Max TTL"}
|
||||
|
||||
GenericTable(headers, rows)
|
||||
}
|
||||
|
||||
func PrintAllDynamicSecretLeases(dynamicSecretLeases []infisicalModels.DynamicSecretLease) {
|
||||
rows := [][]string{}
|
||||
const timeformat = "02-Jan-2006 03:04:05 PM"
|
||||
for _, el := range dynamicSecretLeases {
|
||||
rows = append(rows, []string{el.Id, el.ExpireAt.Local().Format(timeformat), el.CreatedAt.Local().Format(timeformat)})
|
||||
}
|
||||
|
||||
headers := []string{"ID", "Expire At", "Created At"}
|
||||
|
||||
GenericTable(headers, rows)
|
||||
}
|
@@ -94,33 +94,6 @@ func getLongestValues(rows [][3]string) (longestSecretName, longestSecretType in
|
||||
return
|
||||
}
|
||||
|
||||
func GenericTable(headers []string, rows [][]string) {
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.SetStyle(table.StyleLight)
|
||||
|
||||
// t.SetTitle(tableOptions.Title)
|
||||
t.Style().Options.DrawBorder = true
|
||||
t.Style().Options.SeparateHeader = true
|
||||
t.Style().Options.SeparateColumns = true
|
||||
|
||||
tableHeaders := table.Row{}
|
||||
for _, header := range headers {
|
||||
tableHeaders = append(tableHeaders, header)
|
||||
}
|
||||
|
||||
t.AppendHeader(tableHeaders)
|
||||
for _, row := range rows {
|
||||
tableRow := table.Row{}
|
||||
for _, val := range row {
|
||||
tableRow = append(tableRow, val)
|
||||
}
|
||||
t.AppendRow(tableRow)
|
||||
}
|
||||
|
||||
t.Render()
|
||||
}
|
||||
|
||||
// stringWidth returns the width of a string.
|
||||
// ANSI escape sequences are ignored and double-width characters are handled correctly.
|
||||
func stringWidth(str string) (width int) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
error: CallGetRawSecretsV3: Unsuccessful response [GET https://app.infisical.com/api/v3/secrets/raw?environment=invalid-env&expandSecretReferences=true&include_imports=true&recursive=true&secretPath=%2F&workspaceId=bef697d4-849b-4a75-b284-0922f87f8ba2] [status-code=404] [response={"error":"NotFound","message":"Environment with slug 'invalid-env' in project with ID bef697d4-849b-4a75-b284-0922f87f8ba2 not found","statusCode":404}]
|
||||
error: CallGetRawSecretsV3: Unsuccessful response [GET https://app.infisical.com/api/v3/secrets/raw?environment=invalid-env&expandSecretReferences=true&include_imports=true&recursive=true&secretPath=%2F&workspaceId=bef697d4-849b-4a75-b284-0922f87f8ba2] [status-code=404] [response={"statusCode":404,"message":"Environment with slug 'invalid-env' in project with ID bef697d4-849b-4a75-b284-0922f87f8ba2 not found","error":"NotFound"}]
|
||||
|
||||
|
||||
If this issue continues, get support at https://infisical.com/slack
|
||||
|
@@ -1,4 +1,4 @@
|
||||
Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
|
||||
Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
|
||||
┌───────────────┬──────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├───────────────┼──────────────┼─────────────┤
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -42,12 +41,11 @@ var creds = Credentials{
|
||||
func ExecuteCliCommand(command string, args ...string) (string, error) {
|
||||
cmd := exec.Command(command, args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Sprint(err) + ": " + FilterRequestID(strings.TrimSpace(string(output))))
|
||||
return FilterRequestID(strings.TrimSpace(string(output))), err
|
||||
fmt.Println(fmt.Sprint(err) + ": " + string(output))
|
||||
return strings.TrimSpace(string(output)), err
|
||||
}
|
||||
return FilterRequestID(strings.TrimSpace(string(output))), nil
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
||||
func SetupCli() {
|
||||
@@ -69,34 +67,3 @@ func SetupCli() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func FilterRequestID(input string) string {
|
||||
// Find the JSON part of the error message
|
||||
start := strings.Index(input, "{")
|
||||
end := strings.LastIndex(input, "}") + 1
|
||||
|
||||
if start == -1 || end == -1 {
|
||||
return input
|
||||
}
|
||||
|
||||
jsonPart := input[:start] // Pre-JSON content
|
||||
|
||||
// Parse the JSON object
|
||||
var errorObj map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(input[start:end]), &errorObj); err != nil {
|
||||
return input
|
||||
}
|
||||
|
||||
// Remove requestId field
|
||||
delete(errorObj, "requestId")
|
||||
delete(errorObj, "reqId")
|
||||
|
||||
// Convert back to JSON
|
||||
filtered, err := json.Marshal(errorObj)
|
||||
if err != nil {
|
||||
return input
|
||||
}
|
||||
|
||||
// Reconstruct the full string
|
||||
return jsonPart + string(filtered) + input[end:]
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package tests
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/bradleyjkemp/cupaloy/v2"
|
||||
)
|
||||
|
||||
@@ -95,29 +96,28 @@ func TestUserAuth_SecretsGetAll(t *testing.T) {
|
||||
// testUserAuth_SecretsGetAllWithoutConnection(t)
|
||||
}
|
||||
|
||||
// disabled for the time being
|
||||
// func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
|
||||
// originalConfigFile, err := util.GetConfigFile()
|
||||
// if err != nil {
|
||||
// t.Fatalf("error getting config file")
|
||||
// }
|
||||
// newConfigFile := originalConfigFile
|
||||
func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
|
||||
originalConfigFile, err := util.GetConfigFile()
|
||||
if err != nil {
|
||||
t.Fatalf("error getting config file")
|
||||
}
|
||||
newConfigFile := originalConfigFile
|
||||
|
||||
// // set it to a URL that will always be unreachable
|
||||
// newConfigFile.LoggedInUserDomain = "http://localhost:4999"
|
||||
// util.WriteConfigFile(&newConfigFile)
|
||||
// set it to a URL that will always be unreachable
|
||||
newConfigFile.LoggedInUserDomain = "http://localhost:4999"
|
||||
util.WriteConfigFile(&newConfigFile)
|
||||
|
||||
// // restore config file
|
||||
// defer util.WriteConfigFile(&originalConfigFile)
|
||||
// restore config file
|
||||
defer util.WriteConfigFile(&originalConfigFile)
|
||||
|
||||
// output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent")
|
||||
// if err != nil {
|
||||
// t.Fatalf("error running CLI command: %v", err)
|
||||
// }
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent")
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// // Use cupaloy to snapshot test the output
|
||||
// err = cupaloy.Snapshot(output)
|
||||
// if err != nil {
|
||||
// t.Fatalf("snapshot failed: %v", err)
|
||||
// }
|
||||
// }
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
@@ -12,18 +12,6 @@ To request time off, just submit a request in Rippling and let Maidul know at le
|
||||
|
||||
Since Infisical's team is globally distributed, it is hard for us to keep track of all the various national holidays across many different countries. Whether you'd like to celebrate Christmas or National Brisket Day (which, by the way, is on May 28th), you are welcome to take PTO on those days – just let Maidul know at least a week ahead so that we can adjust our planning.
|
||||
|
||||
## Winter break
|
||||
## Winter Break
|
||||
|
||||
Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla).
|
||||
|
||||
## Parental leave
|
||||
|
||||
At Infisical, we recognize that parental leave is a special and important time, significantly different from a typical vacation. We’re proud to offer parental leave to everyone, regardless of gender, and whether you’ve become a parent through childbirth or adoption.
|
||||
|
||||
For team members who have been with Infisical for over a year by the time of your child’s birth or adoption, you are eligible for up to 12 weeks of paid parental leave. This leave will be provided in one continuous block to allow you uninterrupted time with your family. If you have been with Infisical for less than a year, we will follow the parental leave provisions required by your local jurisdiction.
|
||||
|
||||
While we trust your judgment, parental leave is intended to be a distinct benefit and is not designed to be combined with our unlimited PTO policy. To ensure fairness and balance, we generally discourage combining parental leave with an extended vacation.
|
||||
|
||||
When you’re ready, please notify Maidul about your plans for parental leave, ideally at least four months in advance. This allows us to support you fully and arrange any necessary logistics, including salary adjustments and statutory paperwork.
|
||||
|
||||
We’re here to support you as you embark on this exciting new chapter in your life!
|
||||
Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla).
|
@@ -1,295 +0,0 @@
|
||||
---
|
||||
title: "infisical dynamic-secrets"
|
||||
description: "Perform dynamic secret operations directly with the CLI"
|
||||
---
|
||||
|
||||
```
|
||||
infisical dynamic-secrets
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
Dynamic secrets are unique secrets generated on demand based on the provided configuration settings. For more details, refer to [dynamics secrets section](/documentation/platform/dynamic-secrets/overview).
|
||||
|
||||
This command enables you to perform list, lease, renew lease, and revoke lease operations on dynamic secrets within your Infisical project.
|
||||
|
||||
### Sub-commands
|
||||
|
||||
<Accordion title="infisical dynamic-secrets">
|
||||
Use this command to print out all of the dynamic secrets in your project.
|
||||
|
||||
```bash
|
||||
$ infisical dynamic-secrets
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
<Accordion title="INFISICAL_TOKEN">
|
||||
Used to fetch dynamic secrets via a [machine identity](/documentation/platform/identities/machine-identities) instead of logged-in credentials. Simply, export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages.
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
|
||||
Used to disable the check for new CLI versions. This can improve the time it takes to run this command. Recommended for production environments.
|
||||
|
||||
To use, simply export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_DISABLE_UPDATE_CHECK=true
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
### Flags
|
||||
|
||||
<Accordion title="--projectId">
|
||||
The project ID to fetch dynamic secrets from. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets --projectId=<project-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--token">
|
||||
The authenticated token to fetch dynamic secrets from. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets --token=<token>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--env">
|
||||
Used to select the environment name on which actions should be taken. Default
|
||||
value: `dev`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--path">
|
||||
Use to select the project folder on which dynamic secrets will be accessed.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets --path="/" --env=dev
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
<Accordion title="infisical dynamic-secrets lease create">
|
||||
This command is used to create a new lease for a dynamic secret.
|
||||
|
||||
```bash
|
||||
$ infisical dynamic-secrets lease create <dynamic-secret-name>
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
<Accordion title="--env">
|
||||
Used to select the environment name on which actions should be taken. Default
|
||||
value: `dev`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--plain">
|
||||
The `--plain` flag will output dynamic secret lease credentials values without formatting, one per line.
|
||||
Default value: `false`
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease create dynamic-secret-postgres --plain
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--path">
|
||||
The `--path` flag indicates which project folder dynamic secrets will be injected from.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease create <dynamic-secret-name> --path="/" --env=dev
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--projectId">
|
||||
The project ID of the dynamic secrets to lease from. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease create <dynamic-secret-name> --projectId=<project-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--token">
|
||||
The authenticated token to create dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease create <dynamic-secret-name> --token=<token>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--ttl">
|
||||
The lease lifetime. If not provided, the default TTL of the dynamic secret root credential will be used.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease create <dynamic-secret-name> --ttl=<ttl>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="infisical dynamic-secrets lease list">
|
||||
This command is used to list leases for a dynamic secret.
|
||||
|
||||
```bash
|
||||
$ infisical dynamic-secrets lease list <dynamic-secret-name>
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
<Accordion title="--env">
|
||||
Used to select the environment name on which actions should be taken. Default
|
||||
value: `dev`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--path">
|
||||
The `--path` flag indicates which project folder dynamic secrets will be injected from.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease list <dynamic-secret-name> --path="/" --env=dev
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--projectId">
|
||||
The project ID of the dynamic secrets to list leases from. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease list <dynamic-secret-name> --projectId=<project-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--token">
|
||||
The authenticated token to list dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease list <dynamic-secret-name> --token=<token>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="infisical dynamic-secrets lease renew">
|
||||
This command is used to renew a lease before it expires.
|
||||
|
||||
```bash
|
||||
$ infisical dynamic-secrets lease renew <lease-id>
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
<Accordion title="--env">
|
||||
Used to select the environment name on which actions should be taken. Default
|
||||
value: `dev`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--path">
|
||||
The `--path` flag indicates which project folder dynamic secrets will be renewed from.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease renew <lease-id> --path="/" --env=dev
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--projectId">
|
||||
The project ID of the dynamic secret's lease from. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease renew <lease-id> --projectId=<project-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--token">
|
||||
The authenticated token to create dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease renew <lease-id> --token=<token>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--ttl">
|
||||
The lease lifetime. If not provided, the default TTL of the dynamic secret root credential will be used.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease renew <lease-id> --ttl=<ttl>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="infisical dynamic-secrets lease delete">
|
||||
This command is used to delete a lease.
|
||||
|
||||
```bash
|
||||
$ infisical dynamic-secrets lease delete <lease-id>
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
<Accordion title="--env">
|
||||
Used to select the environment name on which actions should be taken. Default
|
||||
value: `dev`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--path">
|
||||
The `--path` flag indicates which project folder dynamic secrets will be deleted from.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease delete <lease-id> --path="/" --env=dev
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--projectId">
|
||||
The project ID of the dynamic secret's lease from. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease delete <lease-id> --projectId=<project-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--token">
|
||||
The authenticated token to delete dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical dynamic-secrets lease delete <lease-id> --token=<token>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</Accordion>
|
@@ -3,15 +3,11 @@ title: "SCIM Overview"
|
||||
description: "Learn how to provision users for Infisical via SCIM."
|
||||
---
|
||||
|
||||
<Note>
|
||||
SCIM provisioning can only be enabled when either SAML or OIDC is setup for
|
||||
the organization.
|
||||
</Note>
|
||||
<Info>
|
||||
SCIM provisioning is a paid feature. If you're using Infisical Cloud, then it
|
||||
is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||
then you should contact sales@infisical.com to purchase an enterprise license
|
||||
to use it.
|
||||
SCIM provisioning is a paid feature.
|
||||
|
||||
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||
then you should contact sales@infisical.com to purchase an enterprise license to use it.
|
||||
</Info>
|
||||
|
||||
You can configure your organization in Infisical to have users and user groups be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc.
|
||||
@@ -24,3 +20,13 @@ SCIM providers:
|
||||
- [Okta SCIM](/documentation/platform/scim/okta)
|
||||
- [Azure SCIM](/documentation/platform/scim/azure)
|
||||
- [JumpCloud SCIM](/documentation/platform/scim/jumpcloud)
|
||||
|
||||
**FAQ**
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Why do SCIM-provisioned users have to finish setting up their account?">
|
||||
Infisical's SCIM implementation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform.
|
||||
|
||||
For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
Before Width: | Height: | Size: 441 KiB |
Before Width: | Height: | Size: 328 KiB |
Before Width: | Height: | Size: 930 KiB |
Before Width: | Height: | Size: 394 KiB |
Before Width: | Height: | Size: 380 KiB |
Before Width: | Height: | Size: 402 KiB |
Before Width: | Height: | Size: 407 KiB |
Before Width: | Height: | Size: 980 KiB |
Before Width: | Height: | Size: 437 KiB |
Before Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 320 KiB |
Before Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 383 KiB |
Before Width: | Height: | Size: 280 KiB |
@@ -1,76 +0,0 @@
|
||||
---
|
||||
title: "Octopus Deploy"
|
||||
description: "Learn how to sync secrets from Infisical to Octopus Deploy"
|
||||
---
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a Service Account for Infisical in Octopus Deploy">
|
||||
Navigate to **Configuration** > **Users** and click on the **Create Service Account** button.
|
||||
|
||||

|
||||
|
||||
Fill out the required fields and click on the **Save** button.
|
||||

|
||||
</Step>
|
||||
<Step title="Generate an API Key for your Service Account">
|
||||
On the **Service Account** user page, expand the **API Keys** section and click on the **New API Key** button.
|
||||
|
||||

|
||||
|
||||
Fill out the required fields and click on the **Generate New** button.
|
||||
|
||||

|
||||
|
||||
<Note>If you configure your access token to expire,
|
||||
you will need to generate a new API key for Infisical prior to this date to keep your integration running.</Note>
|
||||
|
||||
Copy the generated **API Key** and click on the **Close** button.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Create a Service Accounts Team and assign your Service Account">
|
||||
<Note>You can skip creating a new team if you already have an Octopus Deploy team configured with
|
||||
the **Project Contributor** role to assign your Service Account to.</Note>
|
||||
|
||||
Navigate to **Configuration** > **Teams** and click on the **Add Team** button.
|
||||
|
||||

|
||||
|
||||
Create a new team for **Service Accounts** and click on the **Save** button.
|
||||

|
||||
|
||||
On the **Members** tab, click on the **Add Member** button, add your **Infisical Service Account** and click on the **Add** button.
|
||||

|
||||
|
||||
On the **User Roles** tab, click on the **Include User Role** button, and add the **Project Contributor** role. Optionally,
|
||||
click on the **Define Scope** button to further refine what projects your Service Account has access to. Click on the **Apply** button once complete.
|
||||

|
||||
|
||||
Save your team changes by clicking on the **Save** button.
|
||||

|
||||
</Step>
|
||||
<Step title="Setup Integration">
|
||||
In Infisical, navigate to your **Project** > **Integrations** page and select the **Octopus Deploy** integration.
|
||||

|
||||
|
||||
Enter your **Instance URL** and **API Key** from **Octopus Deploy** to authorize Infisical.
|
||||

|
||||
|
||||
Select a **Space** and **Project** from **Octopus Deploy** to sync secrets to; configuring additional **Scope Values** as needed. Click on the **Create Integration** button once configured.
|
||||

|
||||
|
||||
Your Infisical secrets will begin to sync to **Octopus Deploy**.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
@@ -42,7 +42,6 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
|
||||
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |
|
||||
| [Travis CI](/integrations/cicd/travisci) | CI/CD | Available |
|
||||
| [Rundeck](/integrations/cicd/rundeck) | CI/CD | Available |
|
||||
| [Octopus Deploy](/integrations/cicd/octopus-deploy) | CI/CD | Available |
|
||||
| [React](/integrations/frameworks/react) | Framework | Available |
|
||||
| [Vue](/integrations/frameworks/vue) | Framework | Available |
|
||||
| [Express](/integrations/frameworks/express) | Framework | Available |
|
||||
|
@@ -316,7 +316,6 @@
|
||||
"cli/commands/init",
|
||||
"cli/commands/run",
|
||||
"cli/commands/secrets",
|
||||
"cli/commands/dynamic-secrets",
|
||||
"cli/commands/export",
|
||||
"cli/commands/token",
|
||||
"cli/commands/service-token",
|
||||
@@ -424,8 +423,7 @@
|
||||
"integrations/cicd/travisci",
|
||||
"integrations/cicd/rundeck",
|
||||
"integrations/cicd/codefresh",
|
||||
"integrations/cloud/checkly",
|
||||
"integrations/cicd/octopus-deploy"
|
||||
"integrations/cloud/checkly"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@@ -2,7 +2,7 @@ const path = require("path");
|
||||
|
||||
const ContentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
connect-src 'self' https://*.posthog.com http://127.0.0.1:*;
|
||||
connect-src 'self' https://*.posthog.com;
|
||||
script-src 'self' https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
|
||||
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
|
||||
child-src https://api.stripe.com;
|
||||
|
8
frontend/package-lock.json
generated
@@ -89,7 +89,7 @@
|
||||
"react-mailchimp-subscribe": "^2.1.3",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-select": "^5.8.3",
|
||||
"react-select": "^5.8.1",
|
||||
"react-table": "^7.8.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
"sanitize-html": "^2.12.1",
|
||||
@@ -21259,9 +21259,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-select": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz",
|
||||
"integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==",
|
||||
"version": "5.8.1",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz",
|
||||
"integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.0",
|
||||
|
@@ -162,4 +162,4 @@
|
||||
"tailwindcss": "3.2",
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -36,8 +36,7 @@ const integrationSlugNameMapping: Mapping = {
|
||||
"hasura-cloud": "Hasura Cloud",
|
||||
rundeck: "Rundeck",
|
||||
"azure-devops": "Azure DevOps",
|
||||
"azure-app-configuration": "Azure App Configuration",
|
||||
"octopus-deploy": "Octopus Deploy"
|
||||
"azure-app-configuration": "Azure App Configuration"
|
||||
};
|
||||
|
||||
const envMapping: Mapping = {
|
||||
|
Before Width: | Height: | Size: 27 KiB |
@@ -1,17 +1,11 @@
|
||||
import { ParsedUrlQuery } from "querystring";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faAngleRight, faCheck, faCopy, faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faAngleRight, faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { createNotification } from "../notifications";
|
||||
import { IconButton, Select, SelectItem, Tooltip } from "../v2";
|
||||
import { Select, SelectItem, Tooltip } from "../v2";
|
||||
|
||||
type Props = {
|
||||
pageName: string;
|
||||
@@ -56,10 +50,6 @@ export default function NavHeader({
|
||||
}: Props): JSX.Element {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const [isCopied, { timedToggle: toggleIsCopied }] = useToggle(false);
|
||||
const [isHoveringCopyButton, setIsHoveringCopyButton] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const secretPathSegments = secretPath.split("/").filter(Boolean);
|
||||
@@ -142,10 +132,8 @@ export default function NavHeader({
|
||||
)}
|
||||
{isFolderMode &&
|
||||
secretPathSegments?.map((folderName, index) => {
|
||||
const query: ParsedUrlQuery & { secretPath: string } = {
|
||||
...router.query,
|
||||
secretPath: `/${secretPathSegments.slice(0, index + 1).join("/")}`
|
||||
};
|
||||
const query = { ...router.query };
|
||||
query.secretPath = `/${secretPathSegments.slice(0, index + 1).join("/")}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -154,59 +142,14 @@ export default function NavHeader({
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||
{index + 1 === secretPathSegments?.length ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span
|
||||
className={twMerge(
|
||||
"text-sm font-semibold transition-all",
|
||||
isHoveringCopyButton ? "text-bunker-200" : "text-bunker-300"
|
||||
)}
|
||||
>
|
||||
{folderName}
|
||||
</span>
|
||||
<Tooltip
|
||||
className="relative right-2"
|
||||
position="bottom"
|
||||
content="Copy secret path"
|
||||
>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="copy"
|
||||
onMouseEnter={() => setIsHoveringCopyButton(true)}
|
||||
onMouseLeave={() => setIsHoveringCopyButton(false)}
|
||||
onClick={() => {
|
||||
if (isCopied) return;
|
||||
|
||||
navigator.clipboard.writeText(query.secretPath);
|
||||
|
||||
createNotification({
|
||||
text: "Copied secret path to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
|
||||
toggleIsCopied(2000);
|
||||
}}
|
||||
className="hover:bg-bunker-100/10"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={!isCopied ? faCopy : faCheck}
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-bunker-300">{folderName}</span>
|
||||
) : (
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{ pathname: "/project/[id]/secrets/[env]", query }}
|
||||
>
|
||||
<a
|
||||
className={twMerge(
|
||||
"text-sm font-semibold transition-all hover:text-primary",
|
||||
isHoveringCopyButton ? "text-primary" : "text-primary/80"
|
||||
)}
|
||||
>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
||||
{folderName}
|
||||
</a>
|
||||
</Link>
|
||||
|
@@ -3,7 +3,6 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateWsTag } from "@app/hooks/api";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
export const secretTagsColors = [
|
||||
{
|
||||
@@ -88,13 +88,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const createTagSchema = z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
|
||||
}),
|
||||
slug: slugSchema({ min: 1, field: "Tag Slug" }),
|
||||
color: z.string().trim()
|
||||
});
|
||||
|
||||
|
@@ -1,68 +0,0 @@
|
||||
import { GroupBase } from "react-select";
|
||||
import ReactSelectCreatable, { CreatableProps } from "react-select/creatable";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
|
||||
|
||||
export const CreatableSelect = <T,>({
|
||||
isMulti,
|
||||
closeMenuOnSelect,
|
||||
...props
|
||||
}: CreatableProps<T, boolean, GroupBase<T>>) => {
|
||||
return (
|
||||
<ReactSelectCreatable
|
||||
isMulti={isMulti}
|
||||
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
|
||||
hideSelectedOptions={false}
|
||||
unstyled
|
||||
styles={{
|
||||
input: (base) => ({
|
||||
...base,
|
||||
"input:focus": {
|
||||
boxShadow: "none"
|
||||
}
|
||||
}),
|
||||
multiValueLabel: (base) => ({
|
||||
...base,
|
||||
whiteSpace: "normal",
|
||||
overflow: "visible"
|
||||
}),
|
||||
control: (base) => ({
|
||||
...base,
|
||||
transition: "none"
|
||||
})
|
||||
}}
|
||||
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
|
||||
classNames={{
|
||||
container: () => "w-full font-inter",
|
||||
control: ({ isFocused }) =>
|
||||
twMerge(
|
||||
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
|
||||
"border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
|
||||
),
|
||||
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
|
||||
input: () => "pl-1 py-0.5",
|
||||
valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-scroll" : ""} gap-1`,
|
||||
singleValue: () => "leading-7 ml-1",
|
||||
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
|
||||
multiValueLabel: () => "leading-6 text-sm",
|
||||
multiValueRemove: () => "hover:text-red text-bunker-400",
|
||||
indicatorsContainer: () => "p-1 gap-1",
|
||||
clearIndicator: () => "p-1 hover:text-red text-bunker-400",
|
||||
indicatorSeparator: () => "bg-bunker-400",
|
||||
dropdownIndicator: () => "text-bunker-200 p-1",
|
||||
menu: () =>
|
||||
"mt-2 border text-sm text-mineshaft-200 bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
||||
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
||||
option: ({ isFocused, isSelected }) =>
|
||||
twMerge(
|
||||
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
|
||||
isSelected && "text-mineshaft-200",
|
||||
"hover:cursor-pointer text-xs px-3 py-2"
|
||||
),
|
||||
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
@@ -1 +0,0 @@
|
||||
export * from "./CreatableSelect";
|
@@ -14,7 +14,6 @@ export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
|
||||
onChange: (date?: Date) => void;
|
||||
popUpProps: PopoverProps;
|
||||
popUpContentProps: PopoverContentProps;
|
||||
dateFormat?: "PPP" | "PP" | "P"; // extend as needed
|
||||
};
|
||||
|
||||
// Doc: https://react-day-picker.js.org/
|
||||
@@ -23,7 +22,6 @@ export const DatePicker = ({
|
||||
onChange,
|
||||
popUpProps,
|
||||
popUpContentProps,
|
||||
dateFormat = "PPP",
|
||||
...props
|
||||
}: DatePickerProps) => {
|
||||
const [timeValue, setTimeValue] = useState<string>(value ? format(value, "HH:mm") : "00:00");
|
||||
@@ -55,7 +53,7 @@ export const DatePicker = ({
|
||||
<Popover {...popUpProps}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline_bg" leftIcon={<FontAwesomeIcon icon={faCalendar} />}>
|
||||
{value ? format(value, dateFormat) : "Pick a date and time"}
|
||||
{value ? format(value, "PPP") : "Pick a date and time"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit p-2" {...popUpContentProps}>
|
||||
|
@@ -1,14 +1,52 @@
|
||||
import Select, { Props } from "react-select";
|
||||
import Select, {
|
||||
ClearIndicatorProps,
|
||||
components,
|
||||
DropdownIndicatorProps,
|
||||
MultiValueRemoveProps,
|
||||
OptionProps,
|
||||
Props
|
||||
} from "react-select";
|
||||
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faChevronDown, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
|
||||
const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
|
||||
return (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<FontAwesomeIcon icon={faChevronDown} size="xs" />
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
export const FilterableSelect = <T,>({
|
||||
isMulti,
|
||||
closeMenuOnSelect,
|
||||
tabSelectsValue = false,
|
||||
...props
|
||||
}: Props<T>) => (
|
||||
const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
|
||||
return (
|
||||
<components.ClearIndicator {...props}>
|
||||
<FontAwesomeIcon icon={faCircleXmark} />
|
||||
</components.ClearIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
const MultiValueRemove = (props: MultiValueRemoveProps) => {
|
||||
return (
|
||||
<components.MultiValueRemove {...props}>
|
||||
<FontAwesomeIcon icon={faXmark} size="xs" />
|
||||
</components.MultiValueRemove>
|
||||
);
|
||||
};
|
||||
|
||||
const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
|
||||
return (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
{children}
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||
)}
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: Props<T>) => (
|
||||
<Select
|
||||
isMulti={isMulti}
|
||||
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
|
||||
@@ -31,7 +69,6 @@ export const FilterableSelect = <T,>({
|
||||
transition: "none"
|
||||
})
|
||||
}}
|
||||
tabSelectsValue={tabSelectsValue}
|
||||
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
|
||||
classNames={{
|
||||
container: () => "w-full font-inter",
|
||||
@@ -42,8 +79,7 @@ export const FilterableSelect = <T,>({
|
||||
),
|
||||
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
|
||||
input: () => "pl-1 py-0.5",
|
||||
valueContainer: () =>
|
||||
`p-1 max-h-[14rem] ${isMulti ? "!overflow-y-auto thin-scrollbar" : ""} gap-1`,
|
||||
valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-scroll" : ""} gap-1`,
|
||||
singleValue: () => "leading-7 ml-1",
|
||||
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
|
||||
multiValueLabel: () => "leading-6 text-sm",
|
||||
@@ -53,7 +89,7 @@ export const FilterableSelect = <T,>({
|
||||
indicatorSeparator: () => "bg-bunker-400",
|
||||
dropdownIndicator: () => "text-bunker-200 p-1",
|
||||
menu: () =>
|
||||
"mt-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
||||
"mt-2 border text-sm text-mineshaft-200 bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
||||
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
||||
option: ({ isFocused, isSelected }) =>
|
||||
twMerge(
|
||||
|
@@ -1,45 +0,0 @@
|
||||
import {
|
||||
ClearIndicatorProps,
|
||||
components,
|
||||
DropdownIndicatorProps,
|
||||
MultiValueRemoveProps,
|
||||
OptionProps
|
||||
} from "react-select";
|
||||
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faChevronDown, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
|
||||
return (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<FontAwesomeIcon icon={faChevronDown} size="xs" />
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
export const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
|
||||
return (
|
||||
<components.ClearIndicator {...props}>
|
||||
<FontAwesomeIcon icon={faCircleXmark} />
|
||||
</components.ClearIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
export const MultiValueRemove = (props: MultiValueRemoveProps) => {
|
||||
return (
|
||||
<components.MultiValueRemove {...props}>
|
||||
<FontAwesomeIcon icon={faXmark} size="xs" />
|
||||
</components.MultiValueRemove>
|
||||
);
|
||||
};
|
||||
|
||||
export const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
|
||||
return (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
{children}
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||
)}
|
||||
</components.Option>
|
||||
);
|
||||
};
|
@@ -17,9 +17,7 @@ import {
|
||||
Project,
|
||||
Service,
|
||||
Team,
|
||||
TeamCityBuildConfig,
|
||||
TGetIntegrationAuthOctopusDeployScopeValuesDTO,
|
||||
TOctopusDeployVariableSetScopeValues
|
||||
TeamCityBuildConfig
|
||||
} from "./types";
|
||||
|
||||
const integrationAuthKeys = {
|
||||
@@ -121,14 +119,7 @@ const integrationAuthKeys = {
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const,
|
||||
getIntegrationAuthOctopusDeploySpaces: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeploySpaces"] as const,
|
||||
getIntegrationAuthOctopusDeployScopeValues: ({
|
||||
integrationAuthId,
|
||||
...params
|
||||
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const
|
||||
}) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||
@@ -488,28 +479,6 @@ const fetchIntegrationAuthTeamCityBuildConfigs = async ({
|
||||
return buildConfigs;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthOctopusDeploySpaces = async (integrationAuthId: string) => {
|
||||
const {
|
||||
data: { spaces }
|
||||
} = await apiRequest.get<{
|
||||
spaces: { Name: string; Slug: string; Id: string; IsDefault: boolean }[];
|
||||
}>(`/api/v1/integration-auth/${integrationAuthId}/octopus-deploy/spaces`);
|
||||
return spaces;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthOctopusDeployScopeValues = async ({
|
||||
integrationAuthId,
|
||||
scope,
|
||||
spaceId,
|
||||
resourceId
|
||||
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) => {
|
||||
const { data } = await apiRequest.get<TOctopusDeployVariableSetScopeValues>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/octopus-deploy/scope-values`,
|
||||
{ params: { scope, spaceId, resourceId } }
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||
@@ -518,24 +487,17 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthApps = (
|
||||
{
|
||||
integrationAuthId,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
azureDevOpsOrgName?: string;
|
||||
workspaceSlug?: string;
|
||||
},
|
||||
options?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof fetchIntegrationAuthApps>>,
|
||||
unknown,
|
||||
Awaited<ReturnType<typeof fetchIntegrationAuthApps>>
|
||||
>
|
||||
) => {
|
||||
export const useGetIntegrationAuthApps = ({
|
||||
integrationAuthId,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
azureDevOpsOrgName?: string;
|
||||
workspaceSlug?: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId, workspaceSlug),
|
||||
queryFn: () =>
|
||||
@@ -545,7 +507,7 @@ export const useGetIntegrationAuthApps = (
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug
|
||||
}),
|
||||
...options
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -797,27 +759,6 @@ export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: stri
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthOctopusDeploySpaces = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthOctopusDeploySpaces(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthOctopusDeploySpaces(integrationAuthId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthOctopusDeployScopeValues = (
|
||||
params: TGetIntegrationAuthOctopusDeployScopeValuesDTO,
|
||||
options?: UseQueryOptions<
|
||||
TOctopusDeployVariableSetScopeValues,
|
||||
unknown,
|
||||
TOctopusDeployVariableSetScopeValues
|
||||
>
|
||||
) =>
|
||||
useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthOctopusDeployScopeValues(params),
|
||||
queryFn: () => fetchIntegrationAuthOctopusDeployScopeValues(params),
|
||||
...options
|
||||
});
|
||||
|
||||
export const useGetIntegrationAuthBitBucketEnvironments = (
|
||||
{
|
||||
integrationAuthId,
|
||||
|
@@ -99,29 +99,3 @@ export type TDuplicateIntegrationAuthDTO = {
|
||||
integrationAuthId: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export enum OctopusDeployScope {
|
||||
Project = "project"
|
||||
// tenant, variable set
|
||||
}
|
||||
|
||||
export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = {
|
||||
integrationAuthId: string;
|
||||
spaceId: string;
|
||||
resourceId: string;
|
||||
scope: OctopusDeployScope;
|
||||
};
|
||||
|
||||
export type TOctopusDeployVariableSetScopeValues = {
|
||||
Environments: { Id: string; Name: string }[];
|
||||
Machines: { Id: string; Name: string }[];
|
||||
Actions: { Id: string; Name: string }[];
|
||||
Roles: { Id: string; Name: string }[];
|
||||
Channels: { Id: string; Name: string }[];
|
||||
TenantTags: { Id: string; Name: string }[];
|
||||
Processes: {
|
||||
ProcessType: string;
|
||||
Id: string;
|
||||
Name: string;
|
||||
}[];
|
||||
};
|
||||
|
@@ -4,7 +4,7 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { TCloudIntegration, TIntegrationWithEnv, TOctopusDeployScopeValues } from "./types";
|
||||
import { TCloudIntegration, TIntegrationWithEnv } from "./types";
|
||||
|
||||
export const integrationQueryKeys = {
|
||||
getIntegrations: () => ["integrations"] as const,
|
||||
@@ -87,7 +87,6 @@ export const useCreateIntegration = () => {
|
||||
shouldMaskSecrets?: boolean;
|
||||
shouldProtectSecrets?: boolean;
|
||||
shouldEnableDelete?: boolean;
|
||||
octopusDeployScopeValues?: TOctopusDeployScopeValues;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
|
@@ -58,21 +58,11 @@ export type TIntegration = {
|
||||
shouldProtectSecrets?: boolean;
|
||||
shouldEnableDelete?: boolean;
|
||||
|
||||
octopusDeployScopeValues?: TOctopusDeployScopeValues;
|
||||
awsIamRole?: string;
|
||||
region?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TOctopusDeployScopeValues = {
|
||||
Environment?: string[];
|
||||
Action?: string[];
|
||||
Channel?: string[];
|
||||
Machine?: string[];
|
||||
ProcessOwner?: string[];
|
||||
Role?: string[];
|
||||
};
|
||||
|
||||
export type TIntegrationWithEnv = TIntegration & {
|
||||
environment: {
|
||||
id: string;
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
export type Kms = {
|
||||
id: string;
|
||||
description: string;
|
||||
@@ -88,13 +89,7 @@ export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
|
||||
]);
|
||||
|
||||
export const AddExternalKmsSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Alias must be a valid slug"
|
||||
}),
|
||||
name: slugSchema({ min: 1, field: "Alias" }),
|
||||
description: z.string().trim().optional(),
|
||||
provider: ExternalKmsInputSchema
|
||||
});
|
||||
|
@@ -52,7 +52,7 @@ export type Invoice = {
|
||||
};
|
||||
|
||||
export type PmtMethod = {
|
||||
_id: string;
|
||||
id: string;
|
||||
brand: string;
|
||||
exp_month: number;
|
||||
exp_year: number;
|
||||
|
@@ -3,16 +3,9 @@ import { useState } from "react";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useDebounce } from "@app/hooks/useDebounce";
|
||||
|
||||
export const usePagination = <T extends string>(
|
||||
initialOrderBy: T,
|
||||
{
|
||||
initPerPage = 100
|
||||
}: {
|
||||
initPerPage?: number;
|
||||
} = {}
|
||||
) => {
|
||||
export const usePagination = <T extends string>(initialOrderBy: T) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(initPerPage);
|
||||
const [perPage, setPerPage] = useState(100);
|
||||
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -33,10 +26,6 @@ export const usePagination = <T extends string>(
|
||||
search,
|
||||
setSearch,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
toggleOrderDirection: () =>
|
||||
setOrderDirection((prev) =>
|
||||
prev === OrderByDirection.DESC ? OrderByDirection.ASC : OrderByDirection.DESC
|
||||
)
|
||||
setOrderBy
|
||||
};
|
||||
};
|
||||
|
@@ -457,11 +457,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
value={currentWorkspace?.id}
|
||||
className="w-full bg-mineshaft-600 py-2.5 font-medium [&>*:first-child]:truncate"
|
||||
onValueChange={(value) => {
|
||||
router.push(`/project/${value}/secrets/overview`);
|
||||
localStorage.setItem("projectData.id", value);
|
||||
// this is not using react query because react query in overview is throwing error when envs are not exact same count
|
||||
// to reproduce change this back to router.push and switch between two projects with different env count
|
||||
// look into this on dashboard revamp
|
||||
window.location.assign(`/project/${value}/secrets/overview`);
|
||||
}}
|
||||
position="popper"
|
||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
||||
|
@@ -1,12 +1,23 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
export const slugSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(32)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug format"
|
||||
});
|
||||
interface SlugSchemaInputs {
|
||||
min?: number;
|
||||
max?: number;
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInputs = {}) => {
|
||||
return z
|
||||
.string()
|
||||
.trim()
|
||||
.min(min, {
|
||||
message: `${field} field must be at least ${min} character${min === 1 ? "" : "s"}`
|
||||
})
|
||||
.max(max, {
|
||||
message: `${field} field must be at most ${max} character${max === 1 ? "" : "s"}`
|
||||
})
|
||||
.refine((v) => slugify(v, { lowercase: true, separator: "-" }) === v, {
|
||||
message: `${field} field can only contain letters, numbers, and hyphens`
|
||||
});
|
||||
};
|
||||
|
@@ -181,7 +181,7 @@ export default function BitBucketCreateIntegrationPage() {
|
||||
onChange={onChange}
|
||||
options={currentWorkspace?.environments}
|
||||
placeholder="Select a project environment"
|
||||
isDisabled={!currentWorkspace?.environments.length}
|
||||
isDisabled={!bitbucketWorkspaces?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
@@ -1,138 +0,0 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { removeTrailingSlash } from "@app/helpers/string";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
const formSchema = z.object({
|
||||
instanceUrl: z.string().min(1, { message: "Instance URL required" }),
|
||||
apiKey: z.string().min(1, { message: "API Key required" })
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
export default function OctopusDeployIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const { mutateAsync, isLoading } = useSaveIntegrationAccessToken();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { control, handleSubmit } = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const onSubmit = async ({ instanceUrl, apiKey }: TForm) => {
|
||||
try {
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace!.id,
|
||||
integration: "octopus-deploy",
|
||||
url: removeTrailingSlash(instanceUrl),
|
||||
accessToken: apiKey
|
||||
});
|
||||
|
||||
router.push(`/integrations/octopus-deploy/create?integrationAuthId=${integrationAuth.id}`);
|
||||
} catch (err: any) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: err.message ?? "Error authorizing integration"
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
>
|
||||
<Head>
|
||||
<title>Authorize Octopus Deploy Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="After adding your credentials, you will be prompted to set up an integration for a particular environment and secret path."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="inline-flex items-center pb-0.5">
|
||||
<Image
|
||||
src="/images/integrations/Octopus Deploy.png"
|
||||
height={30}
|
||||
width={30}
|
||||
alt="Octopus Deploy logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">Octopus Deploy Integration</span>
|
||||
<Link href="https://infisical.com/docs/integrations/cloud/octopus-deploy" passHref>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Octopus Deploy Instance URL"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="px-6"
|
||||
>
|
||||
<Input value={value} onChange={onChange} placeholder="https://xxxx.octopus.app" />
|
||||
</FormControl>
|
||||
)}
|
||||
name="instanceUrl"
|
||||
control={control}
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Octopus Deploy API Key"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="px-6"
|
||||
>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="API-XXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
type="password"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
name="apiKey"
|
||||
control={control}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Connect to Octopus Deploy
|
||||
</Button>
|
||||
</Card>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
OctopusDeployIntegrationPage.requireAuth = true;
|
@@ -1,444 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { SiOctopusdeploy } from "react-icons/si";
|
||||
import { useRouter } from "next/router";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Spinner
|
||||
} from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateIntegration, useGetIntegrationAuthApps } from "@app/hooks/api";
|
||||
import {
|
||||
useGetIntegrationAuthOctopusDeployScopeValues,
|
||||
useGetIntegrationAuthOctopusDeploySpaces
|
||||
} from "@app/hooks/api/integrationAuth/queries";
|
||||
import { OctopusDeployScope } from "@app/hooks/api/integrationAuth/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
scope: z.nativeEnum(OctopusDeployScope),
|
||||
secretPath: z.string().default("/"),
|
||||
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||
targetSpace: z.object({ Name: z.string(), Id: z.string() }),
|
||||
targetResource: z.object({ appId: z.string().optional(), name: z.string() }),
|
||||
targetEnvironments: z.object({ Name: z.string(), Id: z.string() }).array().optional(),
|
||||
targetRoles: z.object({ Name: z.string(), Id: z.string() }).array().optional(),
|
||||
targetMachines: z.object({ Name: z.string(), Id: z.string() }).array().optional(),
|
||||
targetProcesses: z
|
||||
.object({ Name: z.string(), Id: z.string(), ProcessType: z.string() })
|
||||
.array()
|
||||
.optional(),
|
||||
targetActions: z.object({ Name: z.string(), Id: z.string() }).array().optional(),
|
||||
targetChannels: z.object({ Name: z.string(), Id: z.string() }).array().optional()
|
||||
});
|
||||
|
||||
type TFormData = z.infer<typeof formSchema>;
|
||||
|
||||
export default function OctopusDeployCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const createIntegration = useCreateIntegration();
|
||||
|
||||
const { watch, control, reset, handleSubmit } = useForm<TFormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
secretPath: "/",
|
||||
scope: OctopusDeployScope.Project
|
||||
}
|
||||
});
|
||||
|
||||
const integrationAuthId = router.query.integrationAuthId as string;
|
||||
|
||||
const { currentWorkspace, isLoading: isProjectLoading } = useWorkspace();
|
||||
|
||||
const { data: octopusDeploySpaces, isLoading: isLoadingOctopusDeploySpaces } =
|
||||
useGetIntegrationAuthOctopusDeploySpaces((integrationAuthId as string) ?? "");
|
||||
|
||||
const currentSpace = watch("targetSpace", octopusDeploySpaces?.[0]);
|
||||
const currentScope = watch("scope");
|
||||
const sourceEnv = watch("sourceEnvironment");
|
||||
|
||||
const { data: octopusDeployResources, isLoading: isOctopusDeployResourcesLoading } =
|
||||
useGetIntegrationAuthApps(
|
||||
{
|
||||
integrationAuthId,
|
||||
workspaceSlug: currentSpace?.Name
|
||||
// scope once we support other resources than project
|
||||
},
|
||||
{
|
||||
enabled: Boolean(currentSpace ?? octopusDeploySpaces?.find((space) => space.IsDefault))
|
||||
}
|
||||
);
|
||||
|
||||
const currentResource = watch("targetResource", octopusDeployResources?.[0]);
|
||||
|
||||
const { data: octopusDeployScopeValues, isLoading: isOctopusDeployScopeValuesLoading } =
|
||||
useGetIntegrationAuthOctopusDeployScopeValues(
|
||||
{
|
||||
integrationAuthId,
|
||||
spaceId: currentSpace?.Id,
|
||||
resourceId: currentResource?.appId!,
|
||||
scope: currentScope
|
||||
},
|
||||
{ enabled: Boolean(currentSpace && currentResource) }
|
||||
);
|
||||
|
||||
const onSubmit = async ({
|
||||
sourceEnvironment,
|
||||
secretPath,
|
||||
targetEnvironments,
|
||||
targetResource,
|
||||
targetSpace,
|
||||
targetChannels,
|
||||
targetActions,
|
||||
targetMachines,
|
||||
targetProcesses,
|
||||
targetRoles,
|
||||
scope
|
||||
}: TFormData) => {
|
||||
try {
|
||||
await createIntegration.mutateAsync({
|
||||
integrationAuthId,
|
||||
isActive: true,
|
||||
scope,
|
||||
app: targetResource.name,
|
||||
appId: targetResource.appId,
|
||||
targetEnvironment: targetSpace.Name,
|
||||
targetEnvironmentId: targetSpace.Id,
|
||||
metadata: {
|
||||
octopusDeployScopeValues: {
|
||||
Environment: targetEnvironments?.map(({ Id }) => Id),
|
||||
Action: targetActions?.map(({ Id }) => Id),
|
||||
Channel: targetChannels?.map(({ Id }) => Id),
|
||||
ProcessOwner: targetProcesses?.map(({ Id }) => Id),
|
||||
Role: targetRoles?.map(({ Id }) => Id),
|
||||
Machine: targetMachines?.map(({ Id }) => Id)
|
||||
}
|
||||
},
|
||||
sourceEnvironment: sourceEnvironment.slug,
|
||||
secretPath
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created integration"
|
||||
});
|
||||
router.push(`/integrations/${currentWorkspace?.id}`);
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create integration"
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!octopusDeployResources || !octopusDeploySpaces || !currentWorkspace) return;
|
||||
|
||||
reset({
|
||||
targetResource: octopusDeployResources[0],
|
||||
targetSpace: octopusDeploySpaces.find((space) => space.IsDefault),
|
||||
sourceEnvironment: currentWorkspace.environments[0],
|
||||
secretPath: "/",
|
||||
scope: OctopusDeployScope.Project
|
||||
});
|
||||
}, [octopusDeploySpaces, octopusDeployResources, currentWorkspace]);
|
||||
|
||||
if (isProjectLoading || isLoadingOctopusDeploySpaces || isOctopusDeployResourcesLoading)
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center p-24">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
>
|
||||
<Card className="max-w-4xl rounded-md p-8 pt-4">
|
||||
<CardTitle className=" text-center">
|
||||
<SiOctopusdeploy size="1.2rem" className="mr-2 mb-1 inline-block" />
|
||||
Octopus Deploy Integration
|
||||
</CardTitle>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="sourceEnvironment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Project Environment"
|
||||
>
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.slug}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={currentWorkspace?.environments}
|
||||
placeholder="Select a project environment"
|
||||
isDisabled={!currentWorkspace?.environments.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} label="Secrets Path">
|
||||
<SecretPathInput
|
||||
placeholder="/"
|
||||
environment={sourceEnv?.slug}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="col-span-2 flex w-full flex-row items-center pb-2">
|
||||
<div className="w-full border-t border-mineshaft-500" />
|
||||
<span className="mx-2 whitespace-nowrap text-xs text-mineshaft-400">Sync To</span>
|
||||
<div className="w-full border-t border-mineshaft-500" />
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetSpace"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Octopus Deploy Space"
|
||||
>
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.Id}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.Name}
|
||||
onChange={onChange}
|
||||
options={octopusDeploySpaces}
|
||||
placeholder={
|
||||
octopusDeploySpaces?.length ? "Select a space..." : "No spaces found..."
|
||||
}
|
||||
isDisabled={!octopusDeploySpaces?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetResource"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="capitalize"
|
||||
label={`Octopus Deploy ${currentScope}`}
|
||||
>
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.appId!}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={octopusDeployResources}
|
||||
placeholder={
|
||||
octopusDeployResources?.length ? "Select a project..." : "No projects found..."
|
||||
}
|
||||
isDisabled={!octopusDeployResources?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetEnvironments"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Octopus Deploy Environments"
|
||||
isOptional
|
||||
>
|
||||
<FilterableSelect
|
||||
isMulti
|
||||
getOptionValue={(option) => option.Name}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.Name}
|
||||
onChange={onChange}
|
||||
isLoading={isOctopusDeployScopeValuesLoading}
|
||||
options={octopusDeployScopeValues?.Environments}
|
||||
placeholder={
|
||||
octopusDeployScopeValues?.Environments?.length
|
||||
? "Select environments..."
|
||||
: "No environments found..."
|
||||
}
|
||||
isDisabled={!octopusDeployScopeValues?.Environments?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetRoles"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Octopus Deploy Target Tags"
|
||||
isOptional
|
||||
>
|
||||
<FilterableSelect
|
||||
isMulti
|
||||
getOptionValue={(option) => option.Name}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.Name}
|
||||
onChange={onChange}
|
||||
isLoading={isOctopusDeployScopeValuesLoading}
|
||||
options={octopusDeployScopeValues?.Roles}
|
||||
placeholder={
|
||||
octopusDeployScopeValues?.Roles?.length
|
||||
? "Select target tags..."
|
||||
: "No target tags found..."
|
||||
}
|
||||
isDisabled={!octopusDeployScopeValues?.Roles?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetMachines"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Octopus Deploy Targets"
|
||||
isOptional
|
||||
>
|
||||
<FilterableSelect
|
||||
isMulti
|
||||
getOptionValue={(option) => option.Name}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.Name}
|
||||
onChange={onChange}
|
||||
isLoading={isOctopusDeployScopeValuesLoading}
|
||||
options={octopusDeployScopeValues?.Machines}
|
||||
placeholder={
|
||||
octopusDeployScopeValues?.Machines?.length
|
||||
? "Select targets..."
|
||||
: "No targets found..."
|
||||
}
|
||||
isDisabled={!octopusDeployScopeValues?.Machines?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetProcesses"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Octopus Deploy Processes"
|
||||
isOptional
|
||||
>
|
||||
<FilterableSelect
|
||||
isMulti
|
||||
getOptionValue={(option) => option.Name}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.Name}
|
||||
onChange={onChange}
|
||||
isLoading={isOctopusDeployScopeValuesLoading}
|
||||
options={octopusDeployScopeValues?.Processes}
|
||||
placeholder={
|
||||
octopusDeployScopeValues?.Processes?.length
|
||||
? "Select processes..."
|
||||
: "No processes found..."
|
||||
}
|
||||
isDisabled={!octopusDeployScopeValues?.Processes?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetActions"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Octopus Deploy Deployment Steps"
|
||||
isOptional
|
||||
>
|
||||
<FilterableSelect
|
||||
isMulti
|
||||
getOptionValue={(option) => option.Name}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.Name}
|
||||
onChange={onChange}
|
||||
isLoading={isOctopusDeployScopeValuesLoading}
|
||||
options={octopusDeployScopeValues?.Actions}
|
||||
placeholder={
|
||||
octopusDeployScopeValues?.Actions?.length
|
||||
? "Select deployment steps..."
|
||||
: "No deployment steps found..."
|
||||
}
|
||||
isDisabled={!octopusDeployScopeValues?.Actions?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetChannels"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Octopus Deploy Channels"
|
||||
isOptional
|
||||
>
|
||||
<FilterableSelect
|
||||
isMulti
|
||||
getOptionValue={(option) => option.Name}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.Name}
|
||||
onChange={onChange}
|
||||
isLoading={isOctopusDeployScopeValuesLoading}
|
||||
options={octopusDeployScopeValues?.Channels}
|
||||
placeholder={
|
||||
octopusDeployScopeValues?.Channels?.length
|
||||
? "Select channels..."
|
||||
: "No channels found..."
|
||||
}
|
||||
isDisabled={!octopusDeployScopeValues?.Channels?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
className="mt-4"
|
||||
isLoading={createIntegration.isLoading}
|
||||
isDisabled={createIntegration.isLoading || !octopusDeployResources?.length}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
OctopusDeployCreateIntegrationPage.requireAuth = true;
|
@@ -1,6 +1,6 @@
|
||||
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
||||
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
@@ -9,22 +9,20 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowDownAZ,
|
||||
faArrowRight,
|
||||
faArrowUpRightFromSquare,
|
||||
faArrowUpZA,
|
||||
faBorderAll,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faClipboard,
|
||||
faExclamationCircle,
|
||||
faFileShield,
|
||||
faHandPeace,
|
||||
faList,
|
||||
faMagnifyingGlass,
|
||||
faNetworkWired,
|
||||
faPlug,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faStar as faSolidStar,
|
||||
faUserPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -34,15 +32,7 @@ import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { Button, IconButton, Input, Skeleton, UpgradePlanModal } from "@app/components/v2";
|
||||
import { NewProjectModal } from "@app/components/v2/projects";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
@@ -52,9 +42,7 @@ import {
|
||||
useUser,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useRegisterUserAction } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { Workspace } from "@app/hooks/api/types";
|
||||
@@ -93,10 +81,6 @@ enum ProjectsViewMode {
|
||||
LIST = "list"
|
||||
}
|
||||
|
||||
enum ProjectOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
function copyToClipboard(id: string, setState: (value: boolean) => void) {
|
||||
// Get the text field
|
||||
const copyText = document.getElementById(id) as HTMLInputElement;
|
||||
@@ -512,48 +496,26 @@ const OrganizationPage = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0;
|
||||
|
||||
const {
|
||||
setPage,
|
||||
perPage,
|
||||
setPerPage,
|
||||
page,
|
||||
offset,
|
||||
limit,
|
||||
toggleOrderDirection,
|
||||
orderDirection
|
||||
} = usePagination(ProjectOrderBy.Name, { initPerPage: 24 });
|
||||
|
||||
const filteredWorkspaces = useMemo(
|
||||
() =>
|
||||
orgWorkspaces
|
||||
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||
.sort((a, b) =>
|
||||
orderDirection === OrderByDirection.ASC
|
||||
? a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
: b.name.toLowerCase().localeCompare(a.name.toLowerCase())
|
||||
),
|
||||
[searchFilter, page, perPage, orderDirection, offset, limit]
|
||||
const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0;
|
||||
const filteredWorkspaces = orgWorkspaces.filter((ws) =>
|
||||
ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
setPage,
|
||||
offset,
|
||||
totalCount: filteredWorkspaces.length
|
||||
});
|
||||
|
||||
const { workspacesWithFaveProp } = useMemo(() => {
|
||||
const { workspacesWithFaveProp, favoriteWorkspaces, nonFavoriteWorkspaces } = useMemo(() => {
|
||||
const workspacesWithFav = filteredWorkspaces
|
||||
.map((w): Workspace & { isFavorite: boolean } => ({
|
||||
...w,
|
||||
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
||||
}))
|
||||
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite))
|
||||
.slice(offset, limit * page);
|
||||
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite));
|
||||
|
||||
const favWorkspaces = workspacesWithFav.filter((w) => w.isFavorite);
|
||||
const nonFavWorkspaces = workspacesWithFav.filter((w) => !w.isFavorite);
|
||||
|
||||
return {
|
||||
workspacesWithFaveProp: workspacesWithFav
|
||||
workspacesWithFaveProp: workspacesWithFav,
|
||||
favoriteWorkspaces: favWorkspaces,
|
||||
nonFavoriteWorkspaces: nonFavWorkspaces
|
||||
};
|
||||
}, [filteredWorkspaces, projectFavorites]);
|
||||
|
||||
@@ -604,7 +566,7 @@ const OrganizationPage = () => {
|
||||
{isFavorite ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSolidStar}
|
||||
className="text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||
className="text-sm text-mineshaft-300 hover:text-mineshaft-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeProjectFromFavorites(workspace.id);
|
||||
@@ -661,10 +623,11 @@ const OrganizationPage = () => {
|
||||
key={workspace.id}
|
||||
className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
||||
index === 0 && "rounded-t-md"
|
||||
}`}
|
||||
} ${index === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`}
|
||||
>
|
||||
<div className="flex items-center sm:col-span-3 lg:col-span-4">
|
||||
<div className="truncate text-sm text-mineshaft-100">{workspace.name}</div>
|
||||
<FontAwesomeIcon icon={faFileShield} className="text-sm text-primary/70" />
|
||||
<div className="ml-5 truncate text-sm text-mineshaft-100">{workspace.name}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
|
||||
<div className="text-center text-sm text-mineshaft-300">
|
||||
@@ -673,7 +636,7 @@ const OrganizationPage = () => {
|
||||
{isFavorite ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSolidStar}
|
||||
className="ml-6 text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||
className="ml-6 text-sm text-mineshaft-300 hover:text-mineshaft-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeProjectFromFavorites(workspace.id);
|
||||
@@ -693,75 +656,63 @@ const OrganizationPage = () => {
|
||||
</div>
|
||||
);
|
||||
|
||||
let projectsComponents: ReactNode;
|
||||
|
||||
if (filteredWorkspaces.length || isProjectViewLoading) {
|
||||
switch (projectsViewMode) {
|
||||
case ProjectsViewMode.GRID:
|
||||
projectsComponents = (
|
||||
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{isProjectViewLoading &&
|
||||
Array.apply(0, Array(3)).map((_x, i) => (
|
||||
<div
|
||||
key={`workspace-cards-loading-${i + 1}`}
|
||||
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="mt-0 text-lg text-mineshaft-100">
|
||||
<Skeleton className="w-3/4 bg-mineshaft-600" />
|
||||
</div>
|
||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!isProjectViewLoading && (
|
||||
<>
|
||||
{workspacesWithFaveProp.map((workspace) =>
|
||||
renderProjectGridItem(workspace, workspace.isFavorite)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
const projectsGridView = (
|
||||
<>
|
||||
{favoriteWorkspaces.length > 0 && (
|
||||
<>
|
||||
<p className="mt-6 text-xl font-semibold text-white">Favorites</p>
|
||||
<div
|
||||
className={`b grid w-full grid-cols-1 gap-4 ${
|
||||
nonFavoriteWorkspaces.length > 0 && "border-b border-mineshaft-600"
|
||||
} py-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`}
|
||||
>
|
||||
{favoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, true))}
|
||||
</div>
|
||||
);
|
||||
|
||||
break;
|
||||
case ProjectsViewMode.LIST:
|
||||
default:
|
||||
projectsComponents = (
|
||||
<div className="mt-4 w-full rounded-md">
|
||||
{isProjectViewLoading &&
|
||||
Array.apply(0, Array(3)).map((_x, i) => (
|
||||
<div
|
||||
key={`workspace-cards-loading-${i + 1}`}
|
||||
className={`min-w-72 group flex h-12 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
||||
i === 0 && "rounded-t-md"
|
||||
} ${i === 2 && "rounded-b-md border-b"}`}
|
||||
>
|
||||
<Skeleton className="w-full bg-mineshaft-600" />
|
||||
</div>
|
||||
))}
|
||||
{!isProjectViewLoading &&
|
||||
workspacesWithFaveProp.map((workspace, ind) =>
|
||||
renderProjectListItem(workspace, workspace.isFavorite, ind)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else if (orgWorkspaces.length) {
|
||||
projectsComponents = (
|
||||
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
||||
<FontAwesomeIcon
|
||||
icon={faSearch}
|
||||
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
|
||||
/>
|
||||
<div className="text-center font-light">No projects match search...</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{isProjectViewLoading &&
|
||||
Array.apply(0, Array(3)).map((_x, i) => (
|
||||
<div
|
||||
key={`workspace-cards-loading-${i + 1}`}
|
||||
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="mt-0 text-lg text-mineshaft-100">
|
||||
<Skeleton className="w-3/4 bg-mineshaft-600" />
|
||||
</div>
|
||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!isProjectViewLoading &&
|
||||
nonFavoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, false))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
const projectsListView = (
|
||||
<div className="mt-4 w-full rounded-md">
|
||||
{isProjectViewLoading &&
|
||||
Array.apply(0, Array(3)).map((_x, i) => (
|
||||
<div
|
||||
key={`workspace-cards-loading-${i + 1}`}
|
||||
className={`min-w-72 group flex h-12 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
||||
i === 0 && "rounded-t-md"
|
||||
} ${i === 2 && "rounded-b-md border-b"}`}
|
||||
>
|
||||
<Skeleton className="w-full bg-mineshaft-600" />
|
||||
</div>
|
||||
))}
|
||||
{!isProjectViewLoading &&
|
||||
workspacesWithFaveProp.map((workspace, ind) =>
|
||||
renderProjectListItem(workspace, workspace.isFavorite, ind)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
|
||||
@@ -803,24 +754,6 @@ const OrganizationPage = () => {
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
/>
|
||||
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<Tooltip content="Toggle Sort Direction">
|
||||
<IconButton
|
||||
className="min-w-[2.4rem] border-none hover:bg-mineshaft-600"
|
||||
ariaLabel={`Sort ${
|
||||
orderDirection === OrderByDirection.ASC ? "descending" : "ascending"
|
||||
}`}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
colorSchema="secondary"
|
||||
onClick={toggleOrderDirection}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={orderDirection === OrderByDirection.ASC ? faArrowDownAZ : faArrowUpZA}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
@@ -871,24 +804,9 @@ const OrganizationPage = () => {
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
{projectsComponents}
|
||||
{!isProjectViewLoading && Boolean(filteredWorkspaces.length) && (
|
||||
<Pagination
|
||||
className={
|
||||
projectsViewMode === ProjectsViewMode.GRID
|
||||
? "col-span-full border-transparent bg-transparent"
|
||||
: "rounded-b-md border border-mineshaft-600"
|
||||
}
|
||||
perPage={perPage}
|
||||
perPageList={[12, 24, 48, 96]}
|
||||
count={filteredWorkspaces.length}
|
||||
page={page}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{projectsViewMode === ProjectsViewMode.LIST ? projectsListView : projectsGridView}
|
||||
{isWorkspaceEmpty && (
|
||||
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
||||
<div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
||||
<FontAwesomeIcon
|
||||
icon={faFolderOpen}
|
||||
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
|
||||
|
@@ -55,8 +55,6 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
||||
return "Path";
|
||||
case "bitbucket":
|
||||
return "Repository";
|
||||
case "octopus-deploy":
|
||||
return "Project";
|
||||
case "github":
|
||||
if (["github-env", "github-repo"].includes(integration.scope!)) {
|
||||
return "Repository";
|
||||
@@ -106,16 +104,6 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (integration.integration === "octopus-deploy") {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Space" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.targetEnvironment}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
["vercel", "netlify", "railway", "gitlab", "teamcity"].includes(integration.integration) ||
|
||||
(integration.integration === "github" && integration.scope === "github-env")
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||
import { OctopusDeployScopeValues } from "@app/views/IntegrationsPage/IntegrationDetailsPage/components/OctopusDeployScopeValues";
|
||||
|
||||
type Props = {
|
||||
integration: TIntegrationWithEnv;
|
||||
@@ -28,7 +27,6 @@ const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]
|
||||
shouldMaskSecrets: "GitLab Secrets Masking Enabled",
|
||||
shouldProtectSecrets: "GitLab Secret Protection Enabled",
|
||||
shouldEnableDelete: "GitHub Secret Deletion Enabled",
|
||||
octopusDeployScopeValues: "Octopus Deploy Scope Values",
|
||||
awsIamRole: "AWS IAM Role",
|
||||
region: "Region"
|
||||
} as const;
|
||||
@@ -37,9 +35,6 @@ export const IntegrationSettingsSection = ({ integration }: Props) => {
|
||||
const renderValue = <K extends MetadataKey>(key: K, value: MetadataValue<K>) => {
|
||||
if (!value) return null;
|
||||
|
||||
if (key === "octopusDeployScopeValues")
|
||||
return <OctopusDeployScopeValues integration={integration} />;
|
||||
|
||||
// If it's a boolean, we render a generic "Yes" or "No" response.
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "Yes" : "No";
|
||||
@@ -56,7 +51,7 @@ export const IntegrationSettingsSection = ({ integration }: Props) => {
|
||||
}
|
||||
|
||||
if (key === "githubVisibilityRepoIds") {
|
||||
return (value as string[]).join(", ");
|
||||
return value.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,90 +0,0 @@
|
||||
import { FormLabel, Spinner } from "@app/components/v2";
|
||||
import { useGetIntegrationAuthOctopusDeployScopeValues } from "@app/hooks/api/integrationAuth/queries";
|
||||
import {
|
||||
OctopusDeployScope,
|
||||
TOctopusDeployVariableSetScopeValues
|
||||
} from "@app/hooks/api/integrationAuth/types";
|
||||
import { TIntegration, TOctopusDeployScopeValues } from "@app/hooks/api/integrations/types";
|
||||
|
||||
type OctopusDeployScopeValuesProps = {
|
||||
integration: TIntegration;
|
||||
};
|
||||
|
||||
// remove plural since Octopus Deploy can decide whether they want to use singular or plural...
|
||||
const modifyKey = (key: keyof TOctopusDeployVariableSetScopeValues) => {
|
||||
switch (key) {
|
||||
case "Processes":
|
||||
return "ProcessOwner";
|
||||
default:
|
||||
return key.substring(0, key.length - 1);
|
||||
}
|
||||
};
|
||||
|
||||
export const OctopusDeployScopeValues = ({ integration }: OctopusDeployScopeValuesProps) => {
|
||||
const hasScopeValues = Boolean(
|
||||
Object.values(integration.metadata?.octopusDeployScopeValues ?? {}).some(
|
||||
(values) => values.length > 0
|
||||
)
|
||||
);
|
||||
|
||||
const { data: scopeValues = {}, isLoading } = useGetIntegrationAuthOctopusDeployScopeValues(
|
||||
{
|
||||
scope: OctopusDeployScope.Project,
|
||||
spaceId: integration.targetEnvironmentId!,
|
||||
resourceId: integration.appId!,
|
||||
integrationAuthId: integration.integrationAuthId
|
||||
},
|
||||
{
|
||||
enabled: hasScopeValues
|
||||
}
|
||||
);
|
||||
|
||||
if (!integration.metadata?.octopusDeployScopeValues)
|
||||
return <span className="text-sm text-mineshaft-400">Not Configured</span>;
|
||||
|
||||
if (isLoading) return <Spinner size="sm" className="mt-2 ml-2" />;
|
||||
|
||||
const scopeValuesMap = new Map(
|
||||
Object.entries(scopeValues).map(([key, values]) => [
|
||||
modifyKey(key as keyof TOctopusDeployVariableSetScopeValues),
|
||||
new Map((values as { Name: string; Id: string }[]).map((value) => [value.Id, value.Name]))
|
||||
])
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(integration.metadata.octopusDeployScopeValues).map(([key, values]) => {
|
||||
if (!values.length) return null;
|
||||
|
||||
const getLabel = (scope: string) => {
|
||||
switch (scope as keyof TOctopusDeployScopeValues) {
|
||||
case "Role":
|
||||
return "Target Tags";
|
||||
case "Machine":
|
||||
return "Targets";
|
||||
case "ProcessOwner":
|
||||
return "Processes";
|
||||
case "Action":
|
||||
return "Steps";
|
||||
default:
|
||||
return `${scope}s`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4" key={key}>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-200" label={getLabel(key)} />
|
||||
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
{values
|
||||
.map((value) => scopeValuesMap.get(key)?.get(value)!)
|
||||
.map((name) => (
|
||||
<p key={name}>{name}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
@@ -140,9 +140,6 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
|
||||
case "azure-devops":
|
||||
link = `${window.location.origin}/integrations/azure-devops/authorize`;
|
||||
break;
|
||||
case "octopus-deploy":
|
||||
link = `${window.location.origin}/integrations/octopus-deploy/authorize`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|