Compare commits

..

45 Commits

Author SHA1 Message Date
Maidul Islam
c8b93e4467 Update doc to show correct command 2024-08-05 13:11:40 -04:00
Daniel Hougaard
891cb06de0 Update keyringwrapper.go 2024-07-31 16:55:53 +02:00
Maidul Islam
02e8f20cbf remove extra : 2024-07-31 03:14:06 +00:00
Daniel Hougaard
d5f4ce4376 Update vault.go 2024-07-30 10:22:15 +02:00
Maidul Islam
85653a90d5 update phrasing 2024-07-29 22:06:03 -04:00
Daniel Hougaard
879ef2c178 Update keyringwrapper.go 2024-07-29 12:37:58 +02:00
Daniel Hougaard
8777cfe680 Update keyringwrapper.go 2024-07-29 12:34:35 +02:00
Daniel Hougaard
2b630f75aa Update keyringwrapper.go 2024-07-29 12:31:02 +02:00
Daniel Hougaard
91cee20cc8 Minor improvemnets 2024-07-29 12:21:38 +02:00
Daniel Hougaard
4249ec6030 Update login.go 2024-07-29 12:21:31 +02:00
Daniel Hougaard
e7a95e6af2 Update login.go 2024-07-29 12:15:53 +02:00
Daniel Hougaard
a9f04a3c1f Update keyringwrapper.go 2024-07-29 12:13:40 +02:00
Daniel Hougaard
3d380710ee Update keyringwrapper.go 2024-07-29 12:10:42 +02:00
Daniel Hougaard
2177ec6bcc Update vault.go 2024-07-29 12:04:34 +02:00
Daniel Hougaard
070eb2aacd Update keyringwrapper.go 2024-07-26 22:47:46 +02:00
Daniel Hougaard
e619cfa313 feat(cli): set persistent file vault password 2024-07-26 22:47:37 +02:00
Daniel Hougaard
c3038e3ca1 docs: passphrase command 2024-07-26 22:47:07 +02:00
Daniel Hougaard
ff0e7feeee feat(cli): CLI Keyring improvements 2024-07-26 19:14:21 +02:00
BlackMagiq
b39b5bd1a1 Merge pull request #2181 from Infisical/patch-org-role-update
Fix updating org role details should not send empty array of permissions
2024-07-26 07:27:51 -07:00
Tuan Dang
b3d9d91b52 Fix updating org role details should not send empty array of permissions 2024-07-26 06:52:21 -07:00
Maidul Islam
f29862eaf2 Merge pull request #2180 from Infisical/list-ca-endpoint-descriptions
Add descriptions for parameters for LIST (GET) CAs / certificates endpoints
2024-07-25 17:59:57 -04:00
Tuan Dang
7cb174b644 Add descriptions for list cas/certs endpoints 2024-07-25 14:53:41 -07:00
BlackMagiq
e30a0fe8be Merge pull request #2178 from Infisical/cert-search-filtering
Add List CAs / Certificates to Documentation + Filter Options
2024-07-25 09:40:44 -07:00
Tuan Dang
6e6f0252ae Adjust default offsets for cas/certs query 2024-07-25 08:09:21 -07:00
Tuan Dang
2348df7a4d Add list cert, ca + logical filters to docs 2024-07-25 08:06:18 -07:00
Maidul Islam
962cf67dfb Merge pull request #2173 from felixtrav/patch-1
Update envars.mdx - Added PORT
2024-07-25 10:21:06 -04:00
BlackMagiq
32627c20c4 Merge pull request #2176 from Infisical/org-role-cleanup
Cleanup frontend unused org role logic (moved)
2024-07-25 07:17:56 -07:00
Maidul Islam
c50f8fd78c Merge pull request #2175 from akhilmhdh/feat/cli-login-fallback-missing
Missing paste token option in CLI brower login flow
2024-07-25 10:08:57 -04:00
Tuan Dang
977ce09245 Cleanup frontend unused org role logic (moved) 2024-07-25 05:43:57 -07:00
=
08d7dead8c fix(cli): resolved not printing the url on api override 2024-07-25 15:28:54 +05:30
=
a30e06e392 feat: added back missing token paste option in cli login from browser 2024-07-25 15:28:29 +05:30
Maidul Islam
23f3f09cb6 temporarily remove linux deployment 2024-07-24 23:42:36 -04:00
Aeto
5cd0f665fa Update envars.mdx - Added PORT
Added the PORT configuration option to the documentation which controls the port the application listens on.
2024-07-24 19:17:33 -04:00
Daniel Hougaard
443e76c1df Merge pull request #2171 from Infisical/daniel/aarch64-binary-fix
fix(binary): aarch64 binary native bindings fix
2024-07-24 16:33:15 +02:00
Daniel Hougaard
4ea22b6761 Updated ubuntu version 2024-07-24 14:17:19 +00:00
Maidul Islam
ae7e0d0963 Merge pull request #2168 from Infisical/misc/added-email-self-host-conditionals
misc: added checks for formatting email templates for self-hosted or cloud
2024-07-24 09:22:49 -04:00
Daniel Hougaard
ed6c6d54c0 Update build-binaries.yml 2024-07-24 11:16:58 +02:00
Daniel Hougaard
428ff5186f Removed compression for testing 2024-07-24 10:47:20 +02:00
Daniel Hougaard
d07b0d20d6 Update build-binaries.yml 2024-07-24 10:46:55 +02:00
Sheen Capadngan
8e373fe9bf misc: added email formatting for remaining templates 2024-07-24 16:33:41 +08:00
Sheen Capadngan
28087cdcc4 misc: added email self-host conditionals 2024-07-24 00:55:02 +08:00
Vlad Matsiiako
dcef49950d Merge pull request #2167 from Infisical/daniel/ruby-docs
feat(docs): Ruby sdk
2024-07-23 08:36:32 -07:00
Sheen Capadngan
02eea4d886 Merge pull request #2166 from Infisical/misc/updated-cf-worker-integration-doc
misc: updated cf worker integration doc
2024-07-23 21:16:56 +08:00
Sheen Capadngan
d12144a7e7 misc: added highligting 2024-07-23 21:03:46 +08:00
Sheen Capadngan
5fa69235d1 misc: updated cf worker integration doc 2024-07-23 20:40:07 +08:00
40 changed files with 381 additions and 616 deletions

View File

@@ -14,7 +14,6 @@ defaults:
jobs: jobs:
build-and-deploy: build-and-deploy:
runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:
arch: [x64, arm64] arch: [x64, arm64]
@@ -24,6 +23,7 @@ jobs:
target: node20-linux target: node20-linux
- os: win - os: win
target: node20-win target: node20-win
runs-on: ${{ (matrix.arch == 'arm64' && matrix.os == 'linux') && 'ubuntu24-arm64' || 'ubuntu-latest' }}
steps: steps:
- name: Checkout code - name: Checkout code
@@ -49,9 +49,9 @@ jobs:
- name: Package into node binary - name: Package into node binary
run: | run: |
if [ "${{ matrix.os }}" != "linux" ]; then 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 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 fi
# Set up .deb package structure (Debian/Ubuntu only) # Set up .deb package structure (Debian/Ubuntu only)
@@ -84,7 +84,12 @@ jobs:
mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb
- uses: actions/setup-python@v4 - 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) # Publish .deb file to Cloudsmith (Debian/Ubuntu only)
- name: Publish to Cloudsmith (Debian/Ubuntu) - name: Publish to Cloudsmith (Debian/Ubuntu)

View File

@@ -107,7 +107,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
}), }),
name: z.string().trim().optional(), name: z.string().trim().optional(),
description: z.string().trim().optional(), description: z.string().trim().optional(),
permissions: z.any().array() permissions: z.any().array().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@@ -425,6 +425,21 @@ export const PROJECTS = {
}, },
LIST_INTEGRATION_AUTHORIZATION: { LIST_INTEGRATION_AUTHORIZATION: {
workspaceId: "The ID of the project to list integration auths for." 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; } as const;

View File

@@ -317,10 +317,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: slugSchema.describe("The slug of the project to list CAs.") slug: slugSchema.describe(PROJECTS.LIST_CAS.slug)
}), }),
querystring: z.object({ 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: { response: {
200: z.object({ 200: z.object({
@@ -336,11 +340,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
orgId: req.permission.orgId, orgId: req.permission.orgId,
type: ProjectFilterType.SLUG type: ProjectFilterType.SLUG
}, },
status: req.query.status,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type actor: req.permission.type,
...req.query
}); });
return { cas }; return { cas };
} }
@@ -354,11 +358,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: slugSchema.describe("The slug of the project to list certificates.") slug: slugSchema.describe(PROJECTS.LIST_CERTIFICATES.slug)
}), }),
querystring: z.object({ querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0), friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
limit: z.coerce.number().min(1).max(100).default(25) 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: { response: {
200: z.object({ 200: z.object({

View File

@@ -8,19 +8,35 @@ export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
export const certificateDALFactory = (db: TDbClient) => { export const certificateDALFactory = (db: TDbClient) => {
const certificateOrm = ormify(db, TableName.Certificate); const certificateOrm = ormify(db, TableName.Certificate);
const countCertificatesInProject = async (projectId: string) => { const countCertificatesInProject = async ({
projectId,
friendlyName,
commonName
}: {
projectId: string;
friendlyName?: string;
commonName?: string;
}) => {
try { try {
interface CountResult { interface CountResult {
count: string; count: string;
} }
const count = await db let query = db
.replicaNode()(TableName.Certificate) .replicaNode()(TableName.Certificate)
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`) .join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`) .join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
.where(`${TableName.Project}.id`, projectId) .where(`${TableName.Project}.id`, projectId);
.count("*")
.first(); 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); return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) { } catch (error) {

View File

@@ -575,6 +575,10 @@ export const projectServiceFactory = ({
*/ */
const listProjectCas = async ({ const listProjectCas = async ({
status, status,
friendlyName,
commonName,
limit = 25,
offset = 0,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
@@ -596,10 +600,15 @@ export const projectServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
const cas = await certificateAuthorityDAL.find({ const cas = await certificateAuthorityDAL.find(
{
projectId: project.id, projectId: project.id,
...(status && { status }) ...(status && { status }),
}); ...(friendlyName && { friendlyName }),
...(commonName && { commonName })
},
{ offset, limit, sort: [["updatedAt", "desc"]] }
);
return cas; return cas;
}; };
@@ -608,8 +617,10 @@ export const projectServiceFactory = ({
* Return list of certificates for project * Return list of certificates for project
*/ */
const listProjectCertificates = async ({ const listProjectCertificates = async ({
offset, limit = 25,
limit, offset = 0,
friendlyName,
commonName,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
@@ -634,12 +645,18 @@ export const projectServiceFactory = ({
{ {
$in: { $in: {
caId: cas.map((ca) => ca.id) caId: cas.map((ca) => ca.id)
} },
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
}, },
{ offset, limit, sort: [["updatedAt", "desc"]] } { offset, limit, sort: [["updatedAt", "desc"]] }
); );
const count = await certificateDAL.countCertificatesInProject(project.id); const count = await certificateDAL.countCertificatesInProject({
projectId: project.id,
friendlyName,
commonName
});
return { return {
certificates, certificates,

View File

@@ -89,6 +89,10 @@ export type AddUserToWsDTO = {
export type TListProjectCasDTO = { export type TListProjectCasDTO = {
status?: CaStatus; status?: CaStatus;
friendlyName?: string;
offset?: number;
limit?: number;
commonName?: string;
filter: Filter; filter: Filter;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -96,4 +100,6 @@ export type TListProjectCertsDTO = {
filter: Filter; filter: Filter;
offset: number; offset: number;
limit: number; limit: number;
friendlyName?: string;
commonName?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;

View File

@@ -5,6 +5,7 @@ import handlebars from "handlebars";
import { createTransport } from "nodemailer"; import { createTransport } from "nodemailer";
import SMTPTransport from "nodemailer/lib/smtp-transport"; import SMTPTransport from "nodemailer/lib/smtp-transport";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
export type TSmtpConfig = SMTPTransport.Options; export type TSmtpConfig = SMTPTransport.Options;
@@ -12,7 +13,7 @@ export type TSmtpSendMail = {
template: SmtpTemplates; template: SmtpTemplates;
subjectLine: string; subjectLine: string;
recipients: string[]; recipients: string[];
substitutions: unknown; substitutions: object;
}; };
export type TSmtpService = ReturnType<typeof smtpServiceFactory>; export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
@@ -47,9 +48,11 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => {
const isSmtpOn = Boolean(cfg.host); const isSmtpOn = Boolean(cfg.host);
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => { const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
const appCfg = getConfig();
const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8"); const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8");
const temp = handlebars.compile(html); const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions); const htmlToSend = temp({ isCloud: appCfg.isCloud, siteUrl: appCfg.SITE_URL, ...substitutions });
if (isSmtpOn) { if (isSmtpOn) {
await smtp.sendMail({ await smtp.sendMail({
from: cfg.from, from: cfg.from,

View File

@@ -1,19 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>MFA Code</title> <title>MFA Code</title>
</head> </head>
<body> <body>
<h2>Infisical</h2> <h2>Infisical</h2>
<h2>Sign in attempt requires further verification</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> <p>Your MFA code is below — enter it where you started signing in to Infisical.</p>
<h2>{{code}}</h2> <h2>{{code}}</h2>
<p>The MFA code will be valid for 2 minutes.</p> <p>The MFA code will be valid for 2 minutes.</p>
<p>Not you? Contact Infisical or your administrator immediately.</p> <p>Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.</p>
</body> </body>
</html> </html>

View File

@@ -9,12 +9,12 @@
<body> <body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3> <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>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 <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> dashboard</a>.</p>
</body> </body>

View File

@@ -1,19 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Successful login for {{email}} from new device</title> <title>Successful login for {{email}} from new device</title>
</head> </head>
<body> <body>
<h2>Infisical</h2> <h2>Infisical</h2>
<p>We're verifying a recent login for {{email}}:</p> <p>We're verifying a recent login for {{email}}:</p>
<p><strong>Timestamp</strong>: {{timestamp}}</p> <p><strong>Timestamp</strong>: {{timestamp}}</p>
<p><strong>IP address</strong>: {{ip}}</p> <p><strong>IP address</strong>: {{ip}}</p>
<p><strong>User agent</strong>: {{userAgent}}</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> <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> </body>
</html> </html>

View File

@@ -9,6 +9,6 @@
<h2>Reset your password</h2> <h2>Reset your password</h2>
<p>Someone requested a password reset.</p> <p>Someone requested a password reset.</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a> <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> </body>
</html> </html>

View File

@@ -9,7 +9,7 @@
<body> <body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3> <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 <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 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 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>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 <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> dashboard</a>.</p>
</body> </body>

View File

@@ -11,7 +11,7 @@
<h2>Confirm your email address</h2> <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> <p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<h1>{{code}}</h1> <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> </body>
</html> </html>

View File

@@ -24,7 +24,6 @@ import (
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/srp" "github.com/Infisical/infisical-merge/packages/srp"
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
"github.com/chzyer/readline"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
@@ -205,6 +204,7 @@ var loginCmd = &cobra.Command{
if !overrideDomain { if !overrideDomain {
domainQuery = false domainQuery = false
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE) 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 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) time.Sleep(time.Second * 5)
fmt.Println("\n\nOnce login is completed via browser, the CLI should be authenticated automatically.") 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.") 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) log.Debug().Msgf("Callback server listening on port %d", callbackPort)
stdin := readline.NewCancelableStdin(os.Stdin)
go http.Serve(listener, corsHandler) go http.Serve(listener, corsHandler)
go askToPasteJwtToken(stdin, success, failure) go askToPasteJwtToken(success, failure)
for { for {
select { select {
case loginResponse := <-success: case loginResponse := <-success:
_ = closeListener(&listener) _ = closeListener(&listener)
_ = stdin.Close()
fmt.Println("Browser login successful") fmt.Println("Browser login successful")
return loginResponse, nil return loginResponse, nil
case err := <-failure: case err := <-failure:
serverErr := closeListener(&listener) serverErr := closeListener(&listener)
stdErr := stdin.Close() return models.UserCredentials{}, errors.Join(err, serverErr)
return models.UserCredentials{}, errors.Join(err, serverErr, stdErr)
case <-timeout: case <-timeout:
_ = closeListener(&listener) _ = closeListener(&listener)
_ = stdin.Close()
return models.UserCredentials{}, errors.New("server timeout") return models.UserCredentials{}, errors.New("server timeout")
} }
} }

View File

@@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
package cmd package cmd
import ( import (
"encoding/base64"
"fmt" "fmt"
"strings" "strings"
@@ -13,16 +14,104 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"} type VaultBackendType struct {
var AvailableVaults = []string{"auto", "file"} 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{ var vaultSetCmd = &cobra.Command{
Example: `infisical vault set pass`, Example: `infisical vault set file --passphrase <your-passphrase>`,
Use: "set [vault-name]", Use: "set [file|auto] [flags]",
Short: "Used to set the vault backend to store your login details securely at rest", Short: "Used to configure the vault backends",
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
vaultType := args[0]
passphrase, err := cmd.Flags().GetString("passphrase")
if err != nil {
util.HandleError(err, "Unable to get passphrase flag")
}
if vaultType == util.VAULT_BACKEND_FILE_MODE && passphrase != "" {
setFileVaultPassphrase(passphrase)
return
}
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",
Short: "Used to manage where your Infisical login token is saved on your machine",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
printAvailableVaultBackends()
},
}
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 _, vaultType := range AvailableVaults {
fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description)
}
currentVaultBackend, err := util.GetCurrentVaultBackend()
if err != nil {
log.Error().Msgf("printAvailableVaultBackends: unable to print the available vault backend because of error [err=%s]", err)
}
Telemetry.CaptureEvent("cli-command:vault", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("version", util.CLI_VERSION))
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] wantedVaultTypeName := args[0]
currentVaultBackend, err := util.GetCurrentVaultBackend() currentVaultBackend, err := util.GetCurrentVaultBackend()
if err != nil { if err != nil {
@@ -35,7 +124,7 @@ var vaultSetCmd = &cobra.Command{
return return
} }
if wantedVaultTypeName == "auto" || wantedVaultTypeName == "file" { if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE {
configFile, err := util.GetConfigFile() configFile, err := util.GetConfigFile()
if err != nil { if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
@@ -55,39 +144,20 @@ var vaultSetCmd = &cobra.Command{
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION)) Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
} else { } 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, ", ")) 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, ", "))
}
// runCmd represents the run command
var vaultCmd = &cobra.Command{
Use: "vault",
Short: "Used to manage where your Infisical login token is saved on your machine",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
printAvailableVaultBackends()
},
}
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)
} }
currentVaultBackend, err := util.GetCurrentVaultBackend()
if err != nil {
log.Error().Msgf("printAvailableVaultBackends: unable to print the available vault backend because of error [err=%s]", err)
}
Telemetry.CaptureEvent("cli-command:vault", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("version", util.CLI_VERSION))
fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend))
} }
func init() { func init() {
vaultSetCmd.Flags().StringP("passphrase", "p", "", "Set the passphrase for the file vault")
vaultCmd.AddCommand(vaultSetCmd) vaultCmd.AddCommand(vaultSetCmd)
vaultCmd.AddCommand(vaultUseCmd)
rootCmd.AddCommand(vaultCmd) rootCmd.AddCommand(vaultCmd)
} }

View File

@@ -15,6 +15,7 @@ type ConfigFile struct {
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"` LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"` LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
VaultBackendType string `json:"vaultBackendType,omitempty"` VaultBackendType string `json:"vaultBackendType,omitempty"`
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
} }
type LoggedInUser struct { type LoggedInUser struct {

View File

@@ -1,6 +1,7 @@
package util package util
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -54,6 +55,7 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
LoggedInUserDomain: config.INFISICAL_URL, LoggedInUserDomain: config.INFISICAL_URL,
LoggedInUsers: existingConfigFile.LoggedInUsers, LoggedInUsers: existingConfigFile.LoggedInUsers,
VaultBackendType: existingConfigFile.VaultBackendType, VaultBackendType: existingConfigFile.VaultBackendType,
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
} }
configFileMarshalled, err := json.Marshal(configFile) configFileMarshalled, err := json.Marshal(configFile)
@@ -215,6 +217,14 @@ func GetConfigFile() (models.ConfigFile, error) {
return models.ConfigFile{}, err 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 return configFile, nil
} }

View File

@@ -8,6 +8,10 @@ const (
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_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 // Universal Auth
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID" INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"

View File

@@ -1,6 +1,9 @@
package util package util
import ( import (
"encoding/base64"
"github.com/manifoldco/promptui"
"github.com/zalando/go-keyring" "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") 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) { func GetValueInKeyring(key string) (string, error) {
currentVaultBackend, err := GetCurrentVaultBackend() currentVaultBackend, err := GetCurrentVaultBackend()
if err != nil { 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 { func DeleteValueInKeyring(key string) error {
@@ -38,5 +76,11 @@ func DeleteValueInKeyring(key string) error {
return err 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
} }

View File

@@ -11,11 +11,11 @@ func GetCurrentVaultBackend() (string, error) {
} }
if configFile.VaultBackendType == "" { if configFile.VaultBackendType == "" {
return "auto", nil return VAULT_BACKEND_AUTO_MODE, nil
} }
if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" { if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE {
return "auto", nil return VAULT_BACKEND_AUTO_MODE, nil
} }
return configFile.VaultBackendType, nil return configFile.VaultBackendType, nil

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/workspace/{slug}/cas"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/workspace/{slug}/certificates"
---

View File

@@ -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. 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

View File

@@ -15,7 +15,7 @@ Prerequisites:
![integrations cloudflare credentials 1](../../images/integrations/cloudflare/integrations-cloudflare-credentials-1.png) ![integrations cloudflare credentials 1](../../images/integrations/cloudflare/integrations-cloudflare-credentials-1.png)
![integrations cloudflare credentials 2](../../images/integrations/cloudflare/integrations-cloudflare-credentials-2.png) ![integrations cloudflare credentials 2](../../images/integrations/cloudflare/integrations-cloudflare-credentials-2.png)
![integrations cloudflare credentials 3](../../images/integrations/cloudflare/integrations-cloudflare-credentials-3.png) ![integrations cloudflare credentials 3](../../images/integrations/cloudflare/integrations-cloudflare-workers-permission.png)
Copy your [Account ID](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/) from Account > Workers & Pages > Overview 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 breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform. the cloud platform.
</Info> </Info>
</Step> </Step>
<Step title="Start integration"> <Step title="Start integration">
Select which Infisical environment secrets you want to sync to Cloudflare Workers and press create integration to start syncing secrets. Select which Infisical environment secrets you want to sync to Cloudflare Workers and press create integration to start syncing secrets.
![integrations cloudflare](../../images/integrations/cloudflare/integration-cloudflare-workers-create.png) ![integrations cloudflare](../../images/integrations/cloudflare/integration-cloudflare-workers-create.png)
</Step> </Step>
</Steps> </Steps>

View File

@@ -216,13 +216,6 @@
"group": "Self-host Infisical", "group": "Self-host Infisical",
"pages": [ "pages": [
"self-hosting/overview", "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", "group": "Containerized installation methods",
"pages": [ "pages": [
@@ -661,6 +654,7 @@
{ {
"group": "Certificate Authorities", "group": "Certificate Authorities",
"pages": [ "pages": [
"api-reference/endpoints/certificate-authorities/list",
"api-reference/endpoints/certificate-authorities/create", "api-reference/endpoints/certificate-authorities/create",
"api-reference/endpoints/certificate-authorities/read", "api-reference/endpoints/certificate-authorities/read",
"api-reference/endpoints/certificate-authorities/update", "api-reference/endpoints/certificate-authorities/update",
@@ -676,6 +670,7 @@
{ {
"group": "Certificates", "group": "Certificates",
"pages": [ "pages": [
"api-reference/endpoints/certificates/list",
"api-reference/endpoints/certificates/read", "api-reference/endpoints/certificates/read",
"api-reference/endpoints/certificates/revoke", "api-reference/endpoints/certificates/revoke",
"api-reference/endpoints/certificates/delete", "api-reference/endpoints/certificates/delete",

View File

@@ -25,6 +25,10 @@ Used to configure platform-specific security and operational settings
https://app.infisical.com). https://app.infisical.com).
</ParamField> </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> <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`. Telemetry helps us improve Infisical but if you want to dsiable it you may set this to `false`.
</ParamField> </ParamField>

View File

@@ -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. Use our Helm chart to Install Infisical on your Kubernetes cluster.
</Card> </Card>
</CardGroup> </CardGroup>
<CardGroup cols={2}> {/* <CardGroup cols={2}>
<Card <Card
title="Native Deployment" title="Native Deployment"
color="#000000" 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. Install Infisical on your Debian-based instances without containers using our standalone binary with high availability out of the box.
</Card> </Card>
</CardGroup> </CardGroup> */}

View File

@@ -79,7 +79,7 @@ export const useUpdateOrgRole = () => {
data: { role } data: { role }
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, { } = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
...dto, ...dto,
permissions: permissions?.length ? packRules(permissions) : [] permissions: permissions?.length ? packRules(permissions) : undefined
}); });
return role; return role;

View File

@@ -168,16 +168,27 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
const { token: newJwtToken } = await selectOrganization({ organizationId }); const { token: newJwtToken } = await selectOrganization({ organizationId });
const instance = axios.create(); const instance = axios.create();
await instance.post(cliUrl, { const payload = {
...isCliLoginSuccessful.loginResponse, ...isCliLoginSuccessful.loginResponse,
JTWToken: newJwtToken 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");
await navigateUserToOrg(router, organizationId); return;
} }
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs // 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 // 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 // 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 { else {
await navigateUserToOrg(router); await navigateUserToOrg(router);
} }
}
} }
} else { } else {
const isLoginSuccessful = await attemptLoginMfa({ const isLoginSuccessful = await attemptLoginMfa({

View File

@@ -4,6 +4,7 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import HCaptcha from "@hcaptcha/react-hcaptcha"; import HCaptcha from "@hcaptcha/react-hcaptcha";
import axios from "axios"; import axios from "axios";
import { addSeconds, formatISO } from "date-fns";
import jwt_decode from "jwt-decode"; import jwt_decode from "jwt-decode";
import { createNotification } from "@app/components/notifications"; 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 { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input, Spinner } from "@app/components/v2"; import { Button, Input, Spinner } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
@@ -79,11 +81,24 @@ export const PasswordStep = ({
if (callbackPort) { if (callbackPort) {
console.log("organization id was present. new JWT token to be used in CLI:", newJwtToken); console.log("organization id was present. new JWT token to be used in CLI:", newJwtToken);
const instance = axios.create(); const instance = axios.create();
await instance.post(cliUrl, { const payload = {
privateKey, privateKey,
email, email,
JTWToken: newJwtToken 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); await navigateUserToOrg(router, organizationId);
@@ -165,16 +180,26 @@ export const PasswordStep = ({
); );
const instance = axios.create(); const instance = axios.create();
await instance.post(cliUrl, { const payload = {
...isCliLoginSuccessful.loginResponse, ...isCliLoginSuccessful.loginResponse,
JTWToken: newJwtToken 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");
await navigateUserToOrg(router, organizationId); return;
} }
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs // 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 // 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 // case: user has orgs, so we navigate the user to select an org
@@ -186,7 +211,6 @@ export const PasswordStep = ({
await navigateUserToOrg(router); await navigateUserToOrg(router);
} }
} }
}
} else { } else {
const loginAttempt = await attemptLogin({ const loginAttempt = await attemptLogin({
email, email,

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -8,7 +8,7 @@ import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2"; import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils"; import { TFormSchema } from "../../../../RolePage/components/OrgRoleModifySection.utils";
type Props = { type Props = {
isNonEditable?: boolean; isNonEditable?: boolean;

View File

@@ -1 +0,0 @@
export { OrgRoleModifySection } from "./OrgRoleModifySection";

View File

@@ -1,27 +1,9 @@
import { motion } from "framer-motion"; 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"; import { OrgRoleTable } from "./OrgRoleTable";
export const OrgRoleTabSection = () => { export const OrgRoleTabSection = () => {
const { popUp, handlePopUpClose } = usePopUp(["editRole"] as const); return (
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>
) : (
<motion.div <motion.div
key="role-list" key="role-list"
transition={{ duration: 0.1 }} transition={{ duration: 0.1 }}

View File

@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks"; 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 = [ const PERMISSIONS = [
{ action: "read", label: "View" }, { action: "read", label: "View" },

View File

@@ -2,7 +2,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications"; 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 { useOrganization } from "@app/context";
import { useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api"; import { useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api";
import { import {
@@ -10,7 +10,7 @@ import {
formSchema, formSchema,
rolePermission2Form, rolePermission2Form,
TFormSchema TFormSchema
} from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils"; } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
import { RolePermissionRow } from "./RolePermissionRow"; import { RolePermissionRow } from "./RolePermissionRow";