Compare commits
52 Commits
project-ov
...
project-si
Author | SHA1 | Date | |
---|---|---|---|
|
16d215b588 | ||
|
9df9f4a5da | ||
|
afdc704423 | ||
|
57261cf0c8 | ||
|
06f6004993 | ||
|
f3bfb9cc5a | ||
|
48fb77be49 | ||
|
f55bcb93ba | ||
|
d3fb2a6a74 | ||
|
6a23b74481 | ||
|
602cf4b3c4 | ||
|
84ff71fef2 | ||
|
add5742b8c | ||
|
68f3964206 | ||
|
90374971ae | ||
|
3a1eadba8c | ||
|
5305017ce2 | ||
|
ad8d247cdc | ||
|
3b47d7698b | ||
|
aa9a86df71 | ||
|
33411335ed | ||
|
ca55f19926 | ||
|
3794521c56 | ||
|
728f023263 | ||
|
229706f57f | ||
|
6cf2488326 | ||
|
92ce05283b | ||
|
39d92ce6ff | ||
|
44a026446e | ||
|
539e5b1907 | ||
|
44b02d5324 | ||
|
71fb6f1d11 | ||
|
e64100fab1 | ||
|
5bcf07b32b | ||
|
3b0c48052b | ||
|
df50e3b0f9 | ||
|
bdf2ae40b6 | ||
|
b6c05a2f25 | ||
|
960efb9cf9 | ||
|
aa8d58abad | ||
|
cfb0cc4fea | ||
|
7712df296c | ||
|
7c38932121 | ||
|
69ad9845e1 | ||
|
3d6ea3251e | ||
|
f034adba76 | ||
|
be39e63832 | ||
|
46ad1d47a9 | ||
|
b762816e66 | ||
|
cf275979ba | ||
|
ed7fc0e5cd | ||
|
1ae6213387 |
@@ -74,8 +74,8 @@ CAPTCHA_SECRET=
|
||||
|
||||
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=
|
||||
OTEL_EXPORT_TYPE=
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=false
|
||||
OTEL_EXPORT_TYPE=prometheus
|
||||
OTEL_EXPORT_OTLP_ENDPOINT=
|
||||
OTEL_OTLP_PUSH_INTERVAL=
|
||||
|
||||
|
@@ -69,13 +69,21 @@ RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Required for pkcs11js
|
||||
# Required for pkcs11js and ODBC
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
unixodbc \
|
||||
unixodbc-dev \
|
||||
freetds-dev \
|
||||
freetds-bin \
|
||||
tdsodbc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Configure ODBC
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
|
||||
@@ -91,13 +99,21 @@ ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Required for pkcs11js
|
||||
# Required for pkcs11js and ODBC
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
unixodbc \
|
||||
unixodbc-dev \
|
||||
freetds-dev \
|
||||
freetds-bin \
|
||||
tdsodbc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Configure ODBC
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
|
||||
@@ -108,13 +124,24 @@ RUN mkdir frontend-build
|
||||
# Production stage
|
||||
FROM base AS production
|
||||
|
||||
# Install necessary packages
|
||||
# Install necessary packages including ODBC
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
unixodbc \
|
||||
unixodbc-dev \
|
||||
freetds-dev \
|
||||
freetds-bin \
|
||||
tdsodbc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Configure ODBC in production
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.31.1 \
|
||||
|
@@ -72,8 +72,16 @@ RUN addgroup --system --gid 1001 nodejs \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Required for pkcs11js
|
||||
RUN apk add --no-cache python3 make g++
|
||||
# Install all required dependencies for build
|
||||
RUN apk --update add \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
unixodbc \
|
||||
freetds \
|
||||
unixodbc-dev \
|
||||
libc-dev \
|
||||
freetds-dev
|
||||
|
||||
COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
@@ -88,8 +96,19 @@ FROM base AS backend-runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Required for pkcs11js
|
||||
RUN apk add --no-cache python3 make g++
|
||||
# Install all required dependencies for runtime
|
||||
RUN apk --update add \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
unixodbc \
|
||||
freetds \
|
||||
unixodbc-dev \
|
||||
libc-dev \
|
||||
freetds-dev
|
||||
|
||||
# Configure ODBC
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
@@ -100,11 +119,32 @@ RUN mkdir frontend-build
|
||||
|
||||
# Production stage
|
||||
FROM base AS production
|
||||
|
||||
RUN apk add --upgrade --no-cache ca-certificates
|
||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||
&& apk add infisical=0.31.1 && apk add --no-cache git
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Install all required runtime dependencies
|
||||
RUN apk --update add \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
unixodbc \
|
||||
freetds \
|
||||
unixodbc-dev \
|
||||
libc-dev \
|
||||
freetds-dev \
|
||||
bash \
|
||||
curl \
|
||||
git
|
||||
|
||||
# Configure ODBC in production
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
# Setup user permissions
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 non-root-user
|
||||
|
||||
@@ -127,7 +167,6 @@ ARG CAPTCHA_SITE_KEY
|
||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
||||
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY --from=backend-runner /app /backend
|
||||
|
||||
@@ -149,4 +188,4 @@ EXPOSE 443
|
||||
|
||||
USER non-root-user
|
||||
|
||||
CMD ["./standalone-entrypoint.sh"]
|
||||
CMD ["./standalone-entrypoint.sh"]
|
@@ -9,6 +9,15 @@ RUN apk --update add \
|
||||
make \
|
||||
g++
|
||||
|
||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||
RUN apk add --no-cache \
|
||||
unixodbc \
|
||||
freetds \
|
||||
unixodbc-dev \
|
||||
libc-dev \
|
||||
freetds-dev
|
||||
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only-production
|
||||
|
||||
@@ -28,6 +37,17 @@ RUN apk --update add \
|
||||
make \
|
||||
g++
|
||||
|
||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||
RUN apk add --no-cache \
|
||||
unixodbc \
|
||||
freetds \
|
||||
unixodbc-dev \
|
||||
libc-dev \
|
||||
freetds-dev
|
||||
|
||||
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
RUN npm ci --only-production && npm cache clean --force
|
||||
|
||||
COPY --from=build /app .
|
||||
|
@@ -7,7 +7,7 @@ ARG SOFTHSM2_VERSION=2.5.0
|
||||
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
|
||||
SOFTHSM2_SOURCES=/tmp/softhsm2
|
||||
|
||||
# install build dependencies including python3
|
||||
# install build dependencies including python3 (required for pkcs11js and partially TDS driver)
|
||||
RUN apk --update add \
|
||||
alpine-sdk \
|
||||
autoconf \
|
||||
@@ -19,7 +19,19 @@ RUN apk --update add \
|
||||
make \
|
||||
g++
|
||||
|
||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||
RUN apk add --no-cache \
|
||||
unixodbc \
|
||||
freetds \
|
||||
unixodbc-dev \
|
||||
libc-dev \
|
||||
freetds-dev
|
||||
|
||||
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
# build and install SoftHSM2
|
||||
|
||||
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
|
||||
WORKDIR ${SOFTHSM2_SOURCES}
|
||||
|
||||
|
44
backend/package-lock.json
generated
@@ -33,6 +33,7 @@
|
||||
"@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",
|
||||
@@ -81,6 +82,7 @@
|
||||
"mysql2": "^3.9.8",
|
||||
"nanoid": "^3.3.4",
|
||||
"nodemailer": "^6.9.9",
|
||||
"odbc": "^2.4.9",
|
||||
"openid-client": "^5.6.5",
|
||||
"ora": "^7.0.1",
|
||||
"oracledb": "^6.4.0",
|
||||
@@ -6954,6 +6956,21 @@
|
||||
"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",
|
||||
@@ -17872,6 +17889,27 @@
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/odbc": {
|
||||
"version": "2.4.9",
|
||||
"resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.9.tgz",
|
||||
"integrity": "sha512-sHFWOKfyj4oFYds7YBlN+fq9ZjC2J6CsCN5CNMABpKLp+NZdb8bnanb57OaoDy1VFXEOTE91S+F900J/aIPu6w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.5",
|
||||
"async": "^3.0.1",
|
||||
"node-addon-api": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/odbc/node_modules/node-addon-api": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
|
||||
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oidc-token-hash": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
|
||||
@@ -22375,6 +22413,12 @@
|
||||
"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,6 +141,7 @@
|
||||
"@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",
|
||||
@@ -189,6 +190,7 @@
|
||||
"mysql2": "^3.9.8",
|
||||
"nanoid": "^3.3.4",
|
||||
"nodemailer": "^6.9.9",
|
||||
"odbc": "^2.4.9",
|
||||
"openid-client": "^5.6.5",
|
||||
"ora": "^7.0.1",
|
||||
"oracledb": "^6.4.0",
|
||||
|
@@ -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) {
|
||||
|
@@ -80,7 +80,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin
|
||||
}
|
||||
};
|
||||
|
||||
const addUserToInfisicalGroup = async (userId: string) => {
|
||||
const $addUserToInfisicalGroup = async (userId: string) => {
|
||||
// figure out if the default user is already in the group, if it is, then we shouldn't add it again
|
||||
|
||||
const addUserToGroupCommand = new ModifyUserGroupCommand({
|
||||
@@ -96,7 +96,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin
|
||||
await ensureInfisicalGroupExists(clusterName);
|
||||
|
||||
await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user
|
||||
await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists()
|
||||
await $addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists()
|
||||
|
||||
return {
|
||||
userId: creationInput.UserId,
|
||||
|
@@ -33,7 +33,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
|
||||
const client = new IAMClient({
|
||||
region: providerInputs.region,
|
||||
credentials: {
|
||||
@@ -47,7 +47,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client.send(new GetUserCommand({})).then(() => true);
|
||||
return isConnected;
|
||||
@@ -55,7 +55,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||
@@ -118,7 +118,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
|
||||
|
@@ -23,7 +23,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getToken = async (
|
||||
const $getToken = async (
|
||||
tenantId: string,
|
||||
applicationId: string,
|
||||
clientSecret: string
|
||||
@@ -51,13 +51,13 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
return data.success;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||
};
|
||||
|
||||
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
|
||||
const data = await getToken(tenantId, applicationId, clientSecret);
|
||||
const data = await $getToken(tenantId, applicationId, clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
|
@@ -27,7 +27,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
|
||||
const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||
const client = new cassandra.Client({
|
||||
sslOptions,
|
||||
@@ -47,7 +47,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
||||
await client.shutdown();
|
||||
@@ -56,7 +56,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
@@ -82,7 +82,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const { keyspace } = providerInputs;
|
||||
@@ -101,7 +101,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { keyspace } = providerInputs;
|
||||
|
@@ -24,7 +24,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
||||
const connection = new ElasticSearchClient({
|
||||
node: {
|
||||
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
|
||||
@@ -55,7 +55,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const infoResponse = await connection
|
||||
.info()
|
||||
@@ -67,7 +67,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
@@ -85,7 +85,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
await connection.security.deleteUser({
|
||||
username: entityId
|
||||
|
@@ -6,16 +6,17 @@ import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { ElasticSearchProvider } from "./elastic-search";
|
||||
import { LdapProvider } from "./ldap";
|
||||
import { DynamicSecretProviders } from "./models";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
|
||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||
import { MongoDBProvider } from "./mongo-db";
|
||||
import { RabbitMqProvider } from "./rabbit-mq";
|
||||
import { RedisDatabaseProvider } from "./redis";
|
||||
import { SapAseProvider } from "./sap-ase";
|
||||
import { SapHanaProvider } from "./sap-hana";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
import { TotpProvider } from "./totp";
|
||||
|
||||
export const buildDynamicSecretProviders = () => ({
|
||||
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||
@@ -29,5 +30,6 @@ export const buildDynamicSecretProviders = () => ({
|
||||
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
||||
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
||||
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
||||
[DynamicSecretProviders.Totp]: TotpProvider()
|
||||
[DynamicSecretProviders.Totp]: TotpProvider(),
|
||||
[DynamicSecretProviders.SapAse]: SapAseProvider()
|
||||
});
|
||||
|
@@ -52,7 +52,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = ldapjs.createClient({
|
||||
url: providerInputs.url,
|
||||
@@ -83,7 +83,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
return client.connected;
|
||||
};
|
||||
|
||||
@@ -191,7 +191,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
if (providerInputs.credentialType === LdapCredentialType.Static) {
|
||||
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
|
||||
@@ -235,7 +235,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
if (providerInputs.credentialType === LdapCredentialType.Static) {
|
||||
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
|
||||
|
@@ -4,7 +4,8 @@ export enum SqlProviders {
|
||||
Postgres = "postgres",
|
||||
MySQL = "mysql2",
|
||||
Oracle = "oracledb",
|
||||
MsSQL = "mssql"
|
||||
MsSQL = "mssql",
|
||||
SapAse = "sap-ase"
|
||||
}
|
||||
|
||||
export enum ElasticSearchAuthTypes {
|
||||
@@ -118,6 +119,16 @@ export const DynamicSecretCassandraSchema = z.object({
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretSapAseSchema = z.object({
|
||||
host: z.string().trim().toLowerCase(),
|
||||
port: z.number(),
|
||||
database: z.string().trim(),
|
||||
username: z.string().trim(),
|
||||
password: z.string().trim(),
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim()
|
||||
});
|
||||
|
||||
export const DynamicSecretAwsIamSchema = z.object({
|
||||
accessKey: z.string().trim().min(1),
|
||||
secretAccessKey: z.string().trim().min(1),
|
||||
@@ -274,12 +285,14 @@ export enum DynamicSecretProviders {
|
||||
Ldap = "ldap",
|
||||
SapHana = "sap-hana",
|
||||
Snowflake = "snowflake",
|
||||
Totp = "totp"
|
||||
Totp = "totp",
|
||||
SapAse = "sap-ase"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.SapAse), inputs: DynamicSecretSapAseSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }),
|
||||
|
@@ -22,7 +22,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
|
||||
const client = axios.create({
|
||||
baseURL: "https://cloud.mongodb.com/api/atlas",
|
||||
headers: {
|
||||
@@ -40,7 +40,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client({
|
||||
method: "GET",
|
||||
@@ -59,7 +59,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
@@ -87,7 +87,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const isExisting = await client({
|
||||
@@ -114,7 +114,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
@@ -23,7 +23,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
||||
const isSrv = !providerInputs.port;
|
||||
const uri = isSrv
|
||||
? `mongodb+srv://${providerInputs.host}`
|
||||
@@ -42,7 +42,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client
|
||||
.db(providerInputs.database)
|
||||
@@ -55,7 +55,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
@@ -74,7 +74,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
|
||||
|
@@ -84,7 +84,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
|
||||
auth: {
|
||||
@@ -105,7 +105,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const infoResponse = await connection.get("/whoami").then(() => true);
|
||||
|
||||
@@ -114,7 +114,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
@@ -134,7 +134,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
|
||||
|
||||
|
@@ -55,7 +55,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
|
||||
let connection: Redis | null = null;
|
||||
try {
|
||||
connection = new Redis({
|
||||
@@ -92,7 +92,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const pingResponse = await connection
|
||||
.ping()
|
||||
@@ -104,7 +104,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
@@ -126,7 +126,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
|
||||
@@ -143,7 +143,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const connection = await getClient(providerInputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
145
backend/src/ee/services/dynamic-secret/providers/sap-ase.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import odbc from "odbc";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(25);
|
||||
};
|
||||
|
||||
enum SapCommands {
|
||||
CreateLogin = "sp_addlogin",
|
||||
DropLogin = "sp_droplogin"
|
||||
}
|
||||
|
||||
export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs);
|
||||
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapAseSchema>, useMaster?: boolean) => {
|
||||
const connectionString =
|
||||
`DRIVER={FreeTDS};` +
|
||||
`SERVER=${providerInputs.host};` +
|
||||
`PORT=${providerInputs.port};` +
|
||||
`DATABASE=${useMaster ? "master" : providerInputs.database};` +
|
||||
`UID=${providerInputs.username};` +
|
||||
`PWD=${providerInputs.password};` +
|
||||
`TDS_VERSION=5.0`;
|
||||
|
||||
const client = await odbc.connect(connectionString);
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const masterClient = await $getClient(providerInputs, true);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||
const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||
|
||||
if (!resultFromSelectedDatabase.version) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SAP ASE connection, version query failed"
|
||||
});
|
||||
}
|
||||
|
||||
if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SAP ASE connection (master), version mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const username = `inf_${generateUsername()}`;
|
||||
const password = `${generatePassword()}`;
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
const masterClient = await $getClient(providerInputs, true);
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password
|
||||
});
|
||||
|
||||
const queries = creationStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
||||
|
||||
for await (const query of queries) {
|
||||
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
||||
// If not done, then the newly created user won't be able to authenticate.
|
||||
await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query);
|
||||
}
|
||||
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, username: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement, { noEscape: true })({
|
||||
username
|
||||
});
|
||||
|
||||
const queries = revokeStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
const masterClient = await $getClient(providerInputs, true);
|
||||
|
||||
// Get all processes for this login and kill them. If there are active connections to the database when drop login happens, it will throw an error.
|
||||
const result = await masterClient.query<{ spid?: string }>(`sp_who '${username}'`);
|
||||
|
||||
if (result && result.length > 0) {
|
||||
for await (const row of result) {
|
||||
if (row.spid) {
|
||||
await masterClient.query(`KILL ${row.spid.trim()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for await (const query of queries) {
|
||||
await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query);
|
||||
}
|
||||
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (_: unknown, username: string) => {
|
||||
// No need for renewal
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@@ -32,7 +32,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
|
||||
const client = hdb.createClient({
|
||||
host: providerInputs.host,
|
||||
port: providerInputs.port,
|
||||
@@ -64,9 +64,9 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const testResult: boolean = await new Promise((resolve, reject) => {
|
||||
const testResult = await new Promise<boolean>((resolve, reject) => {
|
||||
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
|
||||
if (err) {
|
||||
reject();
|
||||
@@ -86,7 +86,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
@@ -114,7 +114,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const revoke = async (inputs: unknown, username: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
@@ -139,7 +139,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
try {
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
|
@@ -34,7 +34,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
|
||||
const client = snowflake.createConnection({
|
||||
account: `${providerInputs.orgId}-${providerInputs.accountId}`,
|
||||
username: providerInputs.username,
|
||||
@@ -49,7 +49,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
let isValidConnection: boolean;
|
||||
|
||||
@@ -72,7 +72,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
@@ -107,7 +107,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
const revoke = async (inputs: unknown, username: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
try {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||
@@ -135,7 +135,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const client = await getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
try {
|
||||
const expiration = getDaysToExpiry(new Date(expireAt));
|
||||
|
@@ -32,7 +32,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
||||
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||
const db = knex({
|
||||
client: providerInputs.client,
|
||||
@@ -52,7 +52,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await getClient(providerInputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
|
||||
@@ -63,7 +63,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await getClient(providerInputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(providerInputs.client);
|
||||
const password = generatePassword(providerInputs.client);
|
||||
@@ -90,7 +90,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await getClient(providerInputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const { database } = providerInputs;
|
||||
@@ -112,7 +112,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const db = await getClient(providerInputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
|
@@ -1082,7 +1082,8 @@ 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."
|
||||
shouldEnableDelete: "The flag to enable deletion of secrets.",
|
||||
octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy."
|
||||
}
|
||||
},
|
||||
UPDATE: {
|
||||
|
@@ -10,7 +10,7 @@ export const GITLAB_URL = "https://gitlab.com";
|
||||
export const IS_PACKAGED = (process as any)?.pkg !== undefined;
|
||||
|
||||
const zodStrBool = z
|
||||
.enum(["true", "false"])
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val === "true");
|
||||
|
||||
|
@@ -5,6 +5,7 @@ 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";
|
||||
|
||||
@@ -1008,4 +1009,118 @@ 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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -119,13 +119,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
if (!userAgent) throw new Error("user agent header is required");
|
||||
const appCfg = getConfig();
|
||||
|
||||
const serverCfg = await getServerCfg();
|
||||
if (!serverCfg.allowSignUp) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Signup's are disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const { user, accessToken, refreshToken, organizationId } =
|
||||
await server.services.signup.completeEmailAccountSignup({
|
||||
...req.body,
|
||||
|
@@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@@ -23,6 +23,7 @@ import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { getServerCfg } from "../super-admin/super-admin-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { UserEncryption } from "../user/user-types";
|
||||
import { TAuthDALFactory } from "./auth-dal";
|
||||
@@ -151,6 +152,8 @@ export const authSignupServiceFactory = ({
|
||||
authorization
|
||||
}: TCompleteAccountSignupDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
const user = await userDAL.findOne({ username: email });
|
||||
if (!user || (user && user.isAccepted)) {
|
||||
throw new Error("Failed to complete account for complete user");
|
||||
@@ -163,6 +166,12 @@ export const authSignupServiceFactory = ({
|
||||
authMethod = userAuthMethod;
|
||||
organizationId = orgId;
|
||||
} else {
|
||||
// disallow signup if disabled. we are not doing this for providerAuthToken because we allow signups via saml or sso
|
||||
if (!serverCfg.allowSignUp) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Signup's are disabled"
|
||||
});
|
||||
}
|
||||
validateSignUpAuthorization(authorization, user.id);
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
/* 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";
|
||||
@@ -1087,6 +1088,33 @@ 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,
|
||||
@@ -1260,6 +1288,13 @@ 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,6 +1,7 @@
|
||||
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";
|
||||
@@ -9,7 +10,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, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
@@ -20,6 +21,7 @@ import { getApps } from "./integration-app-list";
|
||||
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||
import {
|
||||
OctopusDeployScope,
|
||||
TBitbucketEnvironment,
|
||||
TBitbucketWorkspace,
|
||||
TChecklyGroups,
|
||||
@@ -38,6 +40,8 @@ import {
|
||||
TIntegrationAuthGithubOrgsDTO,
|
||||
TIntegrationAuthHerokuPipelinesDTO,
|
||||
TIntegrationAuthNorthflankSecretGroupDTO,
|
||||
TIntegrationAuthOctopusDeployProjectScopeValuesDTO,
|
||||
TIntegrationAuthOctopusDeploySpacesDTO,
|
||||
TIntegrationAuthQoveryEnvironmentsDTO,
|
||||
TIntegrationAuthQoveryOrgsDTO,
|
||||
TIntegrationAuthQoveryProjectDTO,
|
||||
@@ -48,6 +52,7 @@ import {
|
||||
TIntegrationAuthVercelBranchesDTO,
|
||||
TNorthflankSecretGroup,
|
||||
TOauthExchangeDTO,
|
||||
TOctopusDeployVariableSet,
|
||||
TSaveIntegrationAccessTokenDTO,
|
||||
TTeamCityBuildConfig,
|
||||
TVercelBranches
|
||||
@@ -1521,6 +1526,88 @@ 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,
|
||||
@@ -1552,6 +1639,8 @@ export const integrationAuthServiceFactory = ({
|
||||
getBitbucketWorkspaces,
|
||||
getBitbucketEnvironments,
|
||||
getIntegrationAccessToken,
|
||||
duplicateIntegrationAuth
|
||||
duplicateIntegrationAuth,
|
||||
getOctopusDeploySpaces,
|
||||
getOctopusDeployScopeValues
|
||||
};
|
||||
};
|
||||
|
@@ -193,3 +193,72 @@ 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,7 +34,8 @@ export enum Integrations {
|
||||
HASURA_CLOUD = "hasura-cloud",
|
||||
RUNDECK = "rundeck",
|
||||
AZURE_DEVOPS = "azure-devops",
|
||||
AZURE_APP_CONFIGURATION = "azure-app-configuration"
|
||||
AZURE_APP_CONFIGURATION = "azure-app-configuration",
|
||||
OCTOPUS_DEPLOY = "octopus-deploy"
|
||||
}
|
||||
|
||||
export enum IntegrationType {
|
||||
@@ -413,6 +414,15 @@ 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 } from "@app/lib/errors";
|
||||
import { BadRequestError, InternalServerError } 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 { TIntegrationsWithEnvironment } from "./integration-auth-types";
|
||||
import { OctopusDeployScope, TIntegrationsWithEnvironment, TOctopusDeployVariableSet } from "./integration-auth-types";
|
||||
import {
|
||||
IntegrationInitialSyncBehavior,
|
||||
IntegrationMappingBehavior,
|
||||
@@ -473,7 +473,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
id: string; // secret URI
|
||||
value: string;
|
||||
attributes: {
|
||||
enabled: true;
|
||||
enabled: boolean;
|
||||
created: number;
|
||||
updated: number;
|
||||
recoveryLevel: string;
|
||||
@@ -509,10 +509,19 @@ 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(
|
||||
getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
|
||||
enabledAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
|
||||
if (!lastSlashIndex) {
|
||||
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/");
|
||||
}
|
||||
@@ -658,6 +667,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
}) => {
|
||||
let isSecretSet = false;
|
||||
let maxTries = 6;
|
||||
if (disabledAzureKeyVaultSecretKeys.includes(key)) return;
|
||||
|
||||
while (!isSecretSet && maxTries > 0) {
|
||||
// try to set secret
|
||||
@@ -4201,6 +4211,61 @@ 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]
|
||||
*
|
||||
@@ -4513,6 +4578,14 @@ 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,5 +46,18 @@ 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)
|
||||
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)
|
||||
});
|
||||
|
@@ -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.3.8
|
||||
github.com/infisical/go-sdk v0.4.3
|
||||
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.3.8 h1:0dGOhF3cwt0q5QzpnUs4lxwBiEza+DQYOyvEn7AfrM0=
|
||||
github.com/infisical/go-sdk v0.3.8/go.mod h1:HHW7DgUqoolyQIUw/9HdpkZ3bDLwWyZ0HEtYiVaDKQw=
|
||||
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/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,6 +205,25 @@ 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,6 +128,10 @@ type GetWorkSpacesResponse struct {
|
||||
} `json:"workspaces"`
|
||||
}
|
||||
|
||||
type GetProjectByIdResponse struct {
|
||||
Project Project `json:"workspace"`
|
||||
}
|
||||
|
||||
type GetOrganizationsResponse struct {
|
||||
Organizations []struct {
|
||||
ID string `json:"id"`
|
||||
@@ -163,6 +167,12 @@ 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"`
|
||||
|
571
cli/packages/cmd/dynamic_secrets.go
Normal file
@@ -0,0 +1,571 @@
|
||||
/*
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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)
|
||||
}
|
39
cli/packages/visualize/dynamic_secret_leases.go
Normal file
@@ -0,0 +1,39 @@
|
||||
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,6 +94,33 @@ 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) {
|
||||
|
@@ -12,6 +12,18 @@ 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).
|
||||
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!
|
||||
|
295
docs/cli/commands/dynamic-secrets.mdx
Normal file
@@ -0,0 +1,295 @@
|
||||
---
|
||||
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>
|
116
docs/documentation/platform/dynamic-secrets/sap-ase.mdx
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: "SAP ASE"
|
||||
description: "Learn how to dynamically generate SAP ASE database account credentials."
|
||||
---
|
||||
|
||||
The Infisical SAP ASE dynamic secret allows you to generate SAP ASE database credentials on demand.
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- Infisical requires that you have a user in your SAP ASE instance, configured with the appropriate permissions. This user will facilitate the creation of new accounts as needed.
|
||||
Ensure the user possesses privileges for creating, dropping, and granting permissions to roles for it to be able to create dynamic secrets.
|
||||
The user used for authentication must have access to the `master` database. You can use the `sa` user for this purpose or create a new user with the necessary permissions.
|
||||
|
||||
- The SAP ASE instance should be reachable by Infisical.
|
||||
|
||||
## Set up Dynamic Secrets with SAP ASE
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Secret Overview Dashboard">
|
||||
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||
</Step>
|
||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Select SAP ASE">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide the inputs for dynamic secret parameters">
|
||||
<ParamField path="Secret Name" type="string" required>
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
The maximum time-to-live for a generated secret
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Host" type="string" required>
|
||||
Your SAP ASE instance host (IP or domain)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Port" type="number" required>
|
||||
Your SAP ASE instance port. On default SAP ASE instances this is usually `5000`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Database" type="number" required>
|
||||
The database name that you want to generate credentials for. This database must exist on the SAP ASE instance.
|
||||
Please note that the user/password used for authentication must have access to this database, **and** the `master` database.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="User" type="string" required>
|
||||
Username that will be used to create dynamic secrets
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Password" type="string" required>
|
||||
Password that will be used to create dynamic secrets
|
||||
</ParamField>
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="(Optional) Modify SQL Statements">
|
||||
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs.
|
||||

|
||||
|
||||
<Warning>
|
||||
Due to SAP ASE limitations, the attached SQL statements are not executed as a transaction.
|
||||
</Warning>
|
||||
|
||||
</Step>
|
||||
<Step title="Click 'Submit'">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||
</Step>
|
||||
<Step title="Generate dynamic secrets">
|
||||
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||
|
||||

|
||||

|
||||
|
||||
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||
|
||||

|
||||
|
||||
<Tip>
|
||||
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4.
|
||||
</Tip>
|
||||
|
||||
|
||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Audit or Revoke Leases
|
||||
|
||||
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||
This will allow you see the lease details and delete the lease ahead of its expiration time.
|
||||
|
||||

|
||||
|
||||
## Renew Leases
|
||||
|
||||
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
|
||||

|
||||
|
||||
<Warning>
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic
|
||||
secret.
|
||||
</Warning>
|
After Width: | Height: | Size: 441 KiB |
After Width: | Height: | Size: 328 KiB |
After Width: | Height: | Size: 930 KiB |
After Width: | Height: | Size: 394 KiB |
After Width: | Height: | Size: 380 KiB |
After Width: | Height: | Size: 402 KiB |
After Width: | Height: | Size: 407 KiB |
After Width: | Height: | Size: 980 KiB |
After Width: | Height: | Size: 437 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 320 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 383 KiB |
After Width: | Height: | Size: 280 KiB |
After Width: | Height: | Size: 152 KiB |
After Width: | Height: | Size: 142 KiB |
After Width: | Height: | Size: 200 KiB |
76
docs/integrations/cicd/octopus-deploy.mdx
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
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,6 +42,7 @@ 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 |
|
||||
|
@@ -188,6 +188,7 @@
|
||||
"documentation/platform/dynamic-secrets/mongo-db",
|
||||
"documentation/platform/dynamic-secrets/azure-entra-id",
|
||||
"documentation/platform/dynamic-secrets/ldap",
|
||||
"documentation/platform/dynamic-secrets/sap-ase",
|
||||
"documentation/platform/dynamic-secrets/sap-hana",
|
||||
"documentation/platform/dynamic-secrets/snowflake",
|
||||
"documentation/platform/dynamic-secrets/totp"
|
||||
@@ -315,6 +316,7 @@
|
||||
"cli/commands/init",
|
||||
"cli/commands/run",
|
||||
"cli/commands/secrets",
|
||||
"cli/commands/dynamic-secrets",
|
||||
"cli/commands/export",
|
||||
"cli/commands/token",
|
||||
"cli/commands/service-token",
|
||||
@@ -422,7 +424,8 @@
|
||||
"integrations/cicd/travisci",
|
||||
"integrations/cicd/rundeck",
|
||||
"integrations/cicd/codefresh",
|
||||
"integrations/cloud/checkly"
|
||||
"integrations/cloud/checkly",
|
||||
"integrations/cicd/octopus-deploy"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@@ -2,7 +2,7 @@ const path = require("path");
|
||||
|
||||
const ContentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
connect-src 'self' https://*.posthog.com;
|
||||
connect-src 'self' https://*.posthog.com http://127.0.0.1:*;
|
||||
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.1",
|
||||
"react-select": "^5.8.3",
|
||||
"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.1",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz",
|
||||
"integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.0",
|
||||
|
@@ -162,4 +162,4 @@
|
||||
"tailwindcss": "3.2",
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
}
|
||||
}
|
@@ -36,7 +36,8 @@ const integrationSlugNameMapping: Mapping = {
|
||||
"hasura-cloud": "Hasura Cloud",
|
||||
rundeck: "Rundeck",
|
||||
"azure-devops": "Azure DevOps",
|
||||
"azure-app-configuration": "Azure App Configuration"
|
||||
"azure-app-configuration": "Azure App Configuration",
|
||||
"octopus-deploy": "Octopus Deploy"
|
||||
};
|
||||
|
||||
const envMapping: Mapping = {
|
||||
|
BIN
frontend/public/images/integrations/Octopus Deploy.png
Normal file
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,68 @@
|
||||
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
frontend/src/components/v2/CreatableSelect/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./CreatableSelect";
|
@@ -14,6 +14,7 @@ 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/
|
||||
@@ -22,6 +23,7 @@ export const DatePicker = ({
|
||||
onChange,
|
||||
popUpProps,
|
||||
popUpContentProps,
|
||||
dateFormat = "PPP",
|
||||
...props
|
||||
}: DatePickerProps) => {
|
||||
const [timeValue, setTimeValue] = useState<string>(value ? format(value, "HH:mm") : "00:00");
|
||||
@@ -53,7 +55,7 @@ export const DatePicker = ({
|
||||
<Popover {...popUpProps}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline_bg" leftIcon={<FontAwesomeIcon icon={faCalendar} />}>
|
||||
{value ? format(value, "PPP") : "Pick a date and time"}
|
||||
{value ? format(value, dateFormat) : "Pick a date and time"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit p-2" {...popUpContentProps}>
|
||||
|
@@ -1,52 +1,14 @@
|
||||
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 Select, { Props } from "react-select";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
|
||||
return (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<FontAwesomeIcon icon={faChevronDown} size="xs" />
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
};
|
||||
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
|
||||
|
||||
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>) => (
|
||||
export const FilterableSelect = <T,>({
|
||||
isMulti,
|
||||
closeMenuOnSelect,
|
||||
tabSelectsValue = false,
|
||||
...props
|
||||
}: Props<T>) => (
|
||||
<Select
|
||||
isMulti={isMulti}
|
||||
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
|
||||
@@ -69,7 +31,14 @@ export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: P
|
||||
transition: "none"
|
||||
})
|
||||
}}
|
||||
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
|
||||
tabSelectsValue={tabSelectsValue}
|
||||
components={{
|
||||
DropdownIndicator,
|
||||
ClearIndicator,
|
||||
MultiValueRemove,
|
||||
Option,
|
||||
...props.components
|
||||
}}
|
||||
classNames={{
|
||||
container: () => "w-full font-inter",
|
||||
control: ({ isFocused }) =>
|
||||
@@ -79,7 +48,8 @@ export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: P
|
||||
),
|
||||
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`,
|
||||
valueContainer: () =>
|
||||
`p-1 max-h-[14rem] ${isMulti ? "!overflow-y-auto thin-scrollbar" : ""} 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",
|
||||
@@ -89,13 +59,13 @@ export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: P
|
||||
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",
|
||||
"mt-2 p-2 border text-sm text-mineshaft-200 thin-scrollbar 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"
|
||||
"hover:cursor-pointer mb-1 rounded text-xs px-3 py-2"
|
||||
),
|
||||
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
||||
}}
|
||||
|
45
frontend/src/components/v2/Select/components/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
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>
|
||||
);
|
||||
};
|
@@ -29,7 +29,8 @@ export enum DynamicSecretProviders {
|
||||
Ldap = "ldap",
|
||||
SapHana = "sap-hana",
|
||||
Snowflake = "snowflake",
|
||||
Totp = "totp"
|
||||
Totp = "totp",
|
||||
SapAse = "sap-ase"
|
||||
}
|
||||
|
||||
export enum SqlProviders {
|
||||
@@ -220,6 +221,18 @@ export type TDynamicSecretProvider =
|
||||
ca?: string | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.SapAse;
|
||||
inputs: {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
database: string;
|
||||
password: string;
|
||||
creationStatement: string;
|
||||
revocationStatement: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.Snowflake;
|
||||
inputs: {
|
||||
|
@@ -17,7 +17,9 @@ import {
|
||||
Project,
|
||||
Service,
|
||||
Team,
|
||||
TeamCityBuildConfig
|
||||
TeamCityBuildConfig,
|
||||
TGetIntegrationAuthOctopusDeployScopeValuesDTO,
|
||||
TOctopusDeployVariableSetScopeValues
|
||||
} from "./types";
|
||||
|
||||
const integrationAuthKeys = {
|
||||
@@ -119,7 +121,14 @@ const integrationAuthKeys = {
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const
|
||||
}) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const,
|
||||
getIntegrationAuthOctopusDeploySpaces: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeploySpaces"] as const,
|
||||
getIntegrationAuthOctopusDeployScopeValues: ({
|
||||
integrationAuthId,
|
||||
...params
|
||||
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||
@@ -479,6 +488,28 @@ 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),
|
||||
@@ -487,17 +518,24 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthApps = ({
|
||||
integrationAuthId,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
azureDevOpsOrgName?: string;
|
||||
workspaceSlug?: 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>>
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId, workspaceSlug),
|
||||
queryFn: () =>
|
||||
@@ -507,7 +545,7 @@ export const useGetIntegrationAuthApps = ({
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug
|
||||
}),
|
||||
enabled: true
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
@@ -759,6 +797,27 @@ 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,3 +99,29 @@ 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 } from "./types";
|
||||
import { TCloudIntegration, TIntegrationWithEnv, TOctopusDeployScopeValues } from "./types";
|
||||
|
||||
export const integrationQueryKeys = {
|
||||
getIntegrations: () => ["integrations"] as const,
|
||||
@@ -87,6 +87,7 @@ export const useCreateIntegration = () => {
|
||||
shouldMaskSecrets?: boolean;
|
||||
shouldProtectSecrets?: boolean;
|
||||
shouldEnableDelete?: boolean;
|
||||
octopusDeployScopeValues?: TOctopusDeployScopeValues;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
|
@@ -58,11 +58,21 @@ 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;
|
||||
|
@@ -52,7 +52,7 @@ export type Invoice = {
|
||||
};
|
||||
|
||||
export type PmtMethod = {
|
||||
id: string;
|
||||
_id: string;
|
||||
brand: string;
|
||||
exp_month: number;
|
||||
exp_year: number;
|
||||
|
@@ -10,7 +10,6 @@ import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
faArrowLeft,
|
||||
@@ -22,15 +21,11 @@ import {
|
||||
faInfo,
|
||||
faMobile,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faStar as faSolidStar
|
||||
faQuestion
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import {
|
||||
@@ -39,20 +34,9 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectItem,
|
||||
UpgradePlanModal
|
||||
MenuItem
|
||||
} from "@app/components/v2";
|
||||
import { NewProjectModal } from "@app/components/v2/projects/NewProjectModal";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription,
|
||||
useUser,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import {
|
||||
useGetAccessRequestsCount,
|
||||
@@ -62,11 +46,9 @@ import {
|
||||
useSelectOrganization
|
||||
} from "@app/hooks/api";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { Workspace } from "@app/hooks/api/types";
|
||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { InsecureConnectionBanner } from "@app/layouts/AppLayout/components/InsecureConnectionBanner";
|
||||
import { ProjectSelect } from "@app/layouts/AppLayout/components/ProjectSelect";
|
||||
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||
import { Mfa } from "@app/views/Login/Mfa";
|
||||
import { CreateOrgModal } from "@app/views/Org/components";
|
||||
@@ -108,23 +90,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
const { workspaces, currentWorkspace } = useWorkspace();
|
||||
const { orgs, currentOrg } = useOrganization();
|
||||
|
||||
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
|
||||
const workspacesWithFaveProp = useMemo(
|
||||
() =>
|
||||
workspaces
|
||||
.map((w): Workspace & { isFavorite: boolean } => ({
|
||||
...w,
|
||||
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
||||
}))
|
||||
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)),
|
||||
[workspaces, projectFavorites]
|
||||
);
|
||||
|
||||
const { user } = useUser();
|
||||
const { subscription } = useSubscription();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
@@ -137,17 +106,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
}, [secretApprovalReqCount, accessApprovalRequestCount]);
|
||||
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||
: true;
|
||||
|
||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"addNewWs",
|
||||
"upgradePlan",
|
||||
"createOrg"
|
||||
] as const);
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -230,38 +191,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
putUserInOrg();
|
||||
}, [router.query.id]);
|
||||
|
||||
const addProjectToFavorites = async (projectId: string) => {
|
||||
try {
|
||||
if (currentOrg?.id) {
|
||||
await updateUserProjectFavorites({
|
||||
orgId: currentOrg?.id,
|
||||
projectFavorites: [...(projectFavorites || []), projectId]
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed to add project to favorites.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeProjectFromFavorites = async (projectId: string) => {
|
||||
try {
|
||||
if (currentOrg?.id) {
|
||||
await updateUserProjectFavorites({
|
||||
orgId: currentOrg?.id,
|
||||
projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)]
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed to remove project from favorites.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (shouldShowMfa) {
|
||||
return (
|
||||
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||
@@ -448,94 +377,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
)}
|
||||
{!router.asPath.includes("org") &&
|
||||
(!router.asPath.includes("personal") && currentWorkspace ? (
|
||||
<div className="mt-5 mb-4 w-full p-3">
|
||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
|
||||
Project
|
||||
</p>
|
||||
<Select
|
||||
defaultValue={currentWorkspace?.id}
|
||||
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);
|
||||
}}
|
||||
position="popper"
|
||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
||||
>
|
||||
<div className="no-scrollbar::-webkit-scrollbar h-full no-scrollbar">
|
||||
{workspacesWithFaveProp
|
||||
.filter((ws) => ws.orgId === currentOrg?.id)
|
||||
.map(({ id, name, isFavorite }) => (
|
||||
<div
|
||||
className={twMerge(
|
||||
"mb-1 grid grid-cols-7 rounded-md hover:bg-mineshaft-500",
|
||||
id === currentWorkspace?.id && "bg-mineshaft-500"
|
||||
)}
|
||||
key={id}
|
||||
>
|
||||
<div className="col-span-6">
|
||||
<SelectItem
|
||||
key={`ws-layout-list-${id}`}
|
||||
value={id}
|
||||
className="transition-none data-[highlighted]:bg-mineshaft-500"
|
||||
>
|
||||
{name}
|
||||
</SelectItem>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center">
|
||||
{isFavorite ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSolidStar}
|
||||
className="text-sm text-mineshaft-300 hover:text-mineshaft-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeProjectFromFavorites(id);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faStar}
|
||||
className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
addProjectToFavorites(id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
||||
<div className="w-full">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Create}
|
||||
a={OrgPermissionSubjects.Workspace}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
if (isAddingProjectsAllowed) {
|
||||
handlePopUpOpen("addNewWs");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add Project
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
</Select>
|
||||
</div>
|
||||
<ProjectSelect />
|
||||
) : (
|
||||
<Link href={`/org/${currentOrg?.id}/overview`}>
|
||||
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||
@@ -813,15 +655,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<NewProjectModal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You have exceeded the number of projects allowed on the free plan."
|
||||
/>
|
||||
<CreateOrgModal
|
||||
isOpen={popUp?.createOrg?.isOpen}
|
||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||
|
@@ -0,0 +1,212 @@
|
||||
import { useMemo } from "react";
|
||||
import { components, MenuProps, OptionProps } from "react-select";
|
||||
import { faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faEye, faPlus, faStar as faSolidStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FilterableSelect, UpgradePlanModal } from "@app/components/v2";
|
||||
import { NewProjectModal } from "@app/components/v2/projects";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||
import { Workspace } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type TWorkspaceWithFaveProp = Workspace & { isFavorite: boolean };
|
||||
|
||||
const ProjectsMenu = ({ children, ...props }: MenuProps<TWorkspaceWithFaveProp>) => {
|
||||
return (
|
||||
<components.Menu {...props}>
|
||||
{children}
|
||||
<hr className="mb-2 h-px border-0 bg-mineshaft-500" />
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
className="w-full bg-mineshaft-700 pt-2 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => props.clearValue()}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add Project
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</components.Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectOption = ({
|
||||
isSelected,
|
||||
children,
|
||||
data,
|
||||
...props
|
||||
}: OptionProps<TWorkspaceWithFaveProp>) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
||||
|
||||
const removeProjectFromFavorites = async (projectId: string) => {
|
||||
try {
|
||||
await updateUserProjectFavorites({
|
||||
orgId: currentOrg!.id,
|
||||
projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)]
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed to remove project from favorites.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addProjectToFavorites = async (projectId: string) => {
|
||||
try {
|
||||
await updateUserProjectFavorites({
|
||||
orgId: currentOrg!.id,
|
||||
projectFavorites: [...(projectFavorites || []), projectId]
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed to add project to favorites.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<components.Option
|
||||
isSelected={isSelected}
|
||||
data={data}
|
||||
{...props}
|
||||
className={twMerge(props.className, isSelected && "bg-mineshaft-500")}
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="mr-2 text-mineshaft-300" icon={faEye} size="sm" />
|
||||
)}
|
||||
<p className="truncate">{children}</p>
|
||||
{data.isFavorite ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSolidStar}
|
||||
className="ml-auto text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await removeProjectFromFavorites(data.id);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faStar}
|
||||
className="ml-auto text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await addProjectToFavorites(data.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProjectSelect = () => {
|
||||
const { workspaces, currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||
: true;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"addNewWs",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const { options, value } = useMemo(() => {
|
||||
const projectOptions = workspaces
|
||||
.map((w): Workspace & { isFavorite: boolean } => ({
|
||||
...w,
|
||||
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
||||
}))
|
||||
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite));
|
||||
|
||||
const currentOption = projectOptions.find((option) => option.id === currentWorkspace?.id);
|
||||
|
||||
if (!currentOption) {
|
||||
return {
|
||||
options: projectOptions,
|
||||
value: null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
options: [
|
||||
currentOption,
|
||||
...projectOptions.filter((option) => option.id !== currentOption.id)
|
||||
],
|
||||
value: currentOption
|
||||
};
|
||||
}, [workspaces, projectFavorites, currentWorkspace]);
|
||||
|
||||
return (
|
||||
<div className="mt-5 mb-4 w-full p-3">
|
||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">Project</p>
|
||||
<FilterableSelect
|
||||
className="text-sm"
|
||||
value={value}
|
||||
filterOption={(option, inputValue) =>
|
||||
option.data.name.toLowerCase().includes(inputValue.toLowerCase())
|
||||
}
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
onChange={(newValue) => {
|
||||
// hacky use of null as indication to create project
|
||||
if (!newValue) {
|
||||
if (isAddingProjectsAllowed) {
|
||||
handlePopUpOpen("addNewWs");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const project = newValue as TWorkspaceWithFaveProp;
|
||||
localStorage.setItem("projectData.id", project.id);
|
||||
// todo(akhi): 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/${project.id}/secrets/overview`);
|
||||
}}
|
||||
options={options}
|
||||
components={{
|
||||
Option: ProjectOption,
|
||||
Menu: ProjectsMenu
|
||||
}}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You have exceeded the number of projects allowed on the free plan."
|
||||
/>
|
||||
|
||||
<NewProjectModal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export * from "./ProjectSelect";
|
@@ -181,7 +181,7 @@ export default function BitBucketCreateIntegrationPage() {
|
||||
onChange={onChange}
|
||||
options={currentWorkspace?.environments}
|
||||
placeholder="Select a project environment"
|
||||
isDisabled={!bitbucketWorkspaces?.length}
|
||||
isDisabled={!currentWorkspace?.environments.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
138
frontend/src/pages/integrations/octopus-deploy/authorize.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
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;
|
444
frontend/src/pages/integrations/octopus-deploy/create.tsx
Normal file
@@ -0,0 +1,444 @@
|
||||
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;
|
@@ -55,6 +55,8 @@ 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";
|
||||
@@ -104,6 +106,16 @@ 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,4 +1,5 @@
|
||||
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||
import { OctopusDeployScopeValues } from "@app/views/IntegrationsPage/IntegrationDetailsPage/components/OctopusDeployScopeValues";
|
||||
|
||||
type Props = {
|
||||
integration: TIntegrationWithEnv;
|
||||
@@ -27,6 +28,7 @@ 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;
|
||||
@@ -35,6 +37,9 @@ 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";
|
||||
@@ -51,7 +56,7 @@ export const IntegrationSettingsSection = ({ integration }: Props) => {
|
||||
}
|
||||
|
||||
if (key === "githubVisibilityRepoIds") {
|
||||
return value.join(", ");
|
||||
return (value as string[]).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,90 @@
|
||||
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,6 +140,9 @@ 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;
|
||||
}
|
||||
|
@@ -76,6 +76,14 @@ export const ConfiguredIntegrationItem = ({
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</div>
|
||||
</div>
|
||||
{integration.integration === "octopus-deploy" && (
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Space" />
|
||||
<div className="overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "qovery" && (
|
||||
<div className="flex flex-row">
|
||||
<div className="ml-2 flex flex-col">
|
||||
@@ -108,6 +116,7 @@ export const ConfiguredIntegrationItem = ({
|
||||
(integration.integration === "qovery" && integration?.scope) ||
|
||||
(integration.integration === "circleci" && "Project") ||
|
||||
(integration.integration === "bitbucket" && "Repository") ||
|
||||
(integration.integration === "octopus-deploy" && "Project") ||
|
||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
|
||||
(integration?.integration === "terraform-cloud" && "Project") ||
|
||||
|
@@ -12,6 +12,7 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem
|
||||
@@ -64,7 +65,7 @@ export const LogsFilter = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (workspacesInOrg.length) {
|
||||
setValue("projectId", workspacesInOrg[0].id);
|
||||
setValue("project", workspacesInOrg[0]);
|
||||
}
|
||||
}, [workspaces]);
|
||||
|
||||
@@ -111,11 +112,34 @@ export const LogsFilter = ({
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"sticky top-20 z-10 flex items-center justify-between bg-bunker-800",
|
||||
"sticky top-20 z-10 flex flex-wrap items-center justify-between bg-bunker-800",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="project"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-12 w-64"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select a project..."
|
||||
options={workspacesInOrg.map(({ name, id }) => ({ name, id }))}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-1 flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="eventType"
|
||||
@@ -123,7 +147,7 @@ export const LogsFilter = ({
|
||||
<FormControl label="Events">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
<div className="inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{selectedEventTypes?.length === 1
|
||||
? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0])
|
||||
?.label
|
||||
@@ -235,37 +259,6 @@ export const LogsFilter = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectId"
|
||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className={twMerge(
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 ",
|
||||
value === undefined && "text-mineshaft-400"
|
||||
)}
|
||||
>
|
||||
{workspacesInOrg.map((project) => (
|
||||
<SelectItem value={String(project.id || "")} key={project.id}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
@@ -275,6 +268,7 @@ export const LogsFilter = ({
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
@@ -294,6 +288,7 @@ export const LogsFilter = ({
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isEndDatePickerOpen,
|
||||
onOpenChange: setIsEndDatePickerOpen
|
||||
@@ -304,27 +299,27 @@ export const LogsFilter = ({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
isLoading={false}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mt-[0.45rem]"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
|
||||
onClick={() =>
|
||||
reset({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId,
|
||||
userAgentType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
project: null
|
||||
})
|
||||
}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={false}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mt-1.5"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
|
||||
onClick={() =>
|
||||
reset({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId,
|
||||
userAgentType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
projectId: undefined
|
||||
})
|
||||
}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -47,7 +47,7 @@ export const LogsSection = ({
|
||||
const { control, reset, watch, setValue } = useForm<AuditLogFilterFormData>({
|
||||
resolver: yupResolver(auditLogFilterFormSchema),
|
||||
defaultValues: {
|
||||
projectId: undefined,
|
||||
project: null,
|
||||
actor: presets?.actorId,
|
||||
eventType: presets?.eventType || [],
|
||||
page: 1,
|
||||
@@ -66,7 +66,7 @@ export const LogsSection = ({
|
||||
const eventType = watch("eventType") as EventType[] | undefined;
|
||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
||||
const actor = watch("actor");
|
||||
const projectId = watch("projectId");
|
||||
const projectId = watch("project")?.id;
|
||||
|
||||
const startDate = watch("startDate");
|
||||
const endDate = watch("endDate");
|
||||
|
@@ -5,7 +5,7 @@ import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
export const auditLogFilterFormSchema = yup
|
||||
.object({
|
||||
eventMetadata: yup.object({}).optional(),
|
||||
projectId: yup.string().optional(),
|
||||
project: yup.object({ id: yup.string().required(), name: yup.string().required() }).nullable(),
|
||||
eventType: yup.array(yup.string().oneOf(Object.values(EventType), "Invalid event type")),
|
||||
actor: yup.string(),
|
||||
userAgentType: yup.string().oneOf(Object.values(UserAgentType), "Invalid user agent type"),
|
||||
|
@@ -4,7 +4,14 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useAddIdentityToWorkspace,
|
||||
@@ -16,8 +23,8 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
projectId: z.string(),
|
||||
role: z.string()
|
||||
project: z.object({ name: z.string(), id: z.string() }),
|
||||
role: z.object({ name: z.string(), slug: z.string() })
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -32,7 +39,9 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle }: Props) => {
|
||||
// TODO: eventually refactor to support adding to multiple projects at once? would lose role granularity unique to project
|
||||
|
||||
const Content = ({ identityId, handlePopUpToggle }: Omit<Props, "popUp">) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { workspaces } = useWorkspace();
|
||||
const { mutateAsync: addIdentityToWorkspace } = useAddIdentityToWorkspace();
|
||||
@@ -47,10 +56,10 @@ export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const projectId = watch("projectId");
|
||||
const projectId = watch("project")?.id;
|
||||
const { data: projectMemberships } = useGetIdentityProjectMemberships(identityId);
|
||||
const { data: project } = useGetWorkspaceById(projectId);
|
||||
const { data: roles } = useGetProjectRoles(project?.id ?? "");
|
||||
const { data: project, isLoading: isProjectLoading } = useGetWorkspaceById(projectId);
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(project?.id ?? "");
|
||||
|
||||
const filteredWorkspaces = useMemo(() => {
|
||||
const wsWorkspaceIds = new Map();
|
||||
@@ -64,12 +73,12 @@ export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle
|
||||
);
|
||||
}, [workspaces, projectMemberships]);
|
||||
|
||||
const onFormSubmit = async ({ projectId: workspaceId, role }: FormData) => {
|
||||
const onFormSubmit = async ({ project: selectedProject, role }: FormData) => {
|
||||
try {
|
||||
await addIdentityToWorkspace({
|
||||
workspaceId,
|
||||
workspaceId: selectedProject.id,
|
||||
identityId,
|
||||
role: role || undefined
|
||||
role: role.slug || undefined
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@@ -91,87 +100,85 @@ export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle
|
||||
}
|
||||
};
|
||||
|
||||
const isProjectSelected = Boolean(projectId);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="project"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={filteredWorkspaces}
|
||||
placeholder="Select project..."
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
isLoading={isProjectSelected && isProjectLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Role"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<FilterableSelect
|
||||
isDisabled={!isProjectSelected}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={roles}
|
||||
isLoading={isProjectSelected && isRolesLoading}
|
||||
placeholder="Select role..."
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle }: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addIdentityToProject?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addIdentityToProject", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Add Identity to Project">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(filteredWorkspaces || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`project-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Role"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(roles || []).map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={`project-role-${slug}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("addIdentityToProject", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<ModalContent bodyClassName="overflow-visible" title="Add Identity to Project">
|
||||
<Content identityId={identityId} handlePopUpToggle={handlePopUpToggle} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
@@ -42,7 +42,7 @@ export const IdentitySection = withPermission(
|
||||
? subscription.identitiesUsed < subscription.identityLimit
|
||||
: true;
|
||||
|
||||
const isEnterprise = subscription?.slug === "enterprise"
|
||||
const isEnterprise = subscription?.slug === "enterprise";
|
||||
|
||||
const onDeleteIdentitySubmit = async (identityId: string) => {
|
||||
try {
|
||||
@@ -105,7 +105,7 @@ export const IdentitySection = withPermission(
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create identity
|
||||
Create Identity
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
|
@@ -1,28 +1,18 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faExclamationCircle
|
||||
} 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,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea,
|
||||
Tooltip
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { isCustomOrgRole } from "@app/helpers/roles";
|
||||
@@ -44,7 +34,16 @@ const EmailSchema = z.string().email().min(1).trim().toLowerCase();
|
||||
|
||||
const addMemberFormSchema = z.object({
|
||||
emails: z.string().min(1).trim().toLowerCase(),
|
||||
projectIds: z.array(z.string().min(1).trim().toLowerCase()).default([]),
|
||||
projects: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
version: z.nativeEnum(ProjectVersion)
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
|
||||
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG)
|
||||
});
|
||||
@@ -72,7 +71,7 @@ export const AddOrgMemberModal = ({
|
||||
const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? "");
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
|
||||
const { data: projects } = useGetUserWorkspaces(true);
|
||||
const { data: projects, isLoading: isProjectsLoading } = useGetUserWorkspaces(true);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -95,18 +94,14 @@ export const AddOrgMemberModal = ({
|
||||
}
|
||||
}, [organizationRoles]);
|
||||
|
||||
const selectedProjectIds = watch("projectIds", []);
|
||||
|
||||
const onAddMembers = async ({
|
||||
emails,
|
||||
organizationRoleSlug,
|
||||
projectIds,
|
||||
projects: selectedProjects,
|
||||
projectRoleSlug
|
||||
}: TAddMemberForm) => {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
const selectedProjects = projects?.filter((project) => projectIds.includes(String(project.id)));
|
||||
|
||||
if (selectedProjects?.length) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const project of selectedProjects) {
|
||||
@@ -144,7 +139,7 @@ export const AddOrgMemberModal = ({
|
||||
organizationId: currentOrg?.id,
|
||||
inviteeEmails: emails.split(",").map((email) => email.trim()),
|
||||
organizationRoleSlug,
|
||||
projects: projectIds.map((id) => ({ id, projectRoleSlug: [projectRoleSlug] }))
|
||||
projects: selectedProjects.map(({ id }) => ({ id, projectRoleSlug: [projectRoleSlug] }))
|
||||
});
|
||||
|
||||
setCompleteInviteLinks(data?.completeInviteLinks ?? null);
|
||||
@@ -182,6 +177,7 @@ export const AddOrgMemberModal = ({
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
title={`Invite others to ${currentOrg?.name}`}
|
||||
subTitle={
|
||||
<div>
|
||||
@@ -236,98 +232,33 @@ export const AddOrgMemberModal = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
name="projects"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Assign users to projects (optional)"
|
||||
label="Assign users to projects"
|
||||
isOptional
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{projects && projects.length > 0 ? (
|
||||
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{selectedProjectIds.length === 1
|
||||
? projects.find((project) => project.id === selectedProjectIds[0])
|
||||
?.name
|
||||
: selectedProjectIds.length === 0
|
||||
? "No projects selected"
|
||||
: `${selectedProjectIds.length} projects selected`}
|
||||
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
No projects found
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="thin-scrollbar z-[100] max-h-80"
|
||||
>
|
||||
{projects && projects.length > 0 ? (
|
||||
projects.map((project) => {
|
||||
const isSelected = selectedProjectIds.includes(String(project.id));
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) =>
|
||||
projects.length > 1 && event.preventDefault()
|
||||
}
|
||||
onClick={() => {
|
||||
if (selectedProjectIds.includes(String(project.id))) {
|
||||
field.onChange(
|
||||
selectedProjectIds.filter(
|
||||
(projectId: string) => projectId !== String(project.id)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([...selectedProjectIds, String(project.id)]);
|
||||
}
|
||||
}}
|
||||
key={`project-id-${project.id}`}
|
||||
icon={
|
||||
isSelected ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="pr-0.5 text-primary"
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-[1.01rem]" />
|
||||
)
|
||||
}
|
||||
iconPos="left"
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
<div className="flex items-center gap-2 capitalize">
|
||||
{project.name}
|
||||
{project.version !== ProjectVersion.V3 && (
|
||||
<Tooltip content="Project is not compatible with this action, please upgrade this project.">
|
||||
<FontAwesomeIcon
|
||||
icon={faExclamationCircle}
|
||||
className="text-xs opacity-50"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<FilterableSelect
|
||||
isMulti
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isLoading={isProjectsLoading}
|
||||
getOptionLabel={(project) => project.name}
|
||||
getOptionValue={(project) => project.id}
|
||||
options={projects}
|
||||
placeholder="Select projects..."
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-fit justify-end">
|
||||
<div className="mt-[0.15rem] flex min-w-fit justify-end">
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectRoleSlug"
|
||||
@@ -340,7 +271,7 @@ export const AddOrgMemberModal = ({
|
||||
>
|
||||
<div>
|
||||
<Select
|
||||
isDisabled={selectedProjectIds.length === 0}
|
||||
isDisabled={watch("projects", []).length === 0}
|
||||
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
|
||||
{...field}
|
||||
onValueChange={(val) => field.onChange(val)}
|
||||
|
@@ -181,7 +181,7 @@ export const IdentityTab = withProjectPermission(
|
||||
onClick={() => handlePopUpOpen("identity")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add identity
|
||||
Add Identity
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
@@ -1,12 +1,19 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Link from "next/link";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||
import { ComboBox } from "@app/components/v2/ComboBox";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Spinner
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useAddIdentityToWorkspace,
|
||||
@@ -16,37 +23,30 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
identity: yup.object({
|
||||
id: yup.string().required("Identity id is required"),
|
||||
name: yup.string().required("Identity name is required")
|
||||
}),
|
||||
role: yup.object({
|
||||
slug: yup.string().required("role slug is required"),
|
||||
name: yup.string().required("role name is required")
|
||||
})
|
||||
})
|
||||
.required();
|
||||
const schema = z.object({
|
||||
identity: z.object({ name: z.string(), id: z.string() }),
|
||||
role: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["identity"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const Content = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const organizationId = currentOrg?.id || "";
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
|
||||
const { data: identityMembershipOrgsData } = useGetIdentityMembershipOrgs({
|
||||
organizationId,
|
||||
limit: 20000 // TODO: this is temp to preserve functionality for larger projects, will replace with combobox in separate PR
|
||||
});
|
||||
const { data: identityMembershipOrgsData, isLoading: isMembershipsLoading } =
|
||||
useGetIdentityMembershipOrgs({
|
||||
organizationId,
|
||||
limit: 20000 // TODO: this is temp to preserve functionality for larger projects, will replace with combobox in separate PR
|
||||
});
|
||||
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships;
|
||||
const { data: identityMembershipsData } = useGetWorkspaceIdentityMemberships({
|
||||
workspaceId,
|
||||
@@ -54,11 +54,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
const identityMemberships = identityMembershipsData?.identityMemberships;
|
||||
|
||||
const {
|
||||
data: roles,
|
||||
isLoading: isRolesLoading,
|
||||
isFetched: isRolesFetched
|
||||
} = useGetProjectRoles(workspaceId);
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||
|
||||
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
||||
|
||||
@@ -76,18 +72,11 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema)
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRolesFetched || !roles) return;
|
||||
|
||||
setValue("role", { name: roles[0]?.name, slug: roles[0]?.slug });
|
||||
}, [isRolesFetched, roles]);
|
||||
|
||||
const onFormSubmit = async ({ identity, role }: FormData) => {
|
||||
try {
|
||||
await addIdentityToWorkspaceMutateAsync({
|
||||
@@ -125,104 +114,93 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (isMembershipsLoading || isRolesLoading)
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center py-10">
|
||||
<Spinner className="text-mineshaft-400" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return filteredIdentityMembershipOrgs.length ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="identity"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select identity..."
|
||||
options={filteredIdentityMembershipOrgs.map((membership) => membership.identity)}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Role"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={roles}
|
||||
placeholder="Select role..."
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.identity?.data ? "Update" : "Add"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="text-sm">
|
||||
All identities in your organization have already been added to this project.
|
||||
</div>
|
||||
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
||||
<Button isDisabled={isRolesLoading} isLoading={isRolesLoading} variant="outline_bg">
|
||||
Create a new identity
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.identity?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("identity", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Add Identity to Project" bodyClassName="overflow-visible">
|
||||
{filteredIdentityMembershipOrgs.length ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="identity"
|
||||
defaultValue={{
|
||||
id: filteredIdentityMembershipOrgs?.[0]?.identity?.id,
|
||||
name: filteredIdentityMembershipOrgs?.[0]?.identity?.name
|
||||
}}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||
<ComboBox
|
||||
className="w-full"
|
||||
by="id"
|
||||
value={{ id: field.value.id, name: field.value.name }}
|
||||
defaultValue={{ id: field.value.id, name: field.value.name }}
|
||||
onSelectChange={(value) => onChange({ id: value.id, name: value.name })}
|
||||
displayValue={(el) => el.name}
|
||||
onFilter={({ value }, filterQuery) =>
|
||||
value.name.toLowerCase().includes(filterQuery.toLowerCase())
|
||||
}
|
||||
items={filteredIdentityMembershipOrgs.map(({ identity }) => ({
|
||||
key: identity.id,
|
||||
value: { id: identity.id, name: identity.name },
|
||||
label: identity.name
|
||||
}))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
defaultValue={{ name: "", slug: "" }}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Role"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<ComboBox
|
||||
className="w-full"
|
||||
by="slug"
|
||||
value={{ slug: field.value.slug, name: field.value.name }}
|
||||
defaultValue={{ slug: field.value.slug, name: field.value.name }}
|
||||
onSelectChange={(value) => onChange({ slug: value.slug, name: value.name })}
|
||||
displayValue={(el) => el.name}
|
||||
onFilter={({ value }, filterQuery) =>
|
||||
value.name.toLowerCase().includes(filterQuery.toLowerCase())
|
||||
}
|
||||
items={(roles || []).map(({ slug, name }) => ({
|
||||
key: slug,
|
||||
value: { slug, name },
|
||||
label: name
|
||||
}))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.identity?.data ? "Update" : "Create"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="text-sm">
|
||||
All identities in your organization have already been added to this project.
|
||||
</div>
|
||||
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
||||
<Button isDisabled={isRolesLoading} isLoading={isRolesLoading} variant="outline_bg">
|
||||
Create a new identity
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<Content popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|