mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 14:28:20 +00:00
Add docs for CA renewal
This commit is contained in:
@@ -22,14 +22,14 @@ jobs:
|
|||||||
# uncomment this when testing locally using nektos/act
|
# uncomment this when testing locally using nektos/act
|
||||||
- uses: KengoTODA/actions-setup-docker-compose@v1
|
- uses: KengoTODA/actions-setup-docker-compose@v1
|
||||||
if: ${{ env.ACT }}
|
if: ${{ env.ACT }}
|
||||||
name: Install `docker-compose` for local simulations
|
name: Install `docker compose` for local simulations
|
||||||
with:
|
with:
|
||||||
version: "2.14.2"
|
version: "2.14.2"
|
||||||
- name: 📦Build the latest image
|
- name: 📦Build the latest image
|
||||||
run: docker build --tag infisical-api .
|
run: docker build --tag infisical-api .
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
- name: Start postgres and redis
|
- name: Start postgres and redis
|
||||||
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
|
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||||
- name: Start the server
|
- name: Start the server
|
||||||
run: |
|
run: |
|
||||||
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
||||||
@@ -72,6 +72,6 @@ jobs:
|
|||||||
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
run: |
|
run: |
|
||||||
docker-compose -f "docker-compose.dev.yml" down
|
docker compose -f "docker-compose.dev.yml" down
|
||||||
docker stop infisical-api
|
docker stop infisical-api
|
||||||
docker remove infisical-api
|
docker remove infisical-api
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- uses: KengoTODA/actions-setup-docker-compose@v1
|
- uses: KengoTODA/actions-setup-docker-compose@v1
|
||||||
if: ${{ env.ACT }}
|
if: ${{ env.ACT }}
|
||||||
name: Install `docker-compose` for local simulations
|
name: Install `docker compose` for local simulations
|
||||||
with:
|
with:
|
||||||
version: "2.14.2"
|
version: "2.14.2"
|
||||||
- name: 🔧 Setup Node 20
|
- name: 🔧 Setup Node 20
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
run: npm install
|
run: npm install
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
- name: Start postgres and redis
|
- name: Start postgres and redis
|
||||||
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
|
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||||
- name: Start integration test
|
- name: Start integration test
|
||||||
run: npm run test:e2e
|
run: npm run test:e2e
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
@@ -44,4 +44,4 @@ jobs:
|
|||||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
run: |
|
run: |
|
||||||
docker-compose -f "docker-compose.dev.yml" down
|
docker compose -f "docker-compose.dev.yml" down
|
||||||
Vendored
+2
@@ -50,6 +50,7 @@ import { TIntegrationServiceFactory } from "@app/services/integration/integratio
|
|||||||
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||||
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||||
|
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||||
@@ -165,6 +166,7 @@ declare module "fastify" {
|
|||||||
rateLimit: TRateLimitServiceFactory;
|
rateLimit: TRateLimitServiceFactory;
|
||||||
userEngagement: TUserEngagementServiceFactory;
|
userEngagement: TUserEngagementServiceFactory;
|
||||||
externalKms: TExternalKmsServiceFactory;
|
externalKms: TExternalKmsServiceFactory;
|
||||||
|
orgAdmin: TOrgAdminServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export enum EventType {
|
|||||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||||
GET_CA_CRL = "get-certificate-authority-crl",
|
GET_CA_CRL = "get-certificate-authority-crl",
|
||||||
ISSUE_CERT = "issue-cert",
|
ISSUE_CERT = "issue-cert",
|
||||||
|
SIGN_CERT = "sign-cert",
|
||||||
GET_CERT = "get-cert",
|
GET_CERT = "get-cert",
|
||||||
DELETE_CERT = "delete-cert",
|
DELETE_CERT = "delete-cert",
|
||||||
REVOKE_CERT = "revoke-cert",
|
REVOKE_CERT = "revoke-cert",
|
||||||
@@ -148,7 +149,8 @@ export enum EventType {
|
|||||||
GET_KMS = "get-kms",
|
GET_KMS = "get-kms",
|
||||||
UPDATE_PROJECT_KMS = "update-project-kms",
|
UPDATE_PROJECT_KMS = "update-project-kms",
|
||||||
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
||||||
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup"
|
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
|
||||||
|
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@@ -1161,6 +1163,15 @@ interface IssueCert {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SignCert {
|
||||||
|
type: EventType.SIGN_CERT;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface GetCert {
|
interface GetCert {
|
||||||
type: EventType.GET_CERT;
|
type: EventType.GET_CERT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -1253,6 +1264,16 @@ interface LoadProjectKmsBackupEvent {
|
|||||||
metadata: Record<string, string>; // no metadata yet
|
metadata: Record<string, string>; // no metadata yet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OrgAdminAccessProjectEvent {
|
||||||
|
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||||
|
metadata: {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
projectId: string;
|
||||||
|
}; // no metadata yet
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -1353,6 +1374,7 @@ export type Event =
|
|||||||
| ImportCaCert
|
| ImportCaCert
|
||||||
| GetCaCrl
|
| GetCaCrl
|
||||||
| IssueCert
|
| IssueCert
|
||||||
|
| SignCert
|
||||||
| GetCert
|
| GetCert
|
||||||
| DeleteCert
|
| DeleteCert
|
||||||
| RevokeCert
|
| RevokeCert
|
||||||
@@ -1363,4 +1385,5 @@ export type Event =
|
|||||||
| GetKmsEvent
|
| GetKmsEvent
|
||||||
| UpdateProjectKmsEvent
|
| UpdateProjectKmsEvent
|
||||||
| GetProjectKmsBackupEvent
|
| GetProjectKmsBackupEvent
|
||||||
| LoadProjectKmsBackupEvent;
|
| LoadProjectKmsBackupEvent
|
||||||
|
| OrgAdminAccessProjectEvent;
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export enum OrgPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
|
AccessAllProjects = "access-all-projects"
|
||||||
|
}
|
||||||
|
|
||||||
export enum OrgPermissionSubjects {
|
export enum OrgPermissionSubjects {
|
||||||
Workspace = "workspace",
|
Workspace = "workspace",
|
||||||
Role = "role",
|
Role = "role",
|
||||||
@@ -22,7 +26,8 @@ export enum OrgPermissionSubjects {
|
|||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
Kms = "kms"
|
Kms = "kms",
|
||||||
|
AdminConsole = "organization-admin-console"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -39,7 +44,8 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
@@ -107,6 +113,8 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1051,7 +1051,7 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
RENEW_CA_CERT: {
|
RENEW_CA_CERT: {
|
||||||
caId: "The ID of the CA to renew the CA certificate for",
|
caId: "The ID of the CA to renew the CA certificate for",
|
||||||
type: "The type of behavior to use for the renewal operation. Currently Infisical is only able to renew a CA certificate with the same key pair.",
|
type: "The type of behavior to use for the renewal operation. Currently Infisical is only able to renew a CA certificate with the same key pair.",
|
||||||
notAfter: "The expiry date and time for the renewed CA certificatre in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
notAfter: "The expiry date and time for the renewed CA certificate in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
certificate: "The renewed CA certificate body",
|
certificate: "The renewed CA certificate body",
|
||||||
certificateChain: "The certificate chain of the CA",
|
certificateChain: "The certificate chain of the CA",
|
||||||
serialNumber: "The serial number of the renewed CA certificate"
|
serialNumber: "The serial number of the renewed CA certificate"
|
||||||
@@ -1062,9 +1062,16 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
certificateChain: "The certificate chain of the CA",
|
certificateChain: "The certificate chain of the CA",
|
||||||
serialNumber: "The serial number of the CA certificate"
|
serialNumber: "The serial number of the CA certificate"
|
||||||
},
|
},
|
||||||
|
GET_CA_CERTS: {
|
||||||
|
caId: "The ID of the CA to get the CA certificates for",
|
||||||
|
certificate: "The certificate body of the CA certificate",
|
||||||
|
certificateChain: "The certificate chain of the CA certificate",
|
||||||
|
serialNumber: "The serial number of the CA certificate",
|
||||||
|
version: "The version of the CA certificate. The version is incremented for each CA renewal operation."
|
||||||
|
},
|
||||||
SIGN_INTERMEDIATE: {
|
SIGN_INTERMEDIATE: {
|
||||||
caId: "The ID of the CA to sign the intermediate certificate with",
|
caId: "The ID of the CA to sign the intermediate certificate with",
|
||||||
csr: "The CSR to sign with the CA",
|
csr: "The pem-encoded CSR to sign with the CA",
|
||||||
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
maxPathLength:
|
maxPathLength:
|
||||||
@@ -1094,6 +1101,21 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
privateKey: "The private key of the issued certificate",
|
privateKey: "The private key of the issued certificate",
|
||||||
serialNumber: "The serial number of the issued certificate"
|
serialNumber: "The serial number of the issued certificate"
|
||||||
},
|
},
|
||||||
|
SIGN_CERT: {
|
||||||
|
caId: "The ID of the CA to issue the certificate from",
|
||||||
|
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
|
||||||
|
friendlyName: "A friendly name for the certificate",
|
||||||
|
commonName: "The common name (CN) for the certificate",
|
||||||
|
altNames:
|
||||||
|
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
|
||||||
|
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
certificate: "The issued certificate",
|
||||||
|
issuingCaCertificate: "The certificate of the issuing CA",
|
||||||
|
certificateChain: "The certificate chain of the issued certificate",
|
||||||
|
serialNumber: "The serial number of the issued certificate"
|
||||||
|
},
|
||||||
GET_CRL: {
|
GET_CRL: {
|
||||||
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
|
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
|
||||||
crl: "The certificate revocation list (CRL) of the CA"
|
crl: "The certificate revocation list (CRL) of the CA"
|
||||||
|
|||||||
@@ -19,23 +19,43 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
|||||||
|
|
||||||
export type TFindFilter<R extends object = object> = Partial<R> & {
|
export type TFindFilter<R extends object = object> = Partial<R> & {
|
||||||
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
||||||
|
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||||
};
|
};
|
||||||
export const buildFindFilter =
|
export const buildFindFilter =
|
||||||
<R extends object = object>({ $in, ...filter }: TFindFilter<R>) =>
|
<R extends object = object>({ $in, $search, ...filter }: TFindFilter<R>) =>
|
||||||
(bd: Knex.QueryBuilder<R, R>) => {
|
(bd: Knex.QueryBuilder<R, R>) => {
|
||||||
void bd.where(filter);
|
void bd.where(filter);
|
||||||
if ($in) {
|
if ($in) {
|
||||||
Object.entries($in).forEach(([key, val]) => {
|
Object.entries($in).forEach(([key, val]) => {
|
||||||
void bd.whereIn(key as never, val as never);
|
if (val) {
|
||||||
|
void bd.whereIn(key as never, val as never);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ($search) {
|
||||||
|
Object.entries($search).forEach(([key, val]) => {
|
||||||
|
if (val) {
|
||||||
|
void bd.whereILike(key as never, val as never);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return bd;
|
return bd;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TFindOpt<R extends object = object> = {
|
export type TFindReturn<TQuery extends Knex.QueryBuilder, TCount extends boolean = false> = Array<
|
||||||
|
Awaited<TQuery>[0] &
|
||||||
|
(TCount extends true
|
||||||
|
? {
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
: unknown)
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TFindOpt<R extends object = object, TCount extends boolean = boolean> = {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
|
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
|
||||||
|
count?: TCount;
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,18 +86,22 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
|||||||
throw new DatabaseError({ error, name: "Find one" });
|
throw new DatabaseError({ error, name: "Find one" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
find: async (
|
find: async <TCount extends boolean = false>(
|
||||||
filter: TFindFilter<Tables[Tname]["base"]>,
|
filter: TFindFilter<Tables[Tname]["base"]>,
|
||||||
{ offset, limit, sort, tx }: TFindOpt<Tables[Tname]["base"]> = {}
|
{ offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {}
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
|
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
|
||||||
|
if (count) {
|
||||||
|
void query.select(db.raw("COUNT(*) OVER() AS count"));
|
||||||
|
void query.select("*");
|
||||||
|
}
|
||||||
if (limit) void query.limit(limit);
|
if (limit) void query.limit(limit);
|
||||||
if (offset) void query.offset(offset);
|
if (offset) void query.offset(offset);
|
||||||
if (sort) {
|
if (sort) {
|
||||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||||
}
|
}
|
||||||
const res = await query;
|
const res = (await query) as TFindReturn<typeof query, TCount>;
|
||||||
return res;
|
return res;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find one" });
|
throw new DatabaseError({ error, name: "Find one" });
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ import { orgDALFactory } from "@app/services/org/org-dal";
|
|||||||
import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
|
import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||||
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||||
|
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||||
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||||
@@ -498,6 +499,16 @@ export const registerRoutes = async (
|
|||||||
keyStore,
|
keyStore,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
const orgAdminService = orgAdminServiceFactory({
|
||||||
|
projectDAL,
|
||||||
|
permissionService,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
|
userDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
});
|
||||||
|
|
||||||
const rateLimitService = rateLimitServiceFactory({
|
const rateLimitService = rateLimitServiceFactory({
|
||||||
rateLimitDAL,
|
rateLimitDAL,
|
||||||
licenseService
|
licenseService
|
||||||
@@ -1113,7 +1124,8 @@ export const registerRoutes = async (
|
|||||||
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
|
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
|
||||||
secretSharing: secretSharingService,
|
secretSharing: secretSharingService,
|
||||||
userEngagement: userEngagementService,
|
userEngagement: userEngagementService,
|
||||||
externalKms: externalKmsService
|
externalKms: externalKmsService,
|
||||||
|
orgAdmin: orgAdminService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
|||||||
@@ -340,16 +340,15 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
description: "Get list of past and current CA certificates for a CA",
|
description: "Get list of past and current CA certificates for a CA",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId)
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.caId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.array(
|
200: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
// TODO: consider not before and not after dates
|
certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificate),
|
||||||
certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificate),
|
certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificateChain),
|
||||||
certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificateChain),
|
serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.serialNumber),
|
||||||
serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.serialNumber),
|
version: z.number().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.version)
|
||||||
version: z.number()
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -441,7 +440,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
csr: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
|
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
|
||||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
|
||||||
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
|
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
|
||||||
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
|
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
|
||||||
@@ -557,7 +556,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
||||||
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
||||||
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
|
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
|
||||||
ttl: z
|
ttl: z
|
||||||
@@ -620,4 +619,81 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:caId/sign-certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Sign certificate from CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId)
|
||||||
|
}),
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
|
||||||
|
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
|
||||||
|
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
|
||||||
|
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
||||||
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
|
||||||
|
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const { ttl, notAfter } = data;
|
||||||
|
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Either ttl or notAfter must be present, but not both",
|
||||||
|
path: ["ttl", "notAfter"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate),
|
||||||
|
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
|
||||||
|
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||||
|
await server.services.certificateAuthority.signCertFromCa({
|
||||||
|
caId: req.params.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SIGN_CERT,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn,
|
||||||
|
serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
issuingCaCertificate,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router";
|
|||||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||||
import { registerIntegrationRouter } from "./integration-router";
|
import { registerIntegrationRouter } from "./integration-router";
|
||||||
import { registerInviteOrgRouter } from "./invite-org-router";
|
import { registerInviteOrgRouter } from "./invite-org-router";
|
||||||
|
import { registerOrgAdminRouter } from "./org-admin-router";
|
||||||
import { registerOrgRouter } from "./organization-router";
|
import { registerOrgRouter } from "./organization-router";
|
||||||
import { registerPasswordRouter } from "./password-router";
|
import { registerPasswordRouter } from "./password-router";
|
||||||
import { registerProjectEnvRouter } from "./project-env-router";
|
import { registerProjectEnvRouter } from "./project-env-router";
|
||||||
@@ -50,6 +51,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||||
await server.register(registerOrgRouter, { prefix: "/organization" });
|
await server.register(registerOrgRouter, { prefix: "/organization" });
|
||||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||||
|
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
|
||||||
await server.register(registerUserRouter, { prefix: "/user" });
|
await server.register(registerUserRouter, { prefix: "/user" });
|
||||||
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ProjectMembershipsSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
|
export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/projects",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
search: z.string().optional(),
|
||||||
|
offset: z.coerce.number().default(0),
|
||||||
|
limit: z.coerce.number().max(100).default(50)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
projects: SanitizedProjectSchema.array(),
|
||||||
|
count: z.coerce.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { projects, count } = await server.services.orgAdmin.listOrgProjects({
|
||||||
|
limit: req.query.limit,
|
||||||
|
offset: req.query.offset,
|
||||||
|
search: req.query.search,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type
|
||||||
|
});
|
||||||
|
return { projects, count };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/projects/:projectId/grant-admin-access",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
membership: ProjectMembershipsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { membership } = await server.services.orgAdmin.grantProjectAdminAccess({
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId
|
||||||
|
});
|
||||||
|
if (req.auth.authMode === AuthMode.JWT) {
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ORG_ADMIN_ACCESS_PROJECT,
|
||||||
|
metadata: {
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
username: req.auth.user.username,
|
||||||
|
email: req.auth.user.email || "",
|
||||||
|
userId: req.auth.userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { membership };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -24,6 +24,40 @@ export const createDistinguishedName = (parts: TDNParts) => {
|
|||||||
return dnParts.join(", ");
|
return dnParts.join(", ");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseDistinguishedName = (dn: string): TDNParts => {
|
||||||
|
const parts: TDNParts = {};
|
||||||
|
const dnParts = dn.split(/,\s*/);
|
||||||
|
|
||||||
|
for (const part of dnParts) {
|
||||||
|
const [key, value] = part.split("=");
|
||||||
|
switch (key.toUpperCase()) {
|
||||||
|
case "C":
|
||||||
|
parts.country = value;
|
||||||
|
break;
|
||||||
|
case "O":
|
||||||
|
parts.organization = value;
|
||||||
|
break;
|
||||||
|
case "OU":
|
||||||
|
parts.ou = value;
|
||||||
|
break;
|
||||||
|
case "ST":
|
||||||
|
parts.province = value;
|
||||||
|
break;
|
||||||
|
case "CN":
|
||||||
|
parts.commonName = value;
|
||||||
|
break;
|
||||||
|
case "L":
|
||||||
|
parts.locality = value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Ignore unrecognized keys
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
};
|
||||||
|
|
||||||
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
|
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
|
||||||
switch (keyAlgorithm) {
|
switch (keyAlgorithm) {
|
||||||
case CertKeyAlgorithm.RSA_4096:
|
case CertKeyAlgorithm.RSA_4096:
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ import {
|
|||||||
getCaCertChain, // TODO: consider rename
|
getCaCertChain, // TODO: consider rename
|
||||||
getCaCertChains,
|
getCaCertChains,
|
||||||
getCaCredentials,
|
getCaCredentials,
|
||||||
keyAlgorithmToAlgCfg
|
keyAlgorithmToAlgCfg,
|
||||||
|
parseDistinguishedName
|
||||||
} from "./certificate-authority-fns";
|
} from "./certificate-authority-fns";
|
||||||
import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
|
import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
|
||||||
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||||
@@ -39,6 +40,7 @@ import {
|
|||||||
TImportCertToCaDTO,
|
TImportCertToCaDTO,
|
||||||
TIssueCertFromCaDTO,
|
TIssueCertFromCaDTO,
|
||||||
TRenewCaCertDTO,
|
TRenewCaCertDTO,
|
||||||
|
TSignCertFromCaDTO,
|
||||||
TSignIntermediateDTO,
|
TSignIntermediateDTO,
|
||||||
TUpdateCaDTO
|
TUpdateCaDTO
|
||||||
} from "./certificate-authority-types";
|
} from "./certificate-authority-types";
|
||||||
@@ -989,7 +991,8 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return new leaf certificate issued by CA with id [caId]
|
* Return new leaf certificate issued by CA with id [caId] and private key.
|
||||||
|
* Note: private key and CSR are generated within Infisical.
|
||||||
*/
|
*/
|
||||||
const issueCertFromCa = async ({
|
const issueCertFromCa = async ({
|
||||||
caId,
|
caId,
|
||||||
@@ -1189,6 +1192,204 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new leaf certificate issued by CA with id [caId].
|
||||||
|
* Note: CSR is generated externally and submitted to Infisical.
|
||||||
|
*/
|
||||||
|
const signCertFromCa = async ({
|
||||||
|
caId,
|
||||||
|
csr,
|
||||||
|
friendlyName,
|
||||||
|
commonName,
|
||||||
|
altNames,
|
||||||
|
ttl,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TSignCertFromCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||||
|
|
||||||
|
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaCert = await kmsDecryptor({
|
||||||
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||||
|
|
||||||
|
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||||
|
|
||||||
|
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
|
||||||
|
if (notAfter) {
|
||||||
|
notAfterDate = new Date(notAfter);
|
||||||
|
} else if (ttl) {
|
||||||
|
notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||||
|
}
|
||||||
|
|
||||||
|
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||||
|
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||||
|
|
||||||
|
// check not before constraint
|
||||||
|
if (notBeforeDate < caCertNotBeforeDate) {
|
||||||
|
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||||
|
|
||||||
|
// check not after constraint
|
||||||
|
if (notAfterDate > caCertNotAfterDate) {
|
||||||
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||||
|
|
||||||
|
const dn = parseDistinguishedName(csrObj.subject);
|
||||||
|
const cn = commonName || dn.commonName;
|
||||||
|
|
||||||
|
if (!cn)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caPrivateKey } = await getCaCredentials({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const extensions: x509.Extension[] = [
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (altNames) {
|
||||||
|
const altNamesArray: {
|
||||||
|
type: "email" | "dns";
|
||||||
|
value: string;
|
||||||
|
}[] = altNames
|
||||||
|
.split(",")
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.map((altName) => {
|
||||||
|
// check if the altName is a valid email
|
||||||
|
if (z.string().email().safeParse(altName).success) {
|
||||||
|
return {
|
||||||
|
type: "email",
|
||||||
|
value: altName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the altName is a valid hostname
|
||||||
|
if (hostnameRegex.test(altName)) {
|
||||||
|
return {
|
||||||
|
type: "dns",
|
||||||
|
value: altName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
|
||||||
|
throw new Error(`Invalid altName: ${altName}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||||
|
extensions.push(altNamesExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||||
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: csrObj.subject,
|
||||||
|
issuer: caCertObj.subject,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingKey: caPrivateKey,
|
||||||
|
publicKey: csrObj.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await certificateDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
friendlyName: friendlyName || csrObj.subject,
|
||||||
|
commonName: cn,
|
||||||
|
altNames,
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedCertificate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return cert;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: leafCert.toString("pem"),
|
||||||
|
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||||
|
issuingCaCertificate,
|
||||||
|
serialNumber,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createCa,
|
createCa,
|
||||||
getCaById,
|
getCaById,
|
||||||
@@ -1200,6 +1401,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
getCaCert,
|
getCaCert,
|
||||||
signIntermediate,
|
signIntermediate,
|
||||||
importCertToCa,
|
importCertToCa,
|
||||||
issueCertFromCa
|
issueCertFromCa,
|
||||||
|
signCertFromCa
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,6 +95,17 @@ export type TIssueCertFromCaDTO = {
|
|||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TSignCertFromCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
csr: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName?: string;
|
||||||
|
altNames: string;
|
||||||
|
ttl: string;
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDNParts = {
|
export type TDNParts = {
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type TOrgAdminDALFactory = ReturnType<typeof orgAdminDALFactory>;
|
||||||
|
|
||||||
|
export const orgAdminDALFactory = () => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
|
||||||
|
|
||||||
|
type TOrgAdminServiceFactoryDep = {
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "find" | "findById" | "findProjectGhostUser">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findOne" | "create" | "transaction" | "delete">;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
||||||
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
||||||
|
|
||||||
|
export const orgAdminServiceFactory = ({
|
||||||
|
permissionService,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
userDAL,
|
||||||
|
projectUserMembershipRoleDAL
|
||||||
|
}: TOrgAdminServiceFactoryDep) => {
|
||||||
|
const listOrgProjects = async ({
|
||||||
|
actor,
|
||||||
|
limit,
|
||||||
|
actorId,
|
||||||
|
offset,
|
||||||
|
search,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TListOrgProjectsDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionAdminConsoleAction.AccessAllProjects,
|
||||||
|
OrgPermissionSubjects.AdminConsole
|
||||||
|
);
|
||||||
|
const projects = await projectDAL.find(
|
||||||
|
{
|
||||||
|
orgId: actorOrgId,
|
||||||
|
$search: {
|
||||||
|
name: search ? `%${search}%` : undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ offset, limit, sort: [["name", "asc"]], count: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const count = projects?.[0]?.count ? parseInt(projects?.[0]?.count, 10) : 0;
|
||||||
|
return { projects, count };
|
||||||
|
};
|
||||||
|
|
||||||
|
const grantProjectAdminAccess = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId
|
||||||
|
}: TAccessProjectDTO) => {
|
||||||
|
const { permission, membership } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionAdminConsoleAction.AccessAllProjects,
|
||||||
|
OrgPermissionSubjects.AdminConsole
|
||||||
|
);
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
|
if (project.version === ProjectVersion.V1) {
|
||||||
|
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// check already there exist a membership if there return it
|
||||||
|
const projectMembership = await projectMembershipDAL.findOne({
|
||||||
|
projectId,
|
||||||
|
userId: actorId
|
||||||
|
});
|
||||||
|
if (projectMembership) {
|
||||||
|
// reset and make the user admin
|
||||||
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
|
await projectUserMembershipRoleDAL.delete({ projectMembershipId: projectMembership.id }, tx);
|
||||||
|
await projectUserMembershipRoleDAL.create(
|
||||||
|
{
|
||||||
|
projectMembershipId: projectMembership.id,
|
||||||
|
role: ProjectMembershipRole.Admin
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return { isExistingMember: true, membership: projectMembership };
|
||||||
|
}
|
||||||
|
|
||||||
|
// missing membership thus add admin back as admin to project
|
||||||
|
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||||
|
if (!ghostUser) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find sudo user"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||||
|
if (!ghostUserLatestKey) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find sudo user latest key"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = await projectBotDAL.findOne({ projectId });
|
||||||
|
if (!bot) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find bot"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
ciphertext: bot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId);
|
||||||
|
if (!userEncryptionKey) throw new BadRequestError({ message: "user encryption key not found" });
|
||||||
|
const [newWsMember] = assignWorkspaceKeysToMembers({
|
||||||
|
decryptKey: ghostUserLatestKey,
|
||||||
|
userPrivateKey: botPrivateKey,
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
orgMembershipId: membership.id,
|
||||||
|
projectMembershipRole: ProjectMembershipRole.Admin,
|
||||||
|
userPublicKey: userEncryptionKey.publicKey
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
|
||||||
|
const newProjectMembership = await projectMembershipDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
userId: actorId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await projectUserMembershipRoleDAL.create(
|
||||||
|
{ projectMembershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin },
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
encryptedKey: newWsMember.workspaceEncryptedKey,
|
||||||
|
nonce: newWsMember.workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.id,
|
||||||
|
receiverId: actorId,
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return newProjectMembership;
|
||||||
|
});
|
||||||
|
return { isExistingMember: false, membership: updatedMembership };
|
||||||
|
};
|
||||||
|
|
||||||
|
return { listOrgProjects, grantProjectAdminAccess };
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TListOrgProjectsDTO = {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TAccessProjectDTO = {
|
||||||
|
projectId: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
@@ -66,10 +66,10 @@ export const getBotKeyFnFactory = (
|
|||||||
await projectBotDAL.create({
|
await projectBotDAL.create({
|
||||||
name: "Infisical Bot (Ghost)",
|
name: "Infisical Bot (Ghost)",
|
||||||
projectId,
|
projectId,
|
||||||
|
isActive: true,
|
||||||
tag,
|
tag,
|
||||||
iv,
|
iv,
|
||||||
encryptedPrivateKey: ciphertext,
|
encryptedPrivateKey: ciphertext,
|
||||||
isActive: true,
|
|
||||||
publicKey: botKey.publicKey,
|
publicKey: botKey.publicKey,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding: encoding,
|
keyEncoding: encoding,
|
||||||
@@ -80,6 +80,12 @@ export const getBotKeyFnFactory = (
|
|||||||
} else {
|
} else {
|
||||||
await projectBotDAL.updateById(bot.id, {
|
await projectBotDAL.updateById(bot.id, {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
publicKey: botKey.publicKey,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding: encoding,
|
||||||
encryptedProjectKey: encryptedWorkspaceKey.ciphertext,
|
encryptedProjectKey: encryptedWorkspaceKey.ciphertext,
|
||||||
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
|
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
|
||||||
senderId: projectV1Keys.userId
|
senderId: projectV1Keys.userId
|
||||||
@@ -89,7 +95,6 @@ export const getBotKeyFnFactory = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const botPrivateKey = getBotPrivateKey({ bot });
|
const botPrivateKey = getBotPrivateKey({ bot });
|
||||||
|
|
||||||
const botKey = decryptAsymmetric({
|
const botKey = decryptAsymmetric({
|
||||||
ciphertext: bot.encryptedProjectKey,
|
ciphertext: bot.encryptedProjectKey,
|
||||||
privateKey: botPrivateKey,
|
privateKey: botPrivateKey,
|
||||||
|
|||||||
@@ -256,7 +256,6 @@ export const projectMembershipServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bot = await projectBotDAL.findOne({ projectId });
|
const bot = await projectBotDAL.findOne({ projectId });
|
||||||
|
|
||||||
if (!bot) {
|
if (!bot) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Failed to find bot"
|
message: "Failed to find bot"
|
||||||
|
|||||||
@@ -490,10 +490,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
...secret,
|
...secret,
|
||||||
value: secret.encryptedValue
|
value: secret.encryptedValue
|
||||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||||
: undefined,
|
: "",
|
||||||
comment: secret.encryptedComment
|
comment: secret.encryptedComment
|
||||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||||
: undefined
|
: ""
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const expandSecretReferences = expandSecretReferencesFactory({
|
const expandSecretReferences = expandSecretReferencesFactory({
|
||||||
|
|||||||
+107
-37
@@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -13,53 +14,56 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"}
|
type VaultBackendType struct {
|
||||||
var AvailableVaults = []string{"auto", "file"}
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
var AvailableVaults = []VaultBackendType{
|
||||||
|
{
|
||||||
|
Name: "auto",
|
||||||
|
Description: "automatically select the system keyring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "file",
|
||||||
|
Description: "encrypted file vault",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var vaultSetCmd = &cobra.Command{
|
var vaultSetCmd = &cobra.Command{
|
||||||
Example: `infisical vault set pass`,
|
Example: `infisical vault set file --passphrase <your-passphrase>`,
|
||||||
Use: "set [vault-name]",
|
Use: "set [file|auto] [flags]",
|
||||||
Short: "Used to set the vault backend to store your login details securely at rest",
|
Short: "Used to configure the vault backends",
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
wantedVaultTypeName := args[0]
|
|
||||||
currentVaultBackend, err := util.GetCurrentVaultBackend()
|
vaultType := args[0]
|
||||||
|
|
||||||
|
passphrase, err := cmd.Flags().GetString("passphrase")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
|
util.HandleError(err, "Unable to get passphrase flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if vaultType == util.VAULT_BACKEND_FILE_MODE && passphrase != "" {
|
||||||
|
setFileVaultPassphrase(passphrase)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if wantedVaultTypeName == string(currentVaultBackend) {
|
util.PrintWarning("This command has been deprecated. Please use 'infisical vault use [file|auto]' to select which vault to use.\n")
|
||||||
log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend)
|
selectVaultTypeCmd(cmd, args)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if wantedVaultTypeName == "auto" || wantedVaultTypeName == "file" {
|
|
||||||
configFile, err := util.GetConfigFile()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
configFile.VaultBackendType = wantedVaultTypeName // save selected vault
|
|
||||||
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login
|
|
||||||
|
|
||||||
err = util.WriteConfigFile(&configFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName)
|
|
||||||
|
|
||||||
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
|
|
||||||
} else {
|
|
||||||
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(AvailableVaults, ", "))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var vaultUseCmd = &cobra.Command{
|
||||||
|
Example: `infisical vault use [file|auto]`,
|
||||||
|
Use: "use [file|auto]",
|
||||||
|
Short: "Used to select the the type of vault backend to store sensitive data securely at rest",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Run: selectVaultTypeCmd,
|
||||||
|
}
|
||||||
|
|
||||||
// runCmd represents the run command
|
// runCmd represents the run command
|
||||||
var vaultCmd = &cobra.Command{
|
var vaultCmd = &cobra.Command{
|
||||||
Use: "vault",
|
Use: "vault",
|
||||||
@@ -71,10 +75,30 @@ var vaultCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setFileVaultPassphrase(passphrase string) {
|
||||||
|
configFile, err := util.GetConfigFile()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode with base64
|
||||||
|
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase))
|
||||||
|
configFile.VaultBackendPassphrase = encodedPassphrase
|
||||||
|
|
||||||
|
err = util.WriteConfigFile(&configFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.PrintSuccessMessage("\nSuccessfully, set passphrase for file vault.\n")
|
||||||
|
}
|
||||||
|
|
||||||
func printAvailableVaultBackends() {
|
func printAvailableVaultBackends() {
|
||||||
fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:")
|
fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:")
|
||||||
for _, backend := range AvailableVaultsAndDescriptions {
|
for _, vaultType := range AvailableVaults {
|
||||||
fmt.Printf("\n- %s", backend)
|
fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentVaultBackend, err := util.GetCurrentVaultBackend()
|
currentVaultBackend, err := util.GetCurrentVaultBackend()
|
||||||
@@ -87,7 +111,53 @@ func printAvailableVaultBackends() {
|
|||||||
fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend))
|
fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selectVaultTypeCmd(cmd *cobra.Command, args []string) {
|
||||||
|
wantedVaultTypeName := args[0]
|
||||||
|
currentVaultBackend, err := util.GetCurrentVaultBackend()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if wantedVaultTypeName == string(currentVaultBackend) {
|
||||||
|
log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE {
|
||||||
|
configFile, err := util.GetConfigFile()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile.VaultBackendType = wantedVaultTypeName // save selected vault
|
||||||
|
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login
|
||||||
|
|
||||||
|
err = util.WriteConfigFile(&configFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName)
|
||||||
|
|
||||||
|
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
|
||||||
|
} else {
|
||||||
|
var availableVaultsNames []string
|
||||||
|
for _, vault := range AvailableVaults {
|
||||||
|
availableVaultsNames = append(availableVaultsNames, vault.Name)
|
||||||
|
}
|
||||||
|
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
||||||
|
vaultSetCmd.Flags().StringP("passphrase", "p", "", "Set the passphrase for the file vault")
|
||||||
|
|
||||||
vaultCmd.AddCommand(vaultSetCmd)
|
vaultCmd.AddCommand(vaultSetCmd)
|
||||||
|
vaultCmd.AddCommand(vaultUseCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(vaultCmd)
|
rootCmd.AddCommand(vaultCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ type UserCredentials struct {
|
|||||||
|
|
||||||
// The file struct for Infisical config file
|
// The file struct for Infisical config file
|
||||||
type ConfigFile struct {
|
type ConfigFile struct {
|
||||||
LoggedInUserEmail string `json:"loggedInUserEmail"`
|
LoggedInUserEmail string `json:"loggedInUserEmail"`
|
||||||
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
|
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
|
||||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||||
|
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoggedInUser struct {
|
type LoggedInUser struct {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -50,10 +51,11 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configFile := models.ConfigFile{
|
configFile := models.ConfigFile{
|
||||||
LoggedInUserEmail: userCredentials.Email,
|
LoggedInUserEmail: userCredentials.Email,
|
||||||
LoggedInUserDomain: config.INFISICAL_URL,
|
LoggedInUserDomain: config.INFISICAL_URL,
|
||||||
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
||||||
VaultBackendType: existingConfigFile.VaultBackendType,
|
VaultBackendType: existingConfigFile.VaultBackendType,
|
||||||
|
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
|
||||||
}
|
}
|
||||||
|
|
||||||
configFileMarshalled, err := json.Marshal(configFile)
|
configFileMarshalled, err := json.Marshal(configFile)
|
||||||
@@ -215,6 +217,14 @@ func GetConfigFile() (models.ConfigFile, error) {
|
|||||||
return models.ConfigFile{}, err
|
return models.ConfigFile{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if configFile.VaultBackendPassphrase != "" {
|
||||||
|
decodedPassphrase, err := base64.StdEncoding.DecodeString(configFile.VaultBackendPassphrase)
|
||||||
|
if err != nil {
|
||||||
|
return models.ConfigFile{}, fmt.Errorf("GetConfigFile: Unable to decode base64 passphrase [err=%s]", err)
|
||||||
|
}
|
||||||
|
os.Setenv("INFISICAL_VAULT_FILE_PASSPHRASE", string(decodedPassphrase))
|
||||||
|
}
|
||||||
|
|
||||||
return configFile, nil
|
return configFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ const (
|
|||||||
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
|
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
|
||||||
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
|
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
|
||||||
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
|
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
|
||||||
|
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.
|
||||||
|
|
||||||
|
VAULT_BACKEND_AUTO_MODE = "auto"
|
||||||
|
VAULT_BACKEND_FILE_MODE = "file"
|
||||||
|
|
||||||
// Universal Auth
|
// Universal Auth
|
||||||
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
|
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/manifoldco/promptui"
|
||||||
"github.com/zalando/go-keyring"
|
"github.com/zalando/go-keyring"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,16 +23,51 @@ func SetValueInKeyring(key, value string) error {
|
|||||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
|
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
|
err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
configFile, _ := GetConfigFile()
|
||||||
|
|
||||||
|
if configFile.VaultBackendPassphrase == "" {
|
||||||
|
PrintWarning("System keyring could not be used, falling back to `file` vault for sensitive data storage.")
|
||||||
|
passphrasePrompt := promptui.Prompt{
|
||||||
|
Label: "Enter the passphrase to use for keyring encryption",
|
||||||
|
}
|
||||||
|
passphrase, err := passphrasePrompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase))
|
||||||
|
configFile.VaultBackendPassphrase = encodedPassphrase
|
||||||
|
err = WriteConfigFile(&configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We call this function at last to trigger the environment variable to be set
|
||||||
|
GetConfigFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetValueInKeyring(key string) (string, error) {
|
func GetValueInKeyring(key string) (string, error) {
|
||||||
currentVaultBackend, err := GetCurrentVaultBackend()
|
currentVaultBackend, err := GetCurrentVaultBackend()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
|
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again")
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
|
value, err := keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
value, err = keyring.Get(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key)
|
||||||
|
}
|
||||||
|
return value, err
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteValueInKeyring(key string) error {
|
func DeleteValueInKeyring(key string) error {
|
||||||
@@ -38,5 +76,11 @@ func DeleteValueInKeyring(key string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
|
err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err = keyring.Delete(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ func GetCurrentVaultBackend() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if configFile.VaultBackendType == "" {
|
if configFile.VaultBackendType == "" {
|
||||||
return "auto", nil
|
return VAULT_BACKEND_AUTO_MODE, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" {
|
if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE {
|
||||||
return "auto", nil
|
return VAULT_BACKEND_AUTO_MODE, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return configFile.VaultBackendType, nil
|
return configFile.VaultBackendType, nil
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: "Meetings"
|
||||||
|
sidebarTitle: "Meetings"
|
||||||
|
description: "The guide to meetings at Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
## "Let's schedule a meeting about this"
|
||||||
|
|
||||||
|
Being a remote-first company, we try to be as async as possible. When an issue arises, it's best to create a public Slack thread and tag all the necessary team members. Otherwise, if you were to "put a meeting on a calendar", the decision making process will inevitable slow down by at least a day (e.g., trying to find the right time for folks in different time zones is not always straightforward).
|
||||||
|
|
||||||
|
In other words, we have almost no (recurring) meetings and prefer written communication or quick Slack huddles.
|
||||||
|
|
||||||
|
## Weekly All-hands
|
||||||
|
|
||||||
|
All-hands is the single recurring meeting that we run every Monday at 8:30am PT. Typically, we would discuss everything important that happened during the previous week and plan out the week ahead. This is also an opportunity to bring up any important topics in front of the whole company (but feel free to post those in Slack too).
|
||||||
+2
-1
@@ -59,7 +59,8 @@
|
|||||||
"handbook/onboarding",
|
"handbook/onboarding",
|
||||||
"handbook/spending-money",
|
"handbook/spending-money",
|
||||||
"handbook/time-off",
|
"handbook/time-off",
|
||||||
"handbook/hiring"
|
"handbook/hiring",
|
||||||
|
"handbook/meetings"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List CA certificates"
|
||||||
|
openapi: "GET /api/v1/pki/ca/{caId}/ca-certificates"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Renew"
|
||||||
|
openapi: "POST /api/v1/pki/ca/{caId}/renew"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Sign certificate"
|
||||||
|
openapi: "POST /api/v1/pki/ca/{caId}/sign-certificate"
|
||||||
|
---
|
||||||
@@ -32,6 +32,6 @@ description: "Change the vault type in Infisical"
|
|||||||
|
|
||||||
To safeguard your login details when using the CLI, Infisical places them in a system vault or an encrypted text file, protected by a passphrase that only the user knows.
|
To safeguard your login details when using the CLI, Infisical places them in a system vault or an encrypted text file, protected by a passphrase that only the user knows.
|
||||||
|
|
||||||
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell</Tip>
|
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, use the `infisical vault set file --passphrase <your-passphrase>` CLI command to specify your password once.</Tip>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Before you begin, you'll first need to choose a method of authentication with AW
|
|||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Create the Managing User IAM Role">
|
<Step title="Create the Managing User IAM Role">
|
||||||
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||||

|

|
||||||
|
|
||||||
2. Select **AWS Account** as the **Trusted Entity Type**.
|
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||||
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted.
|
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted.
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
</Steps>
|
</Steps>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="API">
|
<Tab title="API">
|
||||||
To create a certificate, make an API request to the [Create Certificate](/api-reference/endpoints/certificate-authorities/sign-intermediate) API endpoint,
|
To create a certificate, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
|
||||||
specifying the issuing CA.
|
specifying the issuing CA.
|
||||||
|
|
||||||
### Sample request
|
### Sample request
|
||||||
@@ -84,6 +84,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"commonName": "My Certificate",
|
"commonName": "My Certificate",
|
||||||
|
"ttl": "1y",
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -103,6 +104,31 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time.
|
Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-cert) API endpoint, specifying the issuing CA.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/sign-certificate' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-raw '{
|
||||||
|
"csr": "...",
|
||||||
|
"ttl": "1y",
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
certificate: "...",
|
||||||
|
certificateChain: "...",
|
||||||
|
issuingCaCertificate: "...",
|
||||||
|
privateKey: "...",
|
||||||
|
serialNumber: "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ A typical workflow for setting up a Private CA hierarchy consists of the followi
|
|||||||
intermediate certificate back to the intermediate CA as part of Step 2.
|
intermediate certificate back to the intermediate CA as part of Step 2.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
## Guide
|
## Guide to Creating a CA Hierarchy
|
||||||
|
|
||||||
In the following steps, we explore how to create a simple Private CA hierarchy
|
In the following steps, we explore how to create a simple Private CA hierarchy
|
||||||
consisting of a root CA and an intermediate CA.
|
consisting of a root CA and an intermediate CA.
|
||||||
@@ -240,6 +240,51 @@ consisting of a root CA and an intermediate CA.
|
|||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
## Guide to CA Renewal
|
||||||
|
|
||||||
|
In the following steps, we explore how to renew a CA certificate via same key pair.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical UI">
|
||||||
|
Head to the CA Page of the CA you wish you renew and press **Renew CA** on
|
||||||
|
the left side.  Input a new **Valid Until**
|
||||||
|
date to be used for the renewed CA certificate and press **Renew** to renew
|
||||||
|
the CA. 
|
||||||
|
<Note>
|
||||||
|
The new **Valid Until** date must be within the validity period of the
|
||||||
|
parent CA.
|
||||||
|
</Note>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="API">
|
||||||
|
|
||||||
|
To renew a CA certificate, make an API request to the [Renew CA](/api-reference/endpoints/certificate-authorities/renew) API endpoint, specifying the new `notAfter` date for the CA.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/renew' \
|
||||||
|
--header 'Authorization: Bearer <access-token>' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-raw '{
|
||||||
|
"type": "existing",
|
||||||
|
"notAfter": "2029-06-12"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
certificate: "...",
|
||||||
|
certificateChain: "...",
|
||||||
|
serialNumber: "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
@@ -247,4 +292,8 @@ consisting of a root CA and an intermediate CA.
|
|||||||
Infisical supports `RSA 2048`, `RSA 4096`, `ECDSA P-256`, `ECDSA P-384` key
|
Infisical supports `RSA 2048`, `RSA 4096`, `ECDSA P-256`, `ECDSA P-384` key
|
||||||
algorithms specified at the time of creating a CA.
|
algorithms specified at the time of creating a CA.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
<Accordion title="Does Infisical support CA renewal via new key pair">
|
||||||
|
At the moment, Infisical only supports CA renewal via same key pair. We
|
||||||
|
anticipate supporting CA renewal via new key pair in the coming month.
|
||||||
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 408 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 584 KiB |
+4
-1
@@ -155,7 +155,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Key Management",
|
"group": "Key Management (KMS)",
|
||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/kms/overview",
|
"documentation/platform/kms/overview",
|
||||||
"documentation/platform/kms/aws-kms",
|
"documentation/platform/kms/aws-kms",
|
||||||
@@ -667,11 +667,14 @@
|
|||||||
"api-reference/endpoints/certificate-authorities/read",
|
"api-reference/endpoints/certificate-authorities/read",
|
||||||
"api-reference/endpoints/certificate-authorities/update",
|
"api-reference/endpoints/certificate-authorities/update",
|
||||||
"api-reference/endpoints/certificate-authorities/delete",
|
"api-reference/endpoints/certificate-authorities/delete",
|
||||||
|
"api-reference/endpoints/certificate-authorities/renew",
|
||||||
|
"api-reference/endpoints/certificate-authorities/list-ca-certs",
|
||||||
"api-reference/endpoints/certificate-authorities/csr",
|
"api-reference/endpoints/certificate-authorities/csr",
|
||||||
"api-reference/endpoints/certificate-authorities/cert",
|
"api-reference/endpoints/certificate-authorities/cert",
|
||||||
"api-reference/endpoints/certificate-authorities/sign-intermediate",
|
"api-reference/endpoints/certificate-authorities/sign-intermediate",
|
||||||
"api-reference/endpoints/certificate-authorities/import-cert",
|
"api-reference/endpoints/certificate-authorities/import-cert",
|
||||||
"api-reference/endpoints/certificate-authorities/issue-cert",
|
"api-reference/endpoints/certificate-authorities/issue-cert",
|
||||||
|
"api-reference/endpoints/certificate-authorities/sign-cert",
|
||||||
"api-reference/endpoints/certificate-authorities/crl"
|
"api-reference/endpoints/certificate-authorities/crl"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const DeleteActionModal = ({
|
|||||||
<Input
|
<Input
|
||||||
value={inputData}
|
value={inputData}
|
||||||
onChange={(e) => setInputData(e.target.value)}
|
onChange={(e) => setInputData(e.target.value)}
|
||||||
placeholder="Type confirm..."
|
placeholder={`Type ${deleteKey} here`}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const Pagination = ({
|
|||||||
>
|
>
|
||||||
<div className="mr-6 flex items-center space-x-2">
|
<div className="mr-6 flex items-center space-x-2">
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{(page - 1) * perPage} - {(page - 1) * perPage + perPage} of {count}
|
{(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ export enum OrgPermissionSubjects {
|
|||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
Kms = "kms"
|
Kms = "kms",
|
||||||
|
AdminConsole = "organization-admin-console"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
|
AccessAllProjects = "access-all-projects"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -37,6 +42,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/** Extracts the key and value from a passed in env string based on the provided delimiters. */
|
||||||
|
export const getKeyValue = (pastedContent: string, delimiters: string[]) => {
|
||||||
|
const foundDelimiter = delimiters.find((delimiter) => pastedContent.includes(delimiter));
|
||||||
|
|
||||||
|
if (!foundDelimiter) {
|
||||||
|
return { key: pastedContent.trim(), value: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [key, value] = pastedContent.split(foundDelimiter);
|
||||||
|
return {
|
||||||
|
key: key.trim(),
|
||||||
|
value: (value ?? "").trim()
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -56,7 +56,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.GET_CERT]: "Get certificate",
|
[EventType.GET_CERT]: "Get certificate",
|
||||||
[EventType.DELETE_CERT]: "Delete certificate",
|
[EventType.DELETE_CERT]: "Delete certificate",
|
||||||
[EventType.REVOKE_CERT]: "Revoke certificate",
|
[EventType.REVOKE_CERT]: "Revoke certificate",
|
||||||
[EventType.GET_CERT_BODY]: "Get certificate body"
|
[EventType.GET_CERT_BODY]: "Get certificate body",
|
||||||
|
[EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||||
|
|||||||
@@ -70,5 +70,6 @@ export enum EventType {
|
|||||||
GET_CERT = "get-cert",
|
GET_CERT = "get-cert",
|
||||||
DELETE_CERT = "delete-cert",
|
DELETE_CERT = "delete-cert",
|
||||||
REVOKE_CERT = "revoke-cert",
|
REVOKE_CERT = "revoke-cert",
|
||||||
GET_CERT_BODY = "get-cert-body"
|
GET_CERT_BODY = "get-cert-body",
|
||||||
|
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,6 +579,16 @@ interface GetCertBody {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OrgAdminAccessProjectEvent {
|
||||||
|
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||||
|
metadata: {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
projectId: string;
|
||||||
|
}; // no metadata yet
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -635,7 +645,8 @@ export type Event =
|
|||||||
| GetCert
|
| GetCert
|
||||||
| DeleteCert
|
| DeleteCert
|
||||||
| RevokeCert
|
| RevokeCert
|
||||||
| GetCertBody;
|
| GetCertBody
|
||||||
|
| OrgAdminAccessProjectEvent;
|
||||||
|
|
||||||
export type AuditLog = {
|
export type AuditLog = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export * from "./keys";
|
|||||||
export * from "./kms";
|
export * from "./kms";
|
||||||
export * from "./ldapConfig";
|
export * from "./ldapConfig";
|
||||||
export * from "./oidcConfig";
|
export * from "./oidcConfig";
|
||||||
|
export * from "./orgAdmin";
|
||||||
export * from "./organization";
|
export * from "./organization";
|
||||||
export * from "./projectUserAdditionalPrivilege";
|
export * from "./projectUserAdditionalPrivilege";
|
||||||
export * from "./rateLimit";
|
export * from "./rateLimit";
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useOrgAdminAccessProject } from "./mutation";
|
||||||
|
export { useOrgAdminGetProjects } from "./queries";
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TOrgAdminAccessProjectDTO } from "./types";
|
||||||
|
|
||||||
|
export const useOrgAdminAccessProject = () =>
|
||||||
|
useMutation({
|
||||||
|
mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => {
|
||||||
|
const { data } = await apiRequest.post(
|
||||||
|
`/api/v1/organization-admin/projects/${projectId}/grant-admin-access`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { Workspace } from "../types";
|
||||||
|
import { TOrgAdminGetProjectsDTO } from "./types";
|
||||||
|
|
||||||
|
export const orgAdminQueryKeys = {
|
||||||
|
getProjects: (filter: TOrgAdminGetProjectsDTO) => ["org-admin-projects", filter] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdminGetProjectsDTO) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>(
|
||||||
|
"/api/v1/organization-admin/projects",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
search
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export type TOrgAdminGetProjectsDTO = {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOrgAdminAccessProjectDTO = {
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
@@ -317,6 +317,7 @@ export const useDeleteWorkspace = () => {
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
|
queryClient.invalidateQueries(["org-admin-projects"]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export type Workspace = {
|
|||||||
pitVersionLimit: number;
|
pitVersionLimit: number;
|
||||||
auditLogsRetentionDays: number;
|
auditLogsRetentionDays: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceEnv = {
|
export type WorkspaceEnv = {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface UsePopUpProps {
|
|||||||
export type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
|
export type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
|
||||||
[P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: {
|
[P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
data?: unknown;
|
data?: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -476,10 +476,15 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
{user?.superAdmin && (
|
{user?.superAdmin && (
|
||||||
<Link href="/admin" legacyBehavior>
|
<Link href="/admin" legacyBehavior>
|
||||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||||
Admin Panel
|
Server Admin Panel
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
<Link href={`/org/${currentOrg?.id}/admin`} legacyBehavior>
|
||||||
|
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||||
|
Organization Admin Console
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
<button type="button" onClick={logOutUser} className="w-full">
|
<button type="button" onClick={logOutUser} className="w-full">
|
||||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
|
import { OrgAdminPage } from "@app/views/OrgAdminPage";
|
||||||
|
|
||||||
|
export default function SettingsOrg() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
</Head>
|
||||||
|
<OrgAdminPage />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsOrg.requireAuth = true;
|
||||||
File diff suppressed because it is too large
Load Diff
-133
@@ -1,133 +0,0 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
|
||||||
import { faMoneyBill } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { Checkbox, Select, SelectItem } from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
|
|
||||||
import { TFormSchema } from "../../../../RolePage/components/OrgRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isNonEditable?: boolean;
|
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
|
||||||
control: Control<TFormSchema>;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Permission {
|
|
||||||
NoAccess = "no-access",
|
|
||||||
ReadOnly = "read-only",
|
|
||||||
FullAccess = "full-acess",
|
|
||||||
Custom = "custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
const PERMISSIONS = [
|
|
||||||
{ action: "read", label: "View projects" },
|
|
||||||
{ action: "create", label: "Create new projects" }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props) => {
|
|
||||||
const rule = useWatch({
|
|
||||||
control,
|
|
||||||
name: "permissions.workspace"
|
|
||||||
});
|
|
||||||
const [isCustom, setIsCustom] = useToggle();
|
|
||||||
|
|
||||||
const selectedPermissionCategory = useMemo(() => {
|
|
||||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
|
||||||
const totalActions = PERMISSIONS.length;
|
|
||||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
|
||||||
|
|
||||||
if (isCustom) return Permission.Custom;
|
|
||||||
if (score === 0) return Permission.NoAccess;
|
|
||||||
if (score === totalActions) return Permission.FullAccess;
|
|
||||||
if (score === 1 && rule?.read) return Permission.ReadOnly;
|
|
||||||
|
|
||||||
return Permission.Custom;
|
|
||||||
}, [rule, isCustom]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
|
||||||
else setIsCustom.off();
|
|
||||||
}, [selectedPermissionCategory]);
|
|
||||||
|
|
||||||
const handlePermissionChange = (val: Permission) => {
|
|
||||||
if (val === Permission.Custom) setIsCustom.on();
|
|
||||||
else setIsCustom.off();
|
|
||||||
|
|
||||||
switch (val) {
|
|
||||||
case Permission.NoAccess:
|
|
||||||
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
case Permission.FullAccess:
|
|
||||||
setValue("permissions.workspace", { read: true, create: true }, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
case Permission.ReadOnly:
|
|
||||||
setValue("permissions.workspace", { read: true, create: false }, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"rounded-md bg-mineshaft-800 px-10 py-6",
|
|
||||||
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div>
|
|
||||||
<FontAwesomeIcon icon={faMoneyBill} className="text-4xl" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-grow flex-col">
|
|
||||||
<div className="mb-1 text-lg font-medium">Project</div>
|
|
||||||
<div className="text-xs font-light">
|
|
||||||
View and create new projects in this organization
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
defaultValue={Permission.NoAccess}
|
|
||||||
isDisabled={isNonEditable}
|
|
||||||
value={selectedPermissionCategory}
|
|
||||||
onValueChange={handlePermissionChange}
|
|
||||||
>
|
|
||||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
|
||||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<motion.div
|
|
||||||
initial={false}
|
|
||||||
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
|
|
||||||
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
|
|
||||||
>
|
|
||||||
{isCustom &&
|
|
||||||
PERMISSIONS.map(({ action, label }) => (
|
|
||||||
<Controller
|
|
||||||
name={`permissions.workspace.${action}`}
|
|
||||||
key={`permissions.workspace.${action}`}
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
id={`permissions.workspace.${action}`}
|
|
||||||
isDisabled={isNonEditable}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Checkbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -12,6 +12,12 @@ const generalPermissionSchema = z
|
|||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
|
const adminConsolePermissionSchmea = z
|
||||||
|
.object({
|
||||||
|
"access-all-projects": z.boolean().optional()
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
@@ -23,7 +29,6 @@ export const formSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
workspace: z
|
workspace: z
|
||||||
.object({
|
.object({
|
||||||
read: z.boolean().optional(),
|
|
||||||
create: z.boolean().optional()
|
create: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -38,7 +43,8 @@ export const formSchema = z.object({
|
|||||||
scim: generalPermissionSchema,
|
scim: generalPermissionSchema,
|
||||||
ldap: generalPermissionSchema,
|
ldap: generalPermissionSchema,
|
||||||
billing: generalPermissionSchema,
|
billing: generalPermissionSchema,
|
||||||
identity: generalPermissionSchema
|
identity: generalPermissionSchema,
|
||||||
|
"organization-admin-console": adminConsolePermissionSchmea
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||||
|
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isEditable: boolean;
|
||||||
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
control: Control<TFormSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Permission {
|
||||||
|
NoAccess = "no-access",
|
||||||
|
Custom = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERMISSION_ACTIONS = [
|
||||||
|
{ action: "access-all-projects", label: "Access all organization projects" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: Props) => {
|
||||||
|
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||||
|
const [isCustom, setIsCustom] = useToggle();
|
||||||
|
|
||||||
|
const rule = useWatch({
|
||||||
|
control,
|
||||||
|
name: "permissions.organization-admin-console"
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPermissionCategory = useMemo(() => {
|
||||||
|
if (rule?.["access-all-projects"]) {
|
||||||
|
return Permission.Custom;
|
||||||
|
}
|
||||||
|
return Permission.NoAccess;
|
||||||
|
}, [rule, isCustom]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||||
|
else setIsCustom.off();
|
||||||
|
}, [selectedPermissionCategory]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||||
|
if (isRowCustom) {
|
||||||
|
setIsRowExpanded.on();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePermissionChange = (val: Permission) => {
|
||||||
|
if (!val) return;
|
||||||
|
if (val === Permission.Custom) {
|
||||||
|
setIsRowExpanded.on();
|
||||||
|
setIsCustom.on();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsCustom.off();
|
||||||
|
|
||||||
|
if (val === Permission.NoAccess) {
|
||||||
|
setValue(
|
||||||
|
"permissions.organization-admin-console",
|
||||||
|
{ "access-all-projects": false },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr
|
||||||
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
onClick={() => setIsRowExpanded.toggle()}
|
||||||
|
>
|
||||||
|
<Td>
|
||||||
|
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||||
|
</Td>
|
||||||
|
<Td>Organization Admin Console</Td>
|
||||||
|
<Td>
|
||||||
|
<Select
|
||||||
|
value={selectedPermissionCategory}
|
||||||
|
className="w-40 bg-mineshaft-600"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={handlePermissionChange}
|
||||||
|
isDisabled={!isEditable}
|
||||||
|
>
|
||||||
|
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
{isRowExpanded && (
|
||||||
|
<Tr>
|
||||||
|
<Td
|
||||||
|
colSpan={3}
|
||||||
|
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={`permissions.organization-admin-console.${action}`}
|
||||||
|
key={`permissions.organization-admin-console.${action}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
if (!isEditable) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update default role"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
id={`permissions.organization-admin-console.${action}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||||
|
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isEditable: boolean;
|
||||||
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
control: Control<TFormSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Permission {
|
||||||
|
NoAccess = "no-access",
|
||||||
|
Custom = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERMISSION_ACTIONS = [{ action: "create", label: "Create projects" }] as const;
|
||||||
|
|
||||||
|
export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) => {
|
||||||
|
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||||
|
const [isCustom, setIsCustom] = useToggle();
|
||||||
|
|
||||||
|
const rule = useWatch({
|
||||||
|
control,
|
||||||
|
name: "permissions.workspace"
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPermissionCategory = useMemo(() => {
|
||||||
|
if (rule?.create) {
|
||||||
|
return Permission.Custom;
|
||||||
|
}
|
||||||
|
return Permission.NoAccess;
|
||||||
|
}, [rule, isCustom]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||||
|
else setIsCustom.off();
|
||||||
|
}, [selectedPermissionCategory]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||||
|
if (isRowCustom) {
|
||||||
|
setIsRowExpanded.on();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePermissionChange = (val: Permission) => {
|
||||||
|
if (!val) return;
|
||||||
|
if (val === Permission.Custom) {
|
||||||
|
setIsRowExpanded.on();
|
||||||
|
setIsCustom.on();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsCustom.off();
|
||||||
|
|
||||||
|
if (val === Permission.NoAccess) {
|
||||||
|
setValue("permissions.workspace", { create: false }, { shouldDirty: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr
|
||||||
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
onClick={() => setIsRowExpanded.toggle()}
|
||||||
|
>
|
||||||
|
<Td>
|
||||||
|
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||||
|
</Td>
|
||||||
|
<Td>Project</Td>
|
||||||
|
<Td>
|
||||||
|
<Select
|
||||||
|
value={selectedPermissionCategory}
|
||||||
|
className="w-40 bg-mineshaft-600"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={handlePermissionChange}
|
||||||
|
isDisabled={!isEditable}
|
||||||
|
>
|
||||||
|
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
{isRowExpanded && (
|
||||||
|
<Tr>
|
||||||
|
<Td
|
||||||
|
colSpan={3}
|
||||||
|
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={`permissions.workspace.${action}`}
|
||||||
|
key={`permissions.workspace.${action}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
if (!isEditable) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update default role"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
id={`permissions.organization-admin-console.${action}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
-1
@@ -61,7 +61,10 @@ const getPermissionList = (option: string) => {
|
|||||||
type Props = {
|
type Props = {
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
|
formName: keyof Omit<
|
||||||
|
Exclude<TFormSchema["permissions"], undefined>,
|
||||||
|
"workspace" | "organization-admin-console"
|
||||||
|
>;
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
control: Control<TFormSchema>;
|
control: Control<TFormSchema>;
|
||||||
};
|
};
|
||||||
|
|||||||
+12
@@ -12,6 +12,8 @@ import {
|
|||||||
TFormSchema
|
TFormSchema
|
||||||
} from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
} from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
||||||
|
|
||||||
|
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
|
||||||
|
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||||
import { RolePermissionRow } from "./RolePermissionRow";
|
import { RolePermissionRow } from "./RolePermissionRow";
|
||||||
|
|
||||||
const SIMPLE_PERMISSION_OPTIONS = [
|
const SIMPLE_PERMISSION_OPTIONS = [
|
||||||
@@ -153,6 +155,16 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<OrgRoleWorkspaceRow
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
isEditable={isCustomRole}
|
||||||
|
/>
|
||||||
|
<OrgPermissionAdminConsoleRow
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
isEditable={isCustomRole}
|
||||||
|
/>
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
|
|
||||||
|
import { OrgAdminProjects } from "./components/OrgAdminProjects";
|
||||||
|
|
||||||
|
enum TabSections {
|
||||||
|
Projects = "projects"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrgAdminPage = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState<TabSections>(TabSections.Projects);
|
||||||
|
return (
|
||||||
|
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white">
|
||||||
|
<div className="w-full max-w-6xl px-6">
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-3xl font-semibold text-gray-200">Organization Admin Console</p>
|
||||||
|
</div>
|
||||||
|
<Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}>
|
||||||
|
<TabList>
|
||||||
|
<Tab value={TabSections.Projects}>Projects</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={TabSections.Projects}>
|
||||||
|
<OrgAdminProjects />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faEllipsis, faMagnifyingGlass, faSignIn } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
|
Spinner,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionAdminConsoleAction,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { withPermission } from "@app/hoc";
|
||||||
|
import { useDebounce } from "@app/hooks";
|
||||||
|
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const OrgAdminProjects = withPermission(
|
||||||
|
() => {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const debouncedSearch = useDebounce(search);
|
||||||
|
const [perPage, setPerPage] = useState(25);
|
||||||
|
const router = useRouter();
|
||||||
|
const orgAdminAccessProject = useOrgAdminAccessProject();
|
||||||
|
|
||||||
|
const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({
|
||||||
|
offset: (page - 1) * perPage,
|
||||||
|
limit: perPage,
|
||||||
|
search: debouncedSearch || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const projects = data?.projects || [];
|
||||||
|
const projectCount = data?.count || 0;
|
||||||
|
const isEmpty = !isProjectsLoading && projects.length === 0;
|
||||||
|
|
||||||
|
const handleAccessProject = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
await orgAdminAccessProject.mutateAsync({
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
await router.push({
|
||||||
|
pathname: "/project/[projectId]/secrets/overview",
|
||||||
|
query: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to access project",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key="panel-projects"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Projects</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search by project name"
|
||||||
|
/>
|
||||||
|
<TableContainer className="mt-4">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Slug</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isProjectsLoading && <TableSkeleton columns={4} innerKey="projects" />}
|
||||||
|
{!isProjectsLoading &&
|
||||||
|
projects?.map(({ name, slug, createdAt, id }) => (
|
||||||
|
<Tr key={`project-${id}`} className="group w-full">
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{slug}</Td>
|
||||||
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")}</Td>
|
||||||
|
<Td>
|
||||||
|
<div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="text-bunker-300 hover:text-primary-400 data-[state=open]:text-primary-400"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
handleAccessProject(id);
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faSignIn} />}
|
||||||
|
disabled={
|
||||||
|
orgAdminAccessProject.variables?.projectId === id &&
|
||||||
|
orgAdminAccessProject.isLoading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Access{" "}
|
||||||
|
{orgAdminAccessProject.variables?.projectId === id &&
|
||||||
|
orgAdminAccessProject.isLoading && <Spinner size="xs" />}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isProjectsLoading && (
|
||||||
|
<Pagination
|
||||||
|
count={projectCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isEmpty && <EmptyState title="No projects found" />}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: OrgPermissionAdminConsoleAction.AccessAllProjects,
|
||||||
|
subject: OrgPermissionSubjects.AdminConsole
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgAdminProjects } from "./OrgAdminProjects";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgAdminPage } from "./OrgAdminPage";
|
||||||
@@ -317,6 +317,12 @@ export const LogsTableRow = ({ auditLog }: Props) => {
|
|||||||
})}
|
})}
|
||||||
</Td>
|
</Td>
|
||||||
);
|
);
|
||||||
|
case EventType.ORG_ADMIN_ACCESS_PROJECT:
|
||||||
|
return (
|
||||||
|
<Td>
|
||||||
|
<p>{`Email: ${event.metadata.email}`}</p>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
case EventType.CREATE_CA:
|
case EventType.CREATE_CA:
|
||||||
case EventType.GET_CA:
|
case EventType.GET_CA:
|
||||||
case EventType.UPDATE_CA:
|
case EventType.UPDATE_CA:
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export const CaRenewalModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Create
|
Renew
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ClipboardEvent } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -5,6 +6,7 @@ import { z } from "zod";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
|
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||||
import { useCreateSecretV3 } from "@app/hooks/api";
|
import { useCreateSecretV3 } from "@app/hooks/api";
|
||||||
import { SecretType } from "@app/hooks/api/types";
|
import { SecretType } from "@app/hooks/api/types";
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ export const CreateSecretForm = ({
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
|
setValue,
|
||||||
formState: { errors, isSubmitting }
|
formState: { errors, isSubmitting }
|
||||||
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
||||||
const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm);
|
const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm);
|
||||||
@@ -73,6 +76,16 @@ export const CreateSecretForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const delimitters = [":", "="];
|
||||||
|
const pastedContent = e.clipboardData.getData("text");
|
||||||
|
const { key, value } = getKeyValue(pastedContent, delimitters);
|
||||||
|
|
||||||
|
setValue("key", key);
|
||||||
|
setValue("value", value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -83,10 +96,16 @@ export const CreateSecretForm = ({
|
|||||||
subTitle="Add a secret to the particular environment and folder"
|
subTitle="Add a secret to the particular environment and folder"
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||||
<FormControl label="Key" isRequired isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
|
<FormControl
|
||||||
|
label="Key"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(errors?.key)}
|
||||||
|
errorText={errors?.key?.message}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register("key")}
|
{...register("key")}
|
||||||
placeholder="Type your secret name"
|
placeholder="Type your secret name"
|
||||||
|
onPaste={handlePaste}
|
||||||
autoCapitalization={autoCapitalize}
|
autoCapitalization={autoCapitalize}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
+22
-2
@@ -1,3 +1,4 @@
|
|||||||
|
import { ClipboardEvent } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -17,8 +18,9 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||||
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
||||||
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types";
|
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||||
|
|
||||||
const typeSchema = z
|
const typeSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -54,6 +56,7 @@ export const CreateSecretForm = ({
|
|||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
watch,
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { isSubmitting, errors }
|
formState: { isSubmitting, errors }
|
||||||
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
||||||
const newSecretKey = watch("key");
|
const newSecretKey = watch("key");
|
||||||
@@ -133,6 +136,17 @@ export const CreateSecretForm = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const delimitters = [":", "="];
|
||||||
|
const pastedContent = e.clipboardData.getData("text");
|
||||||
|
const { key, value } = getKeyValue(pastedContent, delimitters);
|
||||||
|
|
||||||
|
setValue("key", key);
|
||||||
|
setValue("value", value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
|
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
@@ -141,10 +155,16 @@ export const CreateSecretForm = ({
|
|||||||
subTitle="Create & update a secret across many environments"
|
subTitle="Create & update a secret across many environments"
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||||
<FormControl label="Key" isRequired isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
|
<FormControl
|
||||||
|
label="Key"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(errors?.key)}
|
||||||
|
errorText={errors?.key?.message}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register("key")}
|
{...register("key")}
|
||||||
placeholder="Type your secret name"
|
placeholder="Type your secret name"
|
||||||
|
onPaste={handlePaste}
|
||||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
Reference in New Issue
Block a user