mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 08:34:52 +00:00
Begin work on ssh host groups
This commit is contained in:
@@ -153,7 +153,7 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
totalCount: Number(members?.[0]?.total_count ?? 0)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all org members" });
|
||||
throw new DatabaseError({ error, name: "Find all user group members" });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export const getDefaultOnPremFeatures = () => {
|
||||
samlSSO: false,
|
||||
scim: false,
|
||||
ldap: false,
|
||||
groups: false,
|
||||
groups: true,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
|
||||
@@ -35,7 +35,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
oidcSSO: false,
|
||||
scim: false,
|
||||
ldap: false,
|
||||
groups: false,
|
||||
groups: true,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
|
||||
@@ -52,7 +52,7 @@ export type TFeatureSet = {
|
||||
secretAccessInsights: false;
|
||||
scim: false;
|
||||
ldap: false;
|
||||
groups: false;
|
||||
groups: true;
|
||||
status: null;
|
||||
trial_end: null;
|
||||
has_used_trial: true;
|
||||
|
||||
@@ -134,6 +134,7 @@ export enum ProjectPermissionSub {
|
||||
SshCertificates = "ssh-certificates",
|
||||
SshCertificateTemplates = "ssh-certificate-templates",
|
||||
SshHosts = "ssh-hosts",
|
||||
SshHostGroups = "ssh-host-groups",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms",
|
||||
@@ -240,6 +241,7 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||
@@ -508,6 +510,12 @@ const GeneralPermissionSchema = [
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SshHostGroups).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
@@ -686,7 +694,8 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates,
|
||||
ProjectPermissionSub.SshCertificateTemplates
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SshHostGroups
|
||||
].forEach((el) => {
|
||||
can(
|
||||
[
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { groupBy, unique } from "@app/lib/fn";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TSshHostGroupDALFactory = ReturnType<typeof sshHostGroupDALFactory>;
|
||||
|
||||
export const sshHostGroupDALFactory = (db: TDbClient) => {
|
||||
const sshHostGroupOrm = ormify(db, TableName.SshHostGroup);
|
||||
|
||||
const findSshHostGroupsWithLoginMappings = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const rows = await (tx || db.replicaNode())(TableName.SshHostGroup)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUser,
|
||||
`${TableName.SshHostGroup}.id`,
|
||||
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.SshHostGroup}.projectId`, projectId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHostGroup),
|
||||
db.ref("name").withSchema(TableName.SshHostGroup),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping)
|
||||
)
|
||||
.orderBy(`${TableName.SshHostGroup}.updatedAt`, "desc");
|
||||
|
||||
const hostsGrouped = groupBy(rows, (r) => r.sshHostGroupId);
|
||||
|
||||
return Object.values(hostsGrouped).map((hostRows) => {
|
||||
const { sshHostGroupId, name } = hostRows[0];
|
||||
const loginMappingGrouped = groupBy(
|
||||
hostRows.filter((r) => r.loginUser),
|
||||
(r) => r.loginUser
|
||||
);
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
}
|
||||
}));
|
||||
return {
|
||||
id: sshHostGroupId,
|
||||
projectId,
|
||||
name,
|
||||
loginMappings
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.SshHostGroup}: FindSshHostGroupsWithLoginMappings` });
|
||||
}
|
||||
};
|
||||
|
||||
const findSshHostGroupByIdWithLoginMappings = async (sshHostGroupId: string, tx?: Knex) => {
|
||||
try {
|
||||
const rows = await (tx || db.replicaNode())(TableName.SshHostGroup)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUser,
|
||||
`${TableName.SshHostGroup}.id`,
|
||||
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.SshHostGroup}.id`, sshHostGroupId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHostGroup),
|
||||
db.ref("name").withSchema(TableName.SshHostGroup),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping)
|
||||
);
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const { sshHostGroupId: id, projectId, name } = rows[0];
|
||||
|
||||
const loginMappingGrouped = groupBy(
|
||||
rows.filter((r) => r.loginUser),
|
||||
(r) => r.loginUser
|
||||
);
|
||||
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
id,
|
||||
projectId,
|
||||
name,
|
||||
loginMappings
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.SshHostGroup}: FindSshHostGroupByIdWithLoginMappings` });
|
||||
}
|
||||
};
|
||||
|
||||
const findAllSshHostsInGroup = async ({
|
||||
sshHostGroupId,
|
||||
offset = 0,
|
||||
limit
|
||||
}: {
|
||||
sshHostGroupId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}) => {
|
||||
try {
|
||||
const query = db
|
||||
.replicaNode()(TableName.SshHostGroupMembership)
|
||||
.where(`${TableName.SshHostGroupMembership}.sshHostGroupId`, sshHostGroupId)
|
||||
.join(TableName.SshHost, `${TableName.SshHostGroupMembership}.sshHostId`, `${TableName.SshHost}.id`)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHost),
|
||||
db.ref("hostname").withSchema(TableName.SshHost),
|
||||
db.ref("alias").withSchema(TableName.SshHost),
|
||||
db.ref("createdAt").withSchema(TableName.SshHostGroupMembership).as("joinedGroupAt"),
|
||||
db.raw(`count(*) OVER() as total_count`)
|
||||
)
|
||||
.offset(offset)
|
||||
.orderBy(`${TableName.SshHost}.hostname`, "asc");
|
||||
|
||||
if (limit) {
|
||||
void query.limit(limit);
|
||||
}
|
||||
|
||||
const hosts = await query;
|
||||
|
||||
return {
|
||||
hosts: hosts.map(({ id, hostname, alias }) => ({
|
||||
id,
|
||||
hostname,
|
||||
alias
|
||||
})),
|
||||
// @ts-expect-error col select is raw and not strongly typed
|
||||
totalCount: Number(hosts?.[0]?.total_count ?? 0)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.SshHostGroupMembership}: FindAllSshHostsInGroup` });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
findSshHostGroupsWithLoginMappings,
|
||||
findSshHostGroupByIdWithLoginMappings,
|
||||
findAllSshHostsInGroup,
|
||||
...sshHostGroupOrm
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TSshHostGroupMembershipDALFactory = ReturnType<typeof sshHostGroupMembershipDALFactory>;
|
||||
|
||||
export const sshHostGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
const sshHostGroupMembershipOrm = ormify(db, TableName.SshHostGroupMembership);
|
||||
|
||||
return {
|
||||
...sshHostGroupMembershipOrm
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SshHostGroupsSchema } from "@app/db/schemas";
|
||||
|
||||
export const sanitizedSshHostGroup = SshHostGroupsSchema.pick({
|
||||
id: true,
|
||||
projectId: true,
|
||||
name: true
|
||||
});
|
||||
@@ -0,0 +1,353 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||
import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||
import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
|
||||
import { TSshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { createSshLoginMappings } from "../ssh-host/ssh-host-fns";
|
||||
import {
|
||||
TAddHostToSshHostGroupDTO,
|
||||
TCreateSshHostGroupDTO,
|
||||
TDeleteSshHostGroupDTO,
|
||||
TGetSshHostGroupDTO,
|
||||
TListSshHostGroupHostsDTO,
|
||||
TRemoveHostFromSshHostGroupDTO,
|
||||
TUpdateSshHostGroupDTO
|
||||
} from "./ssh-host-group-types";
|
||||
|
||||
type TSshHostGroupServiceFactoryDep = {
|
||||
sshHostDAL: TSshHostDALFactory; // TODO: Pick
|
||||
sshHostGroupDAL: Pick<
|
||||
TSshHostGroupDALFactory,
|
||||
| "create"
|
||||
| "updateById"
|
||||
| "findById"
|
||||
| "deleteById"
|
||||
| "transaction"
|
||||
| "findSshHostGroupByIdWithLoginMappings"
|
||||
| "findAllSshHostsInGroup"
|
||||
>;
|
||||
sshHostGroupMembershipDAL: TSshHostGroupMembershipDALFactory; // TODO: Pick
|
||||
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction" | "delete">;
|
||||
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||
};
|
||||
|
||||
export type TSshHostGroupServiceFactory = ReturnType<typeof sshHostGroupServiceFactory>;
|
||||
|
||||
export const sshHostGroupServiceFactory = ({
|
||||
sshHostDAL,
|
||||
sshHostGroupDAL,
|
||||
sshHostGroupMembershipDAL,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService
|
||||
}: TSshHostGroupServiceFactoryDep) => {
|
||||
const createSshHostGroup = async ({
|
||||
projectId,
|
||||
name,
|
||||
loginMappings,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreateSshHostGroupDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
const newSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.create(
|
||||
{
|
||||
projectId,
|
||||
name // TODO: check that this is unique across the whole org
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await createSshLoginMappings({
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
tx
|
||||
});
|
||||
|
||||
const newSshHostGroupWithLoginMappings = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(
|
||||
sshHostGroup.id,
|
||||
tx
|
||||
);
|
||||
if (!newSshHostGroupWithLoginMappings) {
|
||||
throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroup.id}' not found` });
|
||||
}
|
||||
|
||||
return newSshHostGroupWithLoginMappings;
|
||||
});
|
||||
|
||||
return newSshHostGroup;
|
||||
};
|
||||
|
||||
const updateSshHostGroup = async ({
|
||||
sshHostGroupId,
|
||||
name,
|
||||
loginMappings,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TUpdateSshHostGroupDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findById(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
|
||||
await sshHostGroupDAL.updateById(
|
||||
sshHostGroupId,
|
||||
{
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (loginMappings) {
|
||||
await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx);
|
||||
if (loginMappings.length) {
|
||||
await createSshLoginMappings({
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
tx
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSshHostGroupWithLoginMappings = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(
|
||||
sshHostGroup.id,
|
||||
tx
|
||||
);
|
||||
if (!updatedSshHostGroupWithLoginMappings) {
|
||||
throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroup.id}' not found` });
|
||||
}
|
||||
|
||||
return updatedSshHostGroupWithLoginMappings;
|
||||
});
|
||||
|
||||
return updatedSshHostGroup;
|
||||
};
|
||||
|
||||
const getSshHostGroup = async ({
|
||||
sshHostGroupId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetSshHostGroupDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
return sshHostGroup;
|
||||
};
|
||||
|
||||
const deleteSshHostGroup = async ({
|
||||
sshHostGroupId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteSshHostGroupDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
await sshHostGroupDAL.deleteById(sshHostGroupId);
|
||||
|
||||
return sshHostGroup;
|
||||
};
|
||||
|
||||
const listSshHostGroupHosts = async ({
|
||||
sshHostGroupId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TListSshHostGroupHostsDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
// TODO: check
|
||||
const { hosts, totalCount } = await sshHostGroupDAL.findAllSshHostsInGroup({ sshHostGroupId });
|
||||
console.log("hosts: ", hosts);
|
||||
return { hosts, totalCount };
|
||||
};
|
||||
|
||||
const addHostToSshHostGroup = async ({
|
||||
sshHostGroupId,
|
||||
hostId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TAddHostToSshHostGroupDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(hostId);
|
||||
if (!host) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${hostId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (sshHostGroup.projectId !== host.projectId) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${hostId} not found in project ${sshHostGroup.projectId}`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups);
|
||||
// TODO: look over permissioning
|
||||
|
||||
await sshHostGroupMembershipDAL.create({ sshHostGroupId, sshHostId: hostId });
|
||||
|
||||
return host;
|
||||
};
|
||||
|
||||
const removeHostFromSshHostGroup = async ({
|
||||
sshHostGroupId,
|
||||
hostId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TRemoveHostFromSshHostGroupDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(hostId);
|
||||
if (!host) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${hostId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (sshHostGroup.projectId !== host.projectId) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${hostId} not found in project ${sshHostGroup.projectId}`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups);
|
||||
// TODO: look over permissioning
|
||||
|
||||
const sshHostGroupMembership = await sshHostGroupMembershipDAL.findOne({
|
||||
sshHostGroupId,
|
||||
sshHostId: hostId
|
||||
});
|
||||
|
||||
if (!sshHostGroupMembership) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${hostId} not found in SSH host group with ID ${sshHostGroupId}`
|
||||
});
|
||||
}
|
||||
|
||||
await sshHostGroupMembershipDAL.deleteById(sshHostGroupMembership.id);
|
||||
|
||||
return host;
|
||||
};
|
||||
|
||||
return {
|
||||
createSshHostGroup,
|
||||
getSshHostGroup,
|
||||
deleteSshHostGroup,
|
||||
updateSshHostGroup,
|
||||
listSshHostGroupHosts,
|
||||
addHostToSshHostGroup,
|
||||
removeHostFromSshHostGroup
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateSshHostGroupDTO = {
|
||||
name: string;
|
||||
loginMappings: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
name?: string;
|
||||
loginMappings?: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TGetSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TDeleteSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
} & TGenericPermission;
|
||||
export type TListSshHostGroupHostsDTO = {
|
||||
sshHostGroupId: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TAddHostToSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
hostId: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TRemoveHostFromSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
hostId: string;
|
||||
} & TGenericPermission;
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TCreateSshLoginMappingsDTO } from "./ssh-host-types";
|
||||
|
||||
/**
|
||||
* Create SSH login mappings for a given SSH host
|
||||
* or SSH host group.
|
||||
*/
|
||||
export const createSshLoginMappings = async ({
|
||||
sshHostId,
|
||||
sshHostGroupId,
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
tx: outerTx
|
||||
}: TCreateSshLoginMappingsDTO) => {
|
||||
const processCreation = async (tx: Knex) => {
|
||||
// (dangtony98): room to optimize
|
||||
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||
// (dangtony98): should either pass in sshHostId or sshHostGroupId but not both
|
||||
{
|
||||
sshHostId,
|
||||
sshHostGroupId,
|
||||
loginUser
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (allowedPrincipals.usernames.length > 0) {
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: allowedPrincipals.usernames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const foundUsernames = new Set(users.map((u) => u.username));
|
||||
|
||||
for (const uname of allowedPrincipals.usernames) {
|
||||
if (!foundUsernames.has(uname)) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid username: ${uname}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const user of users) {
|
||||
// check that each user has access to the SSH project
|
||||
await permissionService.getUserProjectPermission({
|
||||
userId: user.id,
|
||||
projectId,
|
||||
authMethod: actorAuthMethod,
|
||||
userOrgId: actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
}
|
||||
|
||||
await sshHostLoginUserMappingDAL.insertMany(
|
||||
users.map((user) => ({
|
||||
sshHostLoginUserId: sshHostLoginUser.id,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (outerTx) {
|
||||
return processCreation(outerTx);
|
||||
}
|
||||
|
||||
return sshHostLoginUserDAL.transaction(processCreation);
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getSshPublicKey
|
||||
} from "../ssh/ssh-certificate-authority-fns";
|
||||
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
|
||||
import { createSshLoginMappings } from "./ssh-host-fns";
|
||||
import {
|
||||
TCreateSshHostDTO,
|
||||
TDeleteSshHostDTO,
|
||||
@@ -202,56 +203,18 @@ export const sshHostServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
// (dangtony98): room to optimize
|
||||
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||
{
|
||||
sshHostId: host.id,
|
||||
loginUser
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (allowedPrincipals.usernames.length > 0) {
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: allowedPrincipals.usernames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const foundUsernames = new Set(users.map((u) => u.username));
|
||||
|
||||
for (const uname of allowedPrincipals.usernames) {
|
||||
if (!foundUsernames.has(uname)) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid username: ${uname}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const user of users) {
|
||||
// check that each user has access to the SSH project
|
||||
await permissionService.getUserProjectPermission({
|
||||
userId: user.id,
|
||||
projectId,
|
||||
authMethod: actorAuthMethod,
|
||||
userOrgId: actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
}
|
||||
|
||||
await sshHostLoginUserMappingDAL.insertMany(
|
||||
users.map((user) => ({
|
||||
sshHostLoginUserId: sshHostLoginUser.id,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
await createSshLoginMappings({
|
||||
sshHostId: host.id,
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
tx
|
||||
});
|
||||
|
||||
const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx);
|
||||
if (!newSshHostWithLoginMappings) {
|
||||
@@ -310,54 +273,18 @@ export const sshHostServiceFactory = ({
|
||||
if (loginMappings) {
|
||||
await sshHostLoginUserDAL.delete({ sshHostId: host.id }, tx);
|
||||
if (loginMappings.length) {
|
||||
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||
{
|
||||
sshHostId: host.id,
|
||||
loginUser
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (allowedPrincipals.usernames.length > 0) {
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: allowedPrincipals.usernames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const foundUsernames = new Set(users.map((u) => u.username));
|
||||
|
||||
for (const uname of allowedPrincipals.usernames) {
|
||||
if (!foundUsernames.has(uname)) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid username: ${uname}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const user of users) {
|
||||
await permissionService.getUserProjectPermission({
|
||||
userId: user.id,
|
||||
projectId: host.projectId,
|
||||
authMethod: actorAuthMethod,
|
||||
userOrgId: actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
}
|
||||
|
||||
await sshHostLoginUserMappingDAL.insertMany(
|
||||
users.map((user) => ({
|
||||
sshHostLoginUserId: sshHostLoginUser.id,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
await createSshLoginMappings({
|
||||
sshHostId: host.id,
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId: host.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
tx
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
||||
|
||||
type LoginMapping = {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TCreateSshHostDTO = {
|
||||
hostname: string;
|
||||
alias?: string;
|
||||
userCertTtl: string;
|
||||
hostCertTtl: string;
|
||||
loginMappings: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings: LoginMapping[];
|
||||
userSshCaId?: string;
|
||||
hostSshCaId?: string;
|
||||
} & TProjectPermission;
|
||||
@@ -23,12 +32,7 @@ export type TUpdateSshHostDTO = {
|
||||
alias?: string;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings?: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings?: LoginMapping[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetSshHostDTO = {
|
||||
@@ -48,3 +52,19 @@ export type TIssueSshHostHostCertDTO = {
|
||||
sshHostId: string;
|
||||
publicKey: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
type BaseCreateSshLoginMappingsDTO = {
|
||||
loginMappings: LoginMapping[];
|
||||
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction">;
|
||||
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getUserProjectPermission">;
|
||||
projectId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
export type TCreateSshLoginMappingsDTO =
|
||||
| (BaseCreateSshLoginMappingsDTO & { sshHostId: string; sshHostGroupId?: undefined })
|
||||
| (BaseCreateSshLoginMappingsDTO & { sshHostGroupId: string; sshHostId?: undefined });
|
||||
|
||||
Reference in New Issue
Block a user