mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 12:27:28 +00:00
feat(infisical-pg: completed machine identity services
This commit is contained in:
@@ -16,6 +16,7 @@ module.exports = {
|
|||||||
"consistent-return": "off", // my style
|
"consistent-return": "off", // my style
|
||||||
"import/order": "off", // for simple-import-order
|
"import/order": "off", // for simple-import-order
|
||||||
"import/prefer-default-export": "off", // why
|
"import/prefer-default-export": "off", // why
|
||||||
|
"no-restricted-syntax": "off",
|
||||||
"import/first": "error",
|
"import/first": "error",
|
||||||
"import/newline-after-import": "error",
|
"import/newline-after-import": "error",
|
||||||
"import/no-duplicates": "error",
|
"import/no-duplicates": "error",
|
||||||
|
|||||||
Vendored
+49
@@ -13,6 +13,24 @@ import {
|
|||||||
TBackupPrivateKey,
|
TBackupPrivateKey,
|
||||||
TBackupPrivateKeyInsert,
|
TBackupPrivateKeyInsert,
|
||||||
TBackupPrivateKeyUpdate,
|
TBackupPrivateKeyUpdate,
|
||||||
|
TIdentities,
|
||||||
|
TIdentitiesInsert,
|
||||||
|
TIdentitiesUpdate,
|
||||||
|
TIdentityAccessTokens,
|
||||||
|
TIdentityAccessTokensInsert,
|
||||||
|
TIdentityAccessTokensUpdate,
|
||||||
|
TIdentityOrgMemberships,
|
||||||
|
TIdentityOrgMembershipsInsert,
|
||||||
|
TIdentityOrgMembershipsUpdate,
|
||||||
|
TIdentityProjectMemberships,
|
||||||
|
TIdentityProjectMembershipsInsert,
|
||||||
|
TIdentityProjectMembershipsUpdate,
|
||||||
|
TIdentityUaClientSecrets,
|
||||||
|
TIdentityUaClientSecretsInsert,
|
||||||
|
TIdentityUaClientSecretsUpdate,
|
||||||
|
TIdentityUniversalAuths,
|
||||||
|
TIdentityUniversalAuthsInsert,
|
||||||
|
TIdentityUniversalAuthsUpdate,
|
||||||
TIncidentContacts,
|
TIncidentContacts,
|
||||||
TIncidentContactsInsert,
|
TIncidentContactsInsert,
|
||||||
TIncidentContactsUpdate,
|
TIncidentContactsUpdate,
|
||||||
@@ -208,6 +226,37 @@ declare module "knex/types/tables" {
|
|||||||
TIntegrationAuthsInsert,
|
TIntegrationAuthsInsert,
|
||||||
TIntegrationAuthsUpdate
|
TIntegrationAuthsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.Identity]: Knex.CompositeTableType<
|
||||||
|
TIdentities,
|
||||||
|
TIdentitiesInsert,
|
||||||
|
TIdentitiesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.IdentityUniversalAuth]: Knex.CompositeTableType<
|
||||||
|
TIdentityUniversalAuths,
|
||||||
|
TIdentityUniversalAuthsInsert,
|
||||||
|
TIdentityUniversalAuthsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
|
||||||
|
TIdentityUaClientSecrets,
|
||||||
|
TIdentityUaClientSecretsInsert,
|
||||||
|
TIdentityUaClientSecretsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.IdentityAccessToken]: Knex.CompositeTableType<
|
||||||
|
TIdentityAccessTokens,
|
||||||
|
TIdentityAccessTokensInsert,
|
||||||
|
TIdentityAccessTokensUpdate
|
||||||
|
>;
|
||||||
|
[TableName.IdentityOrgMembership]: Knex.CompositeTableType<
|
||||||
|
TIdentityOrgMemberships,
|
||||||
|
TIdentityOrgMembershipsInsert,
|
||||||
|
TIdentityOrgMembershipsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.IdentityProjectMembership]: Knex.CompositeTableType<
|
||||||
|
TIdentityProjectMemberships,
|
||||||
|
TIdentityProjectMembershipsInsert,
|
||||||
|
TIdentityProjectMembershipsUpdate
|
||||||
|
>;
|
||||||
|
// Junction tables
|
||||||
[TableName.JnSecretTag]: Knex.CompositeTableType<
|
[TableName.JnSecretTag]: Knex.CompositeTableType<
|
||||||
TSecretTagJunction,
|
TSecretTagJunction,
|
||||||
TSecretTagJunctionInsert,
|
TSecretTagJunctionInsert,
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.Identity))) {
|
||||||
|
await knex.schema.createTable(TableName.Identity, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.string("authMethod");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.Identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.Identity);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.Identity);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.IdentityUniversalAuth))) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityUniversalAuth, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("clientId").notNullable();
|
||||||
|
t.integer("accessTokenTTL").defaultTo(7200).notNullable();
|
||||||
|
t.integer("accessTokenMaxTTL").defaultTo(7200).notNullable();
|
||||||
|
t.integer("accessTokenNumUsesLimit").defaultTo(0).notNullable();
|
||||||
|
t.jsonb("clientSecretTrustedIps").notNullable();
|
||||||
|
t.jsonb("accessTokenTrustedIps").notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("identityId").notNullable().unique();
|
||||||
|
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!(await knex.schema.hasTable(TableName.IdentityUaClientSecret))) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityUaClientSecret, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("description").notNullable();
|
||||||
|
t.string("clientSecretPrefix").notNullable();
|
||||||
|
t.string("clientSecretHash").notNullable();
|
||||||
|
t.datetime("clientSecretLastUsedAt");
|
||||||
|
t.integer("clientSecretNumUses").defaultTo(0).notNullable();
|
||||||
|
t.integer("clientSecretNumUsesLimit").defaultTo(0).notNullable();
|
||||||
|
t.integer("clientSecretTTL").defaultTo(0).notNullable();
|
||||||
|
t.boolean("isClientSecretRevoked").defaultTo(false).notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("identityUAId").notNullable();
|
||||||
|
t.foreign("identityUAId")
|
||||||
|
.references("id")
|
||||||
|
.inTable(TableName.IdentityUniversalAuth)
|
||||||
|
.onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.IdentityUniversalAuth);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.IdentityUaClientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityUaClientSecret);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityUniversalAuth);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.IdentityUaClientSecret);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.IdentityUniversalAuth);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.IdentityAccessToken))) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityAccessToken, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("authType").notNullable();
|
||||||
|
t.integer("accessTokenTTL").defaultTo(2592000).notNullable(); // 30 days second
|
||||||
|
t.integer("accessTokenMaxTTL").defaultTo(2592000).notNullable();
|
||||||
|
t.integer("accessTokenNumUses").defaultTo(0).notNullable();
|
||||||
|
t.integer("accessTokenNumUsesLimit").defaultTo(0).notNullable();
|
||||||
|
t.datetime("accessTokenLastUsedAt");
|
||||||
|
t.datetime("accessTokenLastRenewedAt");
|
||||||
|
t.boolean("isAccessTokenRevoked").defaultTo(false).notNullable();
|
||||||
|
t.uuid("identityUAClientSecretId");
|
||||||
|
t.foreign("identityUAClientSecretId")
|
||||||
|
.references("id")
|
||||||
|
.inTable(TableName.IdentityUaClientSecret)
|
||||||
|
.onDelete("CASCADE");
|
||||||
|
t.uuid("identityId").notNullable();
|
||||||
|
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.IdentityAccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityAccessToken);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.IdentityAccessToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.IdentityOrgMembership))) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityOrgMembership, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("role").notNullable();
|
||||||
|
t.uuid("roleId");
|
||||||
|
t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("identityId").notNullable();
|
||||||
|
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.IdentityOrgMembership);
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.IdentityProjectMembership))) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityProjectMembership, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("role").notNullable();
|
||||||
|
t.uuid("roleId");
|
||||||
|
t.foreign("roleId").references("id").inTable(TableName.ProjectRoles);
|
||||||
|
t.uuid("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.uuid("identityId").notNullable();
|
||||||
|
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.IdentityProjectMembership);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityOrgMembership);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityProjectMembership);
|
||||||
|
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.IdentityProjectMembership);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.IdentityOrgMembership);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IdentitiesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
authMethod: z.string().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentities = z.infer<typeof IdentitiesSchema>;
|
||||||
|
export type TIdentitiesInsert = Omit<TIdentities, TImmutableDBKeys>;
|
||||||
|
export type TIdentitiesUpdate = Partial<Omit<TIdentities, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IdentityAccessTokensSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
authType: z.string(),
|
||||||
|
accessTokenTTL: z.number().default(2592000),
|
||||||
|
accessTokenMaxTTL: z.number().default(2592000),
|
||||||
|
accessTokenNumUses: z.number().default(0),
|
||||||
|
accessTokenNumUsesLimit: z.number().default(0),
|
||||||
|
accessTokenLastUsedAt: z.date().nullable().optional(),
|
||||||
|
accessTokenLastRenewedAt: z.date().nullable().optional(),
|
||||||
|
isAccessTokenRevoked: z.boolean().default(false),
|
||||||
|
identityUAClientSecretId: z.string().uuid().nullable().optional(),
|
||||||
|
identityId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>;
|
||||||
|
export type TIdentityAccessTokensInsert = Omit<TIdentityAccessTokens, TImmutableDBKeys>;
|
||||||
|
export type TIdentityAccessTokensUpdate = Partial<Omit<TIdentityAccessTokens, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IdentityOrgMembershipsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
role: z.string(),
|
||||||
|
roleId: z.string().uuid().nullable().optional(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
identityId: z.string().uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentityOrgMemberships = z.infer<typeof IdentityOrgMembershipsSchema>;
|
||||||
|
export type TIdentityOrgMembershipsInsert = Omit<TIdentityOrgMemberships, TImmutableDBKeys>;
|
||||||
|
export type TIdentityOrgMembershipsUpdate = Partial<Omit<TIdentityOrgMemberships, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IdentityProjectMembershipsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
role: z.string(),
|
||||||
|
roleId: z.string().uuid().nullable().optional(),
|
||||||
|
projectId: z.string().uuid(),
|
||||||
|
identityId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentityProjectMemberships = z.infer<typeof IdentityProjectMembershipsSchema>;
|
||||||
|
export type TIdentityProjectMembershipsInsert = Omit<TIdentityProjectMemberships, TImmutableDBKeys>;
|
||||||
|
export type TIdentityProjectMembershipsUpdate = Partial<Omit<TIdentityProjectMemberships, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IdentityUaClientSecretsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
clientSecretPrefix: z.string(),
|
||||||
|
clientSecretHash: z.string(),
|
||||||
|
clientSecretLastUsedAt: z.date().nullable().optional(),
|
||||||
|
clientSecretNumUses: z.number().default(0),
|
||||||
|
clientSecretNumUsesLimit: z.number().default(0),
|
||||||
|
clientSecretTTL: z.number().default(0),
|
||||||
|
isClientSecretRevoked: z.boolean().default(false),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
identityUAId: z.string().uuid()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentityUaClientSecrets = z.infer<typeof IdentityUaClientSecretsSchema>;
|
||||||
|
export type TIdentityUaClientSecretsInsert = Omit<TIdentityUaClientSecrets, TImmutableDBKeys>;
|
||||||
|
export type TIdentityUaClientSecretsUpdate = Partial<
|
||||||
|
Omit<TIdentityUaClientSecrets, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IdentityUniversalAuthsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
clientId: z.string(),
|
||||||
|
accessTokenTTL: z.number().default(7200),
|
||||||
|
accessTokenMaxTTL: z.number().default(7200),
|
||||||
|
accessTokenNumUsesLimit: z.number().default(0),
|
||||||
|
clientSecretTrustedIps: z.unknown(),
|
||||||
|
accessTokenTrustedIps: z.unknown(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
identityId: z.string().uuid()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentityUniversalAuths = z.infer<typeof IdentityUniversalAuthsSchema>;
|
||||||
|
export type TIdentityUniversalAuthsInsert = Omit<TIdentityUniversalAuths, TImmutableDBKeys>;
|
||||||
|
export type TIdentityUniversalAuthsUpdate = Partial<
|
||||||
|
Omit<TIdentityUniversalAuths, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -2,6 +2,12 @@ export * from "./api-keys";
|
|||||||
export * from "./auth-token-sessions";
|
export * from "./auth-token-sessions";
|
||||||
export * from "./auth-tokens";
|
export * from "./auth-tokens";
|
||||||
export * from "./backup-private-key";
|
export * from "./backup-private-key";
|
||||||
|
export * from "./identities";
|
||||||
|
export * from "./identity-access-tokens";
|
||||||
|
export * from "./identity-org-memberships";
|
||||||
|
export * from "./identity-project-memberships";
|
||||||
|
export * from "./identity-ua-client-secrets";
|
||||||
|
export * from "./identity-universal-auths";
|
||||||
export * from "./incident-contacts";
|
export * from "./incident-contacts";
|
||||||
export * from "./integration-auths";
|
export * from "./integration-auths";
|
||||||
export * from "./integrations";
|
export * from "./integrations";
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ export enum TableName {
|
|||||||
IntegrationAuth = "integration_auths",
|
IntegrationAuth = "integration_auths",
|
||||||
ServiceToken = "service_tokens",
|
ServiceToken = "service_tokens",
|
||||||
Webhook = "webhooks",
|
Webhook = "webhooks",
|
||||||
|
Identity = "identities",
|
||||||
|
IdentityAccessToken = "identity_access_tokens",
|
||||||
|
IdentityUniversalAuth = "identity_universal_auths",
|
||||||
|
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||||
|
IdentityOrgMembership = "identity_org_memberships",
|
||||||
|
IdentityProjectMembership = " identity_project_memberships",
|
||||||
JnSecretTag = "secret_tag_junction",
|
JnSecretTag = "secret_tag_junction",
|
||||||
JnSecretVersionTag = "secret_version_tag_junction"
|
JnSecretVersionTag = "secret_version_tag_junction"
|
||||||
}
|
}
|
||||||
@@ -53,6 +59,7 @@ export const ServiceTokenScopes = z
|
|||||||
export enum OrgMembershipRole {
|
export enum OrgMembershipRole {
|
||||||
Admin = "admin",
|
Admin = "admin",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
|
NoAccess = "no-access",
|
||||||
Custom = "custom"
|
Custom = "custom"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,3 +90,7 @@ export enum SecretType {
|
|||||||
Shared = "shared",
|
Shared = "shared",
|
||||||
Personal = "personal"
|
Personal = "personal"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum IdentityAuthMethod {
|
||||||
|
Univeral = "universal"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +1,6 @@
|
|||||||
import {
|
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
|
||||||
AbilityBuilder,
|
|
||||||
buildMongoQueryMatcher,
|
|
||||||
createMongoAbility,
|
|
||||||
MongoAbility
|
|
||||||
} from "@casl/ability";
|
|
||||||
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
|
||||||
import picomatch from "picomatch";
|
|
||||||
|
|
||||||
const $glob: FieldInstruction<string> = {
|
import { conditionsMatcher } from "@app/lib/casl";
|
||||||
type: "field",
|
|
||||||
validate(instruction, value) {
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
throw new Error(`"${instruction.name}" expects value to be a string`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
|
||||||
const secretPath = context.get(object, node.field);
|
|
||||||
const permissionSecretGlobPath = node.value;
|
|
||||||
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
|
||||||
|
|
||||||
export enum OrgPermissionActions {
|
export enum OrgPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
@@ -39,7 +17,8 @@ export enum OrgPermissionSubjects {
|
|||||||
IncidentAccount = "incident-contact",
|
IncidentAccount = "incident-contact",
|
||||||
Sso = "sso",
|
Sso = "sso",
|
||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning"
|
SecretScanning = "secret-scanning",
|
||||||
|
Identity = "identity"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -51,7 +30,8 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing];
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
@@ -94,6 +74,11 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,7 +102,19 @@ const buildMemberPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orgMemberPermissions = buildMemberPermission();
|
export const orgMemberPermissions = buildMemberPermission();
|
||||||
|
|
||||||
|
const buildNoAccessPermission = () => {
|
||||||
|
const { build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
|
return build({ conditionsMatcher });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const orgNoAccessPermissions = buildNoAccessPermission();
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { unpackRules } from "@casl/ability/extra";
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
|
|
||||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
|
||||||
|
import { conditionsMatcher } from "@app/lib/casl";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TOrgRoleDalFactory } from "@app/services/org/org-role-dal";
|
||||||
|
import { TProjectRoleDalFactory } from "@app/services/project-role/project-role-dal";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
conditionsMatcher,
|
|
||||||
orgAdminPermissions,
|
orgAdminPermissions,
|
||||||
orgMemberPermissions,
|
orgMemberPermissions,
|
||||||
|
orgNoAccessPermissions,
|
||||||
OrgPermissionSet
|
OrgPermissionSet
|
||||||
} from "./org-permission";
|
} from "./org-permission";
|
||||||
import { TPermissionDalFactory } from "./permission-dal";
|
import { TPermissionDalFactory } from "./permission-dal";
|
||||||
@@ -21,12 +24,18 @@ import {
|
|||||||
} from "./project-permission";
|
} from "./project-permission";
|
||||||
|
|
||||||
type TPermissionServiceFactoryDep = {
|
type TPermissionServiceFactoryDep = {
|
||||||
|
orgRoleDal: Pick<TOrgRoleDalFactory, "findOne">;
|
||||||
|
projectRoleDal: Pick<TProjectRoleDalFactory, "findOne">;
|
||||||
permissionDal: TPermissionDalFactory;
|
permissionDal: TPermissionDalFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPermissionServiceFactory = ReturnType<typeof permissionServiceFactory>;
|
export type TPermissionServiceFactory = ReturnType<typeof permissionServiceFactory>;
|
||||||
|
|
||||||
export const permissionServiceFactory = ({ permissionDal }: TPermissionServiceFactoryDep) => {
|
export const permissionServiceFactory = ({
|
||||||
|
permissionDal,
|
||||||
|
orgRoleDal,
|
||||||
|
projectRoleDal
|
||||||
|
}: TPermissionServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Get user permission in an organization
|
* Get user permission in an organization
|
||||||
* */
|
* */
|
||||||
@@ -37,9 +46,14 @@ export const permissionServiceFactory = ({ permissionDal }: TPermissionServiceFa
|
|||||||
throw new BadRequestError({ name: "Custom permission not found" });
|
throw new BadRequestError({ name: "Custom permission not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (membership.role === "admin") return { permission: orgAdminPermissions, membership };
|
if (membership.role === OrgMembershipRole.Admin)
|
||||||
if (membership.role === "member") return { permission: orgMemberPermissions, membership };
|
return { permission: orgAdminPermissions, membership };
|
||||||
if (membership.role === "custom") {
|
if (membership.role === OrgMembershipRole.Member)
|
||||||
|
return { permission: orgMemberPermissions, membership };
|
||||||
|
|
||||||
|
if (membership.role === OrgMembershipRole.NoAccess)
|
||||||
|
return { permission: orgNoAccessPermissions, membership };
|
||||||
|
if (membership.role === OrgMembershipRole.Custom) {
|
||||||
const permission = createMongoAbility<OrgPermissionSet>(
|
const permission = createMongoAbility<OrgPermissionSet>(
|
||||||
// akhilmhdh: putting any due to ts incompatiable matching with string and the other
|
// akhilmhdh: putting any due to ts incompatiable matching with string and the other
|
||||||
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(membership.permissions as any),
|
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(membership.permissions as any),
|
||||||
@@ -57,7 +71,7 @@ export const permissionServiceFactory = ({ permissionDal }: TPermissionServiceFa
|
|||||||
const getUserProjectPermission = async (userId: string, projectId: string) => {
|
const getUserProjectPermission = async (userId: string, projectId: string) => {
|
||||||
const membership = await permissionDal.getProjectPermission(userId, projectId);
|
const membership = await permissionDal.getProjectPermission(userId, projectId);
|
||||||
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
|
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
|
||||||
if (membership.role === "custom" && !membership.permissions) {
|
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
|
||||||
throw new BadRequestError({ name: "Custom permission not found" });
|
throw new BadRequestError({ name: "Custom permission not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,10 +121,72 @@ export const permissionServiceFactory = ({ permissionDal }: TPermissionServiceFa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getOrgPermissionByRole = async (role: string, orgId: string) => {
|
||||||
|
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
||||||
|
if (isCustomRole) {
|
||||||
|
const orgRole = await orgRoleDal.findOne({ slug: role, orgId });
|
||||||
|
if (!orgRole) throw new BadRequestError({ message: "Role not found" });
|
||||||
|
return {
|
||||||
|
permission: createMongoAbility<OrgPermissionSet>(
|
||||||
|
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
|
||||||
|
(orgRole.permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]) || []
|
||||||
|
),
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
),
|
||||||
|
role: orgRole
|
||||||
|
};
|
||||||
|
}
|
||||||
|
switch (role) {
|
||||||
|
case OrgMembershipRole.Admin:
|
||||||
|
return { permission: orgAdminPermissions };
|
||||||
|
case OrgMembershipRole.Member:
|
||||||
|
return { permission: orgMemberPermissions };
|
||||||
|
case OrgMembershipRole.NoAccess:
|
||||||
|
return { permission: orgNoAccessPermissions };
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: "Org role not found" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectPermissionByRole = async (role: string, projectId: string) => {
|
||||||
|
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
||||||
|
if (isCustomRole) {
|
||||||
|
const projectRole = await projectRoleDal.findOne({ slug: role, projectId });
|
||||||
|
if (!projectRole) throw new BadRequestError({ message: "Role not found" });
|
||||||
|
return {
|
||||||
|
permission: createMongoAbility<ProjectPermissionSet>(
|
||||||
|
unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
|
||||||
|
(projectRole.permissions as PackRule<
|
||||||
|
RawRuleOf<MongoAbility<ProjectPermissionSet>>
|
||||||
|
>[]) || []
|
||||||
|
),
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
),
|
||||||
|
role: projectRole
|
||||||
|
};
|
||||||
|
}
|
||||||
|
switch (role) {
|
||||||
|
case ProjectMembershipRole.Admin:
|
||||||
|
return { permission: projectAdminPermissions };
|
||||||
|
case ProjectMembershipRole.Member:
|
||||||
|
return { permission: projectMemberPermissions };
|
||||||
|
case ProjectMembershipRole.NoAccess:
|
||||||
|
return { permission: projectNoAccessPermissions };
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: "Org role not found" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getUserOrgPermission,
|
getUserOrgPermission,
|
||||||
getOrgPermission,
|
getOrgPermission,
|
||||||
getUserProjectPermission,
|
getUserProjectPermission,
|
||||||
getProjectPermission
|
getProjectPermission,
|
||||||
|
getOrgPermissionByRole,
|
||||||
|
getProjectPermissionByRole
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +1,6 @@
|
|||||||
import {
|
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||||
AbilityBuilder,
|
|
||||||
buildMongoQueryMatcher,
|
|
||||||
createMongoAbility,
|
|
||||||
ForcedSubject,
|
|
||||||
MongoAbility} from "@casl/ability";
|
|
||||||
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
|
||||||
import picomatch from "picomatch";
|
|
||||||
|
|
||||||
const $glob: FieldInstruction<string> = {
|
import { conditionsMatcher } from "@app/lib/casl";
|
||||||
type: "field",
|
|
||||||
validate(instruction, value) {
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
throw new Error(`"${instruction.name}" expects value to be a string`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
|
||||||
const secretPath = context.get(object, node.field);
|
|
||||||
const permissionSecretGlobPath = node.value;
|
|
||||||
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
|
||||||
|
|
||||||
export enum ProjectPermissionActions {
|
export enum ProjectPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { buildMongoQueryMatcher, MongoAbility } from "@casl/ability";
|
||||||
|
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
||||||
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
|
const $glob: FieldInstruction<string> = {
|
||||||
|
type: "field",
|
||||||
|
validate(instruction, value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error(`"${instruction.name}" expects value to be a string`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
||||||
|
const secretPath = context.get(object, node.field);
|
||||||
|
const permissionSecretGlobPath = node.value;
|
||||||
|
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
||||||
|
*/
|
||||||
|
const extractPermissions = (ability: MongoAbility) =>
|
||||||
|
ability.rules.map((permission) => `${permission.action}_${permission.subject}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
||||||
|
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2: MongoAbility) => {
|
||||||
|
const set1 = new Set(extractPermissions(permissions1));
|
||||||
|
const set2 = new Set(extractPermissions(permissions2));
|
||||||
|
|
||||||
|
for (const perm of set2) {
|
||||||
|
if (!set1.has(perm)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return set1.size >= set2.size;
|
||||||
|
};
|
||||||
@@ -16,9 +16,21 @@ export class UnauthorizedError extends Error {
|
|||||||
|
|
||||||
error: unknown;
|
error: unknown;
|
||||||
|
|
||||||
constructor({ name, error, message }: { message?: string; name: string; error?: unknown }) {
|
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) {
|
||||||
super(message ?? "You are not allowed to access this resourve");
|
super(message ?? "You are not allowed to access this resourve");
|
||||||
this.name = name;
|
this.name = name || "UnauthorizedError";
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ForbiddenRequestError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
|
||||||
|
super(message ?? "You are not allowed to access this resourve");
|
||||||
|
this.name = name || "ForbideenError";
|
||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import net from "node:net";
|
||||||
|
|
||||||
|
import { UnauthorizedError } from "../errors";
|
||||||
|
|
||||||
|
export enum IPType {
|
||||||
|
IPV4 = "ipv4",
|
||||||
|
IPV6 = "ipv6"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return details of IP [ip]:
|
||||||
|
* - If [ip] is a specific IP address then return the IPv4/IPv6 address
|
||||||
|
* - If [ip] is a subnet then return the network IPv4/IPv6 address and prefix
|
||||||
|
*/
|
||||||
|
export const extractIPDetails = (ip: string) => {
|
||||||
|
if (net.isIPv4(ip))
|
||||||
|
return {
|
||||||
|
ipAddress: ip,
|
||||||
|
type: IPType.IPV4
|
||||||
|
};
|
||||||
|
|
||||||
|
if (net.isIPv6(ip))
|
||||||
|
return {
|
||||||
|
ipAddress: ip,
|
||||||
|
type: IPType.IPV6
|
||||||
|
};
|
||||||
|
|
||||||
|
const [ipNet, prefix] = ip.split("/");
|
||||||
|
|
||||||
|
let type;
|
||||||
|
switch (net.isIP(ipNet)) {
|
||||||
|
case 4:
|
||||||
|
type = IPType.IPV4;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
type = IPType.IPV6;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Failed to extract IP details");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ipAddress: ipNet,
|
||||||
|
type,
|
||||||
|
prefix: parseInt(prefix, 10)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given string is a valid CIDR block.
|
||||||
|
*
|
||||||
|
* The function checks if the input string is a valid IPv4 or IPv6 address in CIDR notation.
|
||||||
|
*
|
||||||
|
* CIDR notation includes a network address followed by a slash ('/') and a prefix length.
|
||||||
|
* For IPv4, the prefix length must be between 0 and 32. For IPv6, it must be between 0 and 128.
|
||||||
|
* If the input string is not a valid CIDR block, the function returns `false`.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const isValidCidr = (cidr: string): boolean => {
|
||||||
|
const [ip, prefix] = cidr.split("/");
|
||||||
|
|
||||||
|
const prefixNum = parseInt(prefix, 10);
|
||||||
|
|
||||||
|
// ensure prefix exists and is a number within the appropriate range for each IP version
|
||||||
|
if (
|
||||||
|
!prefix ||
|
||||||
|
Number.isNaN(prefixNum) ||
|
||||||
|
(net.isIPv4(ip) && (prefixNum < 0 || prefixNum > 32)) ||
|
||||||
|
(net.isIPv6(ip) && (prefixNum < 0 || prefixNum > 128))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the IP portion of the CIDR block is a valid IPv4 or IPv6 address
|
||||||
|
if (!net.isIPv4(ip) && !net.isIPv6(ip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given string is a valid IPv4/IPv6 address or a valid CIDR block.
|
||||||
|
*
|
||||||
|
* If the string contains a slash ('/'), it treats the input as a CIDR block and checks its validity.
|
||||||
|
* Otherwise, it treats the string as a standalone IP address (either IPv4 or IPv6) and checks its validity.
|
||||||
|
*
|
||||||
|
* @param {string} input - The string to be checked. It could be an IP address or a CIDR block.
|
||||||
|
* @returns {boolean} Returns `true` if the string is a valid IP address (either IPv4 or IPv6) or a valid CIDR block, `false` otherwise.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const isValidIpOrCidr = (ip: string): boolean => {
|
||||||
|
// if the string contains a slash, treat it as a CIDR block
|
||||||
|
if (ip.includes("/")) {
|
||||||
|
return isValidCidr(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, treat it as a standalone IP address
|
||||||
|
if (net.isIPv4(ip) || net.isIPv6(ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the IP address [ipAddress] against the trusted IPs [trustedIps].
|
||||||
|
*/
|
||||||
|
export const checkIPAgainstBlocklist = ({
|
||||||
|
ipAddress,
|
||||||
|
trustedIps
|
||||||
|
}: {
|
||||||
|
ipAddress: string;
|
||||||
|
trustedIps: {
|
||||||
|
ipAddress: string;
|
||||||
|
type: IPType;
|
||||||
|
prefix: number;
|
||||||
|
}[];
|
||||||
|
}) => {
|
||||||
|
const blockList = new net.BlockList();
|
||||||
|
|
||||||
|
for (const trustedIp of trustedIps) {
|
||||||
|
if (trustedIp.prefix !== undefined) {
|
||||||
|
blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type);
|
||||||
|
} else {
|
||||||
|
blockList.addAddress(trustedIp.ipAddress, trustedIp.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type } = extractIPDetails(ipAddress);
|
||||||
|
const check = blockList.check(ipAddress, type);
|
||||||
|
|
||||||
|
if (!check)
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Failed to authenticate"
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export type TOrgPermission = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TProjectPermission = {
|
export type TProjectPermission = {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const registerRoutes = async (
|
|||||||
const permissionDal = permissionDalFactory(db);
|
const permissionDal = permissionDalFactory(db);
|
||||||
|
|
||||||
// ee services
|
// ee services
|
||||||
const permissionService = permissionServiceFactory({ permissionDal });
|
const permissionService = permissionServiceFactory({ permissionDal, orgRoleDal, projectRoleDal });
|
||||||
|
|
||||||
// service layers
|
// service layers
|
||||||
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
|
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export enum AuthTokenType {
|
|||||||
PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim
|
PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim
|
||||||
API_KEY = "apiKey",
|
API_KEY = "apiKey",
|
||||||
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||||
SERVICE_REFRESH_TOKEN = "serviceRefreshToken"
|
SERVICE_REFRESH_TOKEN = "serviceRefreshToken",
|
||||||
|
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AuthMode {
|
export enum AuthMode {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TIdentityAccessTokenDalFactory = ReturnType<typeof identityAccessTokenDalFactory>;
|
||||||
|
|
||||||
|
export const identityAccessTokenDalFactory = (db: TDbClient) => {
|
||||||
|
const identityAccessTokenOrm = ormify(db, TableName.IdentityAccessToken);
|
||||||
|
return identityAccessTokenOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { AuthTokenType } from "../auth/auth-type";
|
||||||
|
import { TIdentityAccessTokenDalFactory } from "./identity-access-token-dal";
|
||||||
|
import { TRenewAccessTokenDTO } from "./identity-access-token-types";
|
||||||
|
|
||||||
|
type TIdentityAccessTokenServiceFactoryDep = {
|
||||||
|
identityAccessTokenDal: TIdentityAccessTokenDalFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIdentityAccessTokenServiceFactory = ReturnType<
|
||||||
|
typeof identityAccessTokenServiceFactory
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const identityAccessTokenServiceFactory = ({
|
||||||
|
identityAccessTokenDal
|
||||||
|
}: TIdentityAccessTokenServiceFactoryDep) => {
|
||||||
|
const renewAccessToken = async ({ accessToken }: TRenewAccessTokenDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(accessToken, appCfg.JWT_AUTH_SECRET) as JwtPayload;
|
||||||
|
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN)
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const identityAccessToken = await identityAccessTokenDal.findOne({
|
||||||
|
id: decodedToken.identityAccessTokenId,
|
||||||
|
isAccessTokenRevoked: false
|
||||||
|
});
|
||||||
|
if (!identityAccessToken) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const {
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenLastRenewedAt,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
createdAt: accessTokenCreatedAt
|
||||||
|
} = identityAccessToken;
|
||||||
|
// ttl check
|
||||||
|
if (accessTokenTTL > 0) {
|
||||||
|
const currentDate = new Date();
|
||||||
|
if (accessTokenLastRenewedAt) {
|
||||||
|
// access token has been renewed
|
||||||
|
const accessTokenRenewed = new Date(accessTokenLastRenewedAt);
|
||||||
|
const ttlInMilliseconds = accessTokenTTL * 1000;
|
||||||
|
const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
|
if (currentDate > expirationDate)
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Failed to renew MI access token due to TTL expiration"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// access token has never been renewed
|
||||||
|
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
||||||
|
const ttlInMilliseconds = accessTokenTTL * 1000;
|
||||||
|
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
|
if (currentDate > expirationDate)
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Failed to renew MI access token due to TTL expiration"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// max ttl checks
|
||||||
|
if (accessTokenMaxTTL > 0) {
|
||||||
|
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
||||||
|
const ttlInMilliseconds = accessTokenMaxTTL * 1000;
|
||||||
|
const currentDate = new Date();
|
||||||
|
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
|
if (currentDate > expirationDate)
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Failed to renew MI access token due to Max TTL expiration"
|
||||||
|
});
|
||||||
|
|
||||||
|
const extendToDate = new Date(currentDate.getTime() + accessTokenTTL);
|
||||||
|
if (extendToDate > expirationDate)
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Failed to renew MI access token past its Max TTL expiration"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await identityAccessTokenDal.updateById(identityAccessToken.id, {
|
||||||
|
accessTokenLastRenewedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
return identityAccessTokenDal;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { renewAccessToken };
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type TRenewAccessTokenDTO = {
|
||||||
|
accessToken: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TIdentityProjectDalFactory = ReturnType<typeof identityProjectDalFactory>;
|
||||||
|
|
||||||
|
export const identityProjectDalFactory = (db: TDbClient) => {
|
||||||
|
const identityProjectOrm = ormify(db, TableName.IdentityProjectMembership);
|
||||||
|
return identityProjectOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole, TProjectRoles } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TIdentityOrgDalFactory } from "../identity/identity-org-dal";
|
||||||
|
import { TProjectDalFactory } from "../project/project-dal";
|
||||||
|
import { TIdentityProjectDalFactory } from "./identity-project-dal";
|
||||||
|
import {
|
||||||
|
TCreateProjectIdentityDTO,
|
||||||
|
TDeleteProjectIdentityDTO,
|
||||||
|
TListProjectIdentityDTO,
|
||||||
|
TUpdateProjectIdentityDTO
|
||||||
|
} from "./identity-project-types";
|
||||||
|
|
||||||
|
type TIdentityProjectServiceFactoryDep = {
|
||||||
|
identityProjectDal: TIdentityProjectDalFactory;
|
||||||
|
projectDal: Pick<TProjectDalFactory, "findById">;
|
||||||
|
identityOrgMembershipDal: Pick<TIdentityOrgDalFactory, "findOne">;
|
||||||
|
permissionService: Pick<
|
||||||
|
TPermissionServiceFactory,
|
||||||
|
"getProjectPermission" | "getProjectPermissionByRole"
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIdentityProjectServiceFactory = ReturnType<typeof identityProjectServiceFactory>;
|
||||||
|
|
||||||
|
export const identityProjectServiceFactory = ({
|
||||||
|
identityProjectDal,
|
||||||
|
permissionService,
|
||||||
|
identityOrgMembershipDal,
|
||||||
|
projectDal
|
||||||
|
}: TIdentityProjectServiceFactoryDep) => {
|
||||||
|
const createProjectIdentity = async ({
|
||||||
|
identityId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
role
|
||||||
|
}: TCreateProjectIdentityDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.Identity
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingIdentity = await identityProjectDal.findOne({ identityId, projectId });
|
||||||
|
if (existingIdentity)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Identity with id ${identityId} already exists in project with id ${projectId}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const project = await projectDal.findById(projectId);
|
||||||
|
const identityOrgMembership = await identityOrgMembershipDal.findOne({
|
||||||
|
identityId,
|
||||||
|
orgId: project.orgId
|
||||||
|
});
|
||||||
|
if (!identityOrgMembership)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to find identity with id ${identityId}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission: rolePermission, role: customRole } =
|
||||||
|
await permissionService.getProjectPermissionByRole(role, project.id);
|
||||||
|
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPriviledge)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add identity to project with more privileged role"
|
||||||
|
});
|
||||||
|
const isCustomRole = Boolean(customRole);
|
||||||
|
|
||||||
|
const projectIdentity = await identityProjectDal.create({
|
||||||
|
identityId,
|
||||||
|
projectId: project.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : role,
|
||||||
|
roleId: customRole?.id
|
||||||
|
});
|
||||||
|
return projectIdentity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProjectIdentity = async ({
|
||||||
|
projectId,
|
||||||
|
identityId,
|
||||||
|
role,
|
||||||
|
actor,
|
||||||
|
actorId
|
||||||
|
}: TUpdateProjectIdentityDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.Identity
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectIdentity = await identityProjectDal.findOne({ identityId, projectId });
|
||||||
|
if (!projectIdentity)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Identity with id ${identityId} doesn't exists in project with id ${projectId}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
projectIdentity.identityId,
|
||||||
|
projectIdentity.projectId
|
||||||
|
);
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||||
|
if (!hasRequiredPriviledges)
|
||||||
|
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
||||||
|
|
||||||
|
let customRole: TProjectRoles | undefined;
|
||||||
|
if (role) {
|
||||||
|
const { permission: rolePermission, role: customOrgRole } =
|
||||||
|
await permissionService.getProjectPermissionByRole(role, projectIdentity.projectId);
|
||||||
|
|
||||||
|
const isCustomRole = Boolean(customOrgRole);
|
||||||
|
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasRequiredNewRolePermission)
|
||||||
|
throw new BadRequestError({ message: "Failed to create a more privileged identity" });
|
||||||
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updatedProjectIdentity] = await identityProjectDal.update(
|
||||||
|
{ projectId, identityId: projectIdentity.identityId },
|
||||||
|
{
|
||||||
|
role: customRole ? ProjectMembershipRole.Custom : role,
|
||||||
|
roleId: customRole ? customRole.id : null
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return updatedProjectIdentity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProjectIdentity = async ({
|
||||||
|
identityId,
|
||||||
|
actorId,
|
||||||
|
actor
|
||||||
|
}: TDeleteProjectIdentityDTO) => {
|
||||||
|
const identityProjectMembership = await identityProjectDal.findById(identityId);
|
||||||
|
if (!identityProjectMembership)
|
||||||
|
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityProjectMembership.projectId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.Identity
|
||||||
|
);
|
||||||
|
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
identityId,
|
||||||
|
identityProjectMembership.projectId
|
||||||
|
);
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||||
|
if (!hasRequiredPriviledges)
|
||||||
|
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
||||||
|
|
||||||
|
const [deletedIdentity] = await identityProjectDal.delete({ identityId });
|
||||||
|
return deletedIdentity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listProjectIdentities = async ({ projectId, actor, actorId }: TListProjectIdentityDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.Identity
|
||||||
|
);
|
||||||
|
|
||||||
|
const identityMemberhips = await identityProjectDal.find({ projectId });
|
||||||
|
return identityMemberhips;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createProjectIdentity,
|
||||||
|
updateProjectIdentity,
|
||||||
|
deleteProjectIdentity,
|
||||||
|
listProjectIdentities
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateProjectIdentityDTO = {
|
||||||
|
identityId: string;
|
||||||
|
role: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateProjectIdentityDTO = {
|
||||||
|
role: string;
|
||||||
|
identityId: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TDeleteProjectIdentityDTO = {
|
||||||
|
identityId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListProjectIdentityDTO = TProjectPermission;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TIdentityDalFactory = ReturnType<typeof identityDalFactory>;
|
||||||
|
|
||||||
|
export const identityDalFactory = (db: TDbClient) => {
|
||||||
|
const identityOrm = ormify(db, TableName.Identity);
|
||||||
|
return identityOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName,TIdentityOrgMemberships } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TIdentityOrgDalFactory = ReturnType<typeof identityOrgDalFactory>;
|
||||||
|
|
||||||
|
export const identityOrgDalFactory = (db: TDbClient) => {
|
||||||
|
const identityOrgOrm = ormify(db, TableName.IdentityOrgMembership);
|
||||||
|
|
||||||
|
const findOne = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const [data] = await (tx || db)(TableName.IdentityOrgMembership)
|
||||||
|
.where(filter)
|
||||||
|
.join(
|
||||||
|
TableName.Identity,
|
||||||
|
`${TableName.IdentityOrgMembership}.identityId`,
|
||||||
|
`${TableName.Identity}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
||||||
|
.select(db.ref("name").withSchema(TableName.Identity))
|
||||||
|
.select(db.ref("authMethod").withSchema(TableName.Identity));
|
||||||
|
if (data) {
|
||||||
|
const { name, authMethod } = data;
|
||||||
|
return { ...data, identity: { id: data.identityId, name, authMethod } };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindOne" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...identityOrgOrm, findOne };
|
||||||
|
};
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TIdentityDalFactory } from "./identity-dal";
|
||||||
|
import { TIdentityOrgDalFactory } from "./identity-org-dal";
|
||||||
|
import { TCreateIdentityDTO, TDeleteIdentityDTO, TUpdateIdentityDTO } from "./identity-types";
|
||||||
|
|
||||||
|
type TIdentityServiceFactoryDep = {
|
||||||
|
identityDal: TIdentityDalFactory;
|
||||||
|
identityOrgDal: TIdentityOrgDalFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
|
||||||
|
|
||||||
|
export const identityServiceFactory = ({
|
||||||
|
identityDal,
|
||||||
|
identityOrgDal,
|
||||||
|
permissionService
|
||||||
|
}: TIdentityServiceFactoryDep) => {
|
||||||
|
const createIdentity = async ({ name, role, actor, orgId, actorId }: TCreateIdentityDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Create,
|
||||||
|
OrgPermissionSubjects.Identity
|
||||||
|
);
|
||||||
|
|
||||||
|
const { permission: rolePermission, role: customRole } =
|
||||||
|
await permissionService.getOrgPermissionByRole(role, orgId);
|
||||||
|
const isCustomRole = Boolean(customRole);
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasRequiredPriviledges)
|
||||||
|
throw new BadRequestError({ message: "Failed to create a more privileged identity" });
|
||||||
|
|
||||||
|
const identity = await identityDal.transaction(async (tx) => {
|
||||||
|
const newIdentity = await identityDal.create({ name }, tx);
|
||||||
|
await identityOrgDal.create(
|
||||||
|
{
|
||||||
|
identityId: newIdentity.id,
|
||||||
|
orgId,
|
||||||
|
role: isCustomRole ? OrgMembershipRole.Custom : role,
|
||||||
|
roleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return newIdentity;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(akhilmhdh-pg): add audit log here
|
||||||
|
return identity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateIdentity = async ({ id, role, name, actor, actorId }: TUpdateIdentityDTO) => {
|
||||||
|
const identityOrgMembership = await identityOrgDal.findById(id);
|
||||||
|
if (!identityOrgMembership)
|
||||||
|
throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityOrgMembership.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Edit,
|
||||||
|
OrgPermissionSubjects.Identity
|
||||||
|
);
|
||||||
|
|
||||||
|
const { permission: identityRolePermission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
id,
|
||||||
|
identityOrgMembership.orgId
|
||||||
|
);
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||||
|
if (!hasRequiredPriviledges)
|
||||||
|
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
||||||
|
|
||||||
|
let customRole: TOrgRoles | undefined;
|
||||||
|
if (role) {
|
||||||
|
const { permission: rolePermission, role: customOrgRole } =
|
||||||
|
await permissionService.getOrgPermissionByRole(role, identityOrgMembership.orgId);
|
||||||
|
|
||||||
|
const isCustomRole = Boolean(customOrgRole);
|
||||||
|
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasRequiredNewRolePermission)
|
||||||
|
throw new BadRequestError({ message: "Failed to create a more privileged identity" });
|
||||||
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = await identityDal.transaction(async (tx) => {
|
||||||
|
const newIdentity = await identityDal.updateById(id, { name }, tx);
|
||||||
|
if (role) {
|
||||||
|
await identityOrgDal.update(
|
||||||
|
{ identityId: id },
|
||||||
|
{
|
||||||
|
role: customRole ? OrgMembershipRole.Custom : role,
|
||||||
|
roleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return newIdentity;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(akhilmhdh-pg): add audit log here
|
||||||
|
return identity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteIdentity = async ({ actorId, actor, id }: TDeleteIdentityDTO) => {
|
||||||
|
const identityOrgMembership = await identityOrgDal.findById(id);
|
||||||
|
if (!identityOrgMembership)
|
||||||
|
throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityOrgMembership.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Delete,
|
||||||
|
OrgPermissionSubjects.Identity
|
||||||
|
);
|
||||||
|
const { permission: identityRolePermission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
id,
|
||||||
|
identityOrgMembership.orgId
|
||||||
|
);
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||||
|
if (!hasRequiredPriviledges)
|
||||||
|
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
||||||
|
|
||||||
|
const deletedIdentity = await identityDal.deleteById(id);
|
||||||
|
return deletedIdentity;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createIdentity,
|
||||||
|
updateIdentity,
|
||||||
|
deleteIdentity
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateIdentityDTO = {
|
||||||
|
role: string;
|
||||||
|
name: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TUpdateIdentityDTO = {
|
||||||
|
id: string;
|
||||||
|
role: string;
|
||||||
|
name: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TDeleteIdentityDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TUaClientSecretDalFactory = ReturnType<typeof uaClientSecretDalFactory>;
|
||||||
|
|
||||||
|
export const uaClientSecretDalFactory = (db: TDbClient) => {
|
||||||
|
const uaClientSecretOrm = ormify(db, TableName.IdentityUaClientSecret);
|
||||||
|
|
||||||
|
const incrementUsage = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const [doc] = await (tx || db)(TableName.IdentityUaClientSecret)
|
||||||
|
.where({ id })
|
||||||
|
.update({ clientSecretLastUsedAt: new Date() })
|
||||||
|
.increment("clientSecretNumUses", 1)
|
||||||
|
.returning("*");
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "IncrementUsage" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...uaClientSecretOrm, incrementUsage };
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TUniversalAuthDalFactory = ReturnType<typeof universalAuthDalFactory>;
|
||||||
|
|
||||||
|
export const universalAuthDalFactory = (db: TDbClient) => {
|
||||||
|
const universalAuthOrm = ormify(db, TableName.IdentityUniversalAuth);
|
||||||
|
|
||||||
|
return universalAuthOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
|
||||||
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
|
import { TIdentityDalFactory } from "../identity/identity-dal";
|
||||||
|
import { TIdentityOrgDalFactory } from "../identity/identity-org-dal";
|
||||||
|
import { TIdentityAccessTokenDalFactory } from "../identity-access-token/identity-access-token-dal";
|
||||||
|
import { TUaClientSecretDalFactory } from "./ua-client-secret-dal";
|
||||||
|
import { TUniversalAuthDalFactory } from "./universal-auth-dal";
|
||||||
|
import {
|
||||||
|
TAttachUaDTO,
|
||||||
|
TCreateUaClientSecretDTO,
|
||||||
|
TGetUaClientSecretsDTO,
|
||||||
|
TGetUaDTO,
|
||||||
|
TRevokeUaClientSecretDTO,
|
||||||
|
TUpdateUaDTO
|
||||||
|
} from "./universal-auth-types";
|
||||||
|
|
||||||
|
type TUniversalAuthServiceFactoryDep = {
|
||||||
|
universalAuthDal: TUniversalAuthDalFactory;
|
||||||
|
uaClientSecretDal: TUaClientSecretDalFactory;
|
||||||
|
identityAccessTokenDal: TIdentityAccessTokenDalFactory;
|
||||||
|
identityOrgDal: TIdentityOrgDalFactory;
|
||||||
|
identityDal: Pick<TIdentityDalFactory, "updateById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUniversalAuthServiceFactory = ReturnType<typeof universalAuthServiceFactory>;
|
||||||
|
|
||||||
|
export const universalAuthServiceFactory = ({
|
||||||
|
universalAuthDal,
|
||||||
|
uaClientSecretDal,
|
||||||
|
identityAccessTokenDal,
|
||||||
|
identityOrgDal,
|
||||||
|
identityDal,
|
||||||
|
permissionService
|
||||||
|
}: TUniversalAuthServiceFactoryDep) => {
|
||||||
|
const login = async (clientId: string, clientSecret: string) => {
|
||||||
|
const identityUa = await universalAuthDal.findOne({ clientId });
|
||||||
|
if (!identityUa) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
// TODO(akhilmhdh-pg): add ip checking
|
||||||
|
const clientSecrtInfo = await uaClientSecretDal.find({
|
||||||
|
identityUAId: identityUa.id,
|
||||||
|
isClientSecretRevoked: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) =>
|
||||||
|
bcrypt.compareSync(clientSecret, clientSecretHash)
|
||||||
|
);
|
||||||
|
if (!validClientSecretInfo) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } =
|
||||||
|
validClientSecretInfo;
|
||||||
|
if (clientSecretTTL > 0) {
|
||||||
|
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
||||||
|
const ttlInMilliseconds = clientSecretTTL * 1000;
|
||||||
|
const currentDate = new Date();
|
||||||
|
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
|
if (currentDate > expirationTime) {
|
||||||
|
await uaClientSecretDal.updateById(validClientSecretInfo.id, {
|
||||||
|
isClientSecretRevoked: true
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Failed to authenticate identity credentials due to expired client secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses === clientSecretNumUsesLimit) {
|
||||||
|
// number of times client secret can be used for
|
||||||
|
// a login operation reached
|
||||||
|
await uaClientSecretDal.updateById(validClientSecretInfo.id, { isClientSecretRevoked: true });
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message:
|
||||||
|
"Failed to authenticate identity credentials due to client secret number of uses limit reached"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityAccessToken = await universalAuthDal.transaction(async (tx) => {
|
||||||
|
const uaClientSecretDoc = await uaClientSecretDal.incrementUsage(
|
||||||
|
validClientSecretInfo.id,
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
const newToken = await identityAccessTokenDal.create(
|
||||||
|
{
|
||||||
|
identityId: identityUa.identityId,
|
||||||
|
authType: IdentityAuthMethod.Univeral,
|
||||||
|
isAccessTokenRevoked: false,
|
||||||
|
identityUAClientSecretId: uaClientSecretDoc.id,
|
||||||
|
accessTokenTTL: identityUa.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityUa.accessTokenMaxTTL,
|
||||||
|
accessTokenNumUses: 0,
|
||||||
|
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return newToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const accessToken = jwt.sign(
|
||||||
|
{
|
||||||
|
identityId: identityUa.identityId,
|
||||||
|
clientSecretId: validClientSecretInfo.id,
|
||||||
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
|
},
|
||||||
|
appCfg.JWT_AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn:
|
||||||
|
identityAccessToken.accessTokenMaxTTL === 0
|
||||||
|
? undefined
|
||||||
|
: identityAccessToken.accessTokenMaxTTL
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { accessToken, identityUa };
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachUa = async ({
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
identityId,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenTrustedIps,
|
||||||
|
clientSecretTrustedIps,
|
||||||
|
actorId,
|
||||||
|
actor
|
||||||
|
}: TAttachUaDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity.authMethod)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to add universal auth to already configured identity"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||||
|
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityMembershipOrg.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Create,
|
||||||
|
OrgPermissionSubjects.Identity
|
||||||
|
);
|
||||||
|
const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map(
|
||||||
|
(clientSecretTrustedIp) => {
|
||||||
|
// TODO(akhilmhdh-pg): add licence server here
|
||||||
|
if (/* !plan.ipAllowlisting && */ clientSecretTrustedIp.ipAddress !== "0.0.0.0/0")
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||||
|
});
|
||||||
|
if (!isValidIpOrCidr(clientSecretTrustedIp.ipAddress))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
|
});
|
||||||
|
return extractIPDetails(clientSecretTrustedIp.ipAddress);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||||
|
// TODO(akhilmhdh-pg): add licence server here
|
||||||
|
if (/* !plan.ipAllowlisting && */ accessTokenTrustedIp.ipAddress !== "0.0.0.0/0")
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||||
|
});
|
||||||
|
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
|
});
|
||||||
|
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityUa = await universalAuthDal.transaction(async (tx) => {
|
||||||
|
const doc = await universalAuthDal.create(
|
||||||
|
{
|
||||||
|
identityId: identityMembershipOrg.identityId,
|
||||||
|
clientId: crypto.randomUUID(),
|
||||||
|
clientSecretTrustedIps: JSON.stringify(reformattedClientSecretTrustedIps),
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await identityDal.updateById(
|
||||||
|
identityMembershipOrg.identityId,
|
||||||
|
{
|
||||||
|
authMethod: IdentityAuthMethod.Univeral
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
return identityUa;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUa = async ({
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
identityId,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenTrustedIps,
|
||||||
|
clientSecretTrustedIps,
|
||||||
|
actorId,
|
||||||
|
actor
|
||||||
|
}: TUpdateUaDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to updated universal auth"
|
||||||
|
});
|
||||||
|
|
||||||
|
const uaIdentityAuth = await universalAuthDal.findOne({ identityId });
|
||||||
|
|
||||||
|
if (
|
||||||
|
(accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) > 0 &&
|
||||||
|
(accessTokenTTL || uaIdentityAuth.accessTokenMaxTTL) >
|
||||||
|
(accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL)
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityMembershipOrg.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Edit,
|
||||||
|
OrgPermissionSubjects.Identity
|
||||||
|
);
|
||||||
|
const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map(
|
||||||
|
(clientSecretTrustedIp) => {
|
||||||
|
// TODO(akhilmhdh-pg): add licence server here
|
||||||
|
if (/* !plan.ipAllowlisting && */ clientSecretTrustedIp.ipAddress !== "0.0.0.0/0")
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||||
|
});
|
||||||
|
if (!isValidIpOrCidr(clientSecretTrustedIp.ipAddress))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
|
});
|
||||||
|
return extractIPDetails(clientSecretTrustedIp.ipAddress);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||||
|
// TODO(akhilmhdh-pg): add licence server here
|
||||||
|
if (/* !plan.ipAllowlisting && */ accessTokenTrustedIp.ipAddress !== "0.0.0.0/0")
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||||
|
});
|
||||||
|
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
|
});
|
||||||
|
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedUaAuth = await universalAuthDal.updateById(uaIdentityAuth.id, {
|
||||||
|
clientSecretTrustedIps: reformattedClientSecretTrustedIps
|
||||||
|
? JSON.stringify(reformattedClientSecretTrustedIps)
|
||||||
|
: undefined,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||||
|
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
return updatedUaAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIdentityUa = async ({ identityId, actorId, actor }: TGetUaDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have universal auth"
|
||||||
|
});
|
||||||
|
|
||||||
|
const uaIdentityAuth = await universalAuthDal.findOne({ identityId });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityMembershipOrg.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Read,
|
||||||
|
OrgPermissionSubjects.Identity
|
||||||
|
);
|
||||||
|
return uaIdentityAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUaClientSecret = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityId,
|
||||||
|
ttl,
|
||||||
|
description,
|
||||||
|
numUsesLimit
|
||||||
|
}: TCreateUaClientSecretDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have universal auth"
|
||||||
|
});
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityMembershipOrg.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Create,
|
||||||
|
OrgPermissionSubjects.Identity
|
||||||
|
);
|
||||||
|
|
||||||
|
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
identityMembershipOrg.identityId,
|
||||||
|
identityMembershipOrg.orgId
|
||||||
|
);
|
||||||
|
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPriviledge)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add identity to project with more privileged role"
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const clientSecret = crypto.randomBytes(32).toString("hex");
|
||||||
|
const clientSecretHash = await bcrypt.hash(clientSecret, appCfg.SALT_ROUNDS);
|
||||||
|
const identityUniversalAuth = await universalAuthDal.findOne({
|
||||||
|
identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityUaClientSecret = await uaClientSecretDal.create({
|
||||||
|
identityUAId: identityUniversalAuth.id,
|
||||||
|
description,
|
||||||
|
clientSecretPrefix: clientSecret.slice(0, 4),
|
||||||
|
clientSecretHash,
|
||||||
|
clientSecretNumUses: 0,
|
||||||
|
clientSecretNumUsesLimit: numUsesLimit,
|
||||||
|
clientSecretTTL: ttl,
|
||||||
|
isClientSecretRevoked: false
|
||||||
|
});
|
||||||
|
return { clientSecret: identityUaClientSecret, uaAuth: identityUniversalAuth };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUaClientSecrets = async ({ actor, actorId, identityId }: TGetUaClientSecretsDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have universal auth"
|
||||||
|
});
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityMembershipOrg.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Read,
|
||||||
|
OrgPermissionSubjects.Identity
|
||||||
|
);
|
||||||
|
|
||||||
|
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
identityMembershipOrg.identityId,
|
||||||
|
identityMembershipOrg.orgId
|
||||||
|
);
|
||||||
|
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPriviledge)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add identity to project with more privileged role"
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityUniversalAuth = await universalAuthDal.findOne({
|
||||||
|
identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientSecrets = await uaClientSecretDal.findOne({
|
||||||
|
identityUAId: identityUniversalAuth.id,
|
||||||
|
isClientSecretRevoked: false
|
||||||
|
});
|
||||||
|
return clientSecrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const revokeUaClientSecret = async ({
|
||||||
|
identityId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
clientSecretId
|
||||||
|
}: TRevokeUaClientSecretDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have universal auth"
|
||||||
|
});
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityMembershipOrg.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Delete,
|
||||||
|
OrgPermissionSubjects.Identity
|
||||||
|
);
|
||||||
|
|
||||||
|
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
identityMembershipOrg.identityId,
|
||||||
|
identityMembershipOrg.orgId
|
||||||
|
);
|
||||||
|
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPriviledge)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add identity to project with more privileged role"
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientSecret = await uaClientSecretDal.updateById(clientSecretId, {
|
||||||
|
isClientSecretRevoked: true
|
||||||
|
});
|
||||||
|
return clientSecret;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
login,
|
||||||
|
attachUa,
|
||||||
|
updateUa,
|
||||||
|
getIdentityUa,
|
||||||
|
createUaClientSecret,
|
||||||
|
getUaClientSecrets,
|
||||||
|
revokeUaClientSecret
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TAttachUaDTO = {
|
||||||
|
identityId: string;
|
||||||
|
accessTokenTTL: number;
|
||||||
|
accessTokenMaxTTL: number;
|
||||||
|
accessTokenNumUsesLimit: number;
|
||||||
|
clientSecretTrustedIps: { ipAddress: string }[];
|
||||||
|
accessTokenTrustedIps: { ipAddress: string }[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateUaDTO = {
|
||||||
|
identityId: string;
|
||||||
|
accessTokenTTL?: number;
|
||||||
|
accessTokenMaxTTL?: number;
|
||||||
|
accessTokenNumUsesLimit?: number;
|
||||||
|
clientSecretTrustedIps?: { ipAddress: string }[];
|
||||||
|
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetUaDTO = {
|
||||||
|
identityId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TCreateUaClientSecretDTO = {
|
||||||
|
identityId: string;
|
||||||
|
description?: string;
|
||||||
|
numUsesLimit: number;
|
||||||
|
ttl: number;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetUaClientSecretsDTO = {
|
||||||
|
identityId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TRevokeUaClientSecretDTO = {
|
||||||
|
identityId: string;
|
||||||
|
clientSecretId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
Reference in New Issue
Block a user