mirror of
https://github.com/Infisical/infisical.git
synced 2025-06-29 04:31:59 +00:00
Compare commits
26 Commits
conditiona
...
daniel/upd
Author | SHA1 | Date | |
---|---|---|---|
4c8063c532 | |||
f4ae40cb86 | |||
14449b8b41 | |||
bcdcaa33a4 | |||
e8a8542757 | |||
e61d35d824 | |||
714d6831bd | |||
956f75eb43 | |||
bfee34f38d | |||
092b89c59e | |||
3d76ae3399 | |||
23aa97feff | |||
0c5155f8e6 | |||
4afe2f2377 | |||
1e07c2fe23 | |||
191486519f | |||
cab8fb0d8e | |||
8bfd728ce4 | |||
c9eab0af18 | |||
d7dfc531fc | |||
a89bd08c08 | |||
4bfb9e8e74 | |||
c12bfa766c | |||
3432a16d4f | |||
19a403f467 | |||
06a7e804eb |
@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn(TableName.Identity, "hasDeleteProtection");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.boolean("hasDeleteProtection").notNullable().defaultTo(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn(TableName.Identity, "hasDeleteProtection");
|
||||
if (hasCol) {
|
||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.dropColumn("hasDeleteProtection");
|
||||
});
|
||||
}
|
||||
}
|
@ -12,7 +12,8 @@ export const IdentitiesSchema = z.object({
|
||||
name: z.string(),
|
||||
authMethod: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
hasDeleteProtection: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TIdentities = z.infer<typeof IdentitiesSchema>;
|
||||
|
@ -48,7 +48,9 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
|
||||
}),
|
||||
response: {
|
||||
200: GroupsSchema
|
||||
200: GroupsSchema.extend({
|
||||
customRoleSlug: z.string().nullable()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
|
@ -780,6 +780,7 @@ interface CreateIdentityEvent {
|
||||
metadata: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
hasDeleteProtection: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -788,6 +789,7 @@ interface UpdateIdentityEvent {
|
||||
metadata: {
|
||||
identityId: string;
|
||||
name?: string;
|
||||
hasDeleteProtection?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -169,11 +169,29 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.Groups)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
.where(`${TableName.Groups}.id`, id)
|
||||
.select(
|
||||
selectAllTableCols(TableName.Groups),
|
||||
db.ref("slug").as("customRoleSlug").withSchema(TableName.OrgRoles)
|
||||
)
|
||||
.first();
|
||||
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by id" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...groupOrm,
|
||||
findGroups,
|
||||
findByOrgId,
|
||||
findAllGroupPossibleMembers,
|
||||
findGroupsByProjectId,
|
||||
...groupOrm
|
||||
findById
|
||||
};
|
||||
};
|
||||
|
@ -111,12 +111,14 @@ export const IDENTITIES = {
|
||||
CREATE: {
|
||||
name: "The name of the identity to create.",
|
||||
organizationId: "The organization ID to which the identity belongs.",
|
||||
role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'."
|
||||
role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'.",
|
||||
hasDeleteProtection: "Prevents deletion of the identity when enabled."
|
||||
},
|
||||
UPDATE: {
|
||||
identityId: "The ID of the identity to update.",
|
||||
name: "The new name of the identity.",
|
||||
role: "The new role of the identity."
|
||||
role: "The new role of the identity.",
|
||||
hasDeleteProtection: "Prevents deletion of the identity when enabled."
|
||||
},
|
||||
DELETE: {
|
||||
identityId: "The ID of the identity to delete."
|
||||
|
@ -44,6 +44,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
name: z.string().trim().describe(IDENTITIES.CREATE.name),
|
||||
organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId),
|
||||
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role),
|
||||
hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection),
|
||||
metadata: z
|
||||
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
|
||||
.array()
|
||||
@ -75,6 +76,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
type: EventType.CREATE_IDENTITY,
|
||||
metadata: {
|
||||
name: identity.name,
|
||||
hasDeleteProtection: identity.hasDeleteProtection,
|
||||
identityId: identity.id
|
||||
}
|
||||
}
|
||||
@ -86,6 +88,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
properties: {
|
||||
orgId: req.body.organizationId,
|
||||
name: identity.name,
|
||||
hasDeleteProtection: identity.hasDeleteProtection,
|
||||
identityId: identity.id,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
@ -117,6 +120,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name),
|
||||
role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role),
|
||||
hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection),
|
||||
metadata: z
|
||||
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
|
||||
.array()
|
||||
@ -148,6 +152,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
type: EventType.UPDATE_IDENTITY,
|
||||
metadata: {
|
||||
name: identity.name,
|
||||
hasDeleteProtection: identity.hasDeleteProtection,
|
||||
identityId: identity.id
|
||||
}
|
||||
}
|
||||
@ -243,7 +248,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
permissions: true,
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
})
|
||||
})
|
||||
@ -292,7 +297,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
permissions: true,
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
})
|
||||
}).array(),
|
||||
@ -386,7 +391,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
permissions: true,
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
})
|
||||
}).array(),
|
||||
@ -451,7 +456,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
temporaryAccessEndTime: z.date().nullable().optional()
|
||||
})
|
||||
),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
}),
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true, type: true })
|
||||
|
@ -101,6 +101,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
|
||||
db.ref("id").as("identityId").withSchema(TableName.Identity),
|
||||
db.ref("name").as("identityName").withSchema(TableName.Identity),
|
||||
db.ref("hasDeleteProtection").withSchema(TableName.Identity),
|
||||
db.ref("id").withSchema(TableName.IdentityProjectMembership),
|
||||
db.ref("role").withSchema(TableName.IdentityProjectMembershipRole),
|
||||
db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"),
|
||||
@ -130,6 +131,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
data: docs,
|
||||
parentMapper: ({
|
||||
identityName,
|
||||
hasDeleteProtection,
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
@ -151,6 +153,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
identity: {
|
||||
id: identityId,
|
||||
name: identityName,
|
||||
hasDeleteProtection,
|
||||
authMethods: buildAuthMethods({
|
||||
uaId,
|
||||
awsId,
|
||||
|
@ -114,16 +114,18 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
|
||||
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
db.ref("name").withSchema(TableName.Identity),
|
||||
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
|
||||
);
|
||||
|
||||
if (data) {
|
||||
const { name } = data;
|
||||
const { name, hasDeleteProtection } = data;
|
||||
return {
|
||||
...data,
|
||||
identity: {
|
||||
id: data.identityId,
|
||||
name,
|
||||
hasDeleteProtection,
|
||||
authMethods: buildAuthMethods(data)
|
||||
}
|
||||
};
|
||||
@ -155,7 +157,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
|
||||
.select(
|
||||
selectAllTableCols(TableName.IdentityOrgMembership),
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName")
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
|
||||
)
|
||||
.where(filter)
|
||||
.as("paginatedIdentity");
|
||||
@ -245,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("updatedAt").withSchema("paginatedIdentity"),
|
||||
db.ref("identityId").withSchema("paginatedIdentity").as("identityId"),
|
||||
db.ref("identityName").withSchema("paginatedIdentity"),
|
||||
db.ref("hasDeleteProtection").withSchema("paginatedIdentity"),
|
||||
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
@ -286,6 +290,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
crName,
|
||||
identityId,
|
||||
identityName,
|
||||
hasDeleteProtection,
|
||||
role,
|
||||
roleId,
|
||||
id,
|
||||
@ -324,6 +329,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
identity: {
|
||||
id: identityId,
|
||||
name: identityName,
|
||||
hasDeleteProtection,
|
||||
authMethods: buildAuthMethods({
|
||||
uaId,
|
||||
alicloudId,
|
||||
@ -476,6 +482,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership),
|
||||
db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"),
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("hasDeleteProtection").withSchema(TableName.Identity),
|
||||
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
@ -518,6 +525,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
crName,
|
||||
identityId,
|
||||
identityName,
|
||||
hasDeleteProtection,
|
||||
role,
|
||||
roleId,
|
||||
total_count,
|
||||
@ -556,6 +564,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
identity: {
|
||||
id: identityId,
|
||||
name: identityName,
|
||||
hasDeleteProtection,
|
||||
authMethods: buildAuthMethods({
|
||||
uaId,
|
||||
alicloudId,
|
||||
|
@ -47,6 +47,7 @@ export const identityServiceFactory = ({
|
||||
const createIdentity = async ({
|
||||
name,
|
||||
role,
|
||||
hasDeleteProtection,
|
||||
actor,
|
||||
orgId,
|
||||
actorId,
|
||||
@ -96,7 +97,7 @@ export const identityServiceFactory = ({
|
||||
}
|
||||
|
||||
const identity = await identityDAL.transaction(async (tx) => {
|
||||
const newIdentity = await identityDAL.create({ name }, tx);
|
||||
const newIdentity = await identityDAL.create({ name, hasDeleteProtection }, tx);
|
||||
await identityOrgMembershipDAL.create(
|
||||
{
|
||||
identityId: newIdentity.id,
|
||||
@ -138,6 +139,7 @@ export const identityServiceFactory = ({
|
||||
const updateIdentity = async ({
|
||||
id,
|
||||
role,
|
||||
hasDeleteProtection,
|
||||
name,
|
||||
actor,
|
||||
actorId,
|
||||
@ -189,7 +191,11 @@ export const identityServiceFactory = ({
|
||||
}
|
||||
|
||||
const identity = await identityDAL.transaction(async (tx) => {
|
||||
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx);
|
||||
const newIdentity =
|
||||
name || hasDeleteProtection
|
||||
? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx)
|
||||
: await identityDAL.findById(id, tx);
|
||||
|
||||
if (role) {
|
||||
await identityOrgMembershipDAL.updateById(
|
||||
identityOrgMembership.id,
|
||||
@ -272,6 +278,9 @@ export const identityServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
if (identityOrgMembership.identity.hasDeleteProtection)
|
||||
throw new BadRequestError({ message: "Identity has delete protection" });
|
||||
|
||||
const deletedIdentity = await identityDAL.deleteById(id);
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId);
|
||||
|
@ -5,12 +5,14 @@ import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
||||
export type TCreateIdentityDTO = {
|
||||
role: string;
|
||||
name: string;
|
||||
hasDeleteProtection: boolean;
|
||||
metadata?: { key: string; value: string }[];
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TUpdateIdentityDTO = {
|
||||
id: string;
|
||||
role?: string;
|
||||
hasDeleteProtection?: boolean;
|
||||
name?: string;
|
||||
metadata?: { key: string; value: string }[];
|
||||
isActorSuperAdmin?: boolean;
|
||||
|
@ -81,6 +81,7 @@ export type TMachineIdentityCreatedEvent = {
|
||||
event: PostHogEventTypes.MachineIdentityCreated;
|
||||
properties: {
|
||||
name: string;
|
||||
hasDeleteProtection: boolean;
|
||||
orgId: string;
|
||||
identityId: string;
|
||||
};
|
||||
|
@ -396,7 +396,8 @@
|
||||
"pages": [
|
||||
"self-hosting/guides/mongo-to-postgres",
|
||||
"self-hosting/guides/custom-certificates",
|
||||
"self-hosting/guides/automated-bootstrapping"
|
||||
"self-hosting/guides/automated-bootstrapping",
|
||||
"self-hosting/guides/production-hardening"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -664,9 +665,9 @@
|
||||
"sdks/languages/node",
|
||||
"sdks/languages/python",
|
||||
"sdks/languages/java",
|
||||
"sdks/languages/csharp",
|
||||
"sdks/languages/go",
|
||||
"sdks/languages/ruby",
|
||||
"sdks/languages/csharp"
|
||||
"sdks/languages/ruby"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -1,9 +1,10 @@
|
||||
---
|
||||
title: "Infisical .NET SDK"
|
||||
sidebarTitle: ".NET"
|
||||
url: "https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk"
|
||||
icon: "bars"
|
||||
---
|
||||
|
||||
{/*
|
||||
If you're working with C#, the official [Infisical C# SDK](https://github.com/Infisical/sdk/tree/main/languages/csharp) package is the easiest way to fetch and work with secrets for your application.
|
||||
|
||||
- [Nuget Package](https://www.nuget.org/packages/Infisical.Sdk)
|
||||
@ -590,4 +591,4 @@ var decryptedPlaintext = infisical.DecryptSymmetric(decryptOptions);
|
||||
|
||||
#### Returns (string)
|
||||
`Plaintext` (string): The decrypted plaintext.
|
||||
|
||||
*/}
|
||||
|
697
docs/self-hosting/guides/production-hardening.mdx
Normal file
697
docs/self-hosting/guides/production-hardening.mdx
Normal file
@ -0,0 +1,697 @@
|
||||
---
|
||||
title: "Production Hardening"
|
||||
description: "Security hardening recommendations for production Infisical deployments"
|
||||
---
|
||||
|
||||
This document provides specific security hardening recommendations for production Infisical deployments. These recommendations follow Infisical's security model and focus on defense in depth.
|
||||
|
||||
Choose your deployment method below and follow the recommendations for your specific setup. Start with **Universal Security Fundamentals** that apply to all deployments, then follow your deployment-specific section.
|
||||
|
||||
## Universal Security Fundamentals
|
||||
|
||||
These security configurations apply to **all** Infisical deployments regardless of how you deploy.
|
||||
|
||||
### Cryptographic Security
|
||||
|
||||
#### Generate Secure Keys
|
||||
|
||||
Generate strong cryptographic keys for your deployment:
|
||||
|
||||
```bash
|
||||
# Required - Generate secure encryption key
|
||||
ENCRYPTION_KEY=$(openssl rand -hex 16)
|
||||
|
||||
# Required - Generate secure auth secret
|
||||
AUTH_SECRET=$(openssl rand -base64 32)
|
||||
```
|
||||
|
||||
#### Configure Token Lifetimes
|
||||
|
||||
Minimize exposure window for compromised tokens:
|
||||
|
||||
```bash
|
||||
# JWT token configuration (adjust based on security requirements)
|
||||
JWT_AUTH_LIFETIME=15m # Authentication tokens
|
||||
JWT_REFRESH_LIFETIME=24h # Refresh tokens
|
||||
JWT_SERVICE_LIFETIME=1h # Service tokens
|
||||
```
|
||||
|
||||
### Network Security
|
||||
|
||||
#### TLS Configuration
|
||||
|
||||
Configure HTTPS and secure database connections:
|
||||
|
||||
```bash
|
||||
# Enable HTTPS (recommended for production)
|
||||
HTTPS_ENABLED=true
|
||||
|
||||
# Secure PostgreSQL connection with SSL
|
||||
DB_CONNECTION_URI="postgresql://user:pass@host:5432/db?sslmode=require"
|
||||
|
||||
# For base64-encoded SSL certificate
|
||||
DB_ROOT_CERT="<base64-encoded-certificate>"
|
||||
```
|
||||
|
||||
#### Redis Security
|
||||
|
||||
Use authentication and TLS for Redis:
|
||||
|
||||
```bash
|
||||
# Redis with TLS (if supported by your Redis deployment)
|
||||
REDIS_URL="rediss://user:password@redis:6380"
|
||||
|
||||
# Redis Sentinel configuration for high availability
|
||||
REDIS_SENTINEL_HOSTS="192.168.65.254:26379,192.168.65.254:26380"
|
||||
REDIS_SENTINEL_MASTER_NAME="mymaster"
|
||||
REDIS_SENTINEL_ENABLE_TLS=true
|
||||
REDIS_SENTINEL_USERNAME="sentinel_user"
|
||||
REDIS_SENTINEL_PASSWORD="sentinel_password"
|
||||
```
|
||||
|
||||
#### Network Access Controls
|
||||
|
||||
Configure network restrictions and firewall rules:
|
||||
|
||||
```bash
|
||||
# Limit CORS to specific domains
|
||||
CORS_ALLOWED_ORIGINS=["https://your-app.example.com"]
|
||||
|
||||
# Prevent connections to internal/private IP addresses
|
||||
# This blocks access to internal services like metadata endpoints,
|
||||
# internal APIs, databases, and other sensitive infrastructure
|
||||
ALLOW_INTERNAL_IP_CONNECTIONS=false
|
||||
```
|
||||
|
||||
**Implement network firewalls**. Restrict network access to only necessary services:
|
||||
|
||||
- **Required ports**: Infisical API (8080) and HTTPS (if applicable)
|
||||
- **Database access**: Restrict PostgreSQL and Redis to authorized sources only
|
||||
- **Principle**: Default deny incoming, allow only required traffic
|
||||
- **Implementation**: See your deployment-specific section below for exact configuration
|
||||
|
||||
### Application Security
|
||||
|
||||
#### Site Configuration
|
||||
|
||||
Set proper site URL for your Infisical instance:
|
||||
|
||||
```bash
|
||||
# Required - Must be absolute URL with protocol
|
||||
SITE_URL="https://app.infisical.com"
|
||||
```
|
||||
|
||||
#### SMTP Security
|
||||
|
||||
Use TLS for email communications:
|
||||
|
||||
```bash
|
||||
# SMTP with TLS
|
||||
SMTP_HOST="smtp.example.com"
|
||||
SMTP_PORT="587"
|
||||
SMTP_USERNAME="your-smtp-user"
|
||||
SMTP_PASSWORD="your-smtp-password"
|
||||
SMTP_REQUIRE_TLS=true
|
||||
SMTP_IGNORE_TLS=false
|
||||
SMTP_FROM_ADDRESS="noreply@example.com"
|
||||
SMTP_FROM_NAME="Infisical"
|
||||
```
|
||||
|
||||
#### Privacy Configuration
|
||||
|
||||
Control telemetry and data collection:
|
||||
|
||||
```bash
|
||||
# Optional - Disable telemetry (enabled by default)
|
||||
TELEMETRY_ENABLED=false
|
||||
```
|
||||
|
||||
### Database Security
|
||||
|
||||
#### High Availability Configuration
|
||||
|
||||
Configure database read replicas for high availability PostgreSQL setups:
|
||||
|
||||
```bash
|
||||
# Read replica configuration (JSON format)
|
||||
DB_READ_REPLICAS='[{"DB_CONNECTION_URI":"postgresql://user:pass@replica:5432/db?sslmode=require"}]'
|
||||
```
|
||||
|
||||
### Operational Security
|
||||
|
||||
#### User Access Management
|
||||
|
||||
**Establish user off-boarding procedures**. Remove access promptly when users leave:
|
||||
|
||||
1. Remove user from organization
|
||||
2. Revoke active service tokens
|
||||
3. Remove from external identity providers
|
||||
4. Audit access logs for the user's activity
|
||||
5. Rotate any shared secrets the user had access to
|
||||
|
||||
#### Maintenance and Updates
|
||||
|
||||
**Keep frequent upgrade cadence**. Regularly update to the latest Infisical version for your deployment method.
|
||||
|
||||
## Deployment-Specific Hardening
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
These recommendations are specific to Docker deployments of Infisical.
|
||||
|
||||
#### Container Security
|
||||
|
||||
**Use read-only root filesystems**. Prevent runtime modifications while allowing necessary temporary access:
|
||||
|
||||
```bash
|
||||
# Run with read-only filesystem but allow /tmp access
|
||||
docker run --read-only \
|
||||
--tmpfs /tmp:rw,exec,size=1G \
|
||||
infisical/infisical:latest
|
||||
```
|
||||
|
||||
**Note**: Infisical requires temporary directory access for:
|
||||
|
||||
- Secret scanning operations
|
||||
- SSH certificate generation and validation
|
||||
|
||||
The `--tmpfs` mounts provide secure, isolated temporary storage that is:
|
||||
|
||||
- Automatically cleaned up on container restart
|
||||
- Limited in size to prevent disk exhaustion
|
||||
- Isolated from the host system
|
||||
- Wiped on container removal
|
||||
|
||||
**Drop unnecessary capabilities**. Remove all Linux capabilities:
|
||||
|
||||
```bash
|
||||
# Drop all capabilities
|
||||
docker run --cap-drop=ALL infisical/infisical:latest
|
||||
```
|
||||
|
||||
**Use specific image tags**. Never use `latest` tags in production:
|
||||
|
||||
```bash
|
||||
# Use specific version tags
|
||||
docker run infisical/infisical:v0.93.1-postgres
|
||||
```
|
||||
|
||||
#### Resource Management
|
||||
|
||||
**Set resource limits**. Prevent resource exhaustion attacks:
|
||||
|
||||
```bash
|
||||
# Set memory and CPU limits
|
||||
docker run --memory=1g --cpus=0.5 infisical/infisical:latest
|
||||
```
|
||||
|
||||
#### Health Monitoring
|
||||
|
||||
**Configure health checks**. Set up Docker health checks:
|
||||
|
||||
```dockerfile
|
||||
# In Dockerfile or docker-compose.yml
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/api/status || exit 1
|
||||
```
|
||||
|
||||
#### Network Security
|
||||
|
||||
**Host firewall configuration**. Configure host-level firewall for Docker deployments:
|
||||
|
||||
```bash
|
||||
# Docker manages its own iptables rules, but configure host firewall
|
||||
sudo ufw default deny incoming
|
||||
sudo ufw default allow outgoing
|
||||
|
||||
# Allow Docker-mapped ports (adjust based on your port mapping)
|
||||
sudo ufw allow 8080/tcp # If mapping container 8080 to host 8080
|
||||
sudo ufw allow 443/tcp # If terminating HTTPS at host level
|
||||
|
||||
# Enable firewall
|
||||
sudo ufw --force enable
|
||||
|
||||
# Verify Docker iptables integration
|
||||
sudo iptables -L DOCKER
|
||||
```
|
||||
|
||||
#### Maintenance
|
||||
|
||||
**Regular updates**. Monitor [Docker Hub](https://hub.docker.com/r/infisical/infisical/tags) for new releases and update your image tags regularly.
|
||||
|
||||
### Kubernetes Deployment
|
||||
|
||||
These recommendations are specific to Kubernetes deployments of Infisical.
|
||||
|
||||
#### Pod Security
|
||||
|
||||
**Use Pod Security Standards**. Apply restricted security profile:
|
||||
|
||||
```yaml
|
||||
# Namespace-level Pod Security Standards
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: infisical
|
||||
labels:
|
||||
pod-security.kubernetes.io/enforce: restricted
|
||||
pod-security.kubernetes.io/audit: restricted
|
||||
pod-security.kubernetes.io/warn: restricted
|
||||
```
|
||||
|
||||
**Configure security context**. Set comprehensive security context:
|
||||
|
||||
```yaml
|
||||
# Deployment security context
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: infisical
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
fsGroup: 1001
|
||||
containers:
|
||||
- name: infisical
|
||||
image: infisical/infisical:v0.93.1-postgres
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000Mi
|
||||
cpu: 500m
|
||||
requests:
|
||||
cpu: 350m
|
||||
memory: 512Mi
|
||||
```
|
||||
|
||||
#### Network Security
|
||||
|
||||
**Configure network policies**. Restrict pod-to-pod communication:
|
||||
|
||||
```yaml
|
||||
# Example Kubernetes NetworkPolicy
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: infisical-netpol
|
||||
namespace: infisical
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: infisical
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: ingress-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
egress:
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
```
|
||||
|
||||
**Infrastructure firewall considerations**. In addition to the universal host firewalls, implement infrastructure-level security:
|
||||
|
||||
For cloud deployments (AWS Security Groups, Azure NSGs, or GCP Firewall Rules):
|
||||
|
||||
- Allow ingress from load balancer to NodePort/ClusterIP service
|
||||
- Allow egress to managed databases
|
||||
- Block all other traffic
|
||||
|
||||
For on-premises deployments, ensure node-level firewalls allow:
|
||||
|
||||
- Ingress traffic from ingress controllers
|
||||
- Egress traffic to external services (databases, SMTP)
|
||||
|
||||
#### Access Control
|
||||
|
||||
**Use dedicated service accounts**. Create service accounts with minimal permissions:
|
||||
|
||||
```yaml
|
||||
# Service account configuration
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: infisical
|
||||
namespace: infisical
|
||||
automountServiceAccountToken: false
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: infisical
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
serviceAccountName: infisical
|
||||
```
|
||||
|
||||
#### Ingress Security
|
||||
|
||||
**Configure ingress with TLS**. Set up secure ingress:
|
||||
|
||||
```yaml
|
||||
# Secure ingress configuration
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: infisical-ingress
|
||||
namespace: infisical
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- secretName: infisical-tls
|
||||
hosts:
|
||||
- app.example.com
|
||||
rules:
|
||||
- host: app.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: infisical
|
||||
port:
|
||||
number: 8080
|
||||
```
|
||||
|
||||
#### Secret Management
|
||||
|
||||
**Use Kubernetes secrets**. Store sensitive configuration securely:
|
||||
|
||||
```yaml
|
||||
# Kubernetes secret for environment variables
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: infisical-secrets
|
||||
namespace: infisical
|
||||
type: Opaque
|
||||
stringData:
|
||||
AUTH_SECRET: "<generate-with-openssl-rand-base64-32>"
|
||||
ENCRYPTION_KEY: "<generate-with-openssl-rand-hex-16>"
|
||||
DB_CONNECTION_URI: "<your-postgres-connection-string>"
|
||||
REDIS_URL: "<your-redis-connection-string>"
|
||||
SITE_URL: "<your-site-url>"
|
||||
```
|
||||
|
||||
**Note:** Kubernetes secrets are only base64-encoded by default and are not encrypted at rest unless you explicitly enable etcd encryption. For production environments, you should:
|
||||
|
||||
- Enable [etcd encryption at rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/) to protect secrets stored in the cluster
|
||||
- Limit access to etcd and Kubernetes API to only trusted administrators
|
||||
|
||||
#### Health Monitoring
|
||||
|
||||
**Set up health checks**. Configure readiness and liveness probes:
|
||||
|
||||
```yaml
|
||||
# Health check configuration
|
||||
containers:
|
||||
- name: infisical
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/status
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/status
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
```
|
||||
|
||||
#### Infrastructure Considerations
|
||||
|
||||
**Use managed databases (if possible)**. For production deployments, consider using managed PostgreSQL and Redis services instead of in-cluster instances when feasible, as they typically provide better security, backup, and maintenance capabilities.
|
||||
|
||||
#### Maintenance
|
||||
|
||||
**Regular updates**. Monitor [Docker Hub](https://hub.docker.com/r/infisical/infisical/tags) for new releases and update your deployment manifests with new image tags regularly.
|
||||
|
||||
### Linux Binary Deployment
|
||||
|
||||
These recommendations are specific to Linux binary deployments of Infisical.
|
||||
|
||||
#### System User Management
|
||||
|
||||
**Create dedicated user account**. Run Infisical under a dedicated service account:
|
||||
|
||||
```bash
|
||||
# Create dedicated user
|
||||
sudo useradd --system --shell /bin/false --home-dir /opt/infisical infisical
|
||||
|
||||
# Create application directory
|
||||
sudo mkdir -p /opt/infisical
|
||||
sudo chown infisical:infisical /opt/infisical
|
||||
```
|
||||
|
||||
#### Service Configuration
|
||||
|
||||
**Configure systemd service**. Create a secure systemd service:
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/infisical.service
|
||||
[Unit]
|
||||
Description=Infisical Secret Management
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# IMPORTANT: Change from default 'root' user to dedicated service account
|
||||
User=infisical
|
||||
Group=infisical
|
||||
WorkingDirectory=/opt/infisical
|
||||
ExecStart=/opt/infisical/infisical-linux-amd64
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/opt/infisical
|
||||
PrivateTmp=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LimitCORE=0
|
||||
MemorySwapMax=0
|
||||
|
||||
# Environment file
|
||||
EnvironmentFile=/etc/infisical/environment
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
#### Configuration Security
|
||||
|
||||
**Secure environment configuration**. Store environment variables securely:
|
||||
|
||||
```bash
|
||||
# Create secure config directory
|
||||
sudo mkdir -p /etc/infisical
|
||||
sudo chmod 750 /etc/infisical
|
||||
sudo chown root:infisical /etc/infisical
|
||||
|
||||
# Create environment file
|
||||
sudo touch /etc/infisical/environment
|
||||
sudo chmod 640 /etc/infisical/environment
|
||||
sudo chown root:infisical /etc/infisical/environment
|
||||
```
|
||||
|
||||
#### System Security
|
||||
|
||||
**Disable memory swapping**. Prevent sensitive data from being written to disk:
|
||||
|
||||
```bash
|
||||
# Disable swap immediately
|
||||
sudo swapoff -a
|
||||
|
||||
# Disable swap permanently (comment out swap entries)
|
||||
sudo sed -i '/swap/d' /etc/fstab
|
||||
```
|
||||
|
||||
**Disable core dumps**. Prevent potential exposure of encryption keys:
|
||||
|
||||
```bash
|
||||
# Set system-wide core dump limits
|
||||
echo "* hard core 0" | sudo tee -a /etc/security/limits.conf
|
||||
|
||||
# Disable core dumps for current session
|
||||
ulimit -c 0
|
||||
```
|
||||
|
||||
#### File Permissions
|
||||
|
||||
**Secure file permissions**. Set proper permissions on application files:
|
||||
|
||||
```bash
|
||||
# Set binary permissions
|
||||
sudo chmod 755 /opt/infisical/infisical-linux-amd64
|
||||
sudo chown infisical:infisical /opt/infisical/infisical-linux-amd64
|
||||
|
||||
# Set config file permissions
|
||||
sudo chmod 640 /etc/infisical/environment
|
||||
sudo chown root:infisical /etc/infisical/environment
|
||||
```
|
||||
|
||||
#### Network Security
|
||||
|
||||
**Host firewall configuration**. Configure comprehensive firewall for Linux binary deployments:
|
||||
|
||||
```bash
|
||||
# Configure UFW firewall
|
||||
sudo ufw default deny incoming
|
||||
sudo ufw default allow outgoing
|
||||
|
||||
# Allow Infisical API access
|
||||
sudo ufw allow 8080/tcp
|
||||
|
||||
# Allow HTTPS (if terminating TLS at Infisical)
|
||||
sudo ufw allow 443/tcp
|
||||
|
||||
# If running PostgreSQL locally, restrict to localhost
|
||||
sudo ufw allow from 127.0.0.1 to any port 5432
|
||||
|
||||
# If running Redis locally, restrict to localhost
|
||||
sudo ufw allow from 127.0.0.1 to any port 6379
|
||||
|
||||
# Enable firewall
|
||||
sudo ufw --force enable
|
||||
```
|
||||
|
||||
#### System Maintenance
|
||||
|
||||
**Synchronize system clocks**. Ensure accurate time for JWT tokens and audit logs:
|
||||
|
||||
```bash
|
||||
# Install and configure NTP
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ntp
|
||||
sudo systemctl enable ntp
|
||||
sudo systemctl start ntp
|
||||
|
||||
# Verify time synchronization
|
||||
timedatectl status
|
||||
```
|
||||
|
||||
**Regular updates**. Monitor [Cloudsmith releases](https://cloudsmith.io/~infisical/repos/infisical-core/packages) for new binary versions and update your installation regularly.
|
||||
|
||||
## Enterprise Security Features
|
||||
|
||||
### Hardware Security Module (HSM) Integration
|
||||
|
||||
For the highest level of encryption security, integrate with Hardware Security Modules:
|
||||
|
||||
HSM integration provides hardware-protected encryption keys stored on tamper-proof devices, offering superior security for encryption operations:
|
||||
|
||||
- **Supported HSM Providers**: Thales Luna Cloud HSM, AWS CloudHSM, Fortanix HSM
|
||||
- **Root Key Protection**: HSM encrypts Infisical's root encryption keys using hardware-protected keys
|
||||
- **Enterprise Requirements**: Ideal for government, financial, and healthcare organizations
|
||||
|
||||
```bash
|
||||
# HSM Environment Variables (example for production)
|
||||
HSM_LIB_PATH="/path/to/hsm/library.so"
|
||||
HSM_PIN="your-hsm-pin"
|
||||
HSM_SLOT="0"
|
||||
HSM_KEY_LABEL="infisical-root-key"
|
||||
```
|
||||
|
||||
For complete HSM setup instructions, see the [HSM Integration Guide](/documentation/platform/kms/hsm-integration).
|
||||
|
||||
### External Key Management Service (KMS) Integration
|
||||
|
||||
Leverage cloud-native KMS providers for enhanced security and compliance:
|
||||
|
||||
Infisical can integrate with external KMS providers to encrypt project secrets, providing enterprise-grade key management:
|
||||
|
||||
- **Supported Providers**: AWS KMS, Google Cloud KMS, Azure Key Vault (coming soon)
|
||||
- **Workspace Key Protection**: Each project's encryption key is protected by your external KMS
|
||||
- **Envelope Encryption**: Infisical uses your cloud KMS to encrypt/decrypt project workspace keys, which in turn encrypt the actual secret data
|
||||
- **Compliance**: Leverage your cloud provider's compliance certifications (FedRAMP, SOC2, ISO 27001)
|
||||
|
||||
#### Benefits for Production Deployments
|
||||
|
||||
- **Separation of Concerns**: Keys managed in your cloud infrastructure, separate from Infisical
|
||||
- **Regulatory Compliance**: Use your existing compliance-certified KMS infrastructure
|
||||
- **Audit Integration**: KMS operations logged in your cloud provider's audit trails
|
||||
- **Disaster Recovery**: Keys backed by your cloud provider's HA and backup systems
|
||||
- **Access Controls**: Leverage your cloud IAM for KMS access management
|
||||
|
||||
#### Configuration Resources
|
||||
|
||||
For external KMS configuration, see:
|
||||
|
||||
- [AWS KMS Integration](/documentation/platform/kms-configuration/aws-kms)
|
||||
- [GCP KMS Integration](/documentation/platform/kms-configuration/gcp-kms)
|
||||
- [External KMS Overview](/documentation/platform/kms-configuration/overview)
|
||||
|
||||
## Advanced Security Configurations
|
||||
|
||||
### Backup Security
|
||||
|
||||
**Configure backup encryption**. Encrypt PostgreSQL backups:
|
||||
|
||||
```bash
|
||||
# PostgreSQL backup with encryption
|
||||
pg_dump $DB_CONNECTION_URI | gpg --cipher-algo AES256 --compress-algo 1 --symmetric --output backup.sql.gpg
|
||||
```
|
||||
|
||||
### Monitoring and Logging
|
||||
|
||||
**Implement log monitoring**. Set up centralized logging for security analysis and audit trails. Configure your SIEM or logging platform to monitor Infisical operations.
|
||||
|
||||
### Security Updates
|
||||
|
||||
**Regular security updates**. Monitor the [Infisical repository](https://github.com/Infisical/infisical) for security updates and apply them promptly.
|
||||
|
||||
## Compliance and Monitoring
|
||||
|
||||
### Enterprise Compliance Requirements
|
||||
|
||||
For enterprise deployments requiring compliance certifications:
|
||||
|
||||
- Implement audit log retention policies
|
||||
- Set up security event monitoring and alerting
|
||||
- Configure automated vulnerability scanning
|
||||
- Establish incident response procedures
|
||||
- Document security controls for compliance audits
|
||||
|
||||
### Standards Compliance
|
||||
|
||||
**FIPS 140-3 Compliance**. Infisical is actively working on FIPS 140-3 compliance to meet U.S. and Canadian government cryptographic standards. This will provide validated cryptographic modules for organizations requiring certified encryption implementations.
|
@ -242,6 +242,7 @@ interface CreateIdentityEvent {
|
||||
metadata: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
hasDeleteProtection: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -250,6 +251,7 @@ interface UpdateIdentityEvent {
|
||||
metadata: {
|
||||
identityId: string;
|
||||
name?: string;
|
||||
hasDeleteProtection?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -85,12 +85,13 @@ export const useCreateIdentity = () => {
|
||||
export const useUpdateIdentity = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Identity, object, UpdateIdentityDTO>({
|
||||
mutationFn: async ({ identityId, name, role, metadata }) => {
|
||||
mutationFn: async ({ identityId, name, role, hasDeleteProtection, metadata }) => {
|
||||
const {
|
||||
data: { identity }
|
||||
} = await apiRequest.patch(`/api/v1/identities/${identityId}`, {
|
||||
name,
|
||||
role,
|
||||
hasDeleteProtection,
|
||||
metadata
|
||||
});
|
||||
|
||||
|
@ -14,6 +14,7 @@ export type IdentityTrustedIp = {
|
||||
export type Identity = {
|
||||
id: string;
|
||||
name: string;
|
||||
hasDeleteProtection: boolean;
|
||||
authMethods: IdentityAuthMethod[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@ -83,6 +84,7 @@ export type CreateIdentityDTO = {
|
||||
name: string;
|
||||
organizationId: string;
|
||||
role?: string;
|
||||
hasDeleteProtection: boolean;
|
||||
metadata?: { key: string; value: string }[];
|
||||
};
|
||||
|
||||
@ -90,6 +92,7 @@ export type UpdateIdentityDTO = {
|
||||
identityId: string;
|
||||
name?: string;
|
||||
role?: string;
|
||||
hasDeleteProtection?: boolean;
|
||||
organizationId: string;
|
||||
metadata?: { key: string; value: string }[];
|
||||
};
|
||||
|
@ -15,7 +15,8 @@ import {
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent
|
||||
ModalContent,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { findOrgMembershipRole } from "@app/helpers/roles";
|
||||
@ -27,6 +28,7 @@ const schema = z
|
||||
.object({
|
||||
name: z.string().min(1, "Required"),
|
||||
role: z.object({ slug: z.string(), name: z.string() }),
|
||||
hasDeleteProtection: z.boolean(),
|
||||
metadata: z
|
||||
.object({
|
||||
key: z.string().trim().min(1),
|
||||
@ -64,7 +66,8 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: ""
|
||||
name: "",
|
||||
hasDeleteProtection: false
|
||||
}
|
||||
});
|
||||
|
||||
@ -78,6 +81,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
identityId: string;
|
||||
name: string;
|
||||
role: string;
|
||||
hasDeleteProtection: boolean;
|
||||
metadata?: { key: string; value: string }[];
|
||||
customRole: {
|
||||
name: string;
|
||||
@ -91,22 +95,25 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
reset({
|
||||
name: identity.name,
|
||||
role: identity.customRole ?? findOrgMembershipRole(roles, identity.role),
|
||||
hasDeleteProtection: identity.hasDeleteProtection,
|
||||
metadata: identity.metadata
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
name: "",
|
||||
role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole)
|
||||
role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole),
|
||||
hasDeleteProtection: false
|
||||
});
|
||||
}
|
||||
}, [popUp?.identity?.data, roles]);
|
||||
|
||||
const onFormSubmit = async ({ name, role, metadata }: FormData) => {
|
||||
const onFormSubmit = async ({ name, role, metadata, hasDeleteProtection }: FormData) => {
|
||||
try {
|
||||
const identity = popUp?.identity?.data as {
|
||||
identityId: string;
|
||||
name: string;
|
||||
role: string;
|
||||
hasDeleteProtection: boolean;
|
||||
};
|
||||
|
||||
if (identity) {
|
||||
@ -116,6 +123,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
identityId: identity.identityId,
|
||||
name,
|
||||
role: role.slug || undefined,
|
||||
hasDeleteProtection,
|
||||
organizationId: orgId,
|
||||
metadata
|
||||
});
|
||||
@ -127,6 +135,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { id: createdId } = await createMutateAsync({
|
||||
name,
|
||||
role: role.slug || undefined,
|
||||
hasDeleteProtection,
|
||||
organizationId: orgId,
|
||||
metadata
|
||||
});
|
||||
@ -215,6 +224,24 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="hasDeleteProtection"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||
<Switch
|
||||
className="ml-0 mr-2 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
containerClassName="flex-row-reverse w-fit"
|
||||
id="delete-protection-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p>Delete Protection {value ? "Enabled" : "Disabled"}</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<FormLabel label="Metadata" />
|
||||
</div>
|
||||
|
@ -329,7 +329,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuContent align="start" className="mt-3 p-1">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
|
@ -47,13 +47,19 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
rightIcon={<FontAwesomeIcon className="ml-1" icon={faChevronDown} />}
|
||||
rightIcon={
|
||||
<FontAwesomeIcon
|
||||
className="ml-1 transition-transform duration-200 group-data-[state=open]:rotate-180"
|
||||
icon={faChevronDown}
|
||||
/>
|
||||
}
|
||||
colorSchema="secondary"
|
||||
className="group select-none"
|
||||
>
|
||||
Options
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[120px]" align="end">
|
||||
<DropdownMenuContent className="mt-3 min-w-[120px]" align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
@ -68,6 +74,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
handlePopUpOpen("identity", {
|
||||
identityId,
|
||||
name: data.identity.name,
|
||||
hasDeleteProtection: data.identity.hasDeleteProtection,
|
||||
role: data.role,
|
||||
customRole: data.customRole
|
||||
});
|
||||
@ -131,6 +138,12 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{data.identity.name}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Delete Protection</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{data.identity.hasDeleteProtection ? "On" : "Off"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
|
||||
<p className="text-sm text-mineshaft-300">{data.role}</p>
|
||||
|
@ -35,7 +35,7 @@ export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props
|
||||
Options
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[120px]" align="end">
|
||||
<DropdownMenuContent className="mt-3 min-w-[120px]" align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
|
@ -72,7 +72,7 @@ export const GroupsSection = () => {
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddGroupModal()}
|
||||
|
@ -2,10 +2,11 @@ import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faEllipsisV,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash,
|
||||
faUsers
|
||||
faUsers,
|
||||
faUsersSlash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@ -13,6 +14,10 @@ import { format } from "date-fns";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
@ -184,33 +189,42 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Groups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content="Remove">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteGroup", {
|
||||
id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Groups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faUsersSlash} />}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteGroup", {
|
||||
id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove Group From Project
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
@ -3,12 +3,13 @@ import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCircleXmark,
|
||||
faClock,
|
||||
faEllipsisV,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faServer,
|
||||
faXmark
|
||||
faServer
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@ -21,6 +22,10 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
@ -163,20 +168,21 @@ export const IdentityTab = withProjectPermission(
|
||||
>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
<div className="flex w-full justify-end pr-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
>
|
||||
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||
Documentation{" "}
|
||||
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.06rem] ml-1 text-xs"
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
@ -185,7 +191,7 @@ export const IdentityTab = withProjectPermission(
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("identity")}
|
||||
@ -231,7 +237,7 @@ export const IdentityTab = withProjectPermission(
|
||||
</Th>
|
||||
<Th className="w-1/3">Role</Th>
|
||||
<Th>Added on</Th>
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
@ -372,37 +378,46 @@ export const IdentityTab = withProjectPermission(
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td className="flex justify-end space-x-2 opacity-0 duration-300 group-hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
identityId: id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton ariaLabel="more-icon" variant="plain">
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
<Td className="flex justify-end space-x-2">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCircleXmark} />}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
identityId: id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove Identity From Project
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
@ -56,12 +56,12 @@ export const MembersSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addMember")}
|
||||
|
@ -9,8 +9,8 @@ import {
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash,
|
||||
faUsers
|
||||
faUsers,
|
||||
faUserXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@ -416,32 +416,40 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>
|
||||
{userId !== u?.id && (
|
||||
<div className="flex items-center space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("removeMember", { username: u.username });
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton ariaLabel="more-icon" variant="plain">
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faUserXmark} />}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("removeMember", { username: u.username });
|
||||
}}
|
||||
>
|
||||
Remove User From Project
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
|
@ -10,10 +10,10 @@ export const ProjectRoleListTab = withProjectPermission(
|
||||
return (
|
||||
<motion.div
|
||||
key="role-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: -30 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<ProjectRoleList />
|
||||
</motion.div>
|
||||
|
@ -1,4 +1,16 @@
|
||||
import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faEye,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@ -12,6 +24,10 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@ -19,15 +35,27 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types";
|
||||
import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal";
|
||||
import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal";
|
||||
|
||||
enum RolesOrderBy {
|
||||
Name = "name",
|
||||
Slug = "slug"
|
||||
}
|
||||
|
||||
export const ProjectRoleList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
@ -57,6 +85,75 @@ export const ProjectRoleList = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy,
|
||||
search,
|
||||
setSearch,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
setPage,
|
||||
offset
|
||||
} = usePagination<RolesOrderBy>(RolesOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("projectRolesTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("projectRolesTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const filteredRoles = useMemo(
|
||||
() =>
|
||||
roles
|
||||
?.filter((role) => {
|
||||
const { slug, name } = role;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
name.toLowerCase().includes(searchValue) || slug.toLowerCase().includes(searchValue)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [roleOne, roleTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case RolesOrderBy.Slug:
|
||||
return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase());
|
||||
case RolesOrderBy.Name:
|
||||
default:
|
||||
return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase());
|
||||
}
|
||||
}) ?? [],
|
||||
[roles, orderDirection, search, orderBy]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredRoles.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: RolesOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: RolesOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: RolesOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
@ -64,7 +161,7 @@ export const ProjectRoleList = () => {
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("role")}
|
||||
@ -75,18 +172,50 @@ export const ProjectRoleList = () => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search project roles..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Slug</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Slug
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Slug)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Slug)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Slug)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th aria-label="actions" className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||
{roles?.map((role) => {
|
||||
{isRolesLoading && <TableSkeleton columns={3} innerKey="project-roles" />}
|
||||
{filteredRoles?.slice(offset, perPage * page).map((role) => {
|
||||
const { id, name, slug } = role;
|
||||
const isNonMutatable = Object.values(ProjectMembershipRole).includes(
|
||||
slug as ProjectMembershipRole
|
||||
@ -109,88 +238,118 @@ export const ProjectRoleList = () => {
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: `/${currentWorkspace?.type}/$projectId/roles/$roleSlug` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
roleSlug: slug
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
{`${isNonMutatable ? "View" : "Edit"} Role`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("duplicateRole", role);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Duplicate Role
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{!isNonMutatable && (
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[12rem]" sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isNonMutatable ? faEye : faEdit} />}
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", role);
|
||||
navigate({
|
||||
to: `/${currentWorkspace?.type}/$projectId/roles/$roleSlug` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
roleSlug: slug
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Role
|
||||
{`${isNonMutatable ? "View" : "Edit"} Role`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("duplicateRole", role);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Duplicate Role
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{!isNonMutatable && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50",
|
||||
"transition-colors duration-100"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", role);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Role
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredRoles?.length) && (
|
||||
<Pagination
|
||||
count={filteredRoles!.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!filteredRoles?.length && !isRolesLoading && (
|
||||
<EmptyState
|
||||
title={
|
||||
roles?.length
|
||||
? "No project roles match search..."
|
||||
: "This project does not have any roles"
|
||||
}
|
||||
icon={roles?.length ? faSearch : undefined}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
|
@ -96,6 +96,10 @@ import { SecretOverviewSecretRotationRow } from "@app/pages/secret-manager/Overv
|
||||
|
||||
import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
|
||||
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
|
||||
import {
|
||||
HIDDEN_SECRET_VALUE,
|
||||
HIDDEN_SECRET_VALUE_API_MASK
|
||||
} from "../SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
|
||||
@ -509,15 +513,25 @@ export const OverviewPage = () => {
|
||||
env: string,
|
||||
key: string,
|
||||
value: string,
|
||||
secretValueHidden: boolean,
|
||||
type = SecretType.Shared
|
||||
) => {
|
||||
let secretValue: string | undefined = value;
|
||||
|
||||
if (
|
||||
secretValueHidden &&
|
||||
(value === HIDDEN_SECRET_VALUE_API_MASK || value === HIDDEN_SECRET_VALUE)
|
||||
) {
|
||||
secretValue = undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await updateSecretV3({
|
||||
environment: env,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value,
|
||||
secretValue,
|
||||
type
|
||||
});
|
||||
|
||||
|
@ -50,6 +50,7 @@ type Props = {
|
||||
env: string,
|
||||
key: string,
|
||||
value: string,
|
||||
secretValueHidden: boolean,
|
||||
type?: SecretType,
|
||||
secretId?: string
|
||||
) => Promise<void>;
|
||||
@ -147,6 +148,7 @@ export const SecretEditRow = ({
|
||||
environment,
|
||||
secretName,
|
||||
value,
|
||||
secretValueHidden,
|
||||
isOverride ? SecretType.Personal : SecretType.Shared,
|
||||
secretId
|
||||
);
|
||||
@ -166,6 +168,7 @@ export const SecretEditRow = ({
|
||||
environment,
|
||||
secretName,
|
||||
secretValue,
|
||||
secretValueHidden,
|
||||
isOverride ? SecretType.Personal : SecretType.Shared,
|
||||
secretId
|
||||
);
|
||||
@ -257,7 +260,7 @@ export const SecretEditRow = ({
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Tooltip content="save">
|
||||
<Tooltip content="Save">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="submit-value"
|
||||
|
@ -24,6 +24,7 @@ import { useToggle } from "@app/hooks";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { getExpandedRowStyle } from "@app/pages/secret-manager/OverviewPage/components/utils";
|
||||
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
import { SecretEditRow } from "./SecretEditRow";
|
||||
import SecretRenameRow from "./SecretRenameRow";
|
||||
@ -40,6 +41,7 @@ type Props = {
|
||||
env: string,
|
||||
key: string,
|
||||
value: string,
|
||||
secretValueHidden: boolean,
|
||||
type?: SecretType,
|
||||
secretId?: string
|
||||
) => Promise<void>;
|
||||
@ -96,7 +98,7 @@ export const SecretOverviewTableRow = ({
|
||||
);
|
||||
|
||||
if (secret?.secretValueHidden && !secret?.valueOverride) {
|
||||
return canEditSecretValue ? "******" : "";
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
return secret?.valueOverride || secret?.value || importedSecret?.secret?.value || "";
|
||||
};
|
||||
|
@ -62,6 +62,7 @@ import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { camelCaseToSpaces } from "@app/lib/fn/string";
|
||||
|
||||
import { CreateReminderForm } from "./CreateReminderForm";
|
||||
import { HIDDEN_SECRET_VALUE } from "./SecretItem";
|
||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||
|
||||
type Props = {
|
||||
@ -897,7 +898,9 @@ export const SecretDetailSidebar = ({
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
{secretValueHidden ? "******" : secretValue?.replace(/./g, "*")}
|
||||
{secretValueHidden
|
||||
? HIDDEN_SECRET_VALUE
|
||||
: secretValue?.replace(/./g, "*")}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
|
@ -59,6 +59,7 @@ import {
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
|
||||
export const HIDDEN_SECRET_VALUE = "******";
|
||||
export const HIDDEN_SECRET_VALUE_API_MASK = "<hidden-by-infisical>";
|
||||
|
||||
type Props = {
|
||||
secret: SecretV3RawSanitized;
|
||||
|
@ -20,7 +20,7 @@ import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/c
|
||||
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
import { SecretDetailSidebar } from "./SecretDetailSidebar";
|
||||
import { SecretItem } from "./SecretItem";
|
||||
import { HIDDEN_SECRET_VALUE, HIDDEN_SECRET_VALUE_API_MASK, SecretItem } from "./SecretItem";
|
||||
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
||||
|
||||
type Props = {
|
||||
@ -85,6 +85,7 @@ export const SecretListView = ({
|
||||
type: SecretType,
|
||||
key: string,
|
||||
{
|
||||
secretValueHidden,
|
||||
value,
|
||||
comment,
|
||||
reminderRepeatDays,
|
||||
@ -97,6 +98,7 @@ export const SecretListView = ({
|
||||
secretMetadata,
|
||||
isRotatedSecret
|
||||
}: Partial<{
|
||||
secretValueHidden: boolean;
|
||||
value: string;
|
||||
comment: string;
|
||||
reminderRepeatDays: number | null;
|
||||
@ -123,6 +125,15 @@ export const SecretListView = ({
|
||||
}
|
||||
|
||||
if (operation === "update") {
|
||||
let secretValue = value;
|
||||
|
||||
if (
|
||||
secretValueHidden &&
|
||||
(value === HIDDEN_SECRET_VALUE_API_MASK || value === HIDDEN_SECRET_VALUE)
|
||||
) {
|
||||
secretValue = undefined;
|
||||
}
|
||||
|
||||
await updateSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
@ -130,7 +141,7 @@ export const SecretListView = ({
|
||||
secretKey: key,
|
||||
...(!isRotatedSecret && {
|
||||
newSecretName: newKey,
|
||||
secretValue: value || ""
|
||||
secretValue: secretValueHidden ? secretValue : secretValue || ""
|
||||
}),
|
||||
type,
|
||||
tagIds: tags,
|
||||
@ -168,7 +179,7 @@ export const SecretListView = ({
|
||||
},
|
||||
cb?: () => void
|
||||
) => {
|
||||
const { key: oldKey } = orgSecret;
|
||||
const { key: oldKey, secretValueHidden } = orgSecret;
|
||||
const {
|
||||
key,
|
||||
value,
|
||||
@ -246,7 +257,8 @@ export const SecretListView = ({
|
||||
newKey: hasKeyChanged ? key : undefined,
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding,
|
||||
secretMetadata,
|
||||
isRotatedSecret: orgSecret.isRotatedSecret
|
||||
isRotatedSecret: orgSecret.isRotatedSecret,
|
||||
secretValueHidden
|
||||
});
|
||||
if (cb) cb();
|
||||
}
|
||||
|
Reference in New Issue
Block a user