mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-22 10:12:15 +00:00
Compare commits
45 Commits
daniel/rub
...
daniel/key
Author | SHA1 | Date | |
---|---|---|---|
|
c8b93e4467 | ||
|
891cb06de0 | ||
|
02e8f20cbf | ||
|
d5f4ce4376 | ||
|
85653a90d5 | ||
|
879ef2c178 | ||
|
8777cfe680 | ||
|
2b630f75aa | ||
|
91cee20cc8 | ||
|
4249ec6030 | ||
|
e7a95e6af2 | ||
|
a9f04a3c1f | ||
|
3d380710ee | ||
|
2177ec6bcc | ||
|
070eb2aacd | ||
|
e619cfa313 | ||
|
c3038e3ca1 | ||
|
ff0e7feeee | ||
|
b39b5bd1a1 | ||
|
b3d9d91b52 | ||
|
f29862eaf2 | ||
|
7cb174b644 | ||
|
e30a0fe8be | ||
|
6e6f0252ae | ||
|
2348df7a4d | ||
|
962cf67dfb | ||
|
32627c20c4 | ||
|
c50f8fd78c | ||
|
977ce09245 | ||
|
08d7dead8c | ||
|
a30e06e392 | ||
|
23f3f09cb6 | ||
|
5cd0f665fa | ||
|
443e76c1df | ||
|
4ea22b6761 | ||
|
ae7e0d0963 | ||
|
ed6c6d54c0 | ||
|
428ff5186f | ||
|
d07b0d20d6 | ||
|
8e373fe9bf | ||
|
28087cdcc4 | ||
|
dcef49950d | ||
|
02eea4d886 | ||
|
d12144a7e7 | ||
|
5fa69235d1 |
13
.github/workflows/build-binaries.yml
vendored
13
.github/workflows/build-binaries.yml
vendored
@@ -14,7 +14,6 @@ defaults:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
@@ -24,6 +23,7 @@ jobs:
|
||||
target: node20-linux
|
||||
- os: win
|
||||
target: node20-win
|
||||
runs-on: ${{ (matrix.arch == 'arm64' && matrix.os == 'linux') && 'ubuntu24-arm64' || 'ubuntu-latest' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -49,9 +49,9 @@ jobs:
|
||||
- name: Package into node binary
|
||||
run: |
|
||||
if [ "${{ matrix.os }}" != "linux" ]; then
|
||||
pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
|
||||
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
|
||||
else
|
||||
pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
|
||||
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
|
||||
fi
|
||||
|
||||
# Set up .deb package structure (Debian/Ubuntu only)
|
||||
@@ -84,7 +84,12 @@ jobs:
|
||||
mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
with:
|
||||
python-version: "3.x" # Specify the Python version you need
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade cloudsmith-cli
|
||||
|
||||
# Publish .deb file to Cloudsmith (Debian/Ubuntu only)
|
||||
- name: Publish to Cloudsmith (Debian/Ubuntu)
|
||||
|
@@ -107,7 +107,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
name: z.string().trim().optional(),
|
||||
description: z.string().trim().optional(),
|
||||
permissions: z.any().array()
|
||||
permissions: z.any().array().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -425,6 +425,21 @@ export const PROJECTS = {
|
||||
},
|
||||
LIST_INTEGRATION_AUTHORIZATION: {
|
||||
workspaceId: "The ID of the project to list integration auths for."
|
||||
},
|
||||
LIST_CAS: {
|
||||
slug: "The slug of the project to list CAs for.",
|
||||
status: "The status of the CA to filter by.",
|
||||
friendlyName: "The friendly name of the CA to filter by.",
|
||||
commonName: "The common name of the CA to filter by.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th CA.",
|
||||
limit: "The number of CAs to return."
|
||||
},
|
||||
LIST_CERTIFICATES: {
|
||||
slug: "The slug of the project to list certificates for.",
|
||||
friendlyName: "The friendly name of the certificate to filter by.",
|
||||
commonName: "The common name of the certificate to filter by.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th certificate.",
|
||||
limit: "The number of certificates to return."
|
||||
}
|
||||
} as const;
|
||||
|
||||
|
@@ -317,10 +317,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema.describe("The slug of the project to list CAs.")
|
||||
slug: slugSchema.describe(PROJECTS.LIST_CAS.slug)
|
||||
}),
|
||||
querystring: z.object({
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional()
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status),
|
||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CAS.friendlyName),
|
||||
commonName: z.string().optional().describe(PROJECTS.LIST_CAS.commonName),
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CAS.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CAS.limit)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -336,11 +340,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
orgId: req.permission.orgId,
|
||||
type: ProjectFilterType.SLUG
|
||||
},
|
||||
status: req.query.status,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type
|
||||
actor: req.permission.type,
|
||||
...req.query
|
||||
});
|
||||
return { cas };
|
||||
}
|
||||
@@ -354,11 +358,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema.describe("The slug of the project to list certificates.")
|
||||
slug: slugSchema.describe(PROJECTS.LIST_CERTIFICATES.slug)
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).max(100).default(0),
|
||||
limit: z.coerce.number().min(1).max(100).default(25)
|
||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
||||
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -8,19 +8,35 @@ export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
|
||||
export const certificateDALFactory = (db: TDbClient) => {
|
||||
const certificateOrm = ormify(db, TableName.Certificate);
|
||||
|
||||
const countCertificatesInProject = async (projectId: string) => {
|
||||
const countCertificatesInProject = async ({
|
||||
projectId,
|
||||
friendlyName,
|
||||
commonName
|
||||
}: {
|
||||
projectId: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
}) => {
|
||||
try {
|
||||
interface CountResult {
|
||||
count: string;
|
||||
}
|
||||
|
||||
const count = await db
|
||||
let query = db
|
||||
.replicaNode()(TableName.Certificate)
|
||||
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
|
||||
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
|
||||
.where(`${TableName.Project}.id`, projectId)
|
||||
.count("*")
|
||||
.first();
|
||||
.where(`${TableName.Project}.id`, projectId);
|
||||
|
||||
if (friendlyName) {
|
||||
query = query.andWhere(`${TableName.Certificate}.friendlyName`, friendlyName);
|
||||
}
|
||||
|
||||
if (commonName) {
|
||||
query = query.andWhere(`${TableName.Certificate}.commonName`, commonName);
|
||||
}
|
||||
|
||||
const count = await query.count("*").first();
|
||||
|
||||
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||
} catch (error) {
|
||||
|
@@ -575,6 +575,10 @@ export const projectServiceFactory = ({
|
||||
*/
|
||||
const listProjectCas = async ({
|
||||
status,
|
||||
friendlyName,
|
||||
commonName,
|
||||
limit = 25,
|
||||
offset = 0,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
@@ -596,10 +600,15 @@ export const projectServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const cas = await certificateAuthorityDAL.find({
|
||||
projectId: project.id,
|
||||
...(status && { status })
|
||||
});
|
||||
const cas = await certificateAuthorityDAL.find(
|
||||
{
|
||||
projectId: project.id,
|
||||
...(status && { status }),
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
},
|
||||
{ offset, limit, sort: [["updatedAt", "desc"]] }
|
||||
);
|
||||
|
||||
return cas;
|
||||
};
|
||||
@@ -608,8 +617,10 @@ export const projectServiceFactory = ({
|
||||
* Return list of certificates for project
|
||||
*/
|
||||
const listProjectCertificates = async ({
|
||||
offset,
|
||||
limit,
|
||||
limit = 25,
|
||||
offset = 0,
|
||||
friendlyName,
|
||||
commonName,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
@@ -634,12 +645,18 @@ export const projectServiceFactory = ({
|
||||
{
|
||||
$in: {
|
||||
caId: cas.map((ca) => ca.id)
|
||||
}
|
||||
},
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
},
|
||||
{ offset, limit, sort: [["updatedAt", "desc"]] }
|
||||
);
|
||||
|
||||
const count = await certificateDAL.countCertificatesInProject(project.id);
|
||||
const count = await certificateDAL.countCertificatesInProject({
|
||||
projectId: project.id,
|
||||
friendlyName,
|
||||
commonName
|
||||
});
|
||||
|
||||
return {
|
||||
certificates,
|
||||
|
@@ -89,6 +89,10 @@ export type AddUserToWsDTO = {
|
||||
|
||||
export type TListProjectCasDTO = {
|
||||
status?: CaStatus;
|
||||
friendlyName?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
commonName?: string;
|
||||
filter: Filter;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -96,4 +100,6 @@ export type TListProjectCertsDTO = {
|
||||
filter: Filter;
|
||||
offset: number;
|
||||
limit: number;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@@ -5,6 +5,7 @@ import handlebars from "handlebars";
|
||||
import { createTransport } from "nodemailer";
|
||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
export type TSmtpConfig = SMTPTransport.Options;
|
||||
@@ -12,7 +13,7 @@ export type TSmtpSendMail = {
|
||||
template: SmtpTemplates;
|
||||
subjectLine: string;
|
||||
recipients: string[];
|
||||
substitutions: unknown;
|
||||
substitutions: object;
|
||||
};
|
||||
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
|
||||
|
||||
@@ -47,9 +48,11 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
||||
const isSmtpOn = Boolean(cfg.host);
|
||||
|
||||
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
|
||||
const appCfg = getConfig();
|
||||
const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8");
|
||||
const temp = handlebars.compile(html);
|
||||
const htmlToSend = temp(substitutions);
|
||||
const htmlToSend = temp({ isCloud: appCfg.isCloud, siteUrl: appCfg.SITE_URL, ...substitutions });
|
||||
|
||||
if (isSmtpOn) {
|
||||
await smtp.sendMail({
|
||||
from: cfg.from,
|
||||
|
@@ -1,19 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>MFA Code</title>
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>Sign in attempt requires further verification</h2>
|
||||
<p>Your MFA code is below — enter it where you started signing in to Infisical.</p>
|
||||
<h2>{{code}}</h2>
|
||||
<p>The MFA code will be valid for 2 minutes.</p>
|
||||
<p>Not you? Contact Infisical or your administrator immediately.</p>
|
||||
</body>
|
||||
<p>Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -9,12 +9,12 @@
|
||||
|
||||
<body>
|
||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3>
|
||||
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
|
||||
<p>If these are production secrets, please rotate them immediately.</p>
|
||||
|
||||
<p>Once you have taken action, be sure to update the status of the risk in your <a
|
||||
href="https://app.infisical.com/">Infisical
|
||||
href="{{siteUrl}}">Infisical
|
||||
dashboard</a>.</p>
|
||||
</body>
|
||||
|
||||
|
@@ -1,19 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Successful login for {{email}} from new device</title>
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<p>We're verifying a recent login for {{email}}:</p>
|
||||
<p><strong>Timestamp</strong>: {{timestamp}}</p>
|
||||
<p><strong>IP address</strong>: {{ip}}</p>
|
||||
<p><strong>User agent</strong>: {{userAgent}}</p>
|
||||
<p>If you believe that this login is suspicious, please contact Infisical or reset your password immediately.</p>
|
||||
</body>
|
||||
<p>If you believe that this login is suspicious, please contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} or reset your password immediately.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -9,6 +9,6 @@
|
||||
<h2>Reset your password</h2>
|
||||
<p>Someone requested a password reset.</p>
|
||||
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
|
||||
<p>If you didn't initiate this request, please contact us immediately at team@infisical.com</p>
|
||||
<p>If you didn't initiate this request, please contact {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
|
||||
</body>
|
||||
</html>
|
@@ -9,7 +9,7 @@
|
||||
|
||||
<body>
|
||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
|
||||
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
|
||||
by {{pusher_name}} ({{pusher_email}}). If
|
||||
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment
|
||||
@@ -18,7 +18,7 @@
|
||||
<p>If these are production secrets, please rotate them immediately.</p>
|
||||
|
||||
<p>Once you have taken action, be sure to update the status of the risk in your <a
|
||||
href="https://app.infisical.com/">Infisical
|
||||
href="{{siteUrl}}">Infisical
|
||||
dashboard</a>.</p>
|
||||
</body>
|
||||
|
||||
|
@@ -11,7 +11,7 @@
|
||||
<h2>Confirm your email address</h2>
|
||||
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
|
||||
<h1>{{code}}</h1>
|
||||
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
|
||||
<p>Questions about setting up Infisical? {{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/srp"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/manifoldco/promptui"
|
||||
@@ -205,6 +204,7 @@ var loginCmd = &cobra.Command{
|
||||
if !overrideDomain {
|
||||
domainQuery = false
|
||||
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE)
|
||||
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", strings.TrimSuffix(config.INFISICAL_URL, "/api"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -713,7 +713,7 @@ func askForMFACode() string {
|
||||
return mfaVerifyCode
|
||||
}
|
||||
|
||||
func askToPasteJwtToken(stdin *readline.CancelableStdin, success chan models.UserCredentials, failure chan error) {
|
||||
func askToPasteJwtToken(success chan models.UserCredentials, failure chan error) {
|
||||
time.Sleep(time.Second * 5)
|
||||
fmt.Println("\n\nOnce login is completed via browser, the CLI should be authenticated automatically.")
|
||||
fmt.Println("However, if browser fails to communicate with the CLI, please paste the token from the browser below.")
|
||||
@@ -807,26 +807,22 @@ func browserCliLogin() (models.UserCredentials, error) {
|
||||
|
||||
log.Debug().Msgf("Callback server listening on port %d", callbackPort)
|
||||
|
||||
stdin := readline.NewCancelableStdin(os.Stdin)
|
||||
go http.Serve(listener, corsHandler)
|
||||
go askToPasteJwtToken(stdin, success, failure)
|
||||
go askToPasteJwtToken(success, failure)
|
||||
|
||||
for {
|
||||
select {
|
||||
case loginResponse := <-success:
|
||||
_ = closeListener(&listener)
|
||||
_ = stdin.Close()
|
||||
fmt.Println("Browser login successful")
|
||||
return loginResponse, nil
|
||||
|
||||
case err := <-failure:
|
||||
serverErr := closeListener(&listener)
|
||||
stdErr := stdin.Close()
|
||||
return models.UserCredentials{}, errors.Join(err, serverErr, stdErr)
|
||||
return models.UserCredentials{}, errors.Join(err, serverErr)
|
||||
|
||||
case <-timeout:
|
||||
_ = closeListener(&listener)
|
||||
_ = stdin.Close()
|
||||
return models.UserCredentials{}, errors.New("server timeout")
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -13,53 +14,56 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"}
|
||||
var AvailableVaults = []string{"auto", "file"}
|
||||
type VaultBackendType struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
var AvailableVaults = []VaultBackendType{
|
||||
{
|
||||
Name: "auto",
|
||||
Description: "automatically select the system keyring",
|
||||
},
|
||||
{
|
||||
Name: "file",
|
||||
Description: "encrypted file vault",
|
||||
},
|
||||
}
|
||||
|
||||
var vaultSetCmd = &cobra.Command{
|
||||
Example: `infisical vault set pass`,
|
||||
Use: "set [vault-name]",
|
||||
Short: "Used to set the vault backend to store your login details securely at rest",
|
||||
Example: `infisical vault set file --passphrase <your-passphrase>`,
|
||||
Use: "set [file|auto] [flags]",
|
||||
Short: "Used to configure the vault backends",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
wantedVaultTypeName := args[0]
|
||||
currentVaultBackend, err := util.GetCurrentVaultBackend()
|
||||
|
||||
vaultType := args[0]
|
||||
|
||||
passphrase, err := cmd.Flags().GetString("passphrase")
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
|
||||
util.HandleError(err, "Unable to get passphrase flag")
|
||||
}
|
||||
|
||||
if vaultType == util.VAULT_BACKEND_FILE_MODE && passphrase != "" {
|
||||
setFileVaultPassphrase(passphrase)
|
||||
return
|
||||
}
|
||||
|
||||
if wantedVaultTypeName == string(currentVaultBackend) {
|
||||
log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend)
|
||||
return
|
||||
}
|
||||
|
||||
if wantedVaultTypeName == "auto" || wantedVaultTypeName == "file" {
|
||||
configFile, err := util.GetConfigFile()
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
|
||||
return
|
||||
}
|
||||
|
||||
configFile.VaultBackendType = wantedVaultTypeName // save selected vault
|
||||
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login
|
||||
|
||||
err = util.WriteConfigFile(&configFile)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName)
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
|
||||
} else {
|
||||
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(AvailableVaults, ", "))
|
||||
}
|
||||
util.PrintWarning("This command has been deprecated. Please use 'infisical vault use [file|auto]' to select which vault to use.\n")
|
||||
selectVaultTypeCmd(cmd, args)
|
||||
},
|
||||
}
|
||||
|
||||
var vaultUseCmd = &cobra.Command{
|
||||
Example: `infisical vault use [file|auto]`,
|
||||
Use: "use [file|auto]",
|
||||
Short: "Used to select the the type of vault backend to store sensitive data securely at rest",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: selectVaultTypeCmd,
|
||||
}
|
||||
|
||||
// runCmd represents the run command
|
||||
var vaultCmd = &cobra.Command{
|
||||
Use: "vault",
|
||||
@@ -71,10 +75,30 @@ var vaultCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func setFileVaultPassphrase(passphrase string) {
|
||||
configFile, err := util.GetConfigFile()
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err)
|
||||
return
|
||||
}
|
||||
|
||||
// encode with base64
|
||||
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase))
|
||||
configFile.VaultBackendPassphrase = encodedPassphrase
|
||||
|
||||
err = util.WriteConfigFile(&configFile)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err)
|
||||
return
|
||||
}
|
||||
|
||||
util.PrintSuccessMessage("\nSuccessfully, set passphrase for file vault.\n")
|
||||
}
|
||||
|
||||
func printAvailableVaultBackends() {
|
||||
fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:")
|
||||
for _, backend := range AvailableVaultsAndDescriptions {
|
||||
fmt.Printf("\n- %s", backend)
|
||||
for _, vaultType := range AvailableVaults {
|
||||
fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description)
|
||||
}
|
||||
|
||||
currentVaultBackend, err := util.GetCurrentVaultBackend()
|
||||
@@ -87,7 +111,53 @@ func printAvailableVaultBackends() {
|
||||
fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend))
|
||||
}
|
||||
|
||||
func selectVaultTypeCmd(cmd *cobra.Command, args []string) {
|
||||
wantedVaultTypeName := args[0]
|
||||
currentVaultBackend, err := util.GetCurrentVaultBackend()
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
|
||||
return
|
||||
}
|
||||
|
||||
if wantedVaultTypeName == string(currentVaultBackend) {
|
||||
log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend)
|
||||
return
|
||||
}
|
||||
|
||||
if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE {
|
||||
configFile, err := util.GetConfigFile()
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
|
||||
return
|
||||
}
|
||||
|
||||
configFile.VaultBackendType = wantedVaultTypeName // save selected vault
|
||||
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login
|
||||
|
||||
err = util.WriteConfigFile(&configFile)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName)
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
|
||||
} else {
|
||||
var availableVaultsNames []string
|
||||
for _, vault := range AvailableVaults {
|
||||
availableVaultsNames = append(availableVaultsNames, vault.Name)
|
||||
}
|
||||
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
vaultSetCmd.Flags().StringP("passphrase", "p", "", "Set the passphrase for the file vault")
|
||||
|
||||
vaultCmd.AddCommand(vaultSetCmd)
|
||||
vaultCmd.AddCommand(vaultUseCmd)
|
||||
|
||||
rootCmd.AddCommand(vaultCmd)
|
||||
}
|
||||
|
@@ -11,10 +11,11 @@ type UserCredentials struct {
|
||||
|
||||
// The file struct for Infisical config file
|
||||
type ConfigFile struct {
|
||||
LoggedInUserEmail string `json:"loggedInUserEmail"`
|
||||
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
|
||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||
LoggedInUserEmail string `json:"loggedInUserEmail"`
|
||||
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
|
||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
|
||||
}
|
||||
|
||||
type LoggedInUser struct {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -50,10 +51,11 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
|
||||
}
|
||||
|
||||
configFile := models.ConfigFile{
|
||||
LoggedInUserEmail: userCredentials.Email,
|
||||
LoggedInUserDomain: config.INFISICAL_URL,
|
||||
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
||||
VaultBackendType: existingConfigFile.VaultBackendType,
|
||||
LoggedInUserEmail: userCredentials.Email,
|
||||
LoggedInUserDomain: config.INFISICAL_URL,
|
||||
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
||||
VaultBackendType: existingConfigFile.VaultBackendType,
|
||||
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
|
||||
}
|
||||
|
||||
configFileMarshalled, err := json.Marshal(configFile)
|
||||
@@ -215,6 +217,14 @@ func GetConfigFile() (models.ConfigFile, error) {
|
||||
return models.ConfigFile{}, err
|
||||
}
|
||||
|
||||
if configFile.VaultBackendPassphrase != "" {
|
||||
decodedPassphrase, err := base64.StdEncoding.DecodeString(configFile.VaultBackendPassphrase)
|
||||
if err != nil {
|
||||
return models.ConfigFile{}, fmt.Errorf("GetConfigFile: Unable to decode base64 passphrase [err=%s]", err)
|
||||
}
|
||||
os.Setenv("INFISICAL_VAULT_FILE_PASSPHRASE", string(decodedPassphrase))
|
||||
}
|
||||
|
||||
return configFile, nil
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,10 @@ const (
|
||||
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
|
||||
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
|
||||
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
|
||||
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.
|
||||
|
||||
VAULT_BACKEND_AUTO_MODE = "auto"
|
||||
VAULT_BACKEND_FILE_MODE = "file"
|
||||
|
||||
// Universal Auth
|
||||
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
@@ -20,16 +23,51 @@ func SetValueInKeyring(key, value string) error {
|
||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
|
||||
}
|
||||
|
||||
return keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
|
||||
err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
|
||||
|
||||
if err != nil {
|
||||
configFile, _ := GetConfigFile()
|
||||
|
||||
if configFile.VaultBackendPassphrase == "" {
|
||||
PrintWarning("System keyring could not be used, falling back to `file` vault for sensitive data storage.")
|
||||
passphrasePrompt := promptui.Prompt{
|
||||
Label: "Enter the passphrase to use for keyring encryption",
|
||||
}
|
||||
passphrase, err := passphrasePrompt.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase))
|
||||
configFile.VaultBackendPassphrase = encodedPassphrase
|
||||
err = WriteConfigFile(&configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We call this function at last to trigger the environment variable to be set
|
||||
GetConfigFile()
|
||||
}
|
||||
|
||||
err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetValueInKeyring(key string) (string, error) {
|
||||
currentVaultBackend, err := GetCurrentVaultBackend()
|
||||
if err != nil {
|
||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
|
||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again")
|
||||
}
|
||||
|
||||
return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
|
||||
value, err := keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
|
||||
|
||||
if err != nil {
|
||||
value, err = keyring.Get(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key)
|
||||
}
|
||||
return value, err
|
||||
|
||||
}
|
||||
|
||||
func DeleteValueInKeyring(key string) error {
|
||||
@@ -38,5 +76,11 @@ func DeleteValueInKeyring(key string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
|
||||
err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
|
||||
|
||||
if err != nil {
|
||||
err = keyring.Delete(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
@@ -11,11 +11,11 @@ func GetCurrentVaultBackend() (string, error) {
|
||||
}
|
||||
|
||||
if configFile.VaultBackendType == "" {
|
||||
return "auto", nil
|
||||
return VAULT_BACKEND_AUTO_MODE, nil
|
||||
}
|
||||
|
||||
if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" {
|
||||
return "auto", nil
|
||||
if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE {
|
||||
return VAULT_BACKEND_AUTO_MODE, nil
|
||||
}
|
||||
|
||||
return configFile.VaultBackendType, nil
|
||||
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/workspace/{slug}/cas"
|
||||
---
|
4
docs/api-reference/endpoints/certificates/list.mdx
Normal file
4
docs/api-reference/endpoints/certificates/list.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/workspace/{slug}/certificates"
|
||||
---
|
@@ -32,6 +32,6 @@ description: "Change the vault type in Infisical"
|
||||
|
||||
To safeguard your login details when using the CLI, Infisical places them in a system vault or an encrypted text file, protected by a passphrase that only the user knows.
|
||||
|
||||
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell</Tip>
|
||||
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, use the `infisical vault set file --passphrase <your-passphrase>` CLI command to specify your password once.</Tip>
|
||||
|
||||
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 297 KiB |
@@ -15,7 +15,7 @@ Prerequisites:
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Copy your [Account ID](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/) from Account > Workers & Pages > Overview
|
||||
|
||||
@@ -35,10 +35,12 @@ Prerequisites:
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
||||
|
||||
</Step>
|
||||
<Step title="Start integration">
|
||||
Select which Infisical environment secrets you want to sync to Cloudflare Workers and press create integration to start syncing secrets.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
</Steps>
|
||||
|
@@ -216,13 +216,6 @@
|
||||
"group": "Self-host Infisical",
|
||||
"pages": [
|
||||
"self-hosting/overview",
|
||||
{
|
||||
"group": "Native installation methods",
|
||||
"pages": [
|
||||
"self-hosting/deployment-options/native/standalone-binary",
|
||||
"self-hosting/deployment-options/native/high-availability"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Containerized installation methods",
|
||||
"pages": [
|
||||
@@ -661,6 +654,7 @@
|
||||
{
|
||||
"group": "Certificate Authorities",
|
||||
"pages": [
|
||||
"api-reference/endpoints/certificate-authorities/list",
|
||||
"api-reference/endpoints/certificate-authorities/create",
|
||||
"api-reference/endpoints/certificate-authorities/read",
|
||||
"api-reference/endpoints/certificate-authorities/update",
|
||||
@@ -676,6 +670,7 @@
|
||||
{
|
||||
"group": "Certificates",
|
||||
"pages": [
|
||||
"api-reference/endpoints/certificates/list",
|
||||
"api-reference/endpoints/certificates/read",
|
||||
"api-reference/endpoints/certificates/revoke",
|
||||
"api-reference/endpoints/certificates/delete",
|
||||
|
@@ -25,6 +25,10 @@ Used to configure platform-specific security and operational settings
|
||||
https://app.infisical.com).
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="PORT" type="int" default="8080" optional>
|
||||
Specifies the internal port on which the application listens.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional>
|
||||
Telemetry helps us improve Infisical but if you want to dsiable it you may set this to `false`.
|
||||
</ParamField>
|
||||
|
@@ -33,7 +33,7 @@ Choose from a number of deployment options listed below to get started.
|
||||
Use our Helm chart to Install Infisical on your Kubernetes cluster.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
<CardGroup cols={2}>
|
||||
{/* <CardGroup cols={2}>
|
||||
<Card
|
||||
title="Native Deployment"
|
||||
color="#000000"
|
||||
@@ -50,4 +50,4 @@ Choose from a number of deployment options listed below to get started.
|
||||
>
|
||||
Install Infisical on your Debian-based instances without containers using our standalone binary with high availability out of the box.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
</CardGroup> */}
|
||||
|
@@ -79,7 +79,7 @@ export const useUpdateOrgRole = () => {
|
||||
data: { role }
|
||||
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
|
||||
...dto,
|
||||
permissions: permissions?.length ? packRules(permissions) : []
|
||||
permissions: permissions?.length ? packRules(permissions) : undefined
|
||||
});
|
||||
|
||||
return role;
|
||||
|
@@ -168,16 +168,27 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
||||
const { token: newJwtToken } = await selectOrganization({ organizationId });
|
||||
|
||||
const instance = axios.create();
|
||||
await instance.post(cliUrl, {
|
||||
const payload = {
|
||||
...isCliLoginSuccessful.loginResponse,
|
||||
JTWToken: newJwtToken
|
||||
};
|
||||
await instance.post(cliUrl, payload).catch(() => {
|
||||
// if error happens to communicate we set the token with an expiry in sessino storage
|
||||
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
|
||||
sessionStorage.setItem(
|
||||
SessionStorageKeys.CLI_TERMINAL_TOKEN,
|
||||
JSON.stringify({
|
||||
expiry: formatISO(addSeconds(new Date(), 30)),
|
||||
data: window.btoa(JSON.stringify(payload))
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await navigateUserToOrg(router, organizationId);
|
||||
router.push("/cli-redirect");
|
||||
return;
|
||||
}
|
||||
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs
|
||||
// if the user has no orgs, navigate to the create org page
|
||||
else {
|
||||
|
||||
const userOrgs = await fetchOrganizations();
|
||||
|
||||
// case: user has orgs, so we navigate the user to select an org
|
||||
@@ -189,7 +200,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
||||
else {
|
||||
await navigateUserToOrg(router);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
const isLoginSuccessful = await attemptLoginMfa({
|
||||
|
@@ -4,6 +4,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import axios from "axios";
|
||||
import { addSeconds, formatISO } from "date-fns";
|
||||
import jwt_decode from "jwt-decode";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -12,6 +13,7 @@ import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { Button, Input, Spinner } from "@app/components/v2";
|
||||
import { SessionStorageKeys } from "@app/const";
|
||||
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
|
||||
@@ -79,11 +81,24 @@ export const PasswordStep = ({
|
||||
if (callbackPort) {
|
||||
console.log("organization id was present. new JWT token to be used in CLI:", newJwtToken);
|
||||
const instance = axios.create();
|
||||
await instance.post(cliUrl, {
|
||||
const payload = {
|
||||
privateKey,
|
||||
email,
|
||||
JTWToken: newJwtToken
|
||||
};
|
||||
await instance.post(cliUrl, payload).catch(() => {
|
||||
// if error happens to communicate we set the token with an expiry in sessino storage
|
||||
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
|
||||
sessionStorage.setItem(
|
||||
SessionStorageKeys.CLI_TERMINAL_TOKEN,
|
||||
JSON.stringify({
|
||||
expiry: formatISO(addSeconds(new Date(), 30)),
|
||||
data: window.btoa(JSON.stringify(payload))
|
||||
})
|
||||
);
|
||||
});
|
||||
router.push("/cli-redirect");
|
||||
return;
|
||||
}
|
||||
|
||||
await navigateUserToOrg(router, organizationId);
|
||||
@@ -165,26 +180,35 @@ export const PasswordStep = ({
|
||||
);
|
||||
|
||||
const instance = axios.create();
|
||||
await instance.post(cliUrl, {
|
||||
const payload = {
|
||||
...isCliLoginSuccessful.loginResponse,
|
||||
JTWToken: newJwtToken
|
||||
};
|
||||
await instance.post(cliUrl, payload).catch(() => {
|
||||
// if error happens to communicate we set the token with an expiry in sessino storage
|
||||
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
|
||||
sessionStorage.setItem(
|
||||
SessionStorageKeys.CLI_TERMINAL_TOKEN,
|
||||
JSON.stringify({
|
||||
expiry: formatISO(addSeconds(new Date(), 30)),
|
||||
data: window.btoa(JSON.stringify(payload))
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await navigateUserToOrg(router, organizationId);
|
||||
router.push("/cli-redirect");
|
||||
return;
|
||||
}
|
||||
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs
|
||||
// if the user has no orgs, navigate to the create org page
|
||||
else {
|
||||
const userOrgs = await fetchOrganizations();
|
||||
const userOrgs = await fetchOrganizations();
|
||||
|
||||
// case: user has orgs, so we navigate the user to select an org
|
||||
if (userOrgs.length > 0) {
|
||||
navigateToSelectOrganization(callbackPort);
|
||||
}
|
||||
// case: no orgs found, so we navigate the user to create an org
|
||||
else {
|
||||
await navigateUserToOrg(router);
|
||||
}
|
||||
// case: user has orgs, so we navigate the user to select an org
|
||||
if (userOrgs.length > 0) {
|
||||
navigateToSelectOrganization(callbackPort);
|
||||
}
|
||||
// case: no orgs found, so we navigate the user to create an org
|
||||
else {
|
||||
await navigateUserToOrg(router);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@@ -1,249 +0,0 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
faArrowLeft,
|
||||
faCog,
|
||||
faContactCard,
|
||||
faMagnifyingGlass,
|
||||
faMoneyBill,
|
||||
faServer,
|
||||
faSignIn,
|
||||
faUser,
|
||||
faUserCog,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useCreateOrgRole, useUpdateOrgRole } from "@app/hooks/api";
|
||||
import { TOrgRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
import {
|
||||
formRolePermission2API,
|
||||
formSchema,
|
||||
rolePermission2Form,
|
||||
TFormSchema
|
||||
} from "./OrgRoleModifySection.utils";
|
||||
import { SimpleLevelPermissionOption } from "./SimpleLevelPermissionOptions";
|
||||
import { WorkspacePermission } from "./WorkspacePermission";
|
||||
|
||||
type Props = {
|
||||
role?: TOrgRole;
|
||||
onGoBack: VoidFunction;
|
||||
};
|
||||
|
||||
const SIMPLE_PERMISSION_OPTIONS = [
|
||||
{
|
||||
title: "User management",
|
||||
subtitle: "Invite, view and remove users from the organization",
|
||||
icon: faUser,
|
||||
formName: "member"
|
||||
},
|
||||
{
|
||||
title: "Group management",
|
||||
subtitle: "Invite, view and remove user groups from the organization",
|
||||
icon: faUsers,
|
||||
formName: "groups"
|
||||
},
|
||||
{
|
||||
title: "Machine identity management",
|
||||
subtitle: "Create, view, update and remove (machine) identities from the organization",
|
||||
icon: faServer,
|
||||
formName: "identity"
|
||||
},
|
||||
{
|
||||
title: "Billing & usage",
|
||||
subtitle: "Modify organization subscription plan",
|
||||
icon: faMoneyBill,
|
||||
formName: "billing"
|
||||
},
|
||||
{
|
||||
title: "Role management",
|
||||
subtitle: "Create, modify and remove organization roles",
|
||||
icon: faUserCog,
|
||||
formName: "role"
|
||||
},
|
||||
{
|
||||
title: "Incident Contacts",
|
||||
subtitle: "Incident contacts management control",
|
||||
icon: faContactCard,
|
||||
formName: "incident-contact"
|
||||
},
|
||||
{
|
||||
title: "Organization profile",
|
||||
subtitle: "View & update organization metadata such as name",
|
||||
icon: faCog,
|
||||
formName: "settings"
|
||||
},
|
||||
{
|
||||
title: "Secret Scanning",
|
||||
subtitle: "Secret scanning management control",
|
||||
icon: faMagnifyingGlass,
|
||||
formName: "secret-scanning"
|
||||
},
|
||||
{
|
||||
title: "SSO",
|
||||
subtitle: "Define organization level SSO requirements",
|
||||
icon: faSignIn,
|
||||
formName: "sso"
|
||||
},
|
||||
{
|
||||
title: "LDAP",
|
||||
subtitle: "Define organization level LDAP requirements",
|
||||
icon: faSignIn,
|
||||
formName: "ldap"
|
||||
},
|
||||
{
|
||||
title: "SCIM",
|
||||
subtitle: "Define organization level SCIM requirements",
|
||||
icon: faUsers,
|
||||
formName: "scim"
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
|
||||
const isNonEditable = ["owner", "admin", "member", "no-access"].includes(role?.slug || "");
|
||||
const isNewRole = !role?.slug;
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { isSubmitting, isDirty, errors },
|
||||
setValue,
|
||||
control
|
||||
} = useForm<TFormSchema>({
|
||||
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const { mutateAsync: createRole } = useCreateOrgRole();
|
||||
const { mutateAsync: updateRole } = useUpdateOrgRole();
|
||||
|
||||
const handleRoleUpdate = async (el: TFormSchema) => {
|
||||
if (!role?.id) return;
|
||||
|
||||
try {
|
||||
await updateRole({
|
||||
orgId,
|
||||
id: role?.id,
|
||||
...el,
|
||||
permissions: formRolePermission2API(el.permissions)
|
||||
});
|
||||
createNotification({ type: "success", text: "Successfully updated role" });
|
||||
onGoBack();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({ type: "error", text: "Failed to update role" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (el: TFormSchema) => {
|
||||
if (!isNewRole) {
|
||||
await handleRoleUpdate(el);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await createRole({
|
||||
orgId,
|
||||
...el,
|
||||
permissions: formRolePermission2API(el.permissions)
|
||||
});
|
||||
createNotification({ type: "success", text: "Created new role" });
|
||||
onGoBack();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({ type: "error", text: "Failed to create role" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-mineshaft-100">
|
||||
{isNewRole ? "New" : "Edit"} Role
|
||||
</h1>
|
||||
<Button
|
||||
onClick={onGoBack}
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">
|
||||
Organization-level roles allow you to define permissions for resources at a high level
|
||||
across the organization
|
||||
</p>
|
||||
<div className="flex flex-col space-y-6">
|
||||
<FormControl
|
||||
label="Name"
|
||||
isRequired
|
||||
className="mb-0"
|
||||
isError={Boolean(errors?.name)}
|
||||
errorText={errors?.name?.message}
|
||||
>
|
||||
<Input {...register("name")} placeholder="Billing Team" isReadOnly={isNonEditable} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Slug"
|
||||
isRequired
|
||||
isError={Boolean(errors?.slug)}
|
||||
errorText={errors?.slug?.message}
|
||||
>
|
||||
<Input {...register("slug")} placeholder="biller" isReadOnly={isNonEditable} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Description"
|
||||
helperText="A short description about this role"
|
||||
isError={Boolean(errors?.description)}
|
||||
errorText={errors?.description?.message}
|
||||
>
|
||||
<Input {...register("description")} isReadOnly={isNonEditable} />
|
||||
</FormControl>
|
||||
<div className="flex items-center justify-between border-t border-t-mineshaft-800 pt-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-medium">Add Permission</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<WorkspacePermission
|
||||
isNonEditable={isNonEditable}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
/>
|
||||
</div>
|
||||
{SIMPLE_PERMISSION_OPTIONS.map(({ title, subtitle, icon, formName }) => (
|
||||
<div key={`permission-${title}`}>
|
||||
<SimpleLevelPermissionOption
|
||||
isNonEditable={isNonEditable}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
icon={icon}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
formName={formName}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-12 flex items-center space-x-4">
|
||||
<Button
|
||||
type="submit"
|
||||
isDisabled={isSubmitting || isNonEditable || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
{isNewRole ? "Create Role" : "Save Role"}
|
||||
</Button>
|
||||
<Button onClick={onGoBack} variant="outline_bg">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1,204 +0,0 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Checkbox, Select, SelectItem } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "./OrgRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
|
||||
isNonEditable?: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
icon: IconProp;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-acess",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ action: "read", label: "View" },
|
||||
{ action: "create", label: "Create" },
|
||||
{ action: "edit", label: "Modify" },
|
||||
{ action: "delete", label: "Remove" }
|
||||
] as const;
|
||||
|
||||
const SECRET_SCANNING_PERMISSIONS = [
|
||||
{ action: "read", label: "View risks" },
|
||||
{ action: "create", label: "Add integrations" },
|
||||
{ action: "edit", label: "Edit risk status" },
|
||||
{ action: "delete", label: "Remove integrations" }
|
||||
] as const;
|
||||
|
||||
const INCIDENT_CONTACTS_PERMISSIONS = [
|
||||
{ action: "read", label: "View contacts" },
|
||||
{ action: "create", label: "Add new contacts" },
|
||||
{ action: "edit", label: "Edit contacts" },
|
||||
{ action: "delete", label: "Remove contacts" }
|
||||
] as const;
|
||||
|
||||
const MEMBERS_PERMISSIONS = [
|
||||
{ action: "read", label: "View all members" },
|
||||
{ action: "create", label: "Invite members" },
|
||||
{ action: "edit", label: "Edit members" },
|
||||
{ action: "delete", label: "Remove members" }
|
||||
] as const;
|
||||
|
||||
const BILLING_PERMISSIONS = [
|
||||
{ action: "read", label: "View bills" },
|
||||
{ action: "create", label: "Add payment methods" },
|
||||
{ action: "edit", label: "Edit payments" },
|
||||
{ action: "delete", label: "Remove payments" }
|
||||
] as const;
|
||||
|
||||
const getPermissionList = (option: Props["formName"]) => {
|
||||
switch (option) {
|
||||
case "secret-scanning":
|
||||
return SECRET_SCANNING_PERMISSIONS;
|
||||
case "billing":
|
||||
return BILLING_PERMISSIONS;
|
||||
case "incident-contact":
|
||||
return INCIDENT_CONTACTS_PERMISSIONS;
|
||||
case "member":
|
||||
return MEMBERS_PERMISSIONS;
|
||||
default:
|
||||
return PERMISSIONS;
|
||||
}
|
||||
};
|
||||
|
||||
export const SimpleLevelPermissionOption = ({
|
||||
isNonEditable,
|
||||
setValue,
|
||||
control,
|
||||
formName,
|
||||
subtitle,
|
||||
title,
|
||||
icon
|
||||
}: Props) => {
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: `permissions.${formName}`
|
||||
});
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
if (score === 1 && rule?.read) return Permission.ReadOnly;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ read: false, edit: false, create: false, delete: false },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ read: true, edit: true, create: true, delete: true },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ read: true, edit: false, create: false, delete: false },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
default:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ read: false, edit: false, create: false, delete: false },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"rounded-md bg-mineshaft-800 px-10 py-6",
|
||||
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={icon} className="text-4xl" />
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="mb-1 text-lg font-medium">{title}</div>
|
||||
<div className="text-xs font-light">{subtitle}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
defaultValue={Permission.NoAccess}
|
||||
isDisabled={isNonEditable}
|
||||
value={selectedPermissionCategory}
|
||||
onValueChange={handlePermissionChange}
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
|
||||
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
|
||||
>
|
||||
{isCustom &&
|
||||
getPermissionList(formName).map(({ action, label }) => (
|
||||
<Controller
|
||||
name={`permissions.${formName}.${action}`}
|
||||
key={`permissions.${formName}.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.${formName}.${action}`}
|
||||
isDisabled={isNonEditable}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -8,7 +8,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import { Checkbox, Select, SelectItem } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "./OrgRoleModifySection.utils";
|
||||
import { TFormSchema } from "../../../../RolePage/components/OrgRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isNonEditable?: boolean;
|
||||
|
@@ -1 +0,0 @@
|
||||
export { OrgRoleModifySection } from "./OrgRoleModifySection";
|
@@ -1,27 +1,9 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TOrgRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
import { OrgRoleModifySection } from "./OrgRoleModifySection";
|
||||
import { OrgRoleTable } from "./OrgRoleTable";
|
||||
|
||||
export const OrgRoleTabSection = () => {
|
||||
const { popUp, handlePopUpClose } = usePopUp(["editRole"] as const);
|
||||
return popUp.editRole.isOpen ? (
|
||||
<motion.div
|
||||
key="role-modify"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<OrgRoleModifySection
|
||||
role={popUp.editRole.data as TOrgRole}
|
||||
onGoBack={() => handlePopUpClose("editRole")}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
return (
|
||||
<motion.div
|
||||
key="role-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
|
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { TFormSchema } from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils";
|
||||
import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ action: "read", label: "View" },
|
||||
|
@@ -2,7 +2,7 @@ import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button , Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
|
||||
import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api";
|
||||
import {
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
formSchema,
|
||||
rolePermission2Form,
|
||||
TFormSchema
|
||||
} from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils";
|
||||
} from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
||||
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
|
||||
|
Reference in New Issue
Block a user