This commit is contained in:
Tuan Dang
2023-07-24 01:54:09 +07:00
22 changed files with 426 additions and 29 deletions

View File

@ -3,12 +3,15 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets";
import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { IServiceTokenData } from "../../models";
import { requireWorkspaceAuth } from "../../middleware";
import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables";
/**
* Return secrets for workspace with id [workspaceId] and environment
@ -17,11 +20,31 @@ import { BadRequestError } from "../../utils/errors";
* @param res
*/
export const getSecretsRaw = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
let workspaceId = req.query.workspaceId as string;
let environment = req.query.environment as string;
let secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
// if the service token has single scope, it will get all secrets for that scope by default
const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData
if (serviceTokenDetails) {
if (serviceTokenDetails.scopes.length == 1 && !containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)) {
const scope = serviceTokenDetails.scopes[0]
secretPath = scope.secretPath
environment = scope.environment
workspaceId = serviceTokenDetails.workspace.toString()
} else {
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true
})
}
}
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,

View File

@ -44,6 +44,7 @@ import { EELogService, EESecretService } from "../ee/services";
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
import { getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch";
import path from "path";
export const isValidScope = (
authPayload: IServiceTokenData,
@ -60,6 +61,13 @@ export const isValidScope = (
return Boolean(validScope);
};
export function containsGlobPatterns(secretPath: string) {
const globChars = ["*", "?", "[", "]", "{", "}", "**"];
const normalizedPath = path.normalize(secretPath);
return globChars.some(char => normalizedPath.includes(char));
}
/**
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
*

View File

@ -14,6 +14,8 @@ import {
INTEGRATION_CLOUD_66_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL,
INTEGRATION_DIGITAL_OCEAN_API_URL,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_GITHUB,
@ -164,6 +166,11 @@ const getApps = async ({
accessToken,
});
break;
case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM:
apps = await getAppsDigitalOceanAppPlatform({
accessToken
});
break;
case INTEGRATION_CLOUD_66:
apps = await getAppsCloud66({
accessToken,
@ -849,6 +856,48 @@ const getAppsCodefresh = async ({
};
/**
* Return list of applications for DigitalOcean App Platform integration
* @param {Object} obj
* @param {String} obj.accessToken - personal access token for DigitalOcean
* @returns {Object[]} apps - names of DigitalOcean apps
* @returns {String} apps.name - name of DigitalOcean app
* @returns {String} apps.appId - id of DigitalOcean app
*/
const getAppsDigitalOceanAppPlatform = async ({ accessToken }: { accessToken: string }) => {
interface DigitalOceanApp {
id: string;
owner_uuid: string;
spec: Spec;
}
interface Spec {
name: string;
region: string;
envs: Env[];
}
interface Env {
key: string;
value: string;
scope: string;
}
const res = (
await standardRequest.get(`${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
})
).data;
return (res.apps ?? []).map((a: DigitalOceanApp) => ({
name: a.spec.name,
appId: a.id
}));
}
/**
* Return list of applications for Cloud66 integration
* @param {Object} obj

View File

@ -22,6 +22,8 @@ import {
INTEGRATION_CLOUD_66_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL,
INTEGRATION_DIGITAL_OCEAN_API_URL,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_GITHUB,
@ -222,6 +224,13 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM:
await syncSecretsDigitalOceanAppPlatform({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_CLOUD_66:
await syncSecretsCloud66({
integration,
@ -2111,6 +2120,40 @@ const syncSecretsCodefresh = async ({
);
};
/**
* Sync/push [secrets] to DigitalOcean App Platform application with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - personal access token for DigitalOcean
*/
const syncSecretsDigitalOceanAppPlatform = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
await standardRequest.put(
`${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps/${integration.appId}`,
{
spec: {
name: integration.app,
envs: Object.entries(secrets).map(([key, value]) => ({ key, value }))
}
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
/**
* Sync/push [secrets] to Cloud66 application with name [integration.app]
* @param {Object} obj
@ -2128,6 +2171,7 @@ const syncSecretsCloud66 = async ({
secrets: any;
accessToken: string;
}) => {
interface Cloud66Secret {
id: number;
key: string;

View File

@ -8,6 +8,7 @@ import {
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
@ -60,6 +61,7 @@ export interface IIntegration {
| "cloudflare-pages"
| "bitbucket"
| "codefresh"
| "digital-ocean-app-platform"
| "cloud-66"
integrationAuth: Types.ObjectId;
}
@ -151,6 +153,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66,
],

View File

@ -10,6 +10,7 @@ import {
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
@ -47,6 +48,7 @@ export interface IIntegrationAuth extends Document {
| "checkly"
| "cloudflare-pages"
| "codefresh"
| "digital-ocean-app-platform"
| "bitbucket"
| "cloud-66";
teamId: string;
@ -95,6 +97,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66,
],

View File

@ -18,8 +18,8 @@ import {
router.get(
"/raw",
query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(),
query("workspaceId").optional().isString().trim(),
query("environment").optional().isString().trim(),
query("secretPath").default("/").isString().trim(),
query("include_imports").optional().isBoolean().default(false),
validateRequest,
@ -31,14 +31,6 @@ router.get(
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true
}),
secretsController.getSecretsRaw
);

View File

@ -29,6 +29,7 @@ export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
export const INTEGRATION_BITBUCKET = "bitbucket";
export const INTEGRATION_CODEFRESH = "codefresh";
export const INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform";
export const INTEGRATION_CLOUD_66 = "cloud-66";
export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
@ -47,6 +48,7 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66
]);
@ -81,6 +83,7 @@ export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com";
export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com";
export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org";
export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api";
export const INTEGRATION_DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com";
export const INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api";
export const getIntegrationOptions = async () => {
@ -275,6 +278,15 @@ export const getIntegrationOptions = async () => {
clientId: "",
docsLink: "",
},
{
name: "Digital Ocean App Platform",
slug: "digital-ocean-app-platform",
image: "Digital Ocean.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
{
name: "Cloud 66",
slug: "cloud-66",

View File

@ -7,8 +7,7 @@ in plaintext. Effectively, this means each such secret operation only requires 1
<AccordionGroup>
<Accordion title="Retrieve secrets">
Retrieve all secrets for an Infisical project and environment.
Retrieve all secrets for an Infisical project and environment.
<Tabs>
<Tab title="cURL">
```bash
@ -18,7 +17,12 @@ in plaintext. Effectively, this means each such secret operation only requires 1
```
</Tab>
</Tabs>
####
<Info>
When using a [service token](../../../documentation/platform/token) with access to a single environment and path, you don't need to provide request parameters because the server will automatically scope the request to the defined environment/secrets path of the service token used.
For all other cases, request parameters are required.
</Info>
####
<ParamField query="workspaceId" type="string" required>
The ID of the workspace
</ParamField>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1,39 @@
---
title: "Digital Ocean App Platform"
description: "How to sync secrets from Infisical to Digital Ocean App Platform"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Get your Digital Ocean Personal Access Tokens
On Digital Ocean dashboard, navigate to **API > Tokens** and click on "Generate New Token"
![integrations digital ocean dashboard](../../images/integrations-do-dashboard.png)
Name it **infisical**, choose **No expiry**, and make sure to check **Write (optional)**. Then click on "Generate Token" and copy your API token.
![integrations digital ocean token modal](../../images/integrations-do-token-modal.png)
## Navigate to your project's integrations tab
Click on the **Digital Ocean App Platform** tile and enter your API token to grant Infisical access to your Digital Ocean account.
![integrations](../../images/integrations.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
Then enter your Digital Ocean Personal Access Token here. Then click "Connect to Digital Ocean App Platform".
![integrations infisical dashboard digital ocean integration](../../images/integrations-do-enter-token.png)
## Start integration
Select which Infisical environment secrets you want to sync to which Digital Ocean App and click "Create Integration".
![integrations digital ocean select projects](../../images/integrations-do-select-projects.png)
Done!
![integrations digital ocean integration success](../../images/integrations-do-success.png)

View File

@ -208,6 +208,12 @@
"integrations/cloud/aws-secret-manager"
]
},
{
"group": "Digital Ocean",
"pages": [
"integrations/cloud/digital-ocean-app-platform"
]
},
"integrations/cloud/heroku",
"integrations/cloud/vercel",
"integrations/cloud/netlify",

View File

@ -22,6 +22,7 @@ const integrationSlugNameMapping: Mapping = {
"hashicorp-vault": "Vault",
"cloudflare-pages": "Cloudflare Pages",
"codefresh": "Codefresh",
"digital-ocean-app-platform": "Digital Ocean App Platform",
bitbucket: "BitBucket",
"cloud-66": "Cloud 66"
};

View File

@ -0,0 +1,64 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function DigitalOceanAppPlatformCreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("API Key cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem("projectData.id"),
integration: "digital-ocean-app-platform",
accessId: null,
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);
router.push(`/integrations/digital-ocean-app-platform/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Digital Ocean App Platform Integration</CardTitle>
<FormControl
label="Digital Ocean API Key"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to Digital Ocean App Platform
</Button>
</Card>
</div>
);
}
DigitalOceanAppPlatformCreateIntegrationPage.requireAuth = true;

View File

@ -0,0 +1,155 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function DigitalOceanAppPlatformCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetApp, setTargetApp] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId:
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Digital Ocean App Platform Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Digital Ocean App Platform Service" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No services found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
DigitalOceanAppPlatformCreateIntegrationPage.requireAuth = true;

View File

@ -98,6 +98,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "codefresh":
link = `${window.location.origin}/integrations/codefresh/authorize`;
break;
case "digital-ocean-app-platform":
link = `${window.location.origin}/integrations/digital-ocean-app-platform/authorize`;
break;
case "cloud-66":
link = `${window.location.origin}/integrations/cloud-66/authorize`;
break;

View File

@ -67,18 +67,9 @@ export const CloudIntegrationSection = ({
width={70}
alt="integration logo"
/>
{cloudIntegration.name.split(" ").length > 2 ? (
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
<div>{cloudIntegration.name.split(" ")[0]}</div>
<div className="text-base">
{cloudIntegration.name.split(" ")[1]} {cloudIntegration.name.split(" ")[2]}
</div>
</div>
) : (
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegration.name}
</div>
)}
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegration.name}
</div>
{cloudIntegration.isAvailable &&
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
<div className="absolute top-0 right-0 z-40 h-full">