Compare commits

..

80 Commits

Author SHA1 Message Date
Vlad Matsiiako
d1d0667cd5 Update overview.mdx 2024-10-12 22:03:08 -07:00
Maidul Islam
09d28156f8 Merge pull request #2572 from Infisical/vmatsiiako-readme-patch-1
Update README.md
2024-10-10 19:40:45 -07:00
Vlad Matsiiako
fc67c496c5 Update README.md 2024-10-10 19:39:51 -07:00
Maidul Islam
540a1a29b1 Merge pull request #2570 from akhilmhdh/fix/scim-error-response
Resolved response schema mismatch for scim
2024-10-10 13:53:33 -07:00
Maidul Islam
3163adf486 increase depth count 2024-10-10 13:50:03 -07:00
=
e042f9b5e2 feat: made missing errors as internal server error and added depth in scim knex 2024-10-11 01:42:38 +05:30
Daniel Hougaard
05a1b5397b Merge pull request #2571 from Infisical/daniel/envkey-import-bug
fix: handle undefined variable values
2024-10-10 21:23:08 +04:00
Daniel Hougaard
19776df46c fix: handle undefined variable values 2024-10-10 21:13:17 +04:00
Maidul Islam
64fd65aa52 Update requirements.mdx 2024-10-10 08:58:35 -07:00
=
3d58eba78c fix: resolved response schema mismatch for scim 2024-10-10 18:38:29 +05:30
Maidul Islam
565884d089 Merge pull request #2569 from Infisical/maidul-helm-static-dynamic
Make helm chart more dynamic
2024-10-10 00:05:04 -07:00
Maidul Islam
2a83da1cb6 update helm chart version 2024-10-10 00:00:56 -07:00
Maidul Islam
f186ce9649 Add support for existing pg secret 2024-10-09 23:43:37 -07:00
Maidul Islam
6ecfee5faf Merge pull request #2568 from Infisical/daniel/envvar-fix
fix: allow 25MB uploads for migrations
2024-10-09 17:21:09 -07:00
Daniel Hougaard
662f1a31f6 fix: allow 25MB uploads for migrations 2024-10-10 03:37:08 +04:00
Maidul Islam
06f9a1484b Merge pull request #2567 from scott-ray-wilson/fix-unintentional-project-creation
Fix: Prevent Example Project Creation on SSO Signup When Joining Org
2024-10-09 15:01:44 -07:00
Scott Wilson
c90e8ca715 chore: revert prem features 2024-10-09 14:01:16 -07:00
Scott Wilson
6ddc4ce4b1 fix: prevent example project from being created when joining existing org SSO 2024-10-09 13:58:22 -07:00
Maidul Islam
4fffac07fd Merge pull request #2559 from akhilmhdh/fix/ssm-integration-1-1
fix: resolved ssm failing for empty secret in 1-1 mapping
2024-10-09 13:19:22 -07:00
Scott Wilson
75d71d4208 Merge pull request #2549 from scott-ray-wilson/org-default-role
Feat: Default Org Membership Role
2024-10-09 11:55:47 -07:00
Scott Wilson
e38628509d improvement: address more feedback 2024-10-09 11:52:02 -07:00
Scott Wilson
0b247176bb improvements: address feedback 2024-10-09 11:52:02 -07:00
Daniel Hougaard
faad09961d Update OrgRoleTable.tsx 2024-10-09 22:47:14 +04:00
Scott Wilson
98d4f808e5 improvement: set intial org role value in dropdown on add user to default org membership value 2024-10-09 11:04:47 -07:00
Scott Wilson
2ae91db65d Merge pull request #2565 from scott-ray-wilson/add-project-users-multi-select
Feature: Multi-Select Component and Improve Adding Users to Project
2024-10-09 10:45:59 -07:00
Scott Wilson
529328f0ae chore: revert package-lock name 2024-10-09 10:02:42 -07:00
Scott Wilson
e59d9ff3c6 chore: revert prem features 2024-10-09 10:00:38 -07:00
Scott Wilson
4aad36601c feature: add multiselect component and improve adding users to project 2024-10-09 09:58:00 -07:00
=
4aaba3ef9f fix: resolved ssm failing for empty secret in 1-1 mapping 2024-10-09 16:06:48 +05:30
Maidul Islam
b482a9cda7 Add audit log env to prod stage 2024-10-08 20:52:27 -07:00
Scott Wilson
595eb739af Merge pull request #2555 from Infisical/daniel/rpm-binary
feat: rpm binary
2024-10-08 16:08:10 -07:00
Daniel Hougaard
b46bbea0c5 fix: removed debug data & re-add compression 2024-10-09 01:48:23 +04:00
Daniel Hougaard
6dad24ffde Update build-binaries.yml 2024-10-09 01:39:53 +04:00
Daniel Hougaard
f8759b9801 Update build-binaries.yml 2024-10-09 01:14:24 +04:00
Daniel Hougaard
049c77c902 Update build-binaries.yml 2024-10-09 00:50:32 +04:00
Scott Wilson
1478833c9c Merge pull request #2556 from scott-ray-wilson/fix-secret-overview-overflow
Improvement: Secret Overview Table Scroll
2024-10-08 13:24:05 -07:00
Daniel Hougaard
c8d40c6905 fix for corrupt data 2024-10-09 00:17:48 +04:00
Daniel Hougaard
ff815b5f42 Update build-binaries.yml 2024-10-08 23:38:20 +04:00
Scott Wilson
e5138d0e99 Merge pull request #2509 from akhilmhdh/docs/admin-panel
docs: added docs for infisical admin panels
2024-10-08 12:03:00 -07:00
Scott Wilson
f43725a16e fix: move pagination beneath table container to make overflow-scroll more intuitive 2024-10-08 11:57:54 -07:00
Daniel Hougaard
f6c65584bf Update build-binaries.yml 2024-10-08 22:40:33 +04:00
Daniel Hougaard
246020729e Update build-binaries.yml 2024-10-08 22:18:15 +04:00
Daniel Hougaard
63cc4e347d Update build-binaries.yml 2024-10-08 22:17:59 +04:00
Scott Wilson
ecaca82d9a improvement: minor adjustments 2024-10-08 11:07:05 -07:00
Daniel Hougaard
d6ef0d1c83 Merge pull request #2548 from Infisical/daniel/include-env-on-interation
fix: include env on integration api
2024-10-08 22:01:20 +04:00
Daniel Hougaard
f2a7f164e1 Trigger build 2024-10-08 21:58:49 +04:00
Daniel Hougaard
dfbdc46971 fix: rpm binary 2024-10-08 21:56:58 +04:00
Maidul Islam
3049f9e719 Merge pull request #2553 from Infisical/misc/made-partition-operation-separate
misc: made audit log partition opt-in
2024-10-08 09:39:01 -07:00
Sheen Capadngan
391c9abbb0 misc: updated error description 2024-10-08 22:49:11 +08:00
Sheen Capadngan
e191a72ca0 misc: finalized env name 2024-10-08 21:38:38 +08:00
Sheen Capadngan
68c38f228d misc: moved to using env 2024-10-08 21:29:36 +08:00
Sheen Capadngan
a823347c99 misc: added proper deletion of indices 2024-10-08 21:21:32 +08:00
Sheen Capadngan
22b417b50b misc: made partition opt-in 2024-10-08 17:53:53 +08:00
Sheen Capadngan
98ed063ce6 misc: enabled audit log exploration 2024-10-08 12:52:43 +08:00
Maidul Islam
c0fb493f57 Merge pull request #2523 from Infisical/misc/move-audit-logs-to-dedicated
misc: audit log migration + special handing
2024-10-07 16:04:23 -07:00
Scott Wilson
eae5e57346 feat: default org membership role 2024-10-07 15:02:14 -07:00
Sheen Capadngan
f6fcef24c6 misc: added console statement to partition migration 2024-10-08 02:56:10 +08:00
Sheen Capadngan
5bf6f69fca misc: moved to partitionauditlogs schema 2024-10-08 02:44:24 +08:00
Daniel Hougaard
56798f09bf Merge pull request #2544 from Infisical/daniel/project-env-position-fixes
fix: project environment positions
2024-10-07 21:22:38 +04:00
Sheen
4c1253dc87 Merge pull request #2534 from Infisical/doc/oidc-auth-circle-ci
doc: circle ci oidc auth
2024-10-07 23:26:31 +08:00
Meet Shah
09793979c7 Merge pull request #2547 from Infisical/meet/eng-1577-lots-of-content-header-issues-in-console
fix: add CSP directive to allow posthog
2024-10-07 18:56:12 +05:30
Meet
fa360b8208 fix: add CSP directive to allow posthog 2024-10-07 18:28:14 +05:30
Daniel Hougaard
f94e100c30 Update project-env.spec.ts 2024-10-07 13:30:32 +04:00
Daniel Hougaard
33b54e78f9 fix: project environment positions 2024-10-07 12:52:59 +04:00
Sheen Capadngan
98cca7039c misc: addressed comments 2024-10-07 14:00:20 +08:00
Sheen Capadngan
3f7f0a7b0a doc: circle ci oidc auth 2024-10-05 01:56:33 +08:00
Sheen Capadngan
1687d66a0e misc: ignore partitions in generate schema 2024-10-04 22:37:13 +08:00
Sheen Capadngan
cf446a38b3 misc: improved knex import 2024-10-04 22:27:11 +08:00
Sheen Capadngan
36ef87909e Merge remote-tracking branch 'origin/main' into misc/move-audit-logs-to-dedicated 2024-10-04 22:16:46 +08:00
Sheen Capadngan
6bfeac5e98 misc: addressed import knex issue 2024-10-04 22:15:39 +08:00
Sheen Capadngan
d669320385 misc: addressed type issue with knex 2024-10-04 22:06:32 +08:00
Sheen Capadngan
8dbdb79833 misc: finalized partition migration script 2024-10-04 21:43:33 +08:00
Sheen Capadngan
e05f05f9ed misc: added timeout error prompt 2024-10-04 02:41:21 +08:00
Sheen Capadngan
81846d9c67 misc: added timeout for db queries 2024-10-04 02:25:02 +08:00
Sheen Capadngan
723f0e862d misc: finalized partition script 2024-10-04 01:42:24 +08:00
Sheen Capadngan
2d0433b96c misc: initial setup for audit log partition: 2024-10-03 22:47:16 +08:00
Sheen Capadngan
9b1615f2fb misc: migrated json filters to new op 2024-10-03 00:31:23 +08:00
Sheen Capadngan
dc8c3a30bd misc: added project name to publish log 2024-10-02 22:40:33 +08:00
Sheen Capadngan
86cb51364a misc: initial setup for migration of audit logs 2024-10-02 22:30:07 +08:00
=
5856a42807 docs: added docs for infisical admin panels 2024-09-29 20:46:34 +05:30
86 changed files with 1880 additions and 474 deletions

View File

@@ -1 +1,2 @@
DB_CONNECTION_URI=
AUDIT_LOGS_DB_CONNECTION_URI=

View File

@@ -7,7 +7,6 @@ on:
description: "Version number"
required: true
type: string
defaults:
run:
working-directory: ./backend
@@ -49,9 +48,9 @@ jobs:
- name: Package into node binary
run: |
if [ "${{ matrix.os }}" != "linux" ]; then
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
pkg --no-bytecode --public-packages "*" --public --compress GZip --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
else
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
pkg --no-bytecode --public-packages "*" --public --compress GZip --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
fi
# Set up .deb package structure (Debian/Ubuntu only)
@@ -83,6 +82,86 @@ jobs:
dpkg-deb --build infisical-core
mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb
### RPM
# Set up .rpm package structure
- name: Set up .rpm package structure
if: matrix.os == 'linux'
run: |
mkdir -p infisical-core-rpm/usr/local/bin
cp ./binary/infisical-core infisical-core-rpm/usr/local/bin/
chmod +x infisical-core-rpm/usr/local/bin/infisical-core
# Install RPM build tools
- name: Install RPM build tools
if: matrix.os == 'linux'
run: sudo apt-get update && sudo apt-get install -y rpm
# Create .spec file for RPM
- name: Create .spec file for RPM
if: matrix.os == 'linux'
run: |
cat <<EOF > infisical-core.spec
%global _enable_debug_package 0
%global debug_package %{nil}
%global __os_install_post /usr/lib/rpm/brp-compress %{nil}
Name: infisical-core
Version: ${{ github.event.inputs.version }}
Release: 1%{?dist}
Summary: Infisical Core standalone executable
License: Proprietary
URL: https://app.infisical.com
%description
Infisical Core standalone executable (app.infisical.com)
%install
mkdir -p %{buildroot}/usr/local/bin
cp %{_sourcedir}/infisical-core %{buildroot}/usr/local/bin/
%files
/usr/local/bin/infisical-core
%pre
%post
%preun
%postun
EOF
# Build .rpm file
- name: Build .rpm package
if: matrix.os == 'linux'
run: |
# Create necessary directories
mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
# Copy the binary directly to SOURCES
cp ./binary/infisical-core rpmbuild/SOURCES/
# Run rpmbuild with verbose output
rpmbuild -vv -bb \
--define "_topdir $(pwd)/rpmbuild" \
--define "_sourcedir $(pwd)/rpmbuild/SOURCES" \
--define "_rpmdir $(pwd)/rpmbuild/RPMS" \
--target ${{ matrix.arch == 'x64' && 'x86_64' || 'aarch64' }} \
infisical-core.spec
# Try to find the RPM file
find rpmbuild -name "*.rpm"
# Move the RPM file if found
if [ -n "$(find rpmbuild -name '*.rpm')" ]; then
mv $(find rpmbuild -name '*.rpm') ./binary/infisical-core-${{matrix.arch}}.rpm
else
echo "RPM file not found!"
exit 1
fi
- uses: actions/setup-python@v4
with:
python-version: "3.x" # Specify the Python version you need
@@ -97,6 +176,12 @@ jobs:
working-directory: ./backend
run: cloudsmith push deb --republish --no-wait-for-sync --api-key=${{ secrets.CLOUDSMITH_API_KEY }} infisical/infisical-core/any-distro/any-version ./binary/infisical-core-${{ matrix.arch }}.deb
# Publish .rpm file to Cloudsmith (Red Hat-based systems only)
- name: Publish .rpm to Cloudsmith
if: matrix.os == 'linux'
working-directory: ./backend
run: cloudsmith push rpm --republish --no-wait-for-sync --api-key=${{ secrets.CLOUDSMITH_API_KEY }} infisical/infisical-core/any-distro/any-version ./binary/infisical-core-${{ matrix.arch }}.rpm
# Publish .exe file to Cloudsmith (Windows only)
- name: Publish to Cloudsmith (Windows)
if: matrix.os == 'win'

View File

@@ -127,6 +127,7 @@ jobs:
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
AUDIT_LOGS_DB_CONNECTION_URI: ${{ secrets.AUDIT_LOGS_DB_CONNECTION_URI }}
run: |
cd backend
npm install

View File

@@ -135,9 +135,7 @@ Lean about Infisical's code scanning feature [here](https://infisical.com/docs/c
This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo):
<a href="[https://infisical.cal.com/vlad/infisical-demo](https://infisical.cal.com/vlad/infisical-demo)"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo).
## Security
@@ -163,4 +161,3 @@ Not sure where to get started? You can:
- [Twitter](https://twitter.com/infisical) for fast news
- [YouTube](https://www.youtube.com/@infisical_os) for videos on secret management
- [Blog](https://infisical.com/blog) for secret management insights, articles, tutorials, and updates
- [Roadmap](https://www.notion.so/infisical/be2d2585a6694e40889b03aef96ea36b?v=5b19a8127d1a4060b54769567a8785fa) for planned features

View File

@@ -123,7 +123,7 @@ describe("Project Environment Router", async () => {
id: deletedProjectEnvironment.id,
name: mockProjectEnv.name,
slug: mockProjectEnv.slug,
position: 4,
position: 5,
createdAt: expect.any(String),
updatedAt: expect.any(String)
})

View File

@@ -61,7 +61,7 @@
"jwks-rsa": "^3.1.0",
"knex": "^3.0.1",
"ldapjs": "^3.0.7",
"ldif": "^0.5.1",
"ldif": "0.5.1",
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"mongodb": "^6.8.1",

View File

@@ -45,13 +45,19 @@
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
"generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
"auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest",
"auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up",
"auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down",
"auditlog-migration:list": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:list",
"auditlog-migration:status": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:status",
"auditlog-migration:rollback": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:rollback",
"migration:new": "tsx ./scripts/create-migration.ts",
"migration:up": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:status": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"migration:up": "npm run auditlog-migration:up && knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
"migration:down": "npm run auditlog-migration:down && knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
"migration:list": "npm run auditlog-migration:list && knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
"migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"

View File

@@ -90,7 +90,12 @@ const main = async () => {
.whereRaw("table_schema = current_schema()")
.select<{ tableName: string }[]>("table_name as tableName")
.orderBy("table_name")
).filter((el) => !el.tableName.includes("_migrations"));
).filter(
(el) =>
!el.tableName.includes("_migrations") &&
!el.tableName.includes("audit_logs_") &&
el.tableName !== "intermediate_audit_logs"
);
for (let i = 0; i < tables.length; i += 1) {
const { tableName } = tables[i];

View File

@@ -0,0 +1,75 @@
// eslint-disable-next-line
import "ts-node/register";
import dotenv from "dotenv";
import type { Knex } from "knex";
import path from "path";
// Update with your config settings. .
dotenv.config({
path: path.join(__dirname, "../../../.env.migration")
});
dotenv.config({
path: path.join(__dirname, "../../../.env")
});
if (!process.env.AUDIT_LOGS_DB_CONNECTION_URI && !process.env.AUDIT_LOGS_DB_HOST) {
console.info("Dedicated audit log database not found. No further migrations necessary");
process.exit(0);
}
console.info("Executing migration on audit log database...");
export default {
development: {
client: "postgres",
connection: {
connectionString: process.env.AUDIT_LOGS_DB_CONNECTION_URI,
host: process.env.AUDIT_LOGS_DB_HOST,
port: process.env.AUDIT_LOGS_DB_PORT,
user: process.env.AUDIT_LOGS_DB_USER,
database: process.env.AUDIT_LOGS_DB_NAME,
password: process.env.AUDIT_LOGS_DB_PASSWORD,
ssl: process.env.AUDIT_LOGS_DB_ROOT_CERT
? {
rejectUnauthorized: true,
ca: Buffer.from(process.env.AUDIT_LOGS_DB_ROOT_CERT, "base64").toString("ascii")
}
: false
},
pool: {
min: 2,
max: 10
},
seeds: {
directory: "./seeds"
},
migrations: {
tableName: "infisical_migrations"
}
},
production: {
client: "postgres",
connection: {
connectionString: process.env.AUDIT_LOGS_DB_CONNECTION_URI,
host: process.env.AUDIT_LOGS_DB_HOST,
port: process.env.AUDIT_LOGS_DB_PORT,
user: process.env.AUDIT_LOGS_DB_USER,
database: process.env.AUDIT_LOGS_DB_NAME,
password: process.env.AUDIT_LOGS_DB_PASSWORD,
ssl: process.env.AUDIT_LOGS_DB_ROOT_CERT
? {
rejectUnauthorized: true,
ca: Buffer.from(process.env.AUDIT_LOGS_DB_ROOT_CERT, "base64").toString("ascii")
}
: false
},
pool: {
min: 2,
max: 10
},
migrations: {
tableName: "infisical_migrations"
}
}
} as Knex.Config;

View File

@@ -1,2 +1,2 @@
export type { TDbClient } from "./instance";
export { initDbConnection } from "./instance";
export { initAuditLogDbConnection, initDbConnection } from "./instance";

View File

@@ -70,3 +70,45 @@ export const initDbConnection = ({
return db;
};
export const initAuditLogDbConnection = ({
dbConnectionUri,
dbRootCert
}: {
dbConnectionUri: string;
dbRootCert?: string;
}) => {
// akhilmhdh: the default Knex is knex.Knex<any, any[]>. but when assigned with knex({<config>}) the value is knex.Knex<any, unknown[]>
// this was causing issue with files like `snapshot-dal` `findRecursivelySnapshots` this i am explicitly putting the any and unknown[]
// eslint-disable-next-line
const db: Knex<any, unknown[]> = knex({
client: "pg",
connection: {
connectionString: dbConnectionUri,
host: process.env.AUDIT_LOGS_DB_HOST,
// @ts-expect-error I have no clue why only for the port there is a type error
// eslint-disable-next-line
port: process.env.AUDIT_LOGS_DB_PORT,
user: process.env.AUDIT_LOGS_DB_USER,
database: process.env.AUDIT_LOGS_DB_NAME,
password: process.env.AUDIT_LOGS_DB_PASSWORD,
ssl: dbRootCert
? {
rejectUnauthorized: true,
ca: Buffer.from(dbRootCert, "base64").toString("ascii")
}
: false
}
});
// we add these overrides so that auditLogDb and the primary DB are interchangeable
db.primaryNode = () => {
return db;
};
db.replicaNode = () => {
return db;
};
return db;
};

View File

@@ -0,0 +1,161 @@
import kx, { Knex } from "knex";
import { TableName } from "../schemas";
const INTERMEDIATE_AUDIT_LOG_TABLE = "intermediate_audit_logs";
const formatPartitionDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
const createAuditLogPartition = async (knex: Knex, startDate: Date, endDate: Date) => {
const startDateStr = formatPartitionDate(startDate);
const endDateStr = formatPartitionDate(endDate);
const partitionName = `${TableName.AuditLog}_${startDateStr.replace(/-/g, "")}_${endDateStr.replace(/-/g, "")}`;
await knex.schema.raw(
`CREATE TABLE ${partitionName} PARTITION OF ${TableName.AuditLog} FOR VALUES FROM ('${startDateStr}') TO ('${endDateStr}')`
);
};
const up = async (knex: Knex): Promise<void> => {
console.info("Dropping primary key of audit log table...");
await knex.schema.alterTable(TableName.AuditLog, (t) => {
// remove existing keys
t.dropPrimary();
});
// Get all indices of the audit log table and drop them
const indexNames: { rows: { indexname: string }[] } = await knex.raw(
`
SELECT indexname
FROM pg_indexes
WHERE tablename = '${TableName.AuditLog}'
`
);
console.log(
"Deleting existing audit log indices:",
indexNames.rows.map((e) => e.indexname)
);
for await (const row of indexNames.rows) {
await knex.raw(`DROP INDEX IF EXISTS ${row.indexname}`);
}
// renaming audit log to intermediate table
console.log("Renaming audit log table to the intermediate name");
await knex.schema.renameTable(TableName.AuditLog, INTERMEDIATE_AUDIT_LOG_TABLE);
if (!(await knex.schema.hasTable(TableName.AuditLog))) {
const createTableSql = knex.schema
.createTable(TableName.AuditLog, (t) => {
t.uuid("id").defaultTo(knex.fn.uuid());
t.string("actor").notNullable();
t.jsonb("actorMetadata").notNullable();
t.string("ipAddress");
t.string("eventType").notNullable();
t.jsonb("eventMetadata");
t.string("userAgent");
t.string("userAgentType");
t.datetime("expiresAt");
t.timestamps(true, true, true);
t.uuid("orgId");
t.string("projectId");
t.string("projectName");
t.primary(["id", "createdAt"]);
})
.toString();
console.info("Creating partition table...");
await knex.schema.raw(`
${createTableSql} PARTITION BY RANGE ("createdAt");
`);
console.log("Adding indices...");
await knex.schema.alterTable(TableName.AuditLog, (t) => {
t.index(["projectId", "createdAt"]);
t.index(["orgId", "createdAt"]);
t.index("expiresAt");
t.index("orgId");
t.index("projectId");
});
console.log("Adding GIN indices...");
await knex.raw(
`CREATE INDEX IF NOT EXISTS "audit_logs_actorMetadata_idx" ON ${TableName.AuditLog} USING gin("actorMetadata" jsonb_path_ops)`
);
console.log("GIN index for actorMetadata done");
await knex.raw(
`CREATE INDEX IF NOT EXISTS "audit_logs_eventMetadata_idx" ON ${TableName.AuditLog} USING gin("eventMetadata" jsonb_path_ops)`
);
console.log("GIN index for eventMetadata done");
// create default partition
console.log("Creating default partition...");
await knex.schema.raw(`CREATE TABLE ${TableName.AuditLog}_default PARTITION OF ${TableName.AuditLog} DEFAULT`);
const nextDate = new Date();
nextDate.setDate(nextDate.getDate() + 1);
const nextDateStr = formatPartitionDate(nextDate);
console.log("Attaching existing audit log table as a partition...");
await knex.schema.raw(`
ALTER TABLE ${INTERMEDIATE_AUDIT_LOG_TABLE} ADD CONSTRAINT audit_log_old
CHECK ( "createdAt" < DATE '${nextDateStr}' );
ALTER TABLE ${TableName.AuditLog} ATTACH PARTITION ${INTERMEDIATE_AUDIT_LOG_TABLE}
FOR VALUES FROM (MINVALUE) TO ('${nextDateStr}' );
`);
// create partition from now until end of month
console.log("Creating audit log partitions ahead of time... next date:", nextDateStr);
await createAuditLogPartition(knex, nextDate, new Date(nextDate.getFullYear(), nextDate.getMonth() + 1));
// create partitions 4 years ahead
const partitionMonths = 4 * 12;
const partitionPromises: Promise<void>[] = [];
for (let x = 1; x <= partitionMonths; x += 1) {
partitionPromises.push(
createAuditLogPartition(
knex,
new Date(nextDate.getFullYear(), nextDate.getMonth() + x, 1),
new Date(nextDate.getFullYear(), nextDate.getMonth() + (x + 1), 1)
)
);
}
await Promise.all(partitionPromises);
console.log("Partition migration complete");
}
};
export const executeMigration = async (url: string) => {
console.log("Executing migration...");
const knex = kx({
client: "pg",
connection: url
});
await knex.transaction(async (tx) => {
await up(tx);
});
};
const dbUrl = process.env.AUDIT_LOGS_DB_CONNECTION_URI;
if (!dbUrl) {
console.error("Please provide a DB connection URL to the AUDIT_LOGS_DB_CONNECTION_URI env");
process.exit(1);
}
void executeMigration(dbUrl).then(() => {
console.log("Migration: partition-audit-logs DONE");
process.exit(0);
});

View File

@@ -0,0 +1,48 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.AuditLog)) {
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
const doesProjectNameExist = await knex.schema.hasColumn(TableName.AuditLog, "projectName");
await knex.schema.alterTable(TableName.AuditLog, (t) => {
if (doesOrgIdExist) {
t.dropForeign("orgId");
}
if (doesProjectIdExist) {
t.dropForeign("projectId");
}
// add normalized field
if (!doesProjectNameExist) {
t.string("projectName");
}
});
}
}
export async function down(knex: Knex): Promise<void> {
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
const doesProjectNameExist = await knex.schema.hasColumn(TableName.AuditLog, "projectName");
if (await knex.schema.hasTable(TableName.AuditLog)) {
await knex.schema.alterTable(TableName.AuditLog, (t) => {
if (doesOrgIdExist) {
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
}
if (doesProjectIdExist) {
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
}
// remove normalized field
if (doesProjectNameExist) {
t.dropColumn("projectName");
}
});
}
}

View File

@@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
// org default role
if (await knex.schema.hasTable(TableName.Organization)) {
const hasDefaultRoleCol = await knex.schema.hasColumn(TableName.Organization, "defaultMembershipRole");
if (!hasDefaultRoleCol) {
await knex.schema.alterTable(TableName.Organization, (tb) => {
tb.string("defaultMembershipRole").notNullable().defaultTo("member");
});
}
}
}
export async function down(knex: Knex): Promise<void> {
// org default role
if (await knex.schema.hasTable(TableName.Organization)) {
const hasDefaultRoleCol = await knex.schema.hasColumn(TableName.Organization, "defaultMembershipRole");
if (hasDefaultRoleCol) {
await knex.schema.alterTable(TableName.Organization, (tb) => {
tb.dropColumn("defaultMembershipRole");
});
}
}
}

View File

@@ -20,7 +20,8 @@ export const AuditLogsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
orgId: z.string().uuid().nullable().optional(),
projectId: z.string().nullable().optional()
projectId: z.string().nullable().optional(),
projectName: z.string().nullable().optional()
});
export type TAuditLogs = z.infer<typeof AuditLogsSchema>;

View File

@@ -19,7 +19,8 @@ export const OrganizationsSchema = z.object({
authEnforced: z.boolean().default(false).nullable().optional(),
scimEnabled: z.boolean().default(false).nullable().optional(),
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
kmsEncryptedDataKey: zodBuffer.nullable().optional()
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
defaultMembershipRole: z.string().default("member")
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;

View File

@@ -1,8 +1,9 @@
import { Knex } from "knex";
// weird commonjs-related error in the CI requires us to do the import like this
import knex from "knex";
import { TDbClient } from "@app/db";
import { AuditLogsSchema, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { TableName } from "@app/db/schemas";
import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
@@ -46,7 +47,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
eventType?: EventType[];
eventMetadata?: Record<string, string>;
},
tx?: Knex
tx?: knex.Knex
) => {
if (!orgId && !projectId) {
throw new Error("Either orgId or projectId must be provided");
@@ -55,11 +56,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
try {
// Find statements
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
// eslint-disable-next-line func-names
.where(function () {
if (orgId) {
void this.where(`${TableName.Project}.orgId`, orgId).orWhere(`${TableName.AuditLog}.orgId`, orgId);
void this.where(`${TableName.AuditLog}.orgId`, orgId);
} else if (projectId) {
void this.where(`${TableName.AuditLog}.projectId`, projectId);
}
@@ -72,23 +72,19 @@ export const auditLogDALFactory = (db: TDbClient) => {
// Select statements
void sqlQuery
.select(selectAllTableCols(TableName.AuditLog))
.select(
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
)
.limit(limit)
.offset(offset)
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
// Special case: Filter by actor ID
if (actorId) {
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
void sqlQuery.whereRaw(`"actorMetadata" @> jsonb_build_object('userId', ?::text)`, [actorId]);
}
// Special case: Filter by key/value pairs in eventMetadata field
if (eventMetadata && Object.keys(eventMetadata).length) {
Object.entries(eventMetadata).forEach(([key, value]) => {
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object(?::text, ?::text)`, [key, value]);
});
}
@@ -109,30 +105,25 @@ export const auditLogDALFactory = (db: TDbClient) => {
if (endDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
}
const docs = await sqlQuery;
return docs.map((doc) => {
// Our type system refuses to acknowledge that the project name and slug are present in the doc, due to the disjointed query structure above.
// This is a quick and dirty way to get around the types.
const projectDoc = doc as unknown as { projectName: string; projectSlug: string };
// we timeout long running queries to prevent DB resource issues (2 minutes)
const docs = await sqlQuery.timeout(1000 * 120);
return {
...AuditLogsSchema.parse(doc),
...(projectDoc?.projectSlug && {
project: {
name: projectDoc.projectName,
slug: projectDoc.projectSlug
}
})
};
});
return docs;
} catch (error) {
if (error instanceof knex.KnexTimeoutError) {
throw new GatewayTimeoutError({
error,
message: "Failed to fetch audit logs due to timeout. Add more search filters."
});
}
throw new DatabaseError({ error });
}
};
// delete all audit log that have expired
const pruneAuditLog = async (tx?: Knex) => {
const pruneAuditLog = async (tx?: knex.Knex) => {
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
@@ -148,6 +139,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
.where("expiresAt", "<", today)
.select("id")
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
// eslint-disable-next-line no-await-in-loop
deletedAuditLogIds = await (tx || db)(TableName.AuditLog)
.whereIn("id", findExpiredLogSubQuery)

View File

@@ -74,6 +74,7 @@ export const auditLogQueueServiceFactory = ({
actorMetadata: actor.metadata,
userAgent,
projectId,
projectName: project?.name,
ipAddress,
orgId,
eventType: event.type,

View File

@@ -1,14 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import {
OrgMembershipRole,
OrgMembershipStatus,
SecretKeyEncoding,
TableName,
TLdapConfigsUpdate,
TUsers
} from "@app/db/schemas";
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -28,6 +21,7 @@ import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -444,11 +438,14 @@ export const ldapConfigServiceFactory = ({
{ tx }
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgDAL.createMembership(
{
userId: userAlias.userId,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: OrgMembershipStatus.Accepted,
isActive: true
},
@@ -529,12 +526,15 @@ export const ldapConfigServiceFactory = ({
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},

View File

@@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
@@ -23,6 +23,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -187,12 +188,15 @@ export const oidcConfigServiceFactory = ({
{ tx }
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},
@@ -261,12 +265,15 @@ export const oidcConfigServiceFactory = ({
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},

View File

@@ -2,7 +2,6 @@ import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import {
OrgMembershipRole,
OrgMembershipStatus,
SecretKeyEncoding,
TableName,
@@ -26,6 +25,7 @@ import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -369,12 +369,15 @@ export const samlConfigServiceFactory = ({
{ tx }
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},
@@ -472,12 +475,15 @@ export const samlConfigServiceFactory = ({
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},

View File

@@ -16,6 +16,7 @@ import { AuthTokenType } from "@app/services/auth/auth-type";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -318,12 +319,15 @@ export const scimServiceFactory = ({
);
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
orgMembership = await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.NoAccess,
role,
roleId,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},
@@ -391,12 +395,15 @@ export const scimServiceFactory = ({
orgMembership = foundOrgMembership;
if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
orgMembership = await orgMembershipDAL.create(
{
userId: user.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role,
roleId,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},

View File

@@ -533,7 +533,8 @@ export const ENVIRONMENTS = {
CREATE: {
workspaceId: "The ID of the project to create the environment in.",
name: "The name of the environment to create.",
slug: "The slug of the environment to create."
slug: "The slug of the environment to create.",
position: "The position of the environment. The lowest number will be displayed as the first environment."
},
UPDATE: {
workspaceId: "The ID of the project to update the environment in.",

View File

@@ -34,6 +34,12 @@ const envSchema = z
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`
),
AUDIT_LOGS_DB_CONNECTION_URI: zpStr(
z.string().describe("Postgres database connection string for Audit logs").optional()
),
AUDIT_LOGS_DB_ROOT_CERT: zpStr(
z.string().describe("Postgres database base64-encoded CA cert for Audit logs").optional()
),
MAX_LEASE_LIMIT: z.coerce.number().default(10000),
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),

View File

@@ -23,6 +23,18 @@ export class InternalServerError extends Error {
}
}
export class GatewayTimeoutError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
super(message || "Timeout error");
this.name = name || "GatewayTimeoutError";
this.error = error;
}
}
export class UnauthorizedError extends Error {
name: string;

View File

@@ -8,12 +8,14 @@ const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
return filter;
};
export const generateKnexQueryFromScim = (
const processDynamicQuery = (
rootQuery: Knex.QueryBuilder,
rootScimFilter: string,
getAttributeField: (attr: string) => string | null
scimRootFilterAst: Filter,
getAttributeField: (attr: string) => string | null,
depth = 0
) => {
const scimRootFilterAst = parse(rootScimFilter);
if (depth > 20) return;
const stack = [
{
scimFilterAst: scimRootFilterAst,
@@ -75,42 +77,35 @@ export const generateKnexQueryFromScim = (
break;
}
case "and": {
void query.andWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
scimFilterAst.filters.forEach((el) => {
void query.andWhere((subQueryBuilder) => {
processDynamicQuery(subQueryBuilder, el, getAttributeField, depth + 1);
});
});
break;
}
case "or": {
void query.orWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
scimFilterAst.filters.forEach((el) => {
void query.orWhere((subQueryBuilder) => {
processDynamicQuery(subQueryBuilder, el, getAttributeField, depth + 1);
});
});
break;
}
case "not": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: scimFilterAst.filter
});
processDynamicQuery(subQueryBuilder, scimFilterAst.filter, getAttributeField, depth + 1);
});
break;
}
case "[]": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter)
});
void query.where((subQueryBuilder) => {
processDynamicQuery(
subQueryBuilder,
appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter),
getAttributeField,
depth + 1
);
});
break;
}
@@ -119,3 +114,12 @@ export const generateKnexQueryFromScim = (
}
}
};
export const generateKnexQueryFromScim = (
rootQuery: Knex.QueryBuilder,
rootScimFilter: string,
getAttributeField: (attr: string) => string | null
) => {
const scimRootFilterAst = parse(rootScimFilter);
return processDynamicQuery(rootQuery, scimRootFilterAst, getAttributeField);
};

View File

@@ -1,7 +1,7 @@
import dotenv from "dotenv";
import path from "path";
import { initDbConnection } from "./db";
import { initAuditLogDbConnection, initDbConnection } from "./db";
import { keyStoreFactory } from "./keystore/keystore";
import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env";
import { isMigrationMode } from "./lib/fn";
@@ -25,6 +25,13 @@ const run = async () => {
}))
});
const auditLogDb = appCfg.AUDIT_LOGS_DB_CONNECTION_URI
? initAuditLogDbConnection({
dbConnectionUri: appCfg.AUDIT_LOGS_DB_CONNECTION_URI,
dbRootCert: appCfg.AUDIT_LOGS_DB_ROOT_CERT
})
: undefined;
// Case: App is running in packaged mode (binary), and migration mode is enabled.
// Run the migrations and exit the process after completion.
if (IS_PACKAGED && isMigrationMode()) {
@@ -46,7 +53,7 @@ const run = async () => {
const queue = queueServiceFactory(appCfg.REDIS_URL);
const keyStore = keyStoreFactory(appCfg.REDIS_URL);
const server = await main({ db, smtp, logger, queue, keyStore });
const server = await main({ db, auditLogDb, smtp, logger, queue, keyStore });
const bootstrap = await bootstrapCheck({ db });
// eslint-disable-next-line

View File

@@ -30,6 +30,7 @@ import { fastifySwagger } from "./plugins/swagger";
import { registerRoutes } from "./routes";
type TMain = {
auditLogDb?: Knex;
db: Knex;
smtp: TSmtpService;
logger?: Logger;
@@ -38,7 +39,7 @@ type TMain = {
};
// Run the server!
export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TMain) => {
const appCfg = getConfig();
const server = fastify({
logger: appCfg.NODE_ENV === "test" ? false : logger,
@@ -94,7 +95,7 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
await server.register(maintenanceMode);
await server.register(registerRoutes, { smtp, queue, db, keyStore });
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore });
if (appCfg.isProductionMode) {
await server.register(registerExternalNextjs, {

View File

@@ -3,9 +3,12 @@ import fp from "fastify-plugin";
import { DefaultResponseErrorsSchema } from "../routes/sanitizedSchemas";
const isScimRoutes = (pathname: string) =>
pathname.startsWith("/api/v1/scim/Users") || pathname.startsWith("/api/v1/scim/Groups");
export const addErrorsToResponseSchemas = fp(async (server) => {
server.addHook("onRoute", (routeOptions) => {
if (routeOptions.schema && routeOptions.schema.response) {
if (routeOptions.schema && routeOptions.schema.response && !isScimRoutes(routeOptions.path)) {
routeOptions.schema.response = {
...DefaultResponseErrorsSchema,
...routeOptions.schema.response

View File

@@ -7,6 +7,7 @@ import {
BadRequestError,
DatabaseError,
ForbiddenRequestError,
GatewayTimeoutError,
InternalServerError,
NotFoundError,
ScimRequestError,
@@ -25,7 +26,8 @@ enum HttpStatusCodes {
Unauthorized = 401,
Forbidden = 403,
// eslint-disable-next-line @typescript-eslint/no-shadow
InternalServerError = 500
InternalServerError = 500,
GatewayTimeout = 504
}
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
@@ -47,6 +49,10 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
void res
.status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name });
} else if (error instanceof GatewayTimeoutError) {
void res
.status(HttpStatusCodes.GatewayTimeout)
.send({ statusCode: HttpStatusCodes.GatewayTimeout, message: error.message, error: error.name });
} else if (error instanceof ZodError) {
void res
.status(HttpStatusCodes.Unauthorized)
@@ -91,7 +97,11 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
message
});
} else {
void res.send(error);
void res.status(HttpStatusCodes.InternalServerError).send({
statusCode: HttpStatusCodes.InternalServerError,
error: "InternalServerError",
message: "Something went wrong"
});
}
});
});

View File

@@ -214,11 +214,12 @@ import { registerV3Routes } from "./v3";
export const registerRoutes = async (
server: FastifyZodProvider,
{
auditLogDb,
db,
smtp: smtpService,
queue: queueService,
keyStore
}: { db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
}: { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
) => {
const appCfg = getConfig();
if (!appCfg.DISABLE_SECRET_SCANNING) {
@@ -283,7 +284,7 @@ export const registerRoutes = async (
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
const auditLogDAL = auditLogDALFactory(db);
const auditLogDAL = auditLogDALFactory(auditLogDb ?? db);
const auditLogStreamDAL = auditLogStreamDALFactory(db);
const trustedIpDAL = trustedIpDALFactory(db);
const telemetryDAL = telemetryDALFactory(db);
@@ -531,7 +532,7 @@ export const registerRoutes = async (
orgService,
licenseService
});
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL });
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL, orgDAL });
const superAdminService = superAdminServiceFactory({
userDAL,
authService: loginService,

View File

@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import {
@@ -11,8 +12,6 @@ import {
} from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -125,12 +124,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
})
.merge(
z.object({
project: z
.object({
name: z.string(),
slug: z.string()
})
.optional(),
event: z.object({
type: z.string(),
metadata: z.any()
@@ -147,11 +140,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const appCfg = getConfig();
if (appCfg.isCloud) {
throw new BadRequestError({ message: "Infisical cloud audit log is in maintenance mode." });
}
const auditLogs = await server.services.auditLog.listAuditLogs({
filter: {
...req.query,
@@ -168,6 +156,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type
});
return { auditLogs };
}
});
@@ -229,7 +218,15 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
.regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens")
.optional(),
authEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional()
scimEnabled: z.boolean().optional(),
defaultMembershipRoleSlug: z
.string()
.min(1)
.trim()
.refine((v) => slugify(v) === v, {
message: "Membership role must be a valid slug"
})
.optional()
}),
response: {
200: z.object({

View File

@@ -123,6 +123,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
}),
body: z.object({
name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position),
slug: z
.string()
.trim()

View File

@@ -4,9 +4,12 @@ import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const MB25_IN_BYTES = 26214400;
export const registerExternalMigrationRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
bodyLimit: MB25_IN_BYTES,
url: "/env-key",
config: {
rateLimit: readLimit

View File

@@ -187,7 +187,7 @@ export const importDataIntoInfisicalFn = async ({
secretPath: "/",
secretName: secret.name,
type: SecretType.Shared,
secretValue: secret.value
secretValue: secret.value || ""
});
if (!newSecret) {
throw new BadRequestError({ message: `Failed to import secret: [name:${secret.name}] [id:${id}]` });

View File

@@ -18,7 +18,7 @@ export type InfisicalImportData = {
name: string;
id: string;
environmentId: string;
value: string;
value?: string;
}
>;
};

View File

@@ -9,6 +9,7 @@
import {
CreateSecretCommand,
DeleteSecretCommand,
DescribeSecretCommand,
GetSecretValueCommand,
ResourceNotFoundException,
@@ -899,12 +900,21 @@ const syncSecretsAWSSecretManager = async ({
}
if (!isEqual(secretToCompare, secretValue)) {
await secretsManager.send(
new UpdateSecretCommand({
SecretId: secretId,
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue)
})
);
if (secretValue) {
await secretsManager.send(
new UpdateSecretCommand({
SecretId: secretId,
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue)
})
);
// delete it
} else {
await secretsManager.send(
new DeleteSecretCommand({
SecretId: secretId
})
);
}
}
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
@@ -989,16 +999,21 @@ const syncSecretsAWSSecretManager = async ({
} catch (err) {
// case 1: when AWS manager can't find the specified secret
if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send(
new CreateSecretCommand({
Name: secretId,
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue),
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
: []
})
);
if (secretValue) {
await secretsManager.send(
new CreateSecretCommand({
Name: secretId,
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue),
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
: []
})
);
}
// case 2: something unexpected went wrong, so we'll throw the error to reflect the error in the integration sync status
} else {
throw err;

View File

@@ -108,7 +108,9 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
)
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
.where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
.orderBy("firstName")
.orderBy("lastName");
return members.map(({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
@@ -370,6 +372,7 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization),
db.ref("defaultMembershipRole").withSchema(TableName.Organization),
db.ref("externalId").withSchema(TableName.UserAliases)
)
.where({ isGhost: false });

View File

@@ -0,0 +1,54 @@
import { OrgMembershipRole } from "@app/db/schemas";
import { TFeatureSet } from "@app/ee/services/license/license-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
const RESERVED_ORG_ROLE_SLUGS = Object.values(OrgMembershipRole).filter((role) => role !== "custom");
// this is only for updating an org
export const getDefaultOrgMembershipRoleForUpdateOrg = async ({
membershipRoleSlug,
orgRoleDAL,
plan,
orgId
}: {
orgId: string;
membershipRoleSlug: string;
orgRoleDAL: TOrgRoleDALFactory;
plan: TFeatureSet;
}) => {
const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(membershipRoleSlug as OrgMembershipRole);
if (isCustomRole) {
if (!plan?.rbac)
throw new BadRequestError({
message:
"Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role."
});
const customRole = await orgRoleDAL.findOne({ slug: membershipRoleSlug, orgId });
if (!customRole) throw new NotFoundError({ name: "UpdateOrg", message: "Organization role not found" });
// use ID for default role
return customRole.id;
}
// not custom, use reserved slug
return membershipRoleSlug;
};
// this is only for creating an org membership
export const getDefaultOrgMembershipRole = async (
defaultOrgMembershipRole: string // can either be ID or reserved slug
) => {
const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(defaultOrgMembershipRole as OrgMembershipRole);
if (isCustomRole)
return {
roleId: defaultOrgMembershipRole,
role: OrgMembershipRole.Custom
};
// will be reserved slug
return { roleId: undefined, role: defaultOrgMembershipRole as OrgMembershipRole };
};

View File

@@ -11,6 +11,7 @@ import {
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { ActorAuthMethod } from "../auth/auth-type";
import { TOrgRoleDALFactory } from "./org-role-dal";
@@ -18,11 +19,12 @@ import { TOrgRoleDALFactory } from "./org-role-dal";
type TOrgRoleServiceFactoryDep = {
orgRoleDAL: TOrgRoleDALFactory;
permissionService: TPermissionServiceFactory;
orgDAL: TOrgDALFactory;
};
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRoleServiceFactoryDep) => {
export const orgRoleServiceFactory = ({ orgRoleDAL, orgDAL, permissionService }: TOrgRoleServiceFactoryDep) => {
const createRole = async (
userId: string,
orgId: string,
@@ -129,6 +131,19 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
const org = await orgDAL.findOrgById(orgId);
if (!org)
throw new NotFoundError({
message: "Failed to find organization"
});
if (org.defaultMembershipRole === roleId)
throw new BadRequestError({
message: "Cannot delete default org membership role. Please re-assign and try again."
});
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
if (!deletedRole) throw new NotFoundError({ message: "Organization role not found", name: "Update role" });

View File

@@ -32,6 +32,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@@ -264,7 +265,7 @@ export const orgServiceFactory = ({
actorOrgId,
actorAuthMethod,
orgId,
data: { name, slug, authEnforced, scimEnabled }
data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug }
}: TUpdateOrgDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
@@ -298,11 +299,22 @@ export const orgServiceFactory = ({
});
}
let defaultMembershipRole: string | undefined;
if (defaultMembershipRoleSlug) {
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
membershipRoleSlug: defaultMembershipRoleSlug,
orgId,
orgRoleDAL,
plan
});
}
const org = await orgDAL.updateById(orgId, {
name,
slug: slug ? slugify(slug) : undefined,
authEnforced,
scimEnabled
scimEnabled,
defaultMembershipRole
});
if (!org) throw new NotFoundError({ message: "Organization not found" });
return org;

View File

@@ -63,7 +63,13 @@ export type TFindAllWorkspacesDTO = {
};
export type TUpdateOrgDTO = {
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>;
data: Partial<{
name: string;
slug: string;
authEnforced: boolean;
scimEnabled: boolean;
defaultMembershipRoleSlug: string;
}>;
} & TOrgPermission;
export type TGetOrgGroupsDTO = TOrgPermission;

View File

@@ -65,10 +65,16 @@ export const projectEnvDALFactory = (db: TDbClient) => {
}
};
const shiftPositions = async (projectId: string, pos: number, tx?: Knex) => {
// Shift all positions >= the new position up by 1
await (tx || db)(TableName.Environment).where({ projectId }).where("position", ">=", pos).increment("position", 1);
};
return {
...projectEnvOrm,
findBySlugs,
findLastEnvPosition,
updateAllPosition
updateAllPosition,
shiftPositions
};
};

View File

@@ -37,6 +37,7 @@ export const projectEnvServiceFactory = ({
actor,
actorOrgId,
actorAuthMethod,
position,
name,
slug
}: TCreateEnvDTO) => {
@@ -83,9 +84,25 @@ export const projectEnvServiceFactory = ({
}
const env = await projectEnvDAL.transaction(async (tx) => {
if (position !== undefined) {
// Check if there's an environment at the specified position
const existingEnvWithPosition = await projectEnvDAL.findOne({ projectId, position }, tx);
// If there is, then shift positions
if (existingEnvWithPosition) {
await projectEnvDAL.shiftPositions(projectId, position, tx);
}
const doc = await projectEnvDAL.create({ slug, name, projectId, position }, tx);
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
return doc;
}
// If no position is specified, add to the end
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx);
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
return doc;
});
@@ -150,7 +167,11 @@ export const projectEnvServiceFactory = ({
const env = await projectEnvDAL.transaction(async (tx) => {
if (position) {
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
const existingEnvWithPosition = await projectEnvDAL.findOne({ projectId, position }, tx);
if (existingEnvWithPosition && existingEnvWithPosition.id !== oldEnv.id) {
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
}
}
return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx);
});
@@ -199,7 +220,6 @@ export const projectEnvServiceFactory = ({
name: "DeleteEnvironment"
});
await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx);
return doc;
});

View File

@@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
export type TCreateEnvDTO = {
name: string;
slug: string;
position?: number;
} & TProjectPermission;
export type TUpdateEnvDTO = {

View File

@@ -4,6 +4,27 @@ title: "Changelog"
The changelog below reflects new product developments and updates on a monthly basis.
## September 2024
- Improved paginations for identities and secrets.
- Significant improvements to the [Infisical Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs).
- Created [Slack Integration](https://infisical.com/docs/documentation/platform/workflow-integrations/slack-integration#slack-integration) for Access Requests and Approval Workflows.
- Added Dynamic Secrets for [Elaticsearch](https://infisical.com/docs/documentation/platform/dynamic-secrets/elastic-search) and [MongoDB](https://infisical.com/docs/documentation/platform/dynamic-secrets/mongo-db).
- More authentication methods are now supported by Infisical SDKs and Agent.
- Integrations now have dedicated audit logs and an overview screen.
- Added support for secret referencing in the Terraform Provider.
- Released support for [older versions of .NET](https://www.nuget.org/packages/Infisical.Sdk#supportedframeworks-body-tab) via SDK.
- Released Infisical PKI Issuer which works alongside `cert-manager` to manage certificates in Kubernetes.
## August 2024
- Added [Azure DevOps integration](https://infisical.com/docs/integrations/cloud/azure-devops).
- Released ability to hot-reload variables in CLI ([--watch flag](https://infisical.com/docs/cli/commands/run#infisical-run:watch)).
- Added Dynamic Secrets for [Redis](https://infisical.com/docs/documentation/platform/dynamic-secrets/redis).
- Added [Alerting](https://infisical.com/docs/documentation/platform/pki/alerting) for Certificate Management.
- You can now specify roles and project memberships when adding new users.
- Approval workflows now have email notifications.
- Access requests are now integrated with User Groups.
- Released ability to use IAM Roles for AWS Integrations.
## July 2024
- Released the official [Ruby SDK](https://infisical.com/docs/sdks/languages/ruby).
- Increased the speed and efficiency of secret operations.

View File

@@ -0,0 +1,32 @@
---
title: "Organization Admin Console"
description: "Manage your Infisical organization from our organization admin console."
---
The Organization Admin Console provides a user-friendly interface for Infisical organization admins to manage organization-related configurations.
## Accessing the Organization Admin Console
Only organization admins have access to the Organization Admin Console.
![Access Organization Admin Panel](/images/platform/admin-panels/access-org-admin-console.png)
1. Click on the profile icon in the left sidebar.
2. From the dropdown menu, select `Organization Admin Console`.
## Projects Section
![Projects Section](/images/platform/admin-panels/org-admin-console-projects.png)
The Projects Section lists all projects created within your organization, including those you do not have membership in. You can easily search for a project by name using the search bar.
### Accessing a Project in Your Organization
If you want to access a project in which you are not a member but are an organization admin, follow these steps:
![Access project](/images/platform/admin-panels/org-admin-console-access.png)
1. Click on the three-dot icon next to the project you wish to access.
2. Click on the **Access** button.
This will grant you admin permissions for the selected project and generate an audit log of your access, ensuring transparency regarding admin privileges.

View File

@@ -0,0 +1,25 @@
---
description: "Learn about Infisical's Admin Panel."
---
The Infisical Admin Panel allows you to configure and manage various resources within your organization and server.
<CardGroup cols={2}>
<Card
title="Server Admin Panel"
href="./server-admin"
icon="user-tie"
color="#000000"
>
Configure and manage your server settings effectively.
</Card>
<Card
title="Organization Admin Console"
href="./org-admin-console"
icon="sitemap"
color="#000000"
>
Manage settings specific to your organization.
</Card>
</CardGroup>

View File

@@ -0,0 +1,70 @@
---
title: "Server Admin Panel"
description: "Manage your Infisical server from the Server Admin Panel."
---
The Server Admin Panel provides a user interface for Infisical server administrators to configure various parameters as needed. This includes configuring rate limits, managing allowed signups, and more.
## Accessing the Server Admin Panel
The first user who created the account in Infisical is designated as the server administrator. You can access the admin panel by navigating as follows:
![Access Server Admin Panel](/images/platform/admin-panels/access-server-admin-panel.png)
1. Click on the profile icon in the left sidebar.
2. From the dropdown menu, select `Server Admin Panel`.
## General Section
![General Settings](/images/platform/admin-panels/admin-panel-general.png)
### Allow User Signups
This setting controls whether users can sign up for your Infisical instance. The options are:
1. **Anyone**: Any user with access to your instance can sign up.
2. **Disabled**: No one will be able to sign up.
### Restrict Signup Domain
This setting allows only users with specific email domains (such as your organization's domain) to sign up.
### Default Organization
Use this setting if you want all users accessing your Infisical instance to log in through your configured SAML/LDAP provider. This prevents users from manually entering their organization slug during authentication and redirects them to the SAML/LDAP authentication page.
### Trust Emails
By default, Infisical does not trust emails logged in via SAML/LDAP/OIDC due to the potential for email spoofing. Users must verify their email addresses before proceeding. You can disable this validation if you are running an Infisical instance within your organization and trust incoming emails from your members.
## Authentication Section
![Authentication Settings](/images/platform/admin-panels/admin-panel-auths.png)
This section allows you to configure various login and signup methods for your instance.
## Rate Limit Section
![Rate Limit Settings](/images/platform/admin-panels/admin-panel-rate-limits.png)
Configure the rate limits for your Infisical instance across various endpoints. You do not need to redeploy when making changes to rate limits; they will be automatically synchronized to all instances.
<Info>
Note that rate limit configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use.
</Info>
## User Management Section
![User Management](/images/platform/admin-panels/admin-panel-users.png)
The User Management section lists all users who have signed up for your instance. You can search for users using the search bar.
To delete a user from Infisical:
1. Search for the user.
2. Click the cross button next to the user.
3. Confirm the warning popup.
<Info>
Note that user management configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use.
</Info>

View File

@@ -0,0 +1,174 @@
---
title: CircleCI
description: "Learn how to authenticate CircleCI jobs with Infisical using OpenID Connect (OIDC)."
---
**OIDC Auth** is a platform-agnostic JWT-based authentication method that can be used to authenticate from any platform or environment using an identity provider with OpenID Connect.
## Diagram
The following sequence diagram illustrates the OIDC Auth workflow for authenticating CircleCI jobs with Infisical.
```mermaid
sequenceDiagram
participant Client as CircleCI Job
participant Idp as CircleCI Identity Provider
participant Infis as Infisical
Idp->>Client: Step 1: Inject JWT with verifiable claims
Note over Client,Infis: Step 2: Login Operation
Client->>Infis: Send signed JWT to /api/v1/auth/oidc-auth/login
Note over Infis,Idp: Step 3: Query verification
Infis->>Idp: Request JWT public key using OIDC Discovery
Idp-->>Infis: Return public key
Note over Infis: Step 4: JWT validation
Infis->>Client: Return short-lived access token
Note over Client,Infis: Step 5: Access Infisical API with Token
Client->>Infis: Make authenticated requests using the short-lived access token
```
## Concept
At a high-level, Infisical authenticates a client by verifying the JWT and checking that it meets specific requirements (e.g. it is issued by a trusted identity provider) at the `/api/v1/auth/oidc-auth/login` endpoint. If successful,
then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
To be more specific:
1. CircleCI provides the running job with a valid OIDC token specific to the execution.
2. The CircleCI OIDC token is sent to Infisical at the `/api/v1/auth/oidc-auth/login` endpoint.
3. Infisical fetches the public key that was used to sign the identity token provided by CircleCI.
4. Infisical validates the JWT using the public key provided by the identity provider and checks that the subject, audience, and claims of the token matches with the set criteria.
5. If all is well, Infisical returns a short-lived access token that CircleCI jobs can use to make authenticated requests to the Infisical API.
<Note>Infisical needs network-level access to the CircleCI servers.</Note>
## Guide
In the following steps, we explore how to create and use identities to access the Infisical API using the OIDC Auth authentication method.
<Steps>
<Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be redirected to a page where you can manage the identity.
![identities page](/images/platform/identities/identities-page.png)
Since the identity has been configured with Universal Auth by default, you should re-configure it to use OIDC Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new OIDC Auth configuration onto the identity.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities create oidc auth method](/images/platform/identities/identities-org-create-oidc-auth-method.png)
<Warning>Restrict access by configuring the Subject, Audiences, and Claims fields</Warning>
Here's some more guidance on each field:
- OIDC Discovery URL: The URL used to retrieve the OpenID Connect configuration from the identity provider. This will be used to fetch the public key needed for verifying the provided JWT. This should be set to `https://oidc.circleci.com/org/<organization_id>` where `organization_id` refers to the CircleCI organization where the job is being run.
- Issuer: The unique identifier of the identity provider issuing the JWT. This value is used to verify the iss (issuer) claim in the JWT to ensure the token is issued by a trusted provider. This should be set to `https://oidc.circleci.com/org/<organization_id>` as well.
- CA Certificate: The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints. This can be left as blank.
- Subject: The expected principal that is the subject of the JWT. The format of the sub field for CircleCI OIDC tokens is `org/<organization_id>/project/<project_id>/user/<user_id>` where organization_id, project_id, and user_id are UUIDs that identify the CircleCI organization, project, and user, respectively. The user is the CircleCI user that caused this job to run.
- Audiences: A list of intended recipients. This value is checked against the aud (audience) claim in the token. Set this to the CircleCI `organization_id` corresponding to where the job is running.
- Claims: Additional information or attributes that should be present in the JWT for it to be valid. Refer to CircleCI's [documentation](https://circleci.com/docs/openid-connect-tokens) for the complete list of supported claims.
- Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses.
- Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
<Tip>For more details on the appropriate values for the OIDC fields, refer to CircleCI's [documentation](https://circleci.com/docs/openid-connect-tokens). </Tip>
<Info>The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible.</Info>
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Using CircleCI OIDC token to authenticate with Infisical">
The following is an example of how to use the `$CIRCLE_OIDC_TOKEN` with the Infisical [terraform provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) to manage resources in a CircleCI pipeline.
```yml config.yml
version: 2.1
jobs:
terraform-apply:
docker:
- image: hashicorp/terraform:latest
steps:
- checkout
- run:
command: |
export INFISICAL_AUTH_JWT="$CIRCLE_OIDC_TOKEN"
terraform init
terraform apply -auto-approve
workflows:
version: 2
build-and-test:
jobs:
- terraform-apply
```
The Infisical terraform provider expects the `INFISICAL_AUTH_JWT` environment variable to be set to the CircleCI OIDC token.
```hcl main.tf
terraform {
required_providers {
infisical = {
source = "infisical/infisical"
}
}
}
provider "infisical" {
host = "https://app.infisical.com"
auth = {
oidc = {
identity_id = "f2f5ee4c-6223-461a-87c3-406a6b481462"
}
}
}
resource "infisical_access_approval_policy" "prod-access-approval" {
project_id = "09eda1f8-85a3-47a9-8a6f-e27f133b2a36"
name = "my-approval-policy"
environment_slug = "prod"
secret_path = "/"
approvers = [
{
type = "user"
username = "sheen+200@infisical.com"
},
]
required_approvals = 1
enforcement_level = "soft"
}
```
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,
a new access token should be obtained by performing another login operation.
</Note>
</Step>
</Steps>

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

@@ -187,6 +187,14 @@
"documentation/platform/workflow-integrations/slack-integration"
]
},
{
"group": "Admin Panel",
"pages": [
"documentation/platform/admin-panel/overview",
"documentation/platform/admin-panel/server-admin",
"documentation/platform/admin-panel/org-admin-console"
]
},
"documentation/platform/secret-sharing"
]
},
@@ -205,7 +213,8 @@
"group": "OIDC Auth",
"pages": [
"documentation/platform/identities/oidc-auth/general",
"documentation/platform/identities/oidc-auth/github"
"documentation/platform/identities/oidc-auth/github",
"documentation/platform/identities/oidc-auth/circleci"
]
},
"documentation/platform/mfa",

View File

@@ -59,6 +59,7 @@ Redis requirements:
- Use Redis versions 6.x or 7.x. We advise upgrading to at least Redis 6.2.
- Redis Cluster mode is currently not supported; use Redis Standalone, with or without High Availability (HA).
- Redis storage needs are minimal: a setup with 2 vCPU, 4 GB RAM, and 30GB SSD will be sufficient for small deployments.
- Set cache eviction policy to `noeviction`.
## Supported Web Browsers

View File

@@ -2,6 +2,7 @@ const path = require("path");
const ContentSecurityPolicy = `
default-src 'self';
connect-src 'self' https://*.posthog.com;
script-src 'self' https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
child-src https://api.stripe.com;

View File

@@ -1,5 +1,5 @@
{
"name": "relock-npm-lock-v2-SvMQeF",
"name": "frontend",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -40,7 +40,7 @@
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@reduxjs/toolkit": "^1.8.3",
"@sindresorhus/slugify": "^2.2.1",
"@sindresorhus/slugify": "1.1.0",
"@stripe/react-stripe-js": "^1.16.3",
"@stripe/stripe-js": "^1.46.0",
"@tanstack/react-query": "^4.23.0",
@@ -88,6 +88,7 @@
"react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-select": "^5.8.1",
"react-table": "^7.8.0",
"react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1",
@@ -2505,15 +2506,16 @@
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
"integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz",
"integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.1",
"@emotion/memoize": "^0.8.1",
"@emotion/serialize": "^1.1.2",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.2.0",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
@@ -2522,18 +2524,31 @@
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/cache": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
"integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
"version": "11.13.1",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz",
"integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.8.1",
"@emotion/sheet": "^1.2.2",
"@emotion/utils": "^1.2.1",
"@emotion/weak-memoize": "^0.3.1",
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.0",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/cache/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/css": {
"version": "11.11.2",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz",
@@ -2547,9 +2562,10 @@
}
},
"node_modules/@emotion/hash": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
"integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
@@ -2571,18 +2587,49 @@
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
},
"node_modules/@emotion/serialize": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz",
"integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==",
"node_modules/@emotion/react": {
"version": "11.13.3",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz",
"integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.1",
"@emotion/memoize": "^0.8.1",
"@emotion/unitless": "^0.8.1",
"@emotion/utils": "^1.2.1",
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.12.0",
"@emotion/cache": "^11.13.0",
"@emotion/serialize": "^1.3.1",
"@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
"@emotion/utils": "^1.4.0",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz",
"integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.1",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/serialize/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/server": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/server/-/server-11.11.0.tgz",
@@ -2603,9 +2650,10 @@
}
},
"node_modules/@emotion/sheet": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
"integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/stylis": {
"version": "0.8.5",
@@ -2613,28 +2661,31 @@
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
},
"node_modules/@emotion/unitless": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
"dev": true,
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz",
"integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
"integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz",
"integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
@@ -5943,54 +5994,44 @@
"dev": true
},
"node_modules/@sindresorhus/slugify": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz",
"integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-1.1.0.tgz",
"integrity": "sha512-ujZRbmmizX26yS/HnB3P9QNlNa4+UvHh+rIse3RbOXLp8yl6n1TxB4t7NHggtVgS8QmmOtzXo48kCxZGACpkPw==",
"license": "MIT",
"dependencies": {
"@sindresorhus/transliterate": "^1.0.0",
"escape-string-regexp": "^5.0.0"
"@sindresorhus/transliterate": "^0.1.1",
"escape-string-regexp": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sindresorhus/slugify/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": {
"node": ">=12"
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sindresorhus/transliterate": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz",
"integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==",
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-0.1.2.tgz",
"integrity": "sha512-5/kmIOY9FF32nicXH+5yLNTX4NJ4atl7jRgqAJuIn/iyDFXBktOKDxCvyGE/EzmF4ngSUvjXxQUQlQiZ5lfw+w==",
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^5.0.0"
"escape-string-regexp": "^2.0.0",
"lodash.deburr": "^4.1.0"
},
"engines": {
"node": ">=12"
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"node": ">=8"
}
},
"node_modules/@storybook/addon-actions": {
@@ -8854,6 +8895,15 @@
"redux": "^4.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.11",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz",
"integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==",
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.20.6",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
@@ -12678,6 +12728,16 @@
"utila": "~0.4"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -17278,6 +17338,12 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true
},
"node_modules/lodash.deburr": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz",
"integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
@@ -21117,6 +21183,33 @@
"react": ">= 16.3"
}
},
"node_modules/react-select": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz",
"integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.8.1",
"@floating-ui/dom": "^1.0.1",
"@types/react-transition-group": "^4.4.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.6.0",
"react-transition-group": "^4.3.0",
"use-isomorphic-layout-effect": "^1.1.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-select/node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -21171,6 +21264,22 @@
"node": ">=6"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -24246,6 +24355,20 @@
}
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-memo-one": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",

View File

@@ -12,6 +12,11 @@
"storybook": "storybook dev -p 6006 -s ./public",
"build-storybook": "storybook build"
},
"overrides": {
"@storybook/nextjs": {
"sharp": "npm:dry-uninstall"
}
},
"dependencies": {
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
@@ -48,7 +53,7 @@
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@reduxjs/toolkit": "^1.8.3",
"@sindresorhus/slugify": "^2.2.1",
"@sindresorhus/slugify": "1.1.0",
"@stripe/react-stripe-js": "^1.16.3",
"@stripe/stripe-js": "^1.46.0",
"@tanstack/react-query": "^4.23.0",
@@ -96,6 +101,7 @@
"react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-select": "^5.8.1",
"react-table": "^7.8.0",
"react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1",

View File

@@ -0,0 +1,104 @@
import Select, {
ClearIndicatorProps,
components,
DropdownIndicatorProps,
MultiValueRemoveProps,
OptionProps,
Props
} from "react-select";
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
import { faChevronDown, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
const DropdownIndicator = (props: DropdownIndicatorProps) => {
return (
<components.DropdownIndicator {...props}>
<FontAwesomeIcon icon={faChevronDown} size="xs" />
</components.DropdownIndicator>
);
};
const ClearIndicator = (props: ClearIndicatorProps) => {
return (
<components.ClearIndicator {...props}>
<FontAwesomeIcon icon={faCircleXmark} />
</components.ClearIndicator>
);
};
const MultiValueRemove = (props: MultiValueRemoveProps) => {
return (
<components.MultiValueRemove {...props}>
<FontAwesomeIcon icon={faX} size="xs" />
</components.MultiValueRemove>
);
};
const Option = ({ isSelected, children, ...props }: OptionProps) => {
return (
<components.Option isSelected={isSelected} {...props}>
{children}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</components.Option>
);
};
export const MultiSelect = (props: Props) => (
<Select
isMulti
closeMenuOnSelect={false}
hideSelectedOptions={false}
unstyled
styles={{
input: (base) => ({
...base,
"input:focus": {
boxShadow: "none"
}
}),
multiValueLabel: (base) => ({
...base,
whiteSpace: "normal",
overflow: "visible"
}),
control: (base) => ({
...base,
transition: "none"
})
}}
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
classNames={{
container: () => "w-full font-inter",
control: ({ isFocused }) =>
twMerge(
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
"border w-full p-0.5 rounded-md font-inter bg-mineshaft-900 hover:cursor-pointer"
),
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
input: () => "pl-1 py-0.5",
valueContainer: () => "p-1 max-h-[14rem] !overflow-y-scroll gap-1",
singleValue: () => "leading-7 ml-1",
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
multiValueLabel: () => "leading-6 text-sm",
multiValueRemove: () => "hover:text-red text-bunker-400",
indicatorsContainer: () => "p-1 gap-1",
clearIndicator: () => "p-1 hover:text-red text-bunker-400",
indicatorSeparator: () => "bg-bunker-400",
dropdownIndicator: () => "text-bunker-200 p-1",
menu: () =>
"mt-2 border text-sm text-mineshaft-200 bg-mineshaft-900 border-mineshaft-600 rounded-md",
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
option: ({ isFocused, isSelected }) =>
twMerge(
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
isSelected && "text-mineshaft-400",
"hover:cursor-pointer text-xs px-3 py-2"
),
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
}}
{...props}
/>
);

View File

@@ -0,0 +1 @@
export * from "./MultiSelect";

View File

@@ -18,6 +18,7 @@ export * from "./IconButton";
export * from "./Input";
export * from "./Menu";
export * from "./Modal";
export * from "./MultiSelect";
export * from "./NoticeBanner";
export * from "./Pagination";
export * from "./Popoverv2";

View File

@@ -1,5 +1,7 @@
import { useInfiniteQuery, UseInfiniteQueryOptions, useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { Actor, AuditLog, TGetAuditLogsFilter } from "./types";
@@ -28,27 +30,37 @@ export const useGetAuditLogs = (
return useInfiniteQuery({
queryKey: auditLogKeys.getAuditLogs(projectId, filters),
queryFn: async ({ pageParam }) => {
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
"/api/v1/organization/audit-logs",
{
params: {
...filters,
offset: pageParam,
startDate: filters?.startDate?.toISOString(),
endDate: filters?.endDate?.toISOString(),
...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
? {
eventMetadata: Object.entries(filters.eventMetadata)
.map(([key, value]) => `${key}=${value}`)
.join(",")
}
: {}),
...(filters.eventType?.length ? { eventType: filters.eventType.join(",") } : {}),
...(projectId ? { projectId } : {})
try {
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
"/api/v1/organization/audit-logs",
{
params: {
...filters,
offset: pageParam,
startDate: filters?.startDate?.toISOString(),
endDate: filters?.endDate?.toISOString(),
...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
? {
eventMetadata: Object.entries(filters.eventMetadata)
.map(([key, value]) => `${key}=${value}`)
.join(",")
}
: {}),
...(filters.eventType?.length ? { eventType: filters.eventType.join(",") } : {}),
...(projectId ? { projectId } : {})
}
}
);
return data.auditLogs;
} catch (error) {
if (error instanceof AxiosError) {
createNotification({
type: "error",
text: error.response?.data.message
});
}
);
return data.auditLogs;
return [];
}
},
getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined,

View File

@@ -886,8 +886,6 @@ export type AuditLog = {
userAgentType: UserAgentType;
createdAt: string;
updatedAt: string;
project?: {
name: string;
slug: string;
};
projectName?: string;
projectId?: string;
};

View File

@@ -82,12 +82,13 @@ export const useCreateOrg = (options: { invalidate: boolean } = { invalidate: tr
export const useUpdateOrg = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateOrgDTO>({
mutationFn: ({ name, authEnforced, scimEnabled, slug, orgId }) => {
mutationFn: ({ name, authEnforced, scimEnabled, slug, orgId, defaultMembershipRoleSlug }) => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name,
authEnforced,
scimEnabled,
slug
slug,
defaultMembershipRoleSlug
});
},
onSuccess: () => {

View File

@@ -10,6 +10,7 @@ export type Organization = {
orgAuthMethod: string;
scimEnabled: boolean;
slug: string;
defaultMembershipRole: string;
};
export type UpdateOrgDTO = {
@@ -18,6 +19,7 @@ export type UpdateOrgDTO = {
authEnforced?: boolean;
scimEnabled?: boolean;
slug?: string;
defaultMembershipRoleSlug?: string;
};
export type BillingDetails = {

View File

@@ -1,4 +1,3 @@
import { NoticeBanner } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
@@ -11,15 +10,9 @@ export const AuditLogsPage = withPermission(
<div className="w-full max-w-7xl px-6">
<div className="bg-bunker-800 py-6">
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
<NoticeBanner title="The audit logs page is in maintenance" className="mt-4">
We are currently working on improving the performance of audit log queries. During this time, querying logs is temporarily disabled. However, audit logs are still being generated as usual, so there is no disruption to log collection.
</NoticeBanner>
)}
<div />
</div>
{!window.location.origin.includes("https://app.infisical.com") && <LogsSection filterClassName="static p-2" showFilters isOrgAuditLogs />}
<LogsSection filterClassName="static p-2" showFilters isOrgAuditLogs />
</div>
</div>
);

View File

@@ -573,7 +573,7 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
<Td>{formatDate(auditLog.createdAt)}</Td>
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
{isOrgAuditLogs && <Td>{auditLog?.project?.name ?? "N/A"}</Td>}
{isOrgAuditLogs && <Td>{auditLog?.projectName ?? auditLog?.projectId ?? "N/A"}</Td>}
{showActorColumn && renderActor(auditLog.actor)}
{renderSource()}
{renderMetadata(auditLog.event)}

View File

@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import {
faCheckCircle,
@@ -33,6 +34,7 @@ import {
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { isCustomOrgRole } from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable";
import { OrgInviteLink } from "./OrgInviteLink";
@@ -78,7 +80,20 @@ export const AddOrgMemberModal = ({
watch,
reset,
formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
} = useForm<TAddMemberForm>({
resolver: zodResolver(addMemberFormSchema)
});
// set initial form role based off org default role
useEffect(() => {
if (organizationRoles) {
reset({
organizationRoleSlug: isCustomOrgRole(currentOrg?.defaultMembershipRole!)
? organizationRoles?.find((role) => role.id === currentOrg?.defaultMembershipRole)?.slug!
: currentOrg?.defaultMembershipRole
});
}
}, [organizationRoles]);
const selectedProjectIds = watch("projectIds", []);
@@ -207,7 +222,6 @@ export const AddOrgMemberModal = ({
<div>
<Select
className="w-full"
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
{...field}
onValueChange={(val) => field.onChange(val)}
>

View File

@@ -6,6 +6,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
@@ -19,14 +20,30 @@ import {
Td,
Th,
THead,
Tr
Tooltip,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteOrgRole, useGetOrgRoles } from "@app/hooks/api";
import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
import { TOrgRole } from "@app/hooks/api/roles/types";
import { RoleModal } from "@app/views/Org/RolePage/components";
enum OrgMembershipRole {
Admin = "admin",
Member = "member",
NoAccess = "no-access"
}
export const isCustomOrgRole = (slug: string) =>
!Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole);
export const OrgRoleTable = () => {
const router = useRouter();
const { currentOrg } = useOrganization();
@@ -34,12 +51,14 @@ export const OrgRoleTable = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"role",
"deleteRole"
"deleteRole",
"upgradePlan"
] as const);
const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(orgId);
const { mutateAsync: deleteRole } = useDeleteOrgRole();
const { mutateAsync: updateOrg } = useUpdateOrg();
const { subscription } = useSubscription();
const handleRoleDelete = async () => {
const { id } = popUp?.deleteRole?.data as TOrgRole;
@@ -56,6 +75,30 @@ export const OrgRoleTable = () => {
}
};
const handleSetRoleAsDefault = async (defaultMembershipRoleSlug: string) => {
const isCustomRole = isCustomOrgRole(defaultMembershipRoleSlug);
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
description:
"You can set the default org role to a custom role if you upgrade your Infisical plan."
});
return;
}
try {
await updateOrg({
orgId,
defaultMembershipRoleSlug
});
createNotification({ type: "success", text: "Successfully updated default membership role" });
handlePopUpClose("deleteRole");
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to update default membership role" });
}
};
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
@@ -90,14 +133,30 @@ export const OrgRoleTable = () => {
{roles?.map((role) => {
const { id, name, slug } = role;
const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
const isDefaultOrgRole = isCustomOrgRole(slug)
? id === currentOrg?.defaultMembershipRole
: slug === currentOrg?.defaultMembershipRole;
return (
<Tr
key={`role-list-${id}`}
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => router.push(`/org/${orgId}/roles/${id}`)}
>
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
{name}
<Td className="max-w-md">
<div className="flex">
<p className="overflow-hidden text-ellipsis whitespace-nowrap">{name}</p>
{isDefaultOrgRole && (
<Tooltip
content={`Members joining your organization will be assigned the ${name} role unless otherwise specified.`}
>
<div>
<Badge variant="success" className="ml-1">
Default
</Badge>
</div>
</Tooltip>
)}
</div>
</Td>
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
{slug}
@@ -129,29 +188,61 @@ export const OrgRoleTable = () => {
</DropdownMenuItem>
)}
</OrgPermissionCan>
{!isNonMutatable && (
{!isDefaultOrgRole && (
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Role}
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
disabled={!isAllowed}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteRole", role);
handleSetRoleAsDefault(slug);
}}
disabled={!isAllowed}
>
Delete Role
Set as Default Role
</DropdownMenuItem>
)}
</OrgPermissionCan>
)}
{!isNonMutatable && (
<Tooltip
position="left"
content={
isDefaultOrgRole
? "Cannot delete default organization membership role. Re-assign default to allow deletion."
: ""
}
>
<div>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Role}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed && !isDefaultOrgRole
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteRole", role);
}}
disabled={!isAllowed || isDefaultOrgRole}
>
Delete Role
</DropdownMenuItem>
)}
</OrgPermissionCan>
</div>
</Tooltip>
)}
</DropdownMenuContent>
</DropdownMenu>
</Td>
@@ -172,6 +263,11 @@ export const OrgRoleTable = () => {
onClose={() => handlePopUpClose("deleteRole")}
onDeleteApproved={handleRoleDelete}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};

View File

@@ -17,7 +17,8 @@ import {
DropdownMenuTrigger,
FormControl,
Modal,
ModalContent
ModalContent,
MultiSelect
} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
@@ -31,7 +32,7 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = z.object({
orgMembershipIds: z.array(z.string().trim()).min(1),
orgMemberships: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).min(1),
projectRoleSlugs: z.array(z.string().trim().min(1)).min(1)
});
@@ -60,20 +61,20 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
handleSubmit,
reset,
watch,
formState: { isSubmitting }
formState: { isSubmitting, errors }
} = useForm<TAddMemberForm>({
resolver: zodResolver(addMemberFormSchema),
defaultValues: { orgMembershipIds: [], projectRoleSlugs: [ProjectMembershipRole.Member] }
defaultValues: { orgMemberships: [], projectRoleSlugs: [ProjectMembershipRole.Member] }
});
const { mutateAsync: addMembersToProject } = useAddUsersToOrg();
const onAddMember = async ({ orgMembershipIds, projectRoleSlugs }: TAddMemberForm) => {
const onAddMembers = async ({ orgMemberships, projectRoleSlugs }: TAddMemberForm) => {
if (!currentWorkspace) return;
if (!currentOrg?.id) return;
const selectedMembers = orgMembershipIds.map((orgMembershipId) =>
orgUsers?.find((orgUser) => orgUser.id === orgMembershipId)
const selectedMembers = orgMemberships.map((orgMembership) =>
orgUsers?.find((orgUser) => orgUser.id === orgMembership.value)
);
if (!selectedMembers) return;
@@ -118,10 +119,15 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
members?.forEach((member) => {
wsUserUsernames.set(member.user.username, true);
});
return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username));
return (orgUsers || [])
.filter(({ user: u }) => !wsUserUsernames.has(u.username))
.map((member) => ({
value: member.id,
label: `${member.user.firstName} ${member.user.lastName}`
}));
}, [orgUsers, members]);
const selectedOrgMembershipIds = watch("orgMembershipIds");
const selectedOrgMemberships = watch("orgMemberships");
const selectedRoleSlugs = watch("projectRoleSlugs");
return (
@@ -130,175 +136,115 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
onOpenChange={(isOpen) => handlePopUpToggle("addMember", isOpen)}
>
<ModalContent
bodyClassName="overflow-visible"
title={t("section.members.add-dialog.add-member-to-project") as string}
subTitle={t("section.members.add-dialog.user-will-email")}
>
{filteredOrgUsers.length ? (
<form onSubmit={handleSubmit(onAddMember)}>
<div className="flex w-full items-center gap-2">
<div className="w-full">
<Controller
control={control}
name="orgMembershipIds"
render={({ field }) => (
<FormControl label="Invite users to project">
<DropdownMenu>
<DropdownMenuTrigger asChild>
{filteredOrgUsers && filteredOrgUsers.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedOrgMembershipIds.length === 1
? filteredOrgUsers.find(
(orgUser) => orgUser.id === selectedOrgMembershipIds[0]
)?.user.username
: selectedOrgMembershipIds.length === 0
? "No users selected"
: `${selectedOrgMembershipIds.length} users selected`}
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No users found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{filteredOrgUsers && filteredOrgUsers.length > 0 ? (
filteredOrgUsers.map((member) => {
const isSelected = selectedOrgMembershipIds.includes(member.id);
<form onSubmit={handleSubmit(onAddMembers)}>
<div className="flex w-full flex-col items-start gap-2">
<Controller
control={control}
name="orgMemberships"
render={({ field }) => (
<FormControl
className="w-full"
isError={!!errors.orgMemberships?.length}
errorText={errors.orgMemberships?.[0]?.message}
label="Invite users to project"
>
<MultiSelect
className="w-full"
placeholder="Add one or more users..."
isMulti
name="members"
options={filteredOrgUsers}
value={field.value}
onChange={field.onChange}
/>
</FormControl>
)}
/>
return (
<DropdownMenuItem
onSelect={(event) =>
filteredOrgUsers.length > 1 && event.preventDefault()
}
onClick={() => {
if (selectedOrgMembershipIds.includes(String(member.id))) {
field.onChange(
selectedOrgMembershipIds.filter(
(membershipId: string) =>
membershipId !== String(member.id)
)
);
} else {
field.onChange([
...selectedOrgMembershipIds,
String(member.id)
]);
}
}}
key={`membership-id-${member.id}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{member.user.username}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
<Controller
control={control}
name="projectRoleSlugs"
render={({ field }) => (
<FormControl
className="w-full"
label="Select roles"
tooltipText="Select the roles that you wish to assign to the users"
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{roles && roles.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedRoleSlugs.length === 1
? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name
: selectedRoleSlugs.length === 0
? "Select at least one role"
: `${selectedRoleSlugs.length} roles selected`}
<FontAwesomeIcon
icon={faChevronDown}
className={twMerge("ml-2 text-xs")}
/>
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No roles found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{roles && roles.length > 0 ? (
roles.map((role) => {
const isSelected = selectedRoleSlugs.includes(role.slug);
<div className="flex min-w-fit justify-end">
<Controller
control={control}
name="projectRoleSlugs"
render={({ field }) => (
<FormControl
label="Select roles"
tooltipText="Select the roles that you wish to assign to the users"
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{roles && roles.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedRoleSlugs.length === 1
? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name
: selectedRoleSlugs.length === 0
? "Select at least one role"
: `${selectedRoleSlugs.length} roles selected`}
<FontAwesomeIcon
icon={faChevronDown}
className={twMerge("ml-2 text-xs")}
/>
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No roles found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{roles && roles.length > 0 ? (
roles.map((role) => {
const isSelected = selectedRoleSlugs.includes(role.slug);
return (
<DropdownMenuItem
onSelect={(event) => roles.length > 1 && event.preventDefault()}
onClick={() => {
if (selectedRoleSlugs.includes(String(role.slug))) {
field.onChange(
selectedRoleSlugs.filter(
(roleSlug: string) => roleSlug !== String(role.slug)
)
);
} else {
field.onChange([...selectedRoleSlugs, role.slug]);
}
}}
key={`role-slug-${role.slug}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
return (
<DropdownMenuItem
onSelect={(event) => roles.length > 1 && event.preventDefault()}
onClick={() => {
if (selectedRoleSlugs.includes(String(role.slug))) {
field.onChange(
selectedRoleSlugs.filter(
(roleSlug: string) => roleSlug !== String(role.slug)
)
);
} else {
field.onChange([...selectedRoleSlugs, role.slug]);
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{role.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
}}
key={`role-slug-${role.slug}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{role.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
@@ -307,7 +253,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
isLoading={isSubmitting}
isDisabled={
isSubmitting ||
selectedOrgMembershipIds.length === 0 ||
selectedOrgMemberships.length === 0 ||
selectedRoleSlugs.length === 0
}
>

View File

@@ -805,7 +805,7 @@ export const SecretOverviewPage = () => {
resetSelectedEntries={resetSelectedEntries}
/>
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
<TableContainer>
<TableContainer className="rounded-b-none">
<Table>
<THead>
<Tr className="sticky top-0 z-20 border-0">
@@ -1003,24 +1003,24 @@ export const SecretOverviewPage = () => {
</Tr>
</TFoot>
</Table>
{!isOverviewLoading && totalCount > 0 && (
<Pagination
startAdornment={
<SecretTableResourceCount
dynamicSecretCount={totalDynamicSecretCount}
secretCount={totalSecretCount}
folderCount={totalFolderCount}
/>
}
className="border-t border-solid border-t-mineshaft-600"
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
</TableContainer>
{!isOverviewLoading && totalCount > 0 && (
<Pagination
startAdornment={
<SecretTableResourceCount
dynamicSecretCount={totalDynamicSecretCount}
secretCount={totalSecretCount}
folderCount={totalFolderCount}
/>
}
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
</div>
</div>
<CreateSecretForm

View File

@@ -1,13 +1,19 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api";
import { Button, FormControl, Input, Select, SelectItem, Spinner } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import { useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
import { isCustomOrgRole } from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable";
const formSchema = yup.object({
name: yup
@@ -19,36 +25,55 @@ const formSchema = yup.object({
.string()
.matches(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
.required()
.label("Organization Slug")
.label("Organization Slug"),
defaultMembershipRole: yup.string().required().label("Default Membership Role")
});
type FormData = yup.InferType<typeof formSchema>;
export const OrgNameChangeSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { permission } = useOrgPermission();
const { handleSubmit, control, reset } = useForm<FormData>({
resolver: yupResolver(formSchema)
});
const { mutateAsync, isLoading } = useUpdateOrg();
const canReadOrgRoles = permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(
currentOrg?.id!,
canReadOrgRoles
);
const [isFormInitialized, setIsFormInitialized] = useState(false);
useEffect(() => {
if (currentOrg) {
reset({
name: currentOrg.name,
slug: currentOrg.slug
slug: currentOrg.slug,
...(canReadOrgRoles &&
roles?.length && {
// will always be present, can't remove role if default
defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole)
? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug!
: currentOrg.defaultMembershipRole
})
});
setIsFormInitialized(true);
}
}, [currentOrg]);
}, [currentOrg, roles]);
const onFormSubmit = async ({ name, slug }: FormData) => {
const onFormSubmit = async ({ name, slug, defaultMembershipRole }: FormData) => {
try {
if (!currentOrg?.id) return;
if (!currentOrg?.id || !roles?.length) return;
await mutateAsync({
orgId: currentOrg?.id,
name,
slug
slug,
defaultMembershipRoleSlug: defaultMembershipRole
});
createNotification({
@@ -64,6 +89,14 @@ export const OrgNameChangeSection = (): JSX.Element => {
}
};
if (!isFormInitialized) {
return (
<div className="flex h-[25.25rem] w-full items-center justify-center">
<Spinner size="lg" />
</div>
);
}
return (
<form onSubmit={handleSubmit(onFormSubmit)} className="py-4">
<div className="">
@@ -92,6 +125,40 @@ export const OrgNameChangeSection = (): JSX.Element => {
name="slug"
/>
</div>
{canReadOrgRoles && (
<div className="pb-4">
<h2 className="text-md mb-2 text-mineshaft-100">Default Organization Member Role</h2>
<p className="text-mineshaft-400" />
<Controller
defaultValue=""
control={control}
name="defaultMembershipRole"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
helperText="Users joining your organization will be assigned this role unless otherwise specified."
isError={Boolean(error)}
errorText={error?.message}
className="max-w-md"
>
<Select
isDisabled={isRolesLoading}
className="w-full capitalize"
value={value}
onValueChange={!roles?.length ? undefined : onChange}
>
{roles?.map((role) => {
return (
<SelectItem key={role.id} value={role.slug}>
{role.name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
</div>
)}
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button

View File

@@ -176,12 +176,16 @@ export const UserInfoSSOStep = ({
organizationId: orgId
});
const project = await ProjectService.initProject({
projectName: "Example Project"
});
// only create example project if not joining existing org
if (!providerOrganizationName) {
const project = await ProjectService.initProject({
projectName: "Example Project"
});
localStorage.setItem("projectData.id", project.id);
}
localStorage.setItem("orgData.id", orgId);
localStorage.setItem("projectData.id", project.id);
setStep(2);
} catch (error) {

View File

@@ -7,7 +7,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.8
version: 1.2.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to

View File

@@ -55,6 +55,13 @@ spec:
ports:
- containerPort: 8080
env:
{{- if .Values.postgresql.useExistingPostgresSecret.enabled }}
- name: DB_CONNECTION_URI
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.useExistingPostgresSecret.existingConnectionStringSecret.name }}
key: {{ .Values.postgresql.useExistingPostgresSecret.existingConnectionStringSecret.key }}
{{- end }}
{{- if .Values.postgresql.enabled }}
- name: DB_CONNECTION_URI
value: {{ include "infisical.postgresDBConnectionString" . }}

View File

@@ -2,6 +2,7 @@ apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: k8s-wait-for-infisical-schema-migration
namespace: {{ .Release.Namespace }}
rules:
- apiGroups: ["batch"]
resources: ["jobs"]
@@ -10,11 +11,12 @@ rules:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: default
name: infisical-database-schema-migration
namespace: {{ .Release.Namespace }}
subjects:
- kind: ServiceAccount
name: default
namespace: {{ .Release.Namespace }}
name: {{ .Values.infisical.databaseSchemaMigrationJob.serviceAccountName | default "default" }}
namespace: {{ .Values.infisical.databaseSchemaMigrationJob.serviceAccountNamespace | default .Release.Namespace }}
roleRef:
kind: Role
name: k8s-wait-for-infisical-schema-migration

View File

@@ -16,6 +16,7 @@ spec:
app.kubernetes.io/instance: {{ .Release.Name | quote }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
serviceAccountName: {{ .Values.infisical.databaseSchemaMigrationJob.serviceAccountName | default "default" }}
{{- if $infisicalValues.image.imagePullSecrets }}
imagePullSecrets:
{{- toYaml $infisicalValues.image.imagePullSecrets | nindent 6 }}
@@ -26,6 +27,13 @@ spec:
image: "{{ $infisicalValues.image.repository }}:{{ $infisicalValues.image.tag }}"
command: ["npm", "run", "migration:latest"]
env:
{{- if .Values.postgresql.useExistingPostgresSecret.enabled }}
- name: DB_CONNECTION_URI
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.useExistingPostgresSecret.existingConnectionStringSecret.name }}
key: {{ .Values.postgresql.useExistingPostgresSecret.existingConnectionStringSecret.key }}
{{- end }}
{{- if .Values.postgresql.enabled }}
- name: DB_CONNECTION_URI
value: {{ include "infisical.postgresDBConnectionString" . }}

View File

@@ -5,6 +5,10 @@ infisical:
enabled: true
name: infisical
autoDatabaseSchemaMigration: true
databaseSchemaMigrationJob:
serviceAccountNamespace: default
serviceAccountName: default
fullnameOverride: ""
podAnnotations: {}
deploymentAnnotations: {}
@@ -18,6 +22,7 @@ infisical:
affinity: {}
kubeSecretRef: "infisical-secrets"
service:
annotations: {}
type: ClusterIP
@@ -43,6 +48,7 @@ ingress:
# - some.domain.com
postgresql:
# -- When enabled, this will start up a in cluster Postgres
enabled: true
name: "postgresql"
fullnameOverride: "postgresql"
@@ -50,6 +56,15 @@ postgresql:
username: infisical
password: root
database: infisicalDB
useExistingPostgresSecret:
# -- When this is enabled, postgresql.enabled needs to be false
enabled: false
# -- The name from where to get the existing postgresql connection string
existingConnectionStringSecret:
# -- The name of the secret that contains the postgres connection string
name: ""
# -- Secret key name that contains the postgres connection string
key: ""
redis:
enabled: true

View File

@@ -15,6 +15,21 @@ server {
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /api/v3/migrate {
client_max_body_size 25M;
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://backend:4000;
proxy_redirect off;
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /.well-known/est {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -17,6 +17,21 @@ server {
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /api/v3/migrate {
client_max_body_size 25M;
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://backend:4000;
proxy_redirect off;
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /.well-known/est {
proxy_set_header X-Real-RIP $remote_addr;