Merge branch 'main' into snyk-upgrade-ba8f3acf185100a451cbbadcbe68f789

This commit is contained in:
Maidul Islam
2024-03-09 11:05:36 -05:00
committed by GitHub
76 changed files with 1955 additions and 617 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# Required # Required
DB_CONNECTION_URI=postgres://infisical:infisical@db:5432/infisical DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
# JWT # JWT
# Required secrets to sign JWT tokens # Required secrets to sign JWT tokens
+190
View File
@@ -0,0 +1,190 @@
# inspired by https://www.photoroom.com/inside-photoroom/how-we-automated-our-changelog-thanks-to-chatgpt
import os
import requests
import re
from openai import OpenAI
import subprocess
from datetime import datetime
import uuid
# Constants
REPO_OWNER = "infisical"
REPO_NAME = "infisical"
TOKEN = os.environ["GITHUB_TOKEN"]
SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
SLACK_MSG_COLOR = "#36a64f"
headers = {
"Authorization": f"Bearer {TOKEN}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
def set_multiline_output(name, value):
with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
delimiter = uuid.uuid1()
print(f'{name}<<{delimiter}', file=fh)
print(value, file=fh)
print(delimiter, file=fh)
def post_changelog_to_slack(changelog, tag):
slack_payload = {
"text": "Hey team, it's changelog time! :wave:",
"attachments": [
{
"color": SLACK_MSG_COLOR,
"title": f"🗓️Infisical Changelog - {tag}",
"text": changelog,
}
],
}
response = requests.post(SLACK_WEBHOOK_URL, json=slack_payload)
if response.status_code != 200:
raise Exception("Failed to post changelog to Slack.")
def find_previous_release_tag(release_tag:str):
previous_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0", f"{release_tag}^"]).decode("utf-8").strip()
while not(previous_tag.startswith("infisical/")):
previous_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0", f"{previous_tag}^"]).decode("utf-8").strip()
return previous_tag
def get_tag_creation_date(tag_name):
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/git/refs/tags/{tag_name}"
response = requests.get(url, headers=headers)
response.raise_for_status()
commit_sha = response.json()['object']['sha']
commit_url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/commits/{commit_sha}"
commit_response = requests.get(commit_url, headers=headers)
commit_response.raise_for_status()
creation_date = commit_response.json()['commit']['author']['date']
return datetime.strptime(creation_date, '%Y-%m-%dT%H:%M:%SZ')
def fetch_prs_between_tags(previous_tag_date:datetime, release_tag_date:datetime):
# Use GitHub API to fetch PRs merged between the commits
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/pulls?state=closed&merged=true"
response = requests.get(url, headers=headers)
if response.status_code != 200:
raise Exception("Error fetching PRs from GitHub API!")
prs = []
for pr in response.json():
# the idea is as tags happen recently we get last 100 closed PRs and then filter by tag creation date
if pr["merged_at"] and datetime.strptime(pr["merged_at"],'%Y-%m-%dT%H:%M:%SZ') < release_tag_date and datetime.strptime(pr["merged_at"],'%Y-%m-%dT%H:%M:%SZ') > previous_tag_date:
prs.append(pr)
return prs
def extract_commit_details_from_prs(prs):
commit_details = []
for pr in prs:
commit_message = pr["title"]
commit_url = pr["html_url"]
pr_number = pr["number"]
branch_name = pr["head"]["ref"]
issue_numbers = re.findall(r"(www-\d+|web-\d+)", branch_name)
# If no issue numbers are found, add the PR details without issue numbers and URLs
if not issue_numbers:
commit_details.append(
{
"message": commit_message,
"pr_number": pr_number,
"pr_url": commit_url,
"issue_number": None,
"issue_url": None,
}
)
continue
for issue in issue_numbers:
commit_details.append(
{
"message": commit_message,
"pr_number": pr_number,
"pr_url": commit_url,
"issue_number": issue,
}
)
return commit_details
# Function to generate changelog using OpenAI
def generate_changelog_with_openai(commit_details):
commit_messages = []
for details in commit_details:
base_message = f"{details['pr_url']} - {details['message']}"
# Add the issue URL if available
# if details["issue_url"]:
# base_message += f" (Linear Issue: {details['issue_url']})"
commit_messages.append(base_message)
commit_list = "\n".join(commit_messages)
prompt = """
Generate a changelog for Infisical, opensource secretops
The changelog should:
1. Be Informative: Using the provided list of GitHub commits, break them down into categories such as Features, Fixes & Improvements, and Technical Updates. Summarize each commit concisely, ensuring the key points are highlighted.
2. Have a Professional yet Friendly tone: The tone should be balanced, not too corporate or too informal.
3. Celebratory Introduction and Conclusion: Start the changelog with a celebratory note acknowledging the team's hard work and progress. End with a shoutout to the team and wishes for a pleasant weekend.
4. Formatting: you cannot use Markdown formatting, and you can only use emojis for the introductory paragraph or the conclusion paragraph, nowhere else.
5. Links: the syntax to create links is the following: `<http://www.example.com|This message is a link>`.
6. Linear Links: note that the Linear link is optional, include it only if provided.
7. Do not wrap your answer in a codeblock. Just output the text, nothing else
Here's a good example to follow, please try to match the formatting as closely as possible, only changing the content of the changelog and have some liberty with the introduction. Notice the importance of the formatting of a changelog item:
- <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>))
And here's an example of the full changelog:
*Features*
• <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>)
*Fixes & Improvements*
• <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>)
*Technical Updates*
• <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>)
Stay tuned for more exciting updates coming soon!
And here are the commits:
{}
""".format(
commit_list
)
client = OpenAI(api_key=OPENAI_API_KEY)
messages = [{"role": "user", "content": prompt}]
response = client.chat.completions.create(model="gpt-3.5-turbo", messages=messages)
if "error" in response.choices[0].message:
raise Exception("Error generating changelog with OpenAI!")
return response.choices[0].message.content.strip()
if __name__ == "__main__":
try:
# Get the latest and previous release tags
latest_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"]).decode("utf-8").strip()
previous_tag = find_previous_release_tag(latest_tag)
latest_tag_date = get_tag_creation_date(latest_tag)
previous_tag_date = get_tag_creation_date(previous_tag)
prs = fetch_prs_between_tags(previous_tag_date,latest_tag_date)
pr_details = extract_commit_details_from_prs(prs)
# Generate changelog
changelog = generate_changelog_with_openai(pr_details)
post_changelog_to_slack(changelog,latest_tag)
# Print or post changelog to Slack
# set_multiline_output("changelog", changelog)
except Exception as e:
print(str(e))
@@ -0,0 +1,34 @@
name: Generate Changelog
permissions:
contents: write
on:
workflow_dispatch:
push:
tags:
- "infisical/v*.*.*-postgres"
jobs:
generate_changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12.0"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests openai
- name: Generate Changelog and Post to Slack
id: gen-changelog
run: python .github/resources/changelog-generator.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -23,6 +23,8 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- run: git fetch --force --tags - run: git fetch --force --tags
- run: echo "Ref name ${{github.ref_name}}" - run: echo "Ref name ${{github.ref_name}}"
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
+28 -4
View File
@@ -190,10 +190,34 @@ dockers:
- dockerfile: docker/alpine - dockerfile: docker/alpine
goos: linux goos: linux
goarch: amd64 goarch: amd64
use: buildx
ids: ids:
- all-other-builds - all-other-builds
image_templates: image_templates:
- "infisical/cli:{{ .Version }}" - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64"
- "infisical/cli:{{ .Major }}.{{ .Minor }}" - "infisical/cli:latest-amd64"
- "infisical/cli:{{ .Major }}" build_flag_templates:
- "infisical/cli:latest" - "--pull"
- "--platform=linux/amd64"
- dockerfile: docker/alpine
goos: linux
goarch: amd64
use: buildx
ids:
- all-other-builds
image_templates:
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64"
- "infisical/cli:latest-arm64"
build_flag_templates:
- "--pull"
- "--platform=linux/arm64"
docker_manifests:
- name_template: "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
image_templates:
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64"
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64"
- name_template: "infisical/cli:latest"
image_templates:
- "infisical/cli:latest-amd64"
- "infisical/cli:latest-arm64"
-20
View File
@@ -47,7 +47,6 @@
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"mysql2": "^3.9.1", "mysql2": "^3.9.1",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"ora": "^7.0.1", "ora": "^7.0.1",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
@@ -5706,14 +5705,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/cluster-key-slot": { "node_modules/cluster-key-slot": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@ -9258,17 +9249,6 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
}, },
"node_modules/node-cache": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
"dependencies": {
"clone": "2.x"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
-1
View File
@@ -108,7 +108,6 @@
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"mysql2": "^3.9.1", "mysql2": "^3.9.1",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"ora": "^7.0.1", "ora": "^7.0.1",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
+6 -1
View File
@@ -9,7 +9,12 @@ export async function seed(knex: Knex): Promise<void> {
await knex(TableName.Users).del(); await knex(TableName.Users).del();
await knex(TableName.UserEncryptionKey).del(); await knex(TableName.UserEncryptionKey).del();
await knex(TableName.SuperAdmin).del(); await knex(TableName.SuperAdmin).del();
await knex(TableName.SuperAdmin).insert([{ initialized: true, allowSignUp: true }]);
await knex(TableName.SuperAdmin).insert([
// eslint-disable-next-line
// @ts-ignore
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
]);
// Inserts seed entries // Inserts seed entries
const [user] = await knex(TableName.Users) const [user] = await knex(TableName.Users)
.insert([ .insert([
+5
View File
@@ -27,6 +27,7 @@ type TSAMLConfig = {
cert: string; cert: string;
audience: string; audience: string;
wantAuthnResponseSigned?: boolean; wantAuthnResponseSigned?: boolean;
wantAssertionsSigned?: boolean;
disableRequestedAuthnContext?: boolean; disableRequestedAuthnContext?: boolean;
}; };
@@ -82,6 +83,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
samlConfig.audience = `spn:${ssoConfig.issuer}`; samlConfig.audience = `spn:${ssoConfig.issuer}`;
} }
} }
if (ssoConfig.authProvider === SamlProviders.GOOGLE_SAML) {
samlConfig.wantAssertionsSigned = false;
}
(req as unknown as FastifyRequest).ssoConfig = ssoConfig; (req as unknown as FastifyRequest).ssoConfig = ssoConfig;
done(null, samlConfig); done(null, samlConfig);
} catch (error) { } catch (error) {
@@ -24,7 +24,7 @@ export const auditLogQueueServiceFactory = ({
const pushToLog = async (data: TCreateAuditLogDTO) => { const pushToLog = async (data: TCreateAuditLogDTO) => {
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, { await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
removeOnFail: { removeOnFail: {
count: 5 count: 3
}, },
removeOnComplete: true removeOnComplete: true
}); });
@@ -46,6 +46,7 @@ export const auditLogQueueServiceFactory = ({
const ttl = plan.auditLogsRetentionDays * MS_IN_DAY; const ttl = plan.auditLogsRetentionDays * MS_IN_DAY;
// skip inserting if audit log retention is 0 meaning its not supported // skip inserting if audit log retention is 0 meaning its not supported
if (ttl === 0) return; if (ttl === 0) return;
await auditLogDAL.create({ await auditLogDAL.create({
actor: actor.type, actor: actor.type,
actorMetadata: actor.metadata, actorMetadata: actor.metadata,
@@ -5,8 +5,8 @@
// TODO(akhilmhdh): With tony find out the api structure and fill it here // TODO(akhilmhdh): With tony find out the api structure and fill it here
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import NodeCache from "node-cache";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -39,6 +39,7 @@ type TLicenseServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOrgById">; orgDAL: Pick<TOrgDALFactory, "findOrgById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseDAL: TLicenseDALFactory; licenseDAL: TLicenseDALFactory;
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">;
}; };
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>; export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
@@ -46,12 +47,18 @@ export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login"; const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login"; const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
const FEATURE_CACHE_KEY = (orgId: string, projectId?: string) => `${orgId}-${projectId || ""}`; const LICENSE_SERVER_CLOUD_PLAN_TTL = 30; // 30 second
export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: TLicenseServiceFactoryDep) => { const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`;
export const licenseServiceFactory = ({
orgDAL,
permissionService,
licenseDAL,
keyStore
}: TLicenseServiceFactoryDep) => {
let isValidLicense = false; let isValidLicense = false;
let instanceType = InstanceType.OnPrem; let instanceType = InstanceType.OnPrem;
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures(); let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
const featureStore = new NodeCache({ stdTTL: 60 });
const appCfg = getConfig(); const appCfg = getConfig();
const licenseServerCloudApi = setupLicenceRequestWithStore( const licenseServerCloudApi = setupLicenceRequestWithStore(
@@ -75,6 +82,7 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
isValidLicense = true; isValidLicense = true;
return; return;
} }
if (appCfg.LICENSE_KEY) { if (appCfg.LICENSE_KEY) {
const token = await licenseServerOnPremApi.refreshLicence(); const token = await licenseServerOnPremApi.refreshLicence();
if (token) { if (token) {
@@ -100,22 +108,21 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`); logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`);
try { try {
if (instanceType === InstanceType.Cloud) { if (instanceType === InstanceType.Cloud) {
const cachedPlan = featureStore.get<TFeatureSet>(FEATURE_CACHE_KEY(orgId, projectId)); const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId));
if (cachedPlan) return cachedPlan; if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
const org = await orgDAL.findOrgById(orgId); const org = await orgDAL.findOrgById(orgId);
if (!org) throw new BadRequestError({ message: "Org not found" }); if (!org) throw new BadRequestError({ message: "Org not found" });
const { const {
data: { currentPlan } data: { currentPlan }
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>( } = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, `/api/license-server/v1/customers/${org.customerId}/cloud-plan`
{ );
params: { await keyStore.setItemWithExpiry(
workspaceId: projectId FEATURE_CACHE_KEY(org.id),
} LICENSE_SERVER_CLOUD_PLAN_TTL,
} JSON.stringify(currentPlan)
); );
featureStore.set(FEATURE_CACHE_KEY(org.id, projectId), currentPlan);
return currentPlan; return currentPlan;
} }
} catch (error) { } catch (error) {
@@ -123,15 +130,20 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`, `getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`,
error error
); );
await keyStore.setItemWithExpiry(
FEATURE_CACHE_KEY(orgId),
LICENSE_SERVER_CLOUD_PLAN_TTL,
JSON.stringify(onPremFeatures)
);
return onPremFeatures; return onPremFeatures;
} }
return onPremFeatures; return onPremFeatures;
}; };
const refreshPlan = async (orgId: string, projectId?: string) => { const refreshPlan = async (orgId: string) => {
if (instanceType === InstanceType.Cloud) { if (instanceType === InstanceType.Cloud) {
featureStore.del(FEATURE_CACHE_KEY(orgId, projectId)); await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
await getPlan(orgId, projectId); await getPlan(orgId);
} }
}; };
@@ -166,7 +178,7 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
quantity: count quantity: count
}); });
} }
featureStore.del(orgId); await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
} else if (instanceType === InstanceType.EnterpriseOnPrem) { } else if (instanceType === InstanceType.EnterpriseOnPrem) {
const usedSeats = await licenseDAL.countOfOrgMembers(null); const usedSeats = await licenseDAL.countOfOrgMembers(null);
await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { usedSeats }); await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { usedSeats });
@@ -215,7 +227,7 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
`/api/license-server/v1/customers/${organization.customerId}/session/trial`, `/api/license-server/v1/customers/${organization.customerId}/session/trial`,
{ success_url } { success_url }
); );
featureStore.del(FEATURE_CACHE_KEY(orgId)); await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
return { url }; return { url };
}; };
@@ -4,7 +4,8 @@ import { ActorType } from "@app/services/auth/auth-type";
export enum SamlProviders { export enum SamlProviders {
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml" JUMPCLOUD_SAML = "jumpcloud-saml",
GOOGLE_SAML = "google-saml"
} }
export type TCreateSamlCfgDTO = { export type TCreateSamlCfgDTO = {
+3 -1
View File
@@ -194,7 +194,7 @@ export const registerRoutes = async (
projectRoleDAL, projectRoleDAL,
serviceTokenDAL serviceTokenDAL
}); });
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL }); const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
const trustedIpService = trustedIpServiceFactory({ const trustedIpService = trustedIpServiceFactory({
licenseService, licenseService,
projectDAL, projectDAL,
@@ -263,6 +263,8 @@ export const registerRoutes = async (
incidentContactDAL, incidentContactDAL,
tokenService, tokenService,
projectDAL, projectDAL,
projectMembershipDAL,
projectKeyDAL,
smtpService, smtpService,
userDAL, userDAL,
orgBotDAL orgBotDAL
+2 -1
View File
@@ -39,11 +39,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityUa, accessToken, identityAccessToken, validClientSecretInfo } = const { identityUa, accessToken, identityAccessToken, validClientSecretInfo, identityMembershipOrg } =
await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp); await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp);
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH,
metadata: { metadata: {
@@ -87,11 +87,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
params: z.object({ organizationId: z.string().trim() }), params: z.object({ organizationId: z.string().trim() }),
body: z.object({ body: z.object({
name: z.string().trim().optional(), name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
slug: z slug: z
.string() .string()
.trim() .trim()
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens") .max(64, { message: "Slug must be 64 or fewer characters" })
.regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens")
.optional(), .optional(),
authEnforced: z.boolean().optional(), authEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional() scimEnabled: z.boolean().optional()
@@ -222,7 +222,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
workspaceId: z.string().trim() workspaceId: z.string().trim()
}), }),
body: z.object({ body: z.object({
name: z.string().trim().optional(), name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
autoCapitalization: z.boolean().optional() autoCapitalization: z.boolean().optional()
}), }),
response: { response: {
@@ -120,7 +120,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}); });
server.route({ server.route({
url: "/:folderId", url: "/:folderIdOrName",
method: "DELETE", method: "DELETE",
schema: { schema: {
description: "Delete a folder", description: "Delete a folder",
@@ -131,7 +131,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
} }
], ],
params: z.object({ params: z.object({
folderId: z.string() folderIdOrName: z.string()
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
@@ -155,7 +155,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body, ...req.body,
projectId: req.body.workspaceId, projectId: req.body.workspaceId,
id: req.params.folderId, idOrName: req.params.folderIdOrName,
path path
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -54,6 +54,8 @@ export const identityUaServiceFactory = ({
const identityUa = await identityUaDAL.findOne({ clientId }); const identityUa = await identityUaDAL.findOne({ clientId });
if (!identityUa) throw new UnauthorizedError(); if (!identityUa) throw new UnauthorizedError();
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
checkIPAgainstBlocklist({ checkIPAgainstBlocklist({
ipAddress: ip, ipAddress: ip,
trustedIps: identityUa.clientSecretTrustedIps as TIp[] trustedIps: identityUa.clientSecretTrustedIps as TIp[]
@@ -131,7 +133,7 @@ export const identityUaServiceFactory = ({
} }
); );
return { accessToken, identityUa, validClientSecretInfo, identityAccessToken }; return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg };
}; };
const attachUa = async ({ const attachUa = async ({
@@ -649,33 +649,21 @@ export const integrationAuthServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
if (appId) {
if (appId && appId !== "") {
const query = ` const query = `
query project($id: String!) { query project($id: String!) {
project(id: $id) { project(id: $id) {
createdAt services {
deletedAt edges {
id node {
description id
expiredAt name
isPublic }
isTempProject }
isUpdatable }
name }
prDeploys
teamId
updatedAt
upstreamUrl
services {
edges {
node {
id
name
}
}
}
} }
}
`; `;
const variables = { const variables = {
@@ -711,6 +699,7 @@ export const integrationAuthServiceFactory = ({
); );
return edges.map(({ node: { name, id: serviceId } }) => ({ name, serviceId })); return edges.map(({ node: { name, id: serviceId } }) => ({ name, serviceId }));
} }
return []; return [];
}; };
@@ -441,16 +441,19 @@ const syncSecretsAWSParameterStore = async ({
}) => { }) => {
if (!accessId) return; if (!accessId) return;
AWS.config.update({ const config = new AWS.Config({
region: integration.region as string, region: integration.region as string,
accessKeyId: accessId, credentials: {
secretAccessKey: accessToken accessKeyId: accessId,
secretAccessKey: accessToken
}
}); });
const ssm = new AWS.SSM({ const ssm = new AWS.SSM({
apiVersion: "2014-11-06", apiVersion: "2014-11-06",
region: integration.region as string region: integration.region as string
}); });
ssm.config.update(config);
const params = { const params = {
Path: integration.path as string, Path: integration.path as string,
@@ -514,12 +517,6 @@ const syncSecretsAWSParameterStore = async ({
} }
}) })
); );
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
}; };
/** /**
@@ -541,12 +538,6 @@ const syncSecretsAWSSecretManager = async ({
try { try {
if (!accessId) return; if (!accessId) return;
AWS.config.update({
region: integration.region as string,
accessKeyId: accessId,
secretAccessKey: accessToken
});
secretsManager = new SecretsManagerClient({ secretsManager = new SecretsManagerClient({
region: integration.region as string, region: integration.region as string,
credentials: { credentials: {
@@ -575,12 +566,6 @@ const syncSecretsAWSSecretManager = async ({
}) })
); );
} }
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
} catch (err) { } catch (err) {
if (err instanceof ResourceNotFoundException && secretsManager) { if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send( await secretsManager.send(
@@ -590,11 +575,6 @@ const syncSecretsAWSSecretManager = async ({
}) })
); );
} }
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
} }
}; };
@@ -1224,21 +1204,21 @@ const syncSecretsRailway = async ({
} }
`; `;
const input = { const variables = {
projectId: integration.appId, input: {
environmentId: integration.targetEnvironmentId, projectId: integration.appId,
...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}), environmentId: integration.targetEnvironmentId,
replace: true, ...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}),
variables: getSecretKeyValuePair(secrets) replace: true,
variables: getSecretKeyValuePair(secrets)
}
}; };
await request.post( await request.post(
IntegrationUrls.RAILWAY_API_URL, IntegrationUrls.RAILWAY_API_URL,
{ {
query, query,
variables: { variables
input
}
}, },
{ {
headers: { headers: {
+49 -3
View File
@@ -22,6 +22,8 @@ import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
@@ -44,6 +46,8 @@ type TOrgServiceFactoryDep = {
orgRoleDAL: TOrgRoleDALFactory; orgRoleDAL: TOrgRoleDALFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
incidentContactDAL: TIncidentContactsDALFactory; incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">; samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
smtpService: TSmtpService; smtpService: TSmtpService;
@@ -65,6 +69,8 @@ export const orgServiceFactory = ({
permissionService, permissionService,
smtpService, smtpService,
projectDAL, projectDAL,
projectMembershipDAL,
projectKeyDAL,
tokenService, tokenService,
orgBotDAL, orgBotDAL,
licenseService, licenseService,
@@ -503,10 +509,50 @@ export const orgServiceFactory = ({
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
const membership = await orgDAL.deleteMembershipById(membershipId, orgId); const deletedMembership = await orgDAL.transaction(async (tx) => {
const orgMembership = await orgDAL.deleteMembershipById(membershipId, orgId, tx);
await licenseService.updateSubscriptionOrgMemberCount(orgId); if (!orgMembership.userId) {
return membership; await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
}
// Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId);
// Delete all the project memberships of the user in the organization
await projectMembershipDAL.delete(
{
$in: {
id: projectMemberships.map((membership) => membership.id)
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId)
},
receiverId: orgMembership.userId
});
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
}
},
tx
);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
});
return deletedMembership;
}; };
/* /*
@@ -82,5 +82,25 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
} }
}; };
return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByEmail }; const findProjectMembershipsByUserId = async (orgId: string, userId: string) => {
try {
const memberships = await db(TableName.ProjectMembership)
.where({ userId })
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.where({ [`${TableName.Project}.orgId` as "orgId"]: orgId })
.select(selectAllTableCols(TableName.ProjectMembership));
return memberships;
} catch (error) {
throw new DatabaseError({ error, name: "Find project memberships by user id" });
}
};
return {
...projectMemberOrm,
findAllProjectMembers,
findProjectGhostUser,
findMembershipsByEmail,
findProjectMembershipsByUserId
};
}; };
+24 -5
View File
@@ -102,8 +102,11 @@ export const projectQueueFactory = ({
const oldProjectKey = await projectKeyDAL.findLatestProjectKey(data.startedByUserId, data.projectId); const oldProjectKey = await projectKeyDAL.findLatestProjectKey(data.startedByUserId, data.projectId);
if (!project || !oldProjectKey) { if (!project) {
throw new Error("Project or project key not found"); throw new Error("Project not found");
}
if (!oldProjectKey) {
throw new Error("Old project key not found");
} }
if (project.upgradeStatus !== ProjectUpgradeStatus.Failed && project.upgradeStatus !== null) { if (project.upgradeStatus !== ProjectUpgradeStatus.Failed && project.upgradeStatus !== null) {
@@ -267,8 +270,19 @@ export const projectQueueFactory = ({
const user = await userDAL.findUserEncKeyByUserId(key.receiverId); const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId }); const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
if (!user || !orgMembership) { if (!user) {
throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`); throw new Error(`User with ID ${key.receiverId} was not found during upgrade.`);
}
if (!orgMembership) {
// This can happen. Since we don't remove project memberships and project keys when a user is removed from an org, this is a valid case.
logger.info("User is not in organization", {
userId: key.receiverId,
orgId: project.orgId,
projectId: project.id
});
// eslint-disable-next-line no-continue
continue;
} }
const [newMember] = assignWorkspaceKeysToMembers({ const [newMember] = assignWorkspaceKeysToMembers({
@@ -532,7 +546,12 @@ export const projectQueueFactory = ({
logger.error("Failed to upgrade project, because no project was found", data); logger.error("Failed to upgrade project, because no project was found", data);
} else { } else {
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed); await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
logger.error(err, "Failed to upgrade project"); logger.error("Failed to upgrade project", err, {
extra: {
project,
jobData: data
}
});
} }
throw err; throw err;
@@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import path from "path"; import path from "path";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4, validate as uuidValidate } from "uuid";
import { TSecretFoldersInsert } from "@app/db/schemas"; import { TSecretFoldersInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -164,7 +164,7 @@ export const secretFolderServiceFactory = ({
actorOrgId, actorOrgId,
environment, environment,
path: secretPath, path: secretPath,
id idOrName
}: TDeleteFolderDTO) => { }: TDeleteFolderDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@@ -179,7 +179,10 @@ export const secretFolderServiceFactory = ({
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath, tx); const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath, tx);
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
const [doc] = await folderDAL.delete({ envId: env.id, id, parentId: parentFolder.id }, tx); const [doc] = await folderDAL.delete(
{ envId: env.id, [uuidValidate(idOrName) ? "id" : "name"]: idOrName, parentId: parentFolder.id },
tx
);
if (!doc) throw new BadRequestError({ message: "Folder not found", name: "Delete folder" }); if (!doc) throw new BadRequestError({ message: "Folder not found", name: "Delete folder" });
return doc; return doc;
}); });
@@ -16,7 +16,7 @@ export type TUpdateFolderDTO = {
export type TDeleteFolderDTO = { export type TDeleteFolderDTO = {
environment: string; environment: string;
path: string; path: string;
id: string; idOrName: string;
} & TProjectPermission; } & TProjectPermission;
export type TGetFolderDTO = { export type TGetFolderDTO = {
@@ -375,6 +375,10 @@ export const secretServiceFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
if (inputSecret.newSecretName === "") {
throw new BadRequestError({ message: "New secret name cannot be empty" });
}
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id; const folderId = folder.id;
+19
View File
@@ -145,6 +145,25 @@ func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLo
return loginTwoV2Response, nil return loginTwoV2Response, nil
} }
func CallGetAllOrganizations(httpClient *resty.Client) (GetOrganizationsResponse, error) {
var orgResponse GetOrganizationsResponse
response, err := httpClient.
R().
SetResult(&orgResponse).
SetHeader("User-Agent", USER_AGENT).
Get(fmt.Sprintf("%v/v1/organization", config.INFISICAL_URL))
if err != nil {
return GetOrganizationsResponse{}, err
}
if response.IsError() {
return GetOrganizationsResponse{}, fmt.Errorf("CallGetAllOrganizations: Unsuccessful response: [response=%v]", response)
}
return orgResponse, nil
}
func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesResponse, error) { func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesResponse, error) {
var workSpacesResponse GetWorkSpacesResponse var workSpacesResponse GetWorkSpacesResponse
response, err := httpClient. response, err := httpClient.
+15 -8
View File
@@ -120,14 +120,21 @@ type PullSecretsByInfisicalTokenResponse struct {
type GetWorkSpacesResponse struct { type GetWorkSpacesResponse struct {
Workspaces []struct { Workspaces []struct {
ID string `json:"_id"` ID string `json:"_id"`
Name string `json:"name"` Name string `json:"name"`
Plan string `json:"plan,omitempty"` Plan string `json:"plan,omitempty"`
V int `json:"__v"` V int `json:"__v"`
Organization string `json:"organization,omitempty"` OrganizationId string `json:"orgId"`
} `json:"workspaces"` } `json:"workspaces"`
} }
type GetOrganizationsResponse struct {
Organizations []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"organizations"`
}
type Secret struct { type Secret struct {
SecretKeyCiphertext string `json:"secretKeyCiphertext,omitempty"` SecretKeyCiphertext string `json:"secretKeyCiphertext,omitempty"`
SecretKeyIV string `json:"secretKeyIV,omitempty"` SecretKeyIV string `json:"secretKeyIV,omitempty"`
@@ -292,10 +299,10 @@ type GetFoldersV1Response struct {
} }
type CreateFolderV1Request struct { type CreateFolderV1Request struct {
FolderName string `json:"folderName"` FolderName string `json:"name"`
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"` Environment string `json:"environment"`
Directory string `json:"directory"` Path string `json:"path"`
} }
type CreateFolderV1Response struct { type CreateFolderV1Response struct {
@@ -505,5 +512,5 @@ type GetRawSecretsV3Response struct {
SecretComment string `json:"secretComment"` SecretComment string `json:"secretComment"`
} `json:"secrets"` } `json:"secrets"`
Imports []any `json:"imports"` Imports []any `json:"imports"`
ETag string ETag string
} }
+4 -2
View File
@@ -228,7 +228,9 @@ func secretTemplateFunction(accessToken string, existingEtag string, currentEtag
*currentEtag = res.Etag *currentEtag = res.Etag
} }
return res.Secrets, nil expandedSecrets := util.ExpandSecrets(res.Secrets, models.ExpandSecretsAuthentication{UniversalAuthAccessToken: accessToken}, "")
return expandedSecrets, nil
} }
} }
@@ -622,7 +624,7 @@ var agentCmd = &cobra.Command{
} }
if !FileExists(configPath) && agentConfigInBase64 == "" { if !FileExists(configPath) && agentConfigInBase64 == "" {
log.Error().Msgf("No agent config file provided. Please provide a agent config file", configPath) log.Error().Msgf("No agent config file provided at %v. Please provide a agent config file", configPath)
return return
} }
+3 -1
View File
@@ -87,7 +87,9 @@ var exportCmd = &cobra.Command{
var output string var output string
if shouldExpandSecrets { if shouldExpandSecrets {
secrets = util.ExpandSecrets(secrets, infisicalToken, "") secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
InfisicalToken: infisicalToken,
}, "")
} }
secrets = util.FilterSecretsByTag(secrets, tagSlugs) secrets = util.FilterSecretsByTag(secrets, tagSlugs)
output, err = formatEnvs(secrets, format) output, err = formatEnvs(secrets, format)
+28 -15
View File
@@ -5,7 +5,6 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
@@ -52,25 +51,19 @@ var initCmd = &cobra.Command{
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken) httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken)
workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient)
organizationResponse, err := api.CallGetAllOrganizations(httpClient)
if err != nil { if err != nil {
util.HandleError(err, "Unable to pull projects that belong to you") util.HandleError(err, "Unable to pull organizations that belong to you")
} }
workspaces := workspaceResponse.Workspaces organizations := organizationResponse.Organizations
if len(workspaces) == 0 {
message := fmt.Sprintf("You don't have any projects created in Infisical. You must first create a project at %s", util.INFISICAL_TOKEN_NAME)
util.PrintErrorMessageAndExit(message)
}
var workspaceNames []string organizationNames := util.GetOrganizationsNameList(organizationResponse)
for _, workspace := range workspaces {
workspaceNames = append(workspaceNames, workspace.Name)
}
prompt := promptui.Select{ prompt := promptui.Select{
Label: "Which of your Infisical projects would you like to connect this project to?", Label: "Which Infisical organization would you like to select a project from?",
Items: workspaceNames, Items: organizationNames,
Size: 7, Size: 7,
} }
@@ -79,7 +72,27 @@ var initCmd = &cobra.Command{
util.HandleError(err) util.HandleError(err)
} }
err = writeWorkspaceFile(workspaces[index]) selectedOrganization := organizations[index]
workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient)
if err != nil {
util.HandleError(err, "Unable to pull projects that belong to you")
}
filteredWorkspaces, workspaceNames := util.GetWorkspacesInOrganization(workspaceResponse, selectedOrganization.ID)
prompt = promptui.Select{
Label: "Which of your Infisical projects would you like to connect this project to?",
Items: workspaceNames,
Size: 7,
}
index, _, err = prompt.Run()
if err != nil {
util.HandleError(err)
}
err = writeWorkspaceFile(filteredWorkspaces[index])
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
+3 -1
View File
@@ -110,7 +110,9 @@ var runCmd = &cobra.Command{
} }
if shouldExpandSecrets { if shouldExpandSecrets {
secrets = util.ExpandSecrets(secrets, infisicalToken, projectConfigDir) secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
InfisicalToken: infisicalToken,
}, projectConfigDir)
} }
secretsByKey := getSecretsByKeys(secrets) secretsByKey := getSecretsByKeys(secrets)
+24 -2
View File
@@ -7,6 +7,7 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"os"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
@@ -39,6 +40,11 @@ var secretsCmd = &cobra.Command{
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := cmd.Flags().GetString("token")
if infisicalToken == "" {
infisicalToken = os.Getenv(util.INFISICAL_TOKEN_NAME)
}
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
@@ -80,7 +86,9 @@ var secretsCmd = &cobra.Command{
} }
if shouldExpandSecrets { if shouldExpandSecrets {
secrets = util.ExpandSecrets(secrets, infisicalToken, "") secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
InfisicalToken: infisicalToken,
}, "")
} }
visualize.PrintAllSecretDetails(secrets) visualize.PrintAllSecretDetails(secrets)
@@ -406,6 +414,11 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse path flag") util.HandleError(err, "Unable to parse path flag")
} }
showOnlyValue, err := cmd.Flags().GetBool("raw-value")
if err != nil {
util.HandleError(err, "Unable to parse path flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "") secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "")
if err != nil { if err != nil {
util.HandleError(err, "To fetch all secrets") util.HandleError(err, "To fetch all secrets")
@@ -427,7 +440,15 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
} }
} }
visualize.PrintAllSecretDetails(requestedSecrets) if showOnlyValue && len(requestedSecrets) > 1 {
util.PrintErrorMessageAndExit("--raw-value only works with one secret.")
}
if showOnlyValue {
fmt.Printf(requestedSecrets[0].Value)
} else {
visualize.PrintAllSecretDetails(requestedSecrets)
}
Telemetry.CaptureEvent("cli-command:secrets get", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) Telemetry.CaptureEvent("cli-command:secrets get", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION))
} }
@@ -661,6 +682,7 @@ func init() {
secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.AddCommand(secretsGetCmd) secretsCmd.AddCommand(secretsGetCmd)
secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret")
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
secretsCmd.AddCommand(secretsSetCmd) secretsCmd.AddCommand(secretsSetCmd)
+17 -10
View File
@@ -21,11 +21,12 @@ type LoggedInUser struct {
} }
type SingleEnvironmentVariable struct { type SingleEnvironmentVariable struct {
Key string `json:"key"` Key string `json:"key"`
Value string `json:"value"` WorkspaceId string `json:"workspace"`
Type string `json:"type"` Value string `json:"value"`
ID string `json:"_id"` Type string `json:"type"`
Tags []struct { ID string `json:"_id"`
Tags []struct {
ID string `json:"_id"` ID string `json:"_id"`
Name string `json:"name"` Name string `json:"name"`
Slug string `json:"slug"` Slug string `json:"slug"`
@@ -45,11 +46,11 @@ type SingleFolder struct {
} }
type Workspace struct { type Workspace struct {
ID string `json:"_id"` ID string `json:"_id"`
Name string `json:"name"` Name string `json:"name"`
Plan string `json:"plan,omitempty"` Plan string `json:"plan,omitempty"`
V int `json:"__v"` V int `json:"__v"`
Organization string `json:"organization,omitempty"` OrganizationId string `json:"orgId"`
} }
type WorkspaceConfigFile struct { type WorkspaceConfigFile struct {
@@ -68,6 +69,7 @@ type GetAllSecretsParameters struct {
Environment string Environment string
EnvironmentPassedViaFlag bool EnvironmentPassedViaFlag bool
InfisicalToken string InfisicalToken string
UniversalAuthAccessToken string
TagSlugs string TagSlugs string
WorkspaceId string WorkspaceId string
SecretsPath string SecretsPath string
@@ -96,3 +98,8 @@ type DeleteFolderParameters struct {
FolderPath string FolderPath string
InfisicalToken string InfisicalToken string
} }
type ExpandSecretsAuthentication struct {
InfisicalToken string
UniversalAuthAccessToken string
}
+1 -1
View File
@@ -154,7 +154,7 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er
WorkspaceId: params.WorkspaceId, WorkspaceId: params.WorkspaceId,
Environment: params.Environment, Environment: params.Environment,
FolderName: params.FolderName, FolderName: params.FolderName,
Directory: params.FolderPath, Path: params.FolderPath,
} }
apiResponse, err := api.CallCreateFolderV1(httpClient, createFolderRequest) apiResponse, err := api.CallCreateFolderV1(httpClient, createFolderRequest)
+45
View File
@@ -0,0 +1,45 @@
package util
import (
"fmt"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/models"
)
func GetOrganizationsNameList(organizationResponse api.GetOrganizationsResponse) []string {
organizations := organizationResponse.Organizations
if len(organizations) == 0 {
message := fmt.Sprintf("You don't have any organization created in Infisical. You must first create a organization at %s", INFISICAL_DEFAULT_URL)
PrintErrorMessageAndExit(message)
}
var organizationNames []string
for _, workspace := range organizations {
organizationNames = append(organizationNames, workspace.Name)
}
return organizationNames
}
func GetWorkspacesInOrganization(workspaceResponse api.GetWorkSpacesResponse, orgId string) ([]models.Workspace, []string) {
workspaces := workspaceResponse.Workspaces
var filteredWorkspaces []models.Workspace
var workspaceNames []string
for _, workspace := range workspaces {
if workspace.OrganizationId == orgId {
filteredWorkspaces = append(filteredWorkspaces, workspace)
workspaceNames = append(workspaceNames, workspace.Name)
}
}
if len(filteredWorkspaces) == 0 {
message := fmt.Sprintf("You don't have any projects created in Infisical organization. You must first create a project at %s", INFISICAL_DEFAULT_URL)
PrintErrorMessageAndExit(message)
}
return filteredWorkspaces, workspaceNames
}
+27 -97
View File
@@ -179,7 +179,7 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
} }
for _, secret := range rawSecrets.Secrets { for _, secret := range rawSecrets.Secrets {
plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue}) plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, WorkspaceId: secret.Workspace})
} }
// if includeImports { // if includeImports {
@@ -191,7 +191,7 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
return models.PlaintextSecretResult{ return models.PlaintextSecretResult{
Secrets: plainTextSecrets, Secrets: plainTextSecrets,
Hash: rawSecrets.ETag, Etag: rawSecrets.ETag,
}, nil }, nil
} }
@@ -248,11 +248,8 @@ func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tag
} }
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) { func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) {
var infisicalToken string
if params.InfisicalToken == "" { if params.InfisicalToken == "" {
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) params.InfisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
} else {
infisicalToken = params.InfisicalToken
} }
isConnected := CheckIsConnectedToInternet() isConnected := CheckIsConnectedToInternet()
@@ -260,7 +257,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
// var serviceTokenDetails api.GetServiceTokenDetailsResponse // var serviceTokenDetails api.GetServiceTokenDetailsResponse
var errorToReturn error var errorToReturn error
if infisicalToken == "" { if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" {
if isConnected { if isConnected {
log.Debug().Msg("GetAllEnvironmentVariables: Connected to internet, checking logged in creds") log.Debug().Msg("GetAllEnvironmentVariables: Connected to internet, checking logged in creds")
@@ -306,12 +303,6 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
infisicalDotJson.WorkspaceId = params.WorkspaceId infisicalDotJson.WorkspaceId = params.WorkspaceId
} }
// // Verify environment
// err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials)
// if err != nil {
// return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
// }
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId, secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId,
params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport) params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
@@ -332,91 +323,19 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
} }
} else { } else {
log.Debug().Msg("Trying to fetch secrets using service token") if params.InfisicalToken != "" {
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath, params.IncludeImport) log.Debug().Msg("Trying to fetch secrets using service token")
} secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport)
} else if params.UniversalAuthAccessToken != "" {
log.Debug().Msg("Trying to fetch secrets using universal auth")
res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport)
return secretsToReturn, errorToReturn errorToReturn = err
} secretsToReturn = res.Secrets
// func ValidateEnvironmentName(environmentName string, workspaceId string, userLoggedInDetails models.UserCredentials) error {
// httpClient := resty.New()
// httpClient.SetAuthToken(userLoggedInDetails.JTWToken).
// SetHeader("Accept", "application/json")
// response, err := api.CallGetAccessibleEnvironments(httpClient, api.GetAccessibleEnvironmentsRequest{WorkspaceId: workspaceId})
// if err != nil {
// return err
// }
// listOfEnvSlugs := []string{}
// mapOfEnvSlugs := make(map[string]interface{})
// for _, environment := range response.AccessibleEnvironments {
// listOfEnvSlugs = append(listOfEnvSlugs, environment.Slug)
// mapOfEnvSlugs[environment.Slug] = environment
// }
// _, exists := mapOfEnvSlugs[environmentName]
// if !exists {
// HandleError(fmt.Errorf("the environment [%s] does not exist in project with [id=%s]. Only [%s] are available", environmentName, workspaceId, strings.Join(listOfEnvSlugs, ",")))
// }
// return nil
// }
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
return value
}
for _, secret := range secrets {
if secret.Key == variableWeAreLookingFor {
regex := regexp.MustCompile(`\${([^\}]*)}`)
variablesToPopulate := regex.FindAllString(secret.Value, -1)
// case: variable is a constant so return its value
if len(variablesToPopulate) == 0 {
return secret.Value
}
valueToEdit := secret.Value
for _, variableWithSign := range variablesToPopulate {
variableWithoutSign := strings.Trim(variableWithSign, "}")
variableWithoutSign = strings.Trim(variableWithoutSign, "${")
// case: reference to self
if variableWithoutSign == secret.Key {
hashMapOfSelfRefs[variableWithoutSign] = variableWithoutSign
continue
} else {
var expandedVariableValue string
if preComputedVariable, found := hashMapOfCompleteVariables[variableWithoutSign]; found {
expandedVariableValue = preComputedVariable
} else {
expandedVariableValue = getExpandedEnvVariable(secrets, variableWithoutSign, hashMapOfCompleteVariables, hashMapOfSelfRefs)
hashMapOfCompleteVariables[variableWithoutSign] = expandedVariableValue
}
// If after expanding all the vars above, is the current var a self ref? if so no replacement needed for it
if _, found := hashMapOfSelfRefs[variableWithoutSign]; found {
continue
} else {
valueToEdit = strings.ReplaceAll(valueToEdit, variableWithSign, expandedVariableValue)
}
}
}
return valueToEdit
} else {
continue
} }
} }
return "${" + variableWeAreLookingFor + "}" return secretsToReturn, errorToReturn
} }
var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`) var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`)
@@ -428,7 +347,7 @@ func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs ma
interpolatedVal, ok := interpolatedSecs[key] interpolatedVal, ok := interpolatedSecs[key]
if !ok { if !ok {
HandleError(fmt.Errorf("Could not find refered secret - %s", key), "Kindly check whether its provided") HandleError(fmt.Errorf("could not find refered secret - %s", key), "Kindly check whether its provided")
} }
refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1) refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1)
@@ -467,7 +386,7 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod
return secretMapByName return secretMapByName
} }
func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken string, projectConfigPathDir string) []models.SingleEnvironmentVariable { func ExpandSecrets(secrets []models.SingleEnvironmentVariable, auth models.ExpandSecretsAuthentication, projectConfigPathDir string) []models.SingleEnvironmentVariable {
expandedSecs := make(map[string]string) expandedSecs := make(map[string]string)
interpolatedSecs := make(map[string]string) interpolatedSecs := make(map[string]string)
// map[env.secret-path][keyname]Secret // map[env.secret-path][keyname]Secret
@@ -499,8 +418,18 @@ func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken st
uniqKey := fmt.Sprintf("%s.%s", env, secPathDot) uniqKey := fmt.Sprintf("%s.%s", env, secPathDot)
if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok { if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok {
var refSecs []models.SingleEnvironmentVariable
var err error
// if not in cross reference cache, fetch it from server // if not in cross reference cache, fetch it from server
refSecs, err := GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: infisicalToken, SecretsPath: secPath}, projectConfigPathDir) if auth.InfisicalToken != "" {
refSecs, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: auth.InfisicalToken, SecretsPath: secPath}, projectConfigPathDir)
} else if auth.UniversalAuthAccessToken != "" {
refSecs, err = GetAllEnvironmentVariables((models.GetAllSecretsParameters{Environment: env, UniversalAuthAccessToken: auth.UniversalAuthAccessToken, SecretsPath: secPath, WorkspaceId: sec.WorkspaceId}), projectConfigPathDir)
} else {
HandleError(errors.New("no authentication provided"), "Please provide authentication to fetch secrets")
}
if err != nil { if err != nil {
HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid") HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid")
} }
@@ -508,6 +437,7 @@ func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken st
// save it to avoid calling api again for same environment and folder path // save it to avoid calling api again for same environment and folder path
crossEnvRefSecs[uniqKey] = refSecsByKey crossEnvRefSecs[uniqKey] = refSecsByKey
return refSecsByKey[secKey].Value return refSecsByKey[secKey].Value
} else { } else {
return crossRefSec[secKey].Value return crossRefSec[secKey].Value
} }
+66 -31
View File
@@ -8,24 +8,28 @@ infisical secrets
``` ```
## Description ## Description
This command enables you to perform CRUD (create, read, update, delete) operations on secrets within your Infisical project. With it, you can view, create, update, and delete secrets in your environment. This command enables you to perform CRUD (create, read, update, delete) operations on secrets within your Infisical project. With it, you can view, create, update, and delete secrets in your environment.
### Sub-commands ### Sub-commands
<Accordion title="infisical secrets" defaultOpen="true"> <Accordion title="infisical secrets" defaultOpen="true">
Use this command to print out all of the secrets in your project Use this command to print out all of the secrets in your project
```bash ```bash
$ infisical secrets $ infisical secrets
``` ```
### Environment variables
### Environment variables
<Accordion title="INFISICAL_TOKEN"> <Accordion title="INFISICAL_TOKEN">
Used to fetch secrets via a [service token](/documentation/platform/token) apposed to logged in credentials. Simply, export this variable in the terminal before running this command. Used to fetch secrets via a [service token](/documentation/platform/token) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
```bash ```bash
# Example # Example
export INFISICAL_TOKEN=st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec export INFISICAL_TOKEN=st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec
``` ```
</Accordion> </Accordion>
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK"> <Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
@@ -34,22 +38,26 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
To use, simply export this variable in the terminal before running this command. To use, simply export this variable in the terminal before running this command.
```bash ```bash
# Example # Example
export INFISICAL_DISABLE_UPDATE_CHECK=true export INFISICAL_DISABLE_UPDATE_CHECK=true
``` ```
</Accordion> </Accordion>
### Flags ### Flags
<Accordion title="--expand"> <Accordion title="--expand">
Parse shell parameter expansions in your secrets Parse shell parameter expansions in your secrets
Default value: `true` Default value: `true`
</Accordion> </Accordion>
<Accordion title="--env"> <Accordion title="--env">
Used to select the environment name on which actions should be taken on Used to select the environment name on which actions should be taken on
Default value: `dev` Default value: `dev`
</Accordion> </Accordion>
<Accordion title="--path"> <Accordion title="--path">
The `--path` flag indicates which project folder secrets will be injected from. The `--path` flag indicates which project folder secrets will be injected from.
@@ -58,6 +66,7 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
# Example # Example
infisical secrets --path="/" --env=dev infisical secrets --path="/" --env=dev
``` ```
</Accordion> </Accordion>
</Accordion> </Accordion>
@@ -65,38 +74,55 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
<Accordion title="infisical secrets get"> <Accordion title="infisical secrets get">
This command allows you selectively print the requested secrets by name This command allows you selectively print the requested secrets by name
```bash ```bash
$ infisical secrets get <secret-name-a> <secret-name-b> ... $ infisical secrets get <secret-name-a> <secret-name-b> ...
# Example # Example
$ infisical secrets get DOMAIN $ infisical secrets get DOMAIN
``` ```
### Flags
### Flags
<Accordion title="--env"> <Accordion title="--env">
Used to select the environment name on which actions should be taken on Used to select the environment name on which actions should be taken on
Default value: `dev` Default value: `dev`
</Accordion>
<Accordion title="--raw-value">
Used to print the plain value of a single requested secret without any table style.
Default value: `false`
Example: `infisical secrets get DOMAIN --raw-value`
<Tip>
When running in CI/CD environments or in a script, set `INFISICAL_DISABLE_UPDATE_CHECK` env to `true`. This will help hide any CLI update messages and only show the secret value.
</Tip>
</Accordion> </Accordion>
</Accordion> </Accordion>
<Accordion title="infisical secrets set"> <Accordion title="infisical secrets set">
This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value. This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value.
If the secret key does not exist, a new secret will be created using both the key and value provided. If the secret key does not exist, a new secret will be created using both the key and value provided.
```bash ```bash
$ infisical secrets set <key1=value1> <key2=value2>... $ infisical secrets set <key1=value1> <key2=value2>...
## Example ## Example
$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe
``` ```
### Flags ### Flags
<Accordion title="--env"> <Accordion title="--env">
Used to select the environment name on which actions should be taken on Used to select the environment name on which actions should be taken on
Default value: `dev` Default value: `dev`
</Accordion> </Accordion>
<Accordion title="--path"> <Accordion title="--path">
Used to select the project folder in which the secrets will be set. This is useful when creating new secrets under a particular path. Used to select the project folder in which the secrets will be set. This is useful when creating new secrets under a particular path.
@@ -105,43 +131,48 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
# Example # Example
infisical secrets set DOMAIN=example.com --path="common/backend" infisical secrets set DOMAIN=example.com --path="common/backend"
``` ```
</Accordion> </Accordion>
</Accordion> </Accordion>
<Accordion title="infisical secrets delete"> <Accordion title="infisical secrets delete">
This command allows you to delete secrets by their name(s). This command allows you to delete secrets by their name(s).
```bash ```bash
$ infisical secrets delete <keyName1> <keyName2>... $ infisical secrets delete <keyName1> <keyName2>...
## Example ## Example
$ infisical secrets delete STRIPE_API_KEY DOMAIN HASH $ infisical secrets delete STRIPE_API_KEY DOMAIN HASH
``` ```
### Flags
### Flags
<Accordion title="--env"> <Accordion title="--env">
Used to select the environment name on which actions should be taken on Used to select the environment name on which actions should be taken on
Default value: `dev` Default value: `dev`
</Accordion> </Accordion>
<Accordion title="--path"> <Accordion title="--path">
The `--path` flag indicates which project folder secrets will be injected from. The `--path` flag indicates which project folder secrets will be injected from.
```bash ```bash
# Example # Example
infisical secrets delete <keyName1> <keyName2>... --path="/" infisical secrets delete <keyName1> <keyName2>... --path="/"
``` ```
</Accordion> </Accordion>
</Accordion> </Accordion>
<Accordion title="infisical secrets folders"> <Accordion title="infisical secrets folders">
This command allows you to fetch, create and delete folders from within a path from a given project. This command allows you to fetch, create and delete folders from within a path from a given project.
```bash ```bash
$ infisical secrets folders $ infisical secrets folders
``` ```
### sub commands
### sub commands
<Accordion title="get"> <Accordion title="get">
Used to fetch all folders within a path in a given project Used to fetch all folders within a path in a given project
``` ```
@@ -179,6 +210,7 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
Default value: `` Default value: ``
</Accordion> </Accordion>
</Accordion> </Accordion>
<Accordion title="delete"> <Accordion title="delete">
@@ -194,10 +226,11 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
</Accordion> </Accordion>
<Accordion title="--name"> <Accordion title="--name">
Name of the folder to be deleted within selected `--path` Name of the folder to be deleted within selected `--path`
Default value: `` Default value: ``
</Accordion> </Accordion>
</Accordion> </Accordion>
</Accordion> </Accordion>
@@ -210,14 +243,16 @@ To place default values in your example .env file, you can simply include the sy
```bash ```bash
$ infisical secrets generate-example-env $ infisical secrets generate-example-env
## Example ## Example
$ infisical secrets generate-example-env > .example-env $ infisical secrets generate-example-env > .example-env
``` ```
### Flags ### Flags
<Accordion title="--env"> <Accordion title="--env">
Used to select the environment name on which actions should be taken on Used to select the environment name on which actions should be taken on
Default value: `dev` Default value: `dev`
</Accordion> </Accordion>
</Accordion> </Accordion>
@@ -2,7 +2,6 @@
title: "Enhancing Security and Usability: Project Upgrades" title: "Enhancing Security and Usability: Project Upgrades"
--- ---
At Infisical, we're constantly striving to elevate the security and usability standards of our platform to better serve our users. At Infisical, we're constantly striving to elevate the security and usability standards of our platform to better serve our users.
With this commitment in mind, we're excited to introduce our latest addition, non-E2EE projects, aimed at addressing two significant issues while enhancing how clients interact with Infisical programmatically. With this commitment in mind, we're excited to introduce our latest addition, non-E2EE projects, aimed at addressing two significant issues while enhancing how clients interact with Infisical programmatically.
@@ -11,11 +10,11 @@ Additionally, our API lacked the capability to interact with projects without de
These obstacles made API driven automation and collaboration a painful experience for a majority of our users. These obstacles made API driven automation and collaboration a painful experience for a majority of our users.
To overcome these limitations, our upgrade focuses on disabling end-to-end encryption (E2EE) for projects. To overcome these limitations, our upgrade focuses on disabling end-to-end encryption (E2EE) for projects.
While this may raise eyebrows, it's important to understand that this decision is a strategic move to make Infisical easer to use and interact with. While this may raise eyebrows, it's important to understand that this decision is a strategic move to make Infisical easier to use and interact with.
But what does this mean for our users? Essentially nothing, there are no changes required on your end. But what does this mean for our users? Essentially nothing, there are no changes required on your end.
Rest assured, all sensitive data remains encrypted at rest according to the latest industry standards. Rest assured, all sensitive data remains encrypted at rest according to the latest industry standards.
Our commitment to security remains unwavering, and this upgrade is a testament to our dedication to delivering on our promises in both security and usability when it comes to secrets management. Our commitment to security remains unwavering, and this upgrade is a testament to our dedication to delivering on our promises in both security and usability when it comes to secrets management.
To increase consistency with existing and future integrations, all projects created on Infisical from now on will have end-to-end encryption (E2EE) disabled by default. To increase consistency with existing and future integrations, all projects created on Infisical from now on will have end-to-end encryption (E2EE) disabled by default.
This will not only reduce confusion for end users, but will also make the Infisical API seamless to use. This will not only reduce confusion for end users, but will also make the Infisical API seamless to use.
@@ -10,7 +10,7 @@ This means that updating the value of a base secret propagates directly to other
<Note> <Note>
Currently, the secret referencing feature is only supported by the Currently, the secret referencing feature is only supported by the
[Infisical CLI](/cli/overview) and [native integrations](/integrations/overview). [Infisical CLI](/cli/overview), [native integrations](/integrations/overview) and [Infisical Agent](/infisical-agent/overview).
We intend to add support for it to the [Node SDK](https://infisical.com/docs/sdks/languages/node), We intend to add support for it to the [Node SDK](https://infisical.com/docs/sdks/languages/node),
[Python SDK](https://infisical.com/docs/sdks/languages/python), and [Java SDK](https://infisical.com/docs/sdks/languages/java) this quarter. [Python SDK](https://infisical.com/docs/sdks/languages/python), and [Java SDK](https://infisical.com/docs/sdks/languages/java) this quarter.
@@ -0,0 +1,95 @@
---
title: "Google SAML"
description: "Configure Google SAML for Infisical SSO"
---
<Info>
Google SAML SSO feature is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
then you should contact [email protected] to purchase an enterprise license to use it.
</Info>
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
Next, note the **ACS URL** and **SP Entity ID** to use when configuring the Google SAML application.
![Google SAML initial configuration](../../../images/sso/google-saml/init-config.png)
</Step>
<Step title="Create a SAML application in Google">
2.1. In your [Google Admin console](https://support.google.com/a/answer/182076), head to Menu > Apps > Web and mobile apps and
create a **custom SAML app**.
![Google SAML app creation](../../../images/sso/google-saml/create-custom-saml-app.png)
2.2. In the **App details** tab, give the application a unique name like Infisical.
![Google SAML app naming](../../../images/sso/google-saml/name-custom-saml-app.png)
2.3. In the **Google Identity Provider details** tab, copy the **SSO URL**, **Entity ID** and **Certificate**.
![Google SAML custom app details](../../../images/sso/google-saml/custom-saml-app-config.png)
2.4. Back in Infisical, set **SSO URL**, **IdP Entity ID**, and **Certificate** to the corresponding items from step 2.3.
![Google SAML Infisical config](../../../images/sso/google-saml/infisical-config.png)
2.5. Back in the Google Admin console, in the **Service provider details** tab, set the **ACS URL** and **Entity ID** to the corresponding items from step 1.
Also, check the **Signed response** checkbox.
![Google SAML app config 2](../../../images/sso/google-saml/custom-saml-app-config-2.png)
2.6. In the **Attribute mapping** tab, configure the following map:
- **First name** -> **firstName**
- **Last name** -> **lastName**
- **Primary email** -> **email**
![Google SAML attribute mapping](../../../images/sso/google-saml/attribute-mapping.png)
Click **Finish**.
</Step>
<Step title="Assign users in Google Workspace to the application">
Back in your [Google Admin console](https://support.google.com/a/answer/182076), head to Menu > Apps > Web and mobile apps > your SAML app
and press on **User access**.
![Google SAML user access](../../../images/sso/google-saml/user-access.png)
To assign everyone in your organization to the application, click **On for everyone** or **Off for everyone** and then click **Save**.
You can also assign an organizational unit or set of users to an application; you can learn more about that [here](https://support.google.com/a/answer/6087519?hl=en#add_custom_saml&turn_on&verify_sso&&zippy=%2Cstep-add-the-custom-saml-app%2Cstep-turn-on-your-saml-app%2Cstep-verify-that-sso-is-working-with-your-custom-app).
![Google SAML user access assignment](../../../images/sso/google-saml/user-access-assign.png)
</Step>
<Step title="Enable SAML SSO in Infisical">
Enabling SAML SSO allows members in your organization to log into Infisical via Google Workspace.
![Google SAML enable](../../../images/sso/google-saml/enable-saml.png)
</Step>
<Step title="Enforce SAML SSO in Infisical">
Enforcing SAML SSO ensures that members in your organization can only access Infisical
by logging into the organization via Google.
To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Google user with Infisical;
Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO.
<Warning>
We recommend ensuring that your account is provisioned the application in Google
prior to enforcing SAML SSO to prevent any unintended issues.
</Warning>
</Step>
</Steps>
<Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to
set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work:
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>
References:
- Google's guide to [set up your own custom SAML app](https://support.google.com/a/answer/6087519?hl=en#add_custom_saml&turn_on&verify_sso&&zippy=%2Cstep-add-the-custom-saml-app%2Cstep-turn-on-your-saml-app%2Cstep-verify-that-sso-is-working-with-your-custom-app).
@@ -22,3 +22,4 @@ your IdP cannot and will not have access to the decryption key needed to decrypt
- [Okta SAML](/documentation/platform/sso/okta) - [Okta SAML](/documentation/platform/sso/okta)
- [Azure SAML](/documentation/platform/sso/azure) - [Azure SAML](/documentation/platform/sso/azure)
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud) - [JumpCloud SAML](/documentation/platform/sso/jumpcloud)
- [Google SAML](/documentation/platform/sso/google-saml)
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

@@ -0,0 +1,164 @@
---
title: 'Docker Swarm'
description: "How to manage secrets in Docker Swarm services"
---
In this guide, we'll demonstrate how to use Infisical for managing secrets within Docker Swarm.
Specifically, we'll set up a sidecar container using the [Infisical Agent](/infisical-agent/overview), which authenticates with Infisical to retrieve secrets and access tokens.
These secrets are then stored in a shared volume accessible by other services in your Docker Swarm.
## Prerequisites
- Infisical account
- Docker version 20.10.24 or newer
- Basic knowledge of Docker Swarm
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed on your system
- Familiarity with the [Infisical Agent](/infisical-agent/overview)
## Objective
Our goal is to deploy an Nginx instance in your Docker Swarm cluster, configured to display Infisical secrets on its landing page. This will provide hands-on experience in fetching and utilizing secrets from Infisical within Docker Swarm. The principles demonstrated here are also applicable to Docker Compose deployments.
<Steps>
<Step title="Cloning the Guide Assets Repository">
Start by cloning the [Infisical guide assets repository](https://github.com/Infisical/infisical-guides.git) from Github. This repository includes necessary assets for this and other Infisical guides. Focus on the `docker-swarm-with-agent` sub-directory, which we'll use as our working directory.
</Step>
<Step title="Setting Up Authentication with Infisical">
To allow the agent to fetch your Infisical secrets, choose an authentication method for the agent. For this guide, we will use [Universal Auth](/documentation/platform/identities/universal-auth) for authentication. Follow the instructions [here](/documentation/platform/identities/universal-auth) to generate a client ID and client secret.
</Step>
<Step title="Entering Universal Auth Credentials">
Copy the client ID and client secret obtained in the previous step into the `client-id` and `client-secret` text files, respectively.
</Step>
<Step title="Configuring the Infisical Agent">
The Infisical Agent will authenticate using Universal Auth and retrieve secrets for rendering as specified in the template(s).
Adjust the `polling-interval` to control the frequency of secret updates.
In the example template, the secrets are rendered as an HTML page, which will be set as Nginx's home page to demonstrate successful secret retrieval and utilization.
<Tip>
Remember to add your project id, environment slug and path of corresponding Infisical project to the secret template.
</Tip>
<Tabs>
<Tab title="Agent Configuration">
```yaml infisical-agent-config
infisical:
address: "https://app.infisical.com"
auth:
type: "universal-auth"
config:
client-id: "/run/secrets/infisical-universal-auth-client-id"
client-secret: "/run/secrets/infisical-universal-auth-client-secret"
remove_client_secret_on_read: false
sinks:
- type: "file"
config:
path: "/infisical-secrets/access-token"
templates:
- source-path: /run/secrets/nginx-home-page-template
destination-path: /infisical-secrets/index.html
config:
polling-interval: 60s
```
<Info>
Some paths contain `/run/secrets/` because the contents of those files reside in a [Docker secret](https://docs.docker.com/engine/swarm/secrets/#how-docker-manages-secrets).
</Info>
</Tab>
<Tab title="Secret Template for Agent">
```html nginx-home-page-template
<!DOCTYPE html>
<html lang="en">
<body>
<h1>This file is rendered by Infisical agent template engine</h1>
<p>Here are the secrets that have been fetched from Infisical and stored in your volume mount</p>
<ol>
{{- with secret "7df67a5f-d26a-4988-a375-7153c08149da" "dev" "/" }}
{{- range . }}
<li>{{ .Key }}={{ .Value }}</li>
{{- end }}
{{- end }}
</ol>
</body>
</html>
```
</Tab>
</Tabs>
</Step>
<Step title="Creating the Docker Compose File">
Define the `infisical-agent` and `nginx` services in your Docker Compose file. `infisical-agent` will handle secret retrieval and storage. These secrets are stored in a volume, accessible by other services like Nginx.
```yaml docker-compose.yaml
version: "3.1"
services:
infisical-agent:
container_name: infisical-agnet
image: infisical/cli:0.18.0
command: agent --config=/run/secrets/infisical-agent-config
volumes:
- infisical-agent:/infisical-secrets
secrets:
- infisical-universal-auth-client-id
- infisical-universal-auth-client-secret
- infisical-agent-config
- nginx-home-page-template
networks:
- infisical_network
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- infisical-agent:/usr/share/nginx/html
networks:
- infisical_network
volumes:
infisical-agent:
secrets:
infisical-universal-auth-client-id:
file: ./client-id
infisical-universal-auth-client-secret:
file: ./client-secret
infisical-agent-config:
file: ./infisical-agent-config
nginx-home-page-template:
file: ./nginx-home-page-template
networks:
infisical_network:
```
</Step>
<Step title="Initializing Docker Swarm">
```
docker swarm init
```
</Step>
<Step title="Deploying the Docker Stack">
```
docker stack deploy -c docker-compose.yaml agent-demo
```
</Step>
<Step title="Verifying Secret Consumption">
To confirm that secrets are properly rendered and accessible, navigate to `http://localhost`. You should see the Infisical secrets displayed on the Nginx landing page.
![Nginx displaying Infisical secrets](/images/docker-swarm-secrets-complete.png)
</Step>
<Step title="Clean up">
```
docker stack rm agent-demo
```
</Step>
</Steps>
## Considerations
- Secret Updates: Applications that access secrets directly from the volume mount will receive updates in real-time, in accordance with the `polling-interval` set in agent config.
- In-Memory Secrets: If your application loads secrets into memory, the new secrets will be available to the application on the next deployment.
+9 -1
View File
@@ -1,5 +1,5 @@
--- ---
title: "Infisical Agent" title: "Overview"
--- ---
Infisical Agent is a client daemon that simplifies the adoption of Infisical by providing a more scalable and user-friendly approach for applications to interact with Infisical. Infisical Agent is a client daemon that simplifies the adoption of Infisical by providing a more scalable and user-friendly approach for applications to interact with Infisical.
@@ -51,6 +51,9 @@ While specifying an authentication method is mandatory to start the agent, confi
| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | | `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. |
| `templates[].source-path` | The path to the template file that should be used to render secrets. | | `templates[].source-path` | The path to the template file that should be used to render secrets. |
| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. | | `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. |
| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `60s` (optional) |
| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) |
| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) |
## Quick start Infisical Agent ## Quick start Infisical Agent
@@ -76,6 +79,11 @@ sinks:
templates: templates:
- source-path: my-dot-ev-secret-template - source-path: my-dot-ev-secret-template
destination-path: /some/path/.env destination-path: /some/path/.env
config:
polling-interval: 60s
execute:
timeout: 30
command: ./reload-app.sh
``` ```
Above is an example agent configuration file that defines the token authentication method, one sink location (where to deposit access tokens after renewal) and a secret template. Above is an example agent configuration file that defines the token authentication method, one sink location (where to deposit access tokens after renewal) and a secret template.
+2 -4
View File
@@ -9,9 +9,7 @@ The Infisical API (sometimes referred to as the **backend**) contains the core p
## Storage backend ## Storage backend
Infisical relies on a storage backend to store data including users and secrets. Infisical relies on a storage backend to store data including users and secrets. Infisical's storage backend is Postgres.
Currently, the only supported storage backend is [MongoDB](https://www.mongodb.com) but we plan to add support for other options including PostgreSQL in Q1 2024.
## Redis ## Redis
@@ -27,4 +25,4 @@ Clients are any application or infrastructure that connecting to the Infisical A
- Public API: Making API requests directly to the Infisical API. - Public API: Making API requests directly to the Infisical API.
- Client SDK: A platform-specific library with method abstractions for working with secrets. Currently, there are three official SDKs: [Node SDK](https://infisical.com/docs/sdks/languages/node), [Python SDK](https://infisical.com/docs/sdks/languages/python), and [Java SDK](https://infisical.com/docs/sdks/languages/java). - Client SDK: A platform-specific library with method abstractions for working with secrets. Currently, there are three official SDKs: [Node SDK](https://infisical.com/docs/sdks/languages/node), [Python SDK](https://infisical.com/docs/sdks/languages/python), and [Java SDK](https://infisical.com/docs/sdks/languages/java).
- CLI: A terminal-based interface for interacting with the Infisical API. - CLI: A terminal-based interface for interacting with the Infisical API.
- Kubernetes Operator: This operator retrieves secrets from Infisical and securely store - Kubernetes Operator: This operator retrieves secrets from Infisical and securely store
+19 -5
View File
@@ -146,7 +146,8 @@
"documentation/platform/sso/gitlab", "documentation/platform/sso/gitlab",
"documentation/platform/sso/okta", "documentation/platform/sso/okta",
"documentation/platform/sso/azure", "documentation/platform/sso/azure",
"documentation/platform/sso/jumpcloud" "documentation/platform/sso/jumpcloud",
"documentation/platform/sso/google-saml"
] ]
}, },
{ {
@@ -227,12 +228,27 @@
{ {
"group": "Agent", "group": "Agent",
"pages": [ "pages": [
"infisical-agent/overview" "infisical-agent/overview",
{
"group": "Use cases",
"pages": [
"infisical-agent/guides/docker-swarm-with-agent",
"integrations/platforms/ecs-with-agent"
]
}
] ]
}, },
{ {
"group": "Infrastructure Integrations", "group": "Infrastructure Integrations",
"pages": [ "pages": [
{
"group": "Container orchestrators",
"pages": [
"integrations/platforms/kubernetes",
"infisical-agent/guides/docker-swarm-with-agent",
"integrations/platforms/ecs-with-agent"
]
},
{ {
"group": "Docker", "group": "Docker",
"pages": [ "pages": [
@@ -242,10 +258,8 @@
"integrations/platforms/docker-compose" "integrations/platforms/docker-compose"
] ]
}, },
"integrations/platforms/kubernetes",
"integrations/frameworks/terraform", "integrations/frameworks/terraform",
"integrations/platforms/ansible", "integrations/platforms/ansible"
"integrations/platforms/ecs-with-agent"
] ]
}, },
{ {
@@ -58,7 +58,7 @@ Redis requirements:
- Use Redis versions 6.x or 7.x. We advise upgrading to at least Redis 6.2. - Use Redis versions 6.x or 7.x. We advise upgrading to at least Redis 6.2.
- Redis Cluster mode is currently not supported; use Redis Standalone, with or without High Availability (HA). - Redis Cluster mode is currently not supported; use Redis Standalone, with or without High Availability (HA).
- Redis storage needs are minimal: a setup with 1 vCPU, 1 GB RAM, and 1GB SSD will be sufficient for most deployments. - Redis storage needs are minimal: a setup with 1 vCPU, 1 GB RAM, and 1GB SSD will be sufficient for small deployments.
## Supported Web Browsers ## Supported Web Browsers
@@ -68,4 +68,4 @@ Infisical supports a range of web browsers. However, features such as browser-ba
- [Google Chrome](https://www.google.com/chrome/) - [Google Chrome](https://www.google.com/chrome/)
- [Chromium](https://www.chromium.org/getting-involved/dev-channel/) - [Chromium](https://www.chromium.org/getting-involved/dev-channel/)
- [Apple Safari](https://www.apple.com/safari/) - [Apple Safari](https://www.apple.com/safari/)
- [Microsoft Edge](https://www.microsoft.com/en-us/edge?form=MA13FJ) - [Microsoft Edge](https://www.microsoft.com/en-us/edge?form=MA13FJ)
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "npm-proj-1709284350861-0.8379672465905885Iibrej", "name": "frontend",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@@ -30,7 +30,7 @@ export const Checkbox = ({
<div className="flex items-center font-inter text-bunker-300"> <div className="flex items-center font-inter text-bunker-300">
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
className={twMerge( className={twMerge(
"flex items-center justify-center w-4 h-4 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600", "flex items-center flex-shrink-0 justify-center w-4 h-4 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600",
isDisabled && "bg-bunker-400 hover:bg-bunker-400", isDisabled && "bg-bunker-400 hover:bg-bunker-400",
isChecked && "bg-primary hover:bg-primary", isChecked && "bg-primary hover:bg-primary",
Boolean(children) && "mr-3", Boolean(children) && "mr-3",
@@ -46,7 +46,7 @@ export const Checkbox = ({
<FontAwesomeIcon icon={faCheck} size="sm" /> <FontAwesomeIcon icon={faCheck} size="sm" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
<label className="text-sm whitespace-nowrap" htmlFor={id}> <label className="text-sm whitespace-nowrap truncate" htmlFor={id}>
{children} {children}
{isRequired && <span className="pl-1 text-red">*</span>} {isRequired && <span className="pl-1 text-red">*</span>}
</label> </label>
+11 -11
View File
@@ -42,27 +42,27 @@ export const MenuItem = <T extends ElementType = "button">({
const iconRef = useRef(); const iconRef = useRef();
return ( return (
<a onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}> <div onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}>
<li <li
className={twMerge( className={twMerge(
"group px-1 py-2 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50", "duration-50 group mt-0.5 flex cursor-pointer flex-col rounded px-1 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600", isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
isDisabled && "hover:bg-transparent cursor-not-allowed", isDisabled && "cursor-not-allowed hover:bg-transparent",
className className
)} )}
> >
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm"> <motion.span className="flex w-full flex-row items-center justify-start rounded-sm">
<Item <Item
type="button" type="button"
role="menuitem" role="menuitem"
className="flex items-center relative" className="relative flex items-center"
ref={inputRef} ref={inputRef}
{...props} {...props}
> >
<div <div
className={`${ className={`${
isSelected ? "visisble" : "invisible" isSelected ? "visisble" : "invisible"
} -left-[0.28rem] absolute w-[0.07rem] rounded-md h-5 bg-primary`} } absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`}
/> />
{/* {icon && <span className="mr-3 ml-4 w-5 block group-hover:hidden">{icon}</span>} */} {/* {icon && <span className="mr-3 ml-4 w-5 block group-hover:hidden">{icon}</span>} */}
{icon && ( {icon && (
@@ -81,7 +81,7 @@ export const MenuItem = <T extends ElementType = "button">({
{description && <span className="mt-2 text-xs">{description}</span>} {description && <span className="mt-2 text-xs">{description}</span>}
</motion.span> </motion.span>
</li> </li>
</a> </div>
); );
}; };
@@ -103,16 +103,16 @@ export const SubMenuItem = <T extends ElementType = "button">({
<a onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}> <a onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}>
<li <li
className={twMerge( className={twMerge(
"group px-1 py-1 mt-0.5 font-inter flex flex-col text-sm text-mineshaft-300 hover:text-mineshaft-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50", "duration-50 group mt-0.5 flex cursor-pointer flex-col rounded px-1 py-1 font-inter text-sm text-mineshaft-300 transition-all hover:bg-mineshaft-700 hover:text-mineshaft-100",
isDisabled && "hover:bg-transparent cursor-not-allowed", isDisabled && "cursor-not-allowed hover:bg-transparent",
className className
)} )}
> >
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm pl-6"> <motion.span className="flex w-full flex-row items-center justify-start rounded-sm pl-6">
<Item <Item
type="button" type="button"
role="menuitem" role="menuitem"
className="flex items-center relative" className="relative flex items-center"
ref={inputRef} ref={inputRef}
{...props} {...props}
> >
@@ -103,7 +103,7 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.
Upgrade your project version to continue receiving the latest improvements and Upgrade your project version to continue receiving the latest improvements and
patches. patches.
</p> </p>
<Link href="/docs/documentation/platform/project-upgrade"> <Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
<a target="_blank" className="text-primary-400"> <a target="_blank" className="text-primary-400">
Learn more Learn more
</a> </a>
@@ -117,7 +117,7 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.
Upgrading the project version is required to continue receiving the latest Upgrading the project version is required to continue receiving the latest
improvements and patches. improvements and patches.
</p> </p>
<Link href="/docs/documentation/platform/project-upgrade"> <Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
<a target="_blank" className="text-primary-400"> <a target="_blank" className="text-primary-400">
Learn more Learn more
</a> </a>
@@ -74,16 +74,6 @@ export default function RailwayCreateIntegrationPage() {
} }
}, [targetEnvironments]); }, [targetEnvironments]);
useEffect(() => {
if (targetServices) {
if (targetServices.length > 0) {
setTargetServiceId(targetServices[0].serviceId);
} else {
setTargetServiceId("none");
}
}
}, [targetServices]);
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
@@ -124,11 +114,14 @@ export default function RailwayCreateIntegrationPage() {
} }
}; };
const filteredTargetServices = targetServices ? [ { name: "", serviceId: "none" }, ...targetServices ] : [ { name: "", serviceId: "none" } ];
return workspace && return workspace &&
selectedSourceEnvironment && selectedSourceEnvironment &&
integrationAuthApps && integrationAuthApps &&
targetEnvironments && targetEnvironments &&
targetServices ? ( targetServices &&
filteredTargetServices ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8"> <Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Railway Integration</CardTitle> <CardTitle className="text-center">Railway Integration</CardTitle>
@@ -208,20 +201,14 @@ export default function RailwayCreateIntegrationPage() {
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
isDisabled={targetServices.length === 0} isDisabled={targetServices.length === 0}
> >
{targetServices.length > 0 ? ( {filteredTargetServices.map((targetService) => (
targetServices.map((targetService) => (
<SelectItem <SelectItem
value={targetService.serviceId as string} value={targetService.serviceId as string}
key={`target-service-${targetService.serviceId as string}`} key={`target-service-${targetService.serviceId as string}`}
> >
{targetService.name} {targetService.name}
</SelectItem> </SelectItem>
)) ))}
) : (
<SelectItem value="none" key="target-service-none">
No services found
</SelectItem>
)}
</Select> </Select>
</FormControl> </FormControl>
<Button <Button
@@ -81,7 +81,7 @@ export const CreateSecretImportForm = ({
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
const axiosError = err as AxiosError const axiosError = err as AxiosError;
if (axiosError?.response?.status === 401) { if (axiosError?.response?.status === 401) {
createNotification({ createNotification({
text: "You do not have access to the selected environment/path", text: "You do not have access to the selected environment/path",
@@ -1,24 +1,4 @@
/* eslint-disable simple-import-sort/imports */ /* eslint-disable simple-import-sort/imports */
import { memo, useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faCheck,
faClock,
faClose,
faCodeBranch,
faComment,
faCopy,
faEllipsis,
faKey,
faTag,
faTags
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
@@ -48,9 +28,29 @@ import {
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { DecryptedSecret } from "@app/hooks/api/secrets/types"; import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { WsTag } from "@app/hooks/api/types"; import { WsTag } from "@app/hooks/api/types";
import { subject } from "@casl/ability";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faCheck,
faClock,
faClose,
faCodeBranch,
faComment,
faCopy,
faEllipsis,
faKey,
faTag,
faTags
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
import { memo, useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { twMerge } from "tailwind-merge";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
import { CreateReminderForm } from "./CreateReminderForm"; import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
type Props = { type Props = {
secret: DecryptedSecret; secret: DecryptedSecret;
@@ -104,7 +104,8 @@ export const SecretItem = memo(
setValue, setValue,
reset, reset,
getValues, getValues,
formState: { isDirty, isSubmitting } trigger,
formState: { isDirty, isSubmitting, errors }
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
defaultValues: secret, defaultValues: secret,
values: secret, values: secret,
@@ -235,15 +236,18 @@ export const SecretItem = memo(
<Controller <Controller
name="key" name="key"
control={control} control={control}
render={({ field }) => ( render={({ field, fieldState: { error } }) => (
<Input <Input
autoComplete="off" autoComplete="off"
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
autoCapitalization={currentWorkspace?.autoCapitalization} autoCapitalization={currentWorkspace?.autoCapitalization}
variant="plain" variant="plain"
isDisabled={isOverriden} isDisabled={isOverriden}
placeholder={error?.message}
isError={Boolean(error)}
onKeyUp={() => trigger("key")}
{...field} {...field}
className="w-full px-0 focus:text-bunker-100 focus:ring-transparent" className="w-full px-0 placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
/> />
)} )}
/> />
@@ -497,7 +501,7 @@ export const SecretItem = memo(
animate={{ x: 0, opacity: 1 }} animate={{ x: 0, opacity: 1 }}
exit={{ x: -10, opacity: 0 }} exit={{ x: -10, opacity: 0 }}
> >
<Tooltip content="Save"> <Tooltip content={errors.key ? errors.key?.message : "Save"}>
<IconButton <IconButton
ariaLabel="more" ariaLabel="more"
variant="plain" variant="plain"
@@ -507,12 +511,16 @@ export const SecretItem = memo(
"p-0 text-primary opacity-0 group-hover:opacity-100", "p-0 text-primary opacity-0 group-hover:opacity-100",
isDirty && "opacity-100" isDirty && "opacity-100"
)} )}
isDisabled={isSubmitting} isDisabled={isSubmitting || Boolean(errors.key)}
> >
{isSubmitting ? ( {isSubmitting ? (
<Spinner className="m-0 h-4 w-4 p-0" /> <Spinner className="m-0 h-4 w-4 p-0" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} size="lg" className="text-primary" /> <FontAwesomeIcon
icon={faCheck}
size="lg"
className={twMerge("text-primary", errors.key && "text-mineshaft-300")}
/>
)} )}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -206,7 +206,7 @@ export const SecretListView = ({
reminderRepeatDays, reminderRepeatDays,
reminderNote reminderNote
} = modSecret; } = modSecret;
const hasKeyChanged = oldKey !== key; const hasKeyChanged = oldKey !== key && key;
const tagIds = tags?.map(({ id }) => id); const tagIds = tags?.map(({ id }) => id);
const oldTagIds = (orgSecret?.tags || []).map(({ id }) => id); const oldTagIds = (orgSecret?.tags || []).map(({ id }) => id);
@@ -8,7 +8,7 @@ export enum SecretActionType {
} }
export const formSchema = z.object({ export const formSchema = z.object({
key: z.string().trim(), key: z.string().trim().min(1, { message: "Secret key is required" }),
value: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())), value: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
idOverride: z.string().trim().optional(), idOverride: z.string().trim().optional(),
valueOverride: z valueOverride: z
@@ -2,22 +2,31 @@ import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { subject } from "@casl/ability";
import { import {
faAngleDown,
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faFolderBlank, faFolderBlank,
faMagnifyingGlass faFolderPlus,
faMagnifyingGlass,
faPlus
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader"; import NavHeader from "@app/components/navigation/NavHeader";
import { PermissionDeniedBanner } from "@app/components/permissions"; import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
EmptyState, EmptyState,
IconButton, IconButton,
Input, Input,
Modal,
ModalContent,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -30,7 +39,13 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert"; import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
import { useOrganization, useWorkspace } from "@app/context"; import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { import {
useCreateFolder, useCreateFolder,
useCreateSecretV3, useCreateSecretV3,
@@ -42,6 +57,8 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs"; import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection"; import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow"; import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
@@ -110,6 +127,40 @@ export const SecretOverviewPage = () => {
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3(); const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: createFolder } = useCreateFolder();
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"addSecretsInAllEnvs",
"addFolder",
"misc"
] as const);
const handleFolderCreate = async (folderName: string) => {
const promises = userAvailableEnvs.map((env) => {
const environment = env.slug;
return createFolder({
name: folderName,
path: secretPath,
environment,
projectId: workspaceId
});
});
const results = await Promise.allSettled(promises);
const isFoldersAdded = results.some((result) => result.status === "fulfilled");
if (isFoldersAdded) {
handlePopUpClose("addFolder");
createNotification({
type: "success",
text: "Successfully created folder"
});
} else {
createNotification({
type: "error",
text: "Failed to create folder"
});
}
};
const handleSecretCreate = async (env: string, key: string, value: string) => { const handleSecretCreate = async (env: string, key: string, value: string) => {
try { try {
// create folder if not existing // create folder if not existing
@@ -269,210 +320,286 @@ export const SecretOverviewPage = () => {
filteredFolderNames?.length === 0; filteredFolderNames?.length === 0;
return ( return (
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]"> <>
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} /> <div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<div className="relative right-5 ml-4"> <ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
<NavHeader pageName={t("dashboard.title")} isProjectRelated /> <div className="relative right-5 ml-4">
</div> <NavHeader pageName={t("dashboard.title")} isProjectRelated />
<div className="space-y-8">
<div className="mt-6">
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
<p className="text-md text-bunker-300">
Inject your secrets using
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical CLI
</a>
,
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/api"
target="_blank"
rel="noopener noreferrer"
>
Infisical API
</a>
,
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</a>
, and
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
>
more
</a>
.
</p>
</div> </div>
<div className="space-y-8">
<div className="mt-6">
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
<p className="text-md text-bunker-300">
Inject your secrets using
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical CLI
</a>
,
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/api"
target="_blank"
rel="noopener noreferrer"
>
Infisical API
</a>
,
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</a>
, and
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
>
more
</a>
.
</p>
</div>
{currentWorkspace?.version === ProjectVersion.V1 && ( {currentWorkspace?.version === ProjectVersion.V1 && (
<UpgradeProjectAlert project={currentWorkspace} /> <UpgradeProjectAlert project={currentWorkspace} />
)} )}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} /> <FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
<div className="w-80"> <div className="flex flex-row items-center justify-center space-x-2">
<Input <div className="w-80">
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80" <Input
placeholder="Search by secret/folder name..." className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
value={searchFilter} placeholder="Search by secret/folder name..."
onChange={(e) => setSearchFilter(e.target.value)} value={searchFilter}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} onChange={(e) => setSearchFilter(e.target.value)}
/> leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/>
</div>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
>
{(isAllowed) => (
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
className="h-10 rounded-r-none"
isDisabled={!isAllowed}
>
Add Secret
</Button>
)}
</ProjectPermissionCan>
<DropdownMenu
open={popUp.misc.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add-folder-or-import"
variant="outline_bg"
className="rounded-l-none bg-mineshaft-600 p-3"
>
<FontAwesomeIcon icon={faAngleDown} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div className="flex flex-col space-y-1 p-1.5">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
onClick={() => {
handlePopUpOpen("addFolder");
handlePopUpClose("misc");
}}
isDisabled={!isAllowed}
variant="outline_bg"
className="h-10"
isFullWidth
>
Add Folder
</Button>
)}
</ProjectPermissionCan>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div> </div>
</div> </div>
</div> <div className="thin-scrollbar mt-4" ref={parentTableRef}>
<div className="thin-scrollbar mt-4" ref={parentTableRef}> <TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto">
<TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto"> <Table>
<Table> <THead>
<THead> <Tr className="sticky top-0 z-20 border-0">
<Tr className="sticky top-0 z-20 border-0"> <Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0">
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0"> <div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-3.5 pb-3">
<div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-3.5 pb-3"> Name
Name <IconButton
<IconButton variant="plain"
variant="plain" className="ml-2"
className="ml-2" ariaLabel="sort"
ariaLabel="sort" onClick={() => setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))}
onClick={() => setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))} >
> <FontAwesomeIcon icon={sortDir === "asc" ? faArrowDown : faArrowUp} />
<FontAwesomeIcon icon={sortDir === "asc" ? faArrowDown : faArrowUp} /> </IconButton>
</IconButton> </div>
</div> </Th>
</Th> {userAvailableEnvs?.map(({ name, slug }, index) => {
{userAvailableEnvs?.map(({ name, slug }, index) => { const envSecKeyCount = getEnvSecretKeyCount(slug);
const envSecKeyCount = getEnvSecretKeyCount(slug); const missingKeyCount = secKeys.length - envSecKeyCount;
const missingKeyCount = secKeys.length - envSecKeyCount; return (
return ( <Th
<Th className="min-table-row min-w-[11rem] border-b-0 p-0 text-center"
className="min-table-row min-w-[11rem] border-b-0 p-0 text-center" key={`secret-overview-${name}-${index + 1}`}
key={`secret-overview-${name}-${index + 1}`} >
> <div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pt-3.5 pb-[0.83rem]">
<div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pt-3.5 pb-[0.83rem]"> <button
<button type="button"
type="button" className="text-sm font-medium duration-100 hover:text-mineshaft-100"
className="text-sm font-medium duration-100 hover:text-mineshaft-100" onClick={() => handleExploreEnvClick(slug)}
>
{name}
</button>
{missingKeyCount > 0 && (
<Tooltip
className="max-w-none lowercase"
content={`${missingKeyCount} secrets missing\n compared to other environments`}
>
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100">
<span className="text-bunker-100">{missingKeyCount}</span>
</div>
</Tooltip>
)}
</div>
</Th>
);
})}
</Tr>
</THead>
<TBody>
{canViewOverviewPage && isTableLoading && (
<TableSkeleton
columns={userAvailableEnvs.length + 1}
innerKey="secret-overview-loading"
rows={5}
className="bg-mineshaft-700"
/>
)}
{isTableEmpty && !isTableLoading && (
<Tr>
<Td colSpan={userAvailableEnvs.length + 1}>
<EmptyState title="Let's add some secrets" icon={faFolderBlank} iconSize="3x">
<Link
href={{
pathname: "/project/[id]/secrets/[env]",
query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug }
}}
>
<Button
className="mt-4"
variant="outline_bg"
colorSchema="primary"
size="md"
>
Go to {userAvailableEnvs?.[0]?.name}
</Button>
</Link>
</EmptyState>
</Td>
</Tr>
)}
{!isTableLoading &&
filteredFolderNames.map((folderName, index) => (
<SecretOverviewFolderRow
folderName={folderName}
isFolderPresentInEnv={isFolderPresentInEnv}
environments={userAvailableEnvs}
key={`overview-${folderName}-${index + 1}`}
onClick={handleFolderClick}
/>
))}
{!isTableLoading &&
(userAvailableEnvs?.length > 0 ? (
filteredSecretNames.map((key, index) => (
<SecretOverviewTableRow
secretPath={secretPath}
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}
key={`overview-${key}-${index + 1}`}
environments={userAvailableEnvs}
secretKey={key}
getSecretByKey={getSecretByKey}
expandableColWidth={expandableTableWidth}
/>
))
) : (
<PermissionDeniedBanner />
))}
</TBody>
<TFoot>
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
<Td className="sticky left-0 z-10 border-0 bg-mineshaft-800 p-0">
<div
className="w-full border-t border-r border-mineshaft-600"
style={{ height: "45px" }}
/>
</Td>
{userAvailableEnvs.map(({ name, slug }) => (
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0">
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2">
<Button
size="xs"
variant="outline_bg"
isFullWidth
onClick={() => handleExploreEnvClick(slug)} onClick={() => handleExploreEnvClick(slug)}
> >
{name} Explore
</button>
{missingKeyCount > 0 && (
<Tooltip
className="max-w-none lowercase"
content={`${missingKeyCount} secrets missing\n compared to other environments`}
>
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100">
<span className="text-bunker-100">{missingKeyCount}</span>
</div>
</Tooltip>
)}
</div>
</Th>
);
})}
</Tr>
</THead>
<TBody>
{canViewOverviewPage && isTableLoading && (
<TableSkeleton
columns={userAvailableEnvs.length + 1}
innerKey="secret-overview-loading"
rows={5}
className="bg-mineshaft-700"
/>
)}
{isTableEmpty && !isTableLoading && (
<Tr>
<Td colSpan={userAvailableEnvs.length + 1}>
<EmptyState title="Let's add some secrets" icon={faFolderBlank} iconSize="3x">
<Link
href={{
pathname: "/project/[id]/secrets/[env]",
query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug }
}}
>
<Button
className="mt-4"
variant="outline_bg"
colorSchema="primary"
size="md"
>
Go to {userAvailableEnvs?.[0]?.name}
</Button> </Button>
</Link> </div>
</EmptyState> </Td>
</Td> ))}
</Tr> </Tr>
)} </TFoot>
{!isTableLoading && </Table>
filteredFolderNames.map((folderName, index) => ( </TableContainer>
<SecretOverviewFolderRow </div>
folderName={folderName}
isFolderPresentInEnv={isFolderPresentInEnv}
environments={userAvailableEnvs}
key={`overview-${folderName}-${index + 1}`}
onClick={handleFolderClick}
/>
))}
{!isTableLoading &&
(userAvailableEnvs?.length > 0 ? (
filteredSecretNames.map((key, index) => (
<SecretOverviewTableRow
secretPath={secretPath}
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}
key={`overview-${key}-${index + 1}`}
environments={userAvailableEnvs}
secretKey={key}
getSecretByKey={getSecretByKey}
expandableColWidth={expandableTableWidth}
/>
))
) : (
<PermissionDeniedBanner />
))}
</TBody>
<TFoot>
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
<Td className="sticky left-0 z-10 border-0 bg-mineshaft-800 p-0">
<div
className="w-full border-t border-r border-mineshaft-600"
style={{ height: "45px" }}
/>
</Td>
{userAvailableEnvs.map(({ name, slug }) => (
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0">
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2">
<Button
size="xs"
variant="outline_bg"
isFullWidth
onClick={() => handleExploreEnvClick(slug)}
>
Explore
</Button>
</div>
</Td>
))}
</Tr>
</TFoot>
</Table>
</TableContainer>
</div> </div>
</div> <CreateSecretForm
secretPath={secretPath}
isOpen={popUp.addSecretsInAllEnvs.isOpen}
getSecretByKey={getSecretByKey}
onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
decryptFileKey={latestFileKey!}
/>
<Modal
isOpen={popUp.addFolder.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
>
<ModalContent title="Create Folder">
<FolderForm onCreateFolder={handleFolderCreate} />
</ModalContent>
</Modal>
</>
); );
}; };
@@ -0,0 +1,222 @@
import { Controller, useForm } from "react-hook-form";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
Checkbox,
FormControl,
FormLabel,
Input,
Modal,
ModalContent,
SecretInput,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { DecryptedSecret, UserWsKeyPair } from "@app/hooks/api/types";
const typeSchema = z
.object({
key: z.string().min(1, "Key is required"),
value: z.string().optional(),
environments: z.record(z.boolean().optional())
})
.refine((data) => data.key !== undefined, {
message: "Please enter secret name"
});
type TFormSchema = z.infer<typeof typeSchema>;
type Props = {
secretPath?: string;
decryptFileKey: UserWsKeyPair;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
// modal props
isOpen?: boolean;
onClose: () => void;
onTogglePopUp: (isOpen: boolean) => void;
};
export const CreateSecretForm = ({
secretPath = "/",
decryptFileKey,
isOpen,
getSecretByKey,
onClose,
onTogglePopUp
}: Props) => {
const {
register,
handleSubmit,
control,
reset,
watch,
formState: { isSubmitting, errors }
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
const newSecretKey = watch("key");
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const environments = currentWorkspace?.environments || [];
const { createNotification } = useNotificationContext();
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: createFolder } = useCreateFolder();
const handleFormSubmit = async ({ key, value, environments: selectedEnv }: TFormSchema) => {
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]);
const isEnvironmentsSelected = environmentsSelected.length;
if (!isEnvironmentsSelected) {
createNotification({ type: "error", text: "Select at least one environment" });
return;
}
const promises = environmentsSelected.map(async (env) => {
const environment = env.slug;
// create folder if not existing
if (secretPath !== "/") {
// /hello/world -> [hello","world"]
const pathSegment = secretPath.split("/").filter(Boolean);
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
const folderName = pathSegment.at(-1);
if (folderName && parentPath) {
await createFolder({
projectId: workspaceId,
path: parentPath,
environment,
name: folderName
});
}
}
const isEdit = getSecretByKey(environment, key) !== undefined;
if (isEdit) {
return updateSecretV3({
environment,
workspaceId,
secretPath,
secretName: key,
secretValue: value || "",
type: "shared",
latestFileKey: decryptFileKey
});
}
return createSecretV3({
environment,
workspaceId,
secretPath,
secretName: key,
secretValue: value || "",
secretComment: "",
type: "shared",
latestFileKey: decryptFileKey
});
});
const results = await Promise.allSettled(promises);
const isSecretsAdded = results.some((result) => result.status === "fulfilled");
if (isSecretsAdded) {
createNotification({
type: "success",
text: "Secrets created successfully"
});
onClose();
reset();
} else {
createNotification({
type: "error",
text: "Failed to create secrets"
});
}
};
return (
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
<ModalContent
className="max-h-[80vh] overflow-y-auto"
title="Bulk Create & Update"
subTitle="Create & update a secret across many environments"
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
<Input
{...register("key")}
placeholder="Type your secret name"
autoCapitalization={currentWorkspace?.autoCapitalization}
/>
</FormControl>
<Controller
control={control}
name="value"
render={({ field }) => (
<FormControl
label="Value"
isError={Boolean(errors?.value)}
errorText={errors?.value?.message}
>
<SecretInput
{...field}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
<FormLabel label="Environments" className="mb-2" />
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto ">
{environments.map((env) => {
return (
<Controller
name={`environments.${env.slug}`}
key={`secret-input-${env.slug}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`secret-input-${env.slug}`}
>
{env.name}
{getSecretByKey(env.slug, newSecretKey) && (
<Tooltip content="Secret exists. Will be overwritten">
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
</Tooltip>
)}
</Checkbox>
)}
/>
);
})}
</div>
<div className="mt-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="mr-4"
type="submit"
>
Create Secret
</Button>
<Button
key="layout-cancel-create-project"
onClick={onClose}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1 @@
export { CreateSecretForm } from "./CreateSecretForm"
@@ -15,6 +15,7 @@ import { useToggle } from "@app/hooks";
import { DecryptedSecret } from "@app/hooks/api/secrets/types"; import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { SecretEditRow } from "./SecretEditRow"; import { SecretEditRow } from "./SecretEditRow";
import SecretRenameRow from "./SecretRenameRow";
type Props = { type Props = {
secretKey: string; secretKey: string;
@@ -105,6 +106,13 @@ export const SecretOverviewTableRow = ({
width: `calc(${expandableColWidth}px - 1rem)` width: `calc(${expandableColWidth}px - 1rem)`
}} }}
> >
<SecretRenameRow
secretKey={secretKey}
environments={environments}
secretPath={secretPath}
getSecretByKey={getSecretByKey}
/>
<TableContainer> <TableContainer>
<table className="secret-table"> <table className="secret-table">
<thead> <thead>
@@ -0,0 +1,258 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCheck, faClose, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetUserWsKey, useUpdateSecretV3 } from "@app/hooks/api";
import { DecryptedSecret } from "@app/hooks/api/types";
import { SecretActionType } from "@app/views/SecretMainPage/components/SecretListView/SecretListView.utils";
type Props = {
secretKey: string;
secretPath: string;
environments: { name: string; slug: string }[];
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
};
export const formSchema = z.object({
key: z.string().trim().min(1, { message: "Secret key is required" })
});
type TFormSchema = z.infer<typeof formSchema>;
function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }: Props) {
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const { createNotification } = useNotificationContext();
const secrets = environments.map((env) => getSecretByKey(env.slug, secretKey));
const isReadOnly = environments.some((env) => {
const environment = env.slug;
const isSecretInEnvReadOnly =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
) &&
permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
if (isSecretInEnvReadOnly) {
return true;
}
return false;
});
const isOverriden = secrets.some(
(secret) =>
secret?.overrideAction === SecretActionType.Created ||
secret?.overrideAction === SecretActionType.Modified
);
const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const [isSecNameCopied, setIsSecNameCopied] = useToggle(false);
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const {
handleSubmit,
control,
reset,
trigger,
getValues,
formState: { isDirty, isSubmitting, errors }
} = useForm<TFormSchema>({
defaultValues: { key: secretKey },
values: { key: secretKey },
resolver: zodResolver(formSchema)
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isSecNameCopied) {
timer = setTimeout(() => setIsSecNameCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isSecNameCopied]);
const handleFormSubmit = async (data: TFormSchema) => {
if (!data.key) {
createNotification({
type: "error",
text: "Secret name cannot be empty"
});
return;
}
const promises = secrets
.filter((secret) => !!secret)
.map((secret) => {
if (!secret) return null;
return updateSecretV3({
environment: secret?.env,
workspaceId,
secretPath,
secretName: secret.key,
secretId: secret.id,
secretValue: secret.value || "",
type: "shared",
latestFileKey: decryptFileKey!,
tags: secret.tags.map((tag) => tag.id),
secretComment: secret.comment,
secretReminderRepeatDays: secret.reminderRepeatDays,
secretReminderNote: secret.reminderNote,
skipMultilineEncoding: secret.skipMultilineEncoding,
newSecretName: data.key
});
});
await Promise.all(promises)
.then(() => {
createNotification({
type: "success",
text: "Successfully renamed the secret"
});
})
.catch(() => {
createNotification({
type: "error",
text: "Error renaming the secret"
});
});
};
const copyTokenToClipboard = () => {
const [key] = getValues(["key"]);
navigator.clipboard.writeText(key as string);
setIsSecNameCopied.on();
};
return (
<form
onSubmit={handleSubmit(handleFormSubmit)}
className="secret-table relative mb-2 flex w-full flex-row items-center justify-between overflow-hidden rounded-lg border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter"
>
<div className="flex h-11 flex-1 flex-shrink-0 items-center">
<span className="flex h-full min-w-[11rem] items-center justify-start border-r-2 border-mineshaft-600 px-4">
Key
</span>
<Controller
name="key"
control={control}
render={({ field, fieldState: { error } }) => (
<Input
autoComplete="off"
isReadOnly={isReadOnly}
autoCapitalization={currentWorkspace?.autoCapitalization}
variant="plain"
isDisabled={isOverriden}
placeholder={error?.message}
onKeyUp={() => trigger("key")}
isError={Boolean(error)}
{...field}
className="w-full px-2 placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
/>
)}
/>
</div>
{isReadOnly || isOverriden ? (
<span className="mr-5 rounded-md bg-mineshaft-500 px-2">Read Only</span>
) : (
<div className="group flex w-20 items-center justify-center border-l border-mineshaft-500 py-1">
<AnimatePresence exitBeforeEnter>
{!isDirty ? (
<motion.div
key="options"
className="flex flex-shrink-0 items-center space-x-4 px-3"
initial={{ x: 0, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 10, opacity: 0 }}
>
<Tooltip content="Copy secret">
<IconButton
ariaLabel="copy-value"
variant="plain"
size="sm"
className="p-0 opacity-0 group-hover:opacity-100"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isSecNameCopied ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</motion.div>
) : (
<motion.div
key="options-save"
className="flex flex-shrink-0 items-center space-x-4 px-3"
initial={{ x: -10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -10, opacity: 0 }}
>
<Tooltip content={errors.key ? errors.key.message : "Save"}>
<IconButton
ariaLabel="more"
variant="plain"
type="submit"
size="md"
className={twMerge(
"p-0 text-primary opacity-0 group-hover:opacity-100",
isDirty && "opacity-100"
)}
isDisabled={isSubmitting || Boolean(errors.key)}
>
{isSubmitting ? (
<Spinner className="m-0 h-4 w-4 p-0" />
) : (
<FontAwesomeIcon
icon={faCheck}
size="lg"
className={twMerge("text-primary", errors.key && "text-mineshaft-400")}
/>
)}
</IconButton>
</Tooltip>
<Tooltip content="Cancel">
<IconButton
ariaLabel="more"
variant="plain"
size="md"
className={twMerge(
"p-0 opacity-0 group-hover:opacity-100",
isDirty && "opacity-100"
)}
onClick={() => reset()}
isDisabled={isSubmitting}
>
<FontAwesomeIcon icon={faClose} size="lg" />
</IconButton>
</Tooltip>
</motion.div>
)}
</AnimatePresence>
</div>
)}
</form>
);
}
export default SecretRenameRow;
@@ -19,7 +19,8 @@ import { SSOModal } from "./SSOModal";
const ssoAuthProviderMap: { [key: string]: string } = { const ssoAuthProviderMap: { [key: string]: string } = {
"okta-saml": "Okta SAML", "okta-saml": "Okta SAML",
"azure-saml": "Azure SAML", "azure-saml": "Azure SAML",
"jumpcloud-saml": "JumpCloud SAML" "jumpcloud-saml": "JumpCloud SAML",
"google-saml": "Google SAML"
}; };
export const OrgSSOSection = (): JSX.Element => { export const OrgSSOSection = (): JSX.Element => {
@@ -21,13 +21,15 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
enum AuthProvider { enum AuthProvider {
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml" JUMPCLOUD_SAML = "jumpcloud-saml",
GOOGLE_SAML = "google-saml"
} }
const ssoAuthProviders = [ const ssoAuthProviders = [
{ label: "Okta SAML", value: AuthProvider.OKTA_SAML }, { label: "Okta SAML", value: AuthProvider.OKTA_SAML },
{ label: "Azure SAML", value: AuthProvider.AZURE_SAML }, { label: "Azure SAML", value: AuthProvider.AZURE_SAML },
{ label: "JumpCloud SAML", value: AuthProvider.JUMPCLOUD_SAML } { label: "JumpCloud SAML", value: AuthProvider.JUMPCLOUD_SAML },
{ label: "Google SAML", value: AuthProvider.GOOGLE_SAML }
]; ];
const schema = yup const schema = yup
@@ -140,7 +142,15 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
issuer: "IdP Entity ID", issuer: "IdP Entity ID",
issuerPlaceholder: "xxx" issuerPlaceholder: "xxx"
}; };
case AuthProvider.GOOGLE_SAML:
return {
acsUrl: "ACS URL",
entityId: "SP Entity ID",
entryPoint: "SSO URL",
entryPointPlaceholder: "https://accounts.google.com/o/saml2/idp?idpid=xxx",
issuer: "IdP Entity ID",
issuerPlaceholder: "https://accounts.google.com/o/saml2/idp?idpid=xxx"
};
default: default:
return { return {
acsUrl: "ACS URL", acsUrl: "ACS URL",
@@ -0,0 +1,52 @@
import { useCallback } from "react";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button } from "@app/components/v2";
import { useToggle } from "@app/hooks";
type Props = {
value: string;
hoverText: string;
notificationText: string;
children: React.ReactNode;
};
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
const { createNotification } = useNotificationContext();
const copyToClipboard = useCallback(() => {
if (isProjectIdCopied) {
return;
}
setIsProjectIdCopied.on();
navigator.clipboard.writeText(value);
createNotification({
text: notificationText,
type: "success"
});
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
// eslint-disable-next-line consistent-return
return () => clearTimeout(timer);
}, [isProjectIdCopied]);
return (
<Button
colorSchema="secondary"
className="group relative"
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
onClick={copyToClipboard}
>
{children}
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 py-2 px-3 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{hoverText}
</span>
</Button>
);
};
@@ -1,7 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
@@ -9,9 +7,10 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { useRenameWorkspace } from "@app/hooks/api"; import { useRenameWorkspace } from "@app/hooks/api";
import { CopyButton } from "./CopyButton";
const formSchema = yup.object({ const formSchema = yup.object({
name: yup.string().required().label("Project Name") name: yup.string().required().label("Project Name")
}); });
@@ -22,7 +21,6 @@ export const ProjectNameChangeSection = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { mutateAsync, isLoading } = useRenameWorkspace(); const { mutateAsync, isLoading } = useRenameWorkspace();
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: yupResolver(formSchema) }); const { handleSubmit, control, reset } = useForm<FormData>({ resolver: yupResolver(formSchema) });
@@ -34,16 +32,6 @@ export const ProjectNameChangeSection = () => {
} }
}, [currentWorkspace]); }, [currentWorkspace]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isProjectIdCopied) {
timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [setIsProjectIdCopied]);
const onFormSubmit = async ({ name }: FormData) => { const onFormSubmit = async ({ name }: FormData) => {
try { try {
if (!currentWorkspace?.id) return; if (!currentWorkspace?.id) return;
@@ -66,35 +54,28 @@ export const ProjectNameChangeSection = () => {
} }
}; };
const copyProjectIdToClipboard = () => {
navigator.clipboard.writeText(currentWorkspace?.id || "");
setIsProjectIdCopied.on();
createNotification({
text: "Copied Project ID to clipboard",
type: "success"
});
}
return ( return (
<form <form
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4" className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
> >
<div className="flex justify-betweens"> <div className="justify-betweens flex">
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Project Name</h2> <h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Name</h2>
<div> <div className="space-x-2">
<Button <CopyButton
colorSchema="secondary" value={currentWorkspace?.slug || ""}
className="group relative" hoverText="Click to project slug"
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />} notificationText="Copied project slug to clipboard"
onClick={copyProjectIdToClipboard} >
Copy Project Slug
</CopyButton>
<CopyButton
value={currentWorkspace?.id || ""}
hoverText="Click to project ID"
notificationText="Copied project ID to clipboard"
> >
Copy Project ID Copy Project ID
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn"> </CopyButton>
Click to copy
</span>
</Button>
</div> </div>
</div> </div>