mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 19:28:16 +00:00
Merge pull request #3535 from Infisical/feat/addGroupsToSshHosts
feat(ssh-hosts): Add groups to ssh hosts allowed principals
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SshHostLoginUserMapping, "groupId"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SshHostLoginUserMapping, (t) => {
|
||||||
|
t.uuid("groupId").nullable();
|
||||||
|
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||||
|
t.unique(["sshHostLoginUserId", "groupId"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SshHostLoginUserMapping, "groupId")) {
|
||||||
|
await knex.schema.alterTable(TableName.SshHostLoginUserMapping, (t) => {
|
||||||
|
t.dropUnique(["sshHostLoginUserId", "groupId"]);
|
||||||
|
t.dropColumn("groupId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,8 @@ export const SshHostLoginUserMappingsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
sshHostLoginUserId: z.string().uuid(),
|
sshHostLoginUserId: z.string().uuid(),
|
||||||
userId: z.string().uuid().nullable().optional()
|
userId: z.string().uuid().nullable().optional(),
|
||||||
|
groupId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSshHostLoginUserMappings = z.infer<typeof SshHostLoginUserMappingsSchema>;
|
export type TSshHostLoginUserMappings = z.infer<typeof SshHostLoginUserMappingsSchema>;
|
||||||
|
|||||||
@@ -157,10 +157,23 @@ export const groupDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findGroupsByProjectId = async (projectId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.Groups)
|
||||||
|
.join(TableName.GroupProjectMembership, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`)
|
||||||
|
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||||
|
.select(selectAllTableCols(TableName.Groups));
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find groups by project id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
findGroups,
|
findGroups,
|
||||||
findByOrgId,
|
findByOrgId,
|
||||||
findAllGroupPossibleMembers,
|
findAllGroupPossibleMembers,
|
||||||
|
findGroupsByProjectId,
|
||||||
...groupOrm
|
...groupOrm
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -176,7 +176,8 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||||
db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"),
|
db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"),
|
||||||
db.ref("firstName").withSchema(TableName.Users).as("firstName"),
|
db.ref("firstName").withSchema(TableName.Users).as("firstName"),
|
||||||
db.ref("lastName").withSchema(TableName.Users).as("lastName")
|
db.ref("lastName").withSchema(TableName.Users).as("lastName"),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
);
|
);
|
||||||
|
|
||||||
return docs;
|
return docs;
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectGroupPermissions = async (projectId: string) => {
|
const getProjectGroupPermissions = async (projectId: string, filterGroupId?: string) => {
|
||||||
try {
|
try {
|
||||||
const docs = await db
|
const docs = await db
|
||||||
.replicaNode()(TableName.GroupProjectMembership)
|
.replicaNode()(TableName.GroupProjectMembership)
|
||||||
@@ -148,6 +148,11 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`groupCustomRoles.id`
|
`groupCustomRoles.id`
|
||||||
)
|
)
|
||||||
.where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId)
|
.where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId)
|
||||||
|
.where((bd) => {
|
||||||
|
if (filterGroupId) {
|
||||||
|
void bd.where(`${TableName.GroupProjectMembership}.groupId`, "=", filterGroupId);
|
||||||
|
}
|
||||||
|
})
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
||||||
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||||
|
|||||||
@@ -630,6 +630,34 @@ export const permissionServiceFactory = ({
|
|||||||
return { permission };
|
return { permission };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkGroupProjectPermission = async ({
|
||||||
|
groupId,
|
||||||
|
projectId,
|
||||||
|
checkPermissions
|
||||||
|
}: {
|
||||||
|
groupId: string;
|
||||||
|
projectId: string;
|
||||||
|
checkPermissions: ProjectPermissionSet;
|
||||||
|
}) => {
|
||||||
|
const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId, groupId);
|
||||||
|
const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => {
|
||||||
|
const rolePermissions =
|
||||||
|
groupProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission,
|
||||||
|
id: groupProjectPermission.groupId,
|
||||||
|
name: groupProjectPermission.username,
|
||||||
|
membershipId: groupProjectPermission.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return groupPermissions.some((groupPermission) => groupPermission.permission.can(...checkPermissions));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getUserOrgPermission,
|
getUserOrgPermission,
|
||||||
getOrgPermission,
|
getOrgPermission,
|
||||||
@@ -639,6 +667,7 @@ export const permissionServiceFactory = ({
|
|||||||
getOrgPermissionByRole,
|
getOrgPermissionByRole,
|
||||||
getProjectPermissionByRole,
|
getProjectPermissionByRole,
|
||||||
buildOrgPermission,
|
buildOrgPermission,
|
||||||
buildProjectPermissionRules
|
buildProjectPermissionRules,
|
||||||
|
checkGroupProjectPermission
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
)
|
)
|
||||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||||
.where(`${TableName.SshHostGroup}.projectId`, projectId)
|
.where(`${TableName.SshHostGroup}.projectId`, projectId)
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
||||||
@@ -35,7 +36,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("name").withSchema(TableName.SshHostGroup),
|
db.ref("name").withSchema(TableName.SshHostGroup),
|
||||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
db.ref("username").withSchema(TableName.Users),
|
db.ref("username").withSchema(TableName.Users),
|
||||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping)
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
)
|
)
|
||||||
.orderBy(`${TableName.SshHostGroup}.updatedAt`, "desc");
|
.orderBy(`${TableName.SshHostGroup}.updatedAt`, "desc");
|
||||||
|
|
||||||
@@ -69,7 +71,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
|||||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
return {
|
return {
|
||||||
@@ -99,6 +102,7 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
)
|
)
|
||||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||||
.where(`${TableName.SshHostGroup}.id`, sshHostGroupId)
|
.where(`${TableName.SshHostGroup}.id`, sshHostGroupId)
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
||||||
@@ -106,7 +110,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("name").withSchema(TableName.SshHostGroup),
|
db.ref("name").withSchema(TableName.SshHostGroup),
|
||||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
db.ref("username").withSchema(TableName.Users),
|
db.ref("username").withSchema(TableName.Users),
|
||||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping)
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rows.length === 0) return null;
|
if (rows.length === 0) return null;
|
||||||
@@ -121,7 +126,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
|||||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { createSshLoginMappings } from "../ssh-host/ssh-host-fns";
|
import { createSshLoginMappings } from "../ssh-host/ssh-host-fns";
|
||||||
import {
|
import {
|
||||||
@@ -43,8 +44,12 @@ type TSshHostGroupServiceFactoryDep = {
|
|||||||
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction" | "delete">;
|
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction" | "delete">;
|
||||||
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
||||||
userDAL: Pick<TUserDALFactory, "find">;
|
userDAL: Pick<TUserDALFactory, "find">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
permissionService: Pick<
|
||||||
|
TPermissionServiceFactory,
|
||||||
|
"getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission"
|
||||||
|
>;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSshHostGroupServiceFactory = ReturnType<typeof sshHostGroupServiceFactory>;
|
export type TSshHostGroupServiceFactory = ReturnType<typeof sshHostGroupServiceFactory>;
|
||||||
@@ -58,7 +63,8 @@ export const sshHostGroupServiceFactory = ({
|
|||||||
sshHostLoginUserMappingDAL,
|
sshHostLoginUserMappingDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
groupDAL
|
||||||
}: TSshHostGroupServiceFactoryDep) => {
|
}: TSshHostGroupServiceFactoryDep) => {
|
||||||
const createSshHostGroup = async ({
|
const createSshHostGroup = async ({
|
||||||
projectId,
|
projectId,
|
||||||
@@ -127,6 +133,7 @@ export const sshHostGroupServiceFactory = ({
|
|||||||
loginMappings,
|
loginMappings,
|
||||||
sshHostLoginUserDAL,
|
sshHostLoginUserDAL,
|
||||||
sshHostLoginUserMappingDAL,
|
sshHostLoginUserMappingDAL,
|
||||||
|
groupDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -194,6 +201,7 @@ export const sshHostGroupServiceFactory = ({
|
|||||||
loginMappings,
|
loginMappings,
|
||||||
sshHostLoginUserDAL,
|
sshHostLoginUserDAL,
|
||||||
sshHostLoginUserMappingDAL,
|
sshHostLoginUserMappingDAL,
|
||||||
|
groupDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectId: sshHostGroup.projectId,
|
projectId: sshHostGroup.projectId,
|
||||||
|
|||||||
@@ -9,12 +9,7 @@ export type TCreateSshHostGroupDTO = {
|
|||||||
export type TUpdateSshHostGroupDTO = {
|
export type TUpdateSshHostGroupDTO = {
|
||||||
sshHostGroupId: string;
|
sshHostGroupId: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
loginMappings?: {
|
loginMappings?: TLoginMapping[];
|
||||||
loginUser: string;
|
|
||||||
allowedPrincipals: {
|
|
||||||
usernames: string[];
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TGetSshHostGroupDTO = {
|
export type TGetSshHostGroupDTO = {
|
||||||
|
|||||||
@@ -31,8 +31,18 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SshHostLoginUser}.id`,
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
)
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SshHostLoginUserMapping}.userId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.groupId`
|
||||||
|
)
|
||||||
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||||
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
.andWhere((bd) => {
|
||||||
|
void bd
|
||||||
|
.where(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||||
|
.orWhere(`${TableName.UserGroupMembership}.userId`, userId);
|
||||||
|
})
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||||
db.ref("projectId").withSchema(TableName.SshHost),
|
db.ref("projectId").withSchema(TableName.SshHost),
|
||||||
@@ -58,8 +68,17 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
)
|
)
|
||||||
.join(TableName.SshHost, `${TableName.SshHostGroupMembership}.sshHostId`, `${TableName.SshHost}.id`)
|
.join(TableName.SshHost, `${TableName.SshHostGroupMembership}.sshHostId`, `${TableName.SshHost}.id`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.groupId`
|
||||||
|
)
|
||||||
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||||
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
.andWhere((bd) => {
|
||||||
|
void bd
|
||||||
|
.where(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||||
|
.orWhere(`${TableName.UserGroupMembership}.userId`, userId);
|
||||||
|
})
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||||
db.ref("projectId").withSchema(TableName.SshHost),
|
db.ref("projectId").withSchema(TableName.SshHost),
|
||||||
@@ -133,6 +152,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
)
|
)
|
||||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||||
.where(`${TableName.SshHost}.projectId`, projectId)
|
.where(`${TableName.SshHost}.projectId`, projectId)
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||||
@@ -144,6 +164,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
db.ref("username").withSchema(TableName.Users),
|
db.ref("username").withSchema(TableName.Users),
|
||||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug"),
|
||||||
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
)
|
)
|
||||||
@@ -163,10 +184,12 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
)
|
)
|
||||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||||
.select(
|
.select(
|
||||||
db.ref("sshHostId").withSchema(TableName.SshHostGroupMembership),
|
db.ref("sshHostId").withSchema(TableName.SshHostGroupMembership),
|
||||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
db.ref("username").withSchema(TableName.Users)
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
)
|
)
|
||||||
.whereIn(`${TableName.SshHostGroupMembership}.sshHostId`, hostIds);
|
.whereIn(`${TableName.SshHostGroupMembership}.sshHostId`, hostIds);
|
||||||
|
|
||||||
@@ -185,7 +208,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
const directMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
const directMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
},
|
},
|
||||||
source: LoginMappingSource.HOST
|
source: LoginMappingSource.HOST
|
||||||
}));
|
}));
|
||||||
@@ -197,7 +221,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
const groupMappings = Object.entries(inheritedGrouped).map(([loginUser, entries]) => ({
|
const groupMappings = Object.entries(inheritedGrouped).map(([loginUser, entries]) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
},
|
},
|
||||||
source: LoginMappingSource.HOST_GROUP
|
source: LoginMappingSource.HOST_GROUP
|
||||||
}));
|
}));
|
||||||
@@ -229,6 +254,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
)
|
)
|
||||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||||
.where(`${TableName.SshHost}.id`, sshHostId)
|
.where(`${TableName.SshHost}.id`, sshHostId)
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||||
@@ -241,7 +267,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("username").withSchema(TableName.Users),
|
db.ref("username").withSchema(TableName.Users),
|
||||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
db.ref("hostSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rows.length === 0) return null;
|
if (rows.length === 0) return null;
|
||||||
@@ -257,7 +284,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
const directMappings = Object.entries(directGrouped).map(([loginUser, entries]) => ({
|
const directMappings = Object.entries(directGrouped).map(([loginUser, entries]) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
},
|
},
|
||||||
source: LoginMappingSource.HOST
|
source: LoginMappingSource.HOST
|
||||||
}));
|
}));
|
||||||
@@ -275,10 +303,12 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
)
|
)
|
||||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||||
.where(`${TableName.SshHostGroupMembership}.sshHostId`, sshHostId)
|
.where(`${TableName.SshHostGroupMembership}.sshHostId`, sshHostId)
|
||||||
.select(
|
.select(
|
||||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
db.ref("username").withSchema(TableName.Users)
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupGrouped = groupBy(
|
const groupGrouped = groupBy(
|
||||||
@@ -289,7 +319,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
const groupMappings = Object.entries(groupGrouped).map(([loginUser, entries]) => ({
|
const groupMappings = Object.entries(groupGrouped).map(([loginUser, entries]) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
},
|
},
|
||||||
source: LoginMappingSource.HOST_GROUP
|
source: LoginMappingSource.HOST_GROUP
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Knex } from "knex";
|
|||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
import { TCreateSshLoginMappingsDTO } from "./ssh-host-types";
|
import { TCreateSshLoginMappingsDTO } from "./ssh-host-types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,6 +16,7 @@ export const createSshLoginMappings = async ({
|
|||||||
loginMappings,
|
loginMappings,
|
||||||
sshHostLoginUserDAL,
|
sshHostLoginUserDAL,
|
||||||
sshHostLoginUserMappingDAL,
|
sshHostLoginUserMappingDAL,
|
||||||
|
groupDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -35,7 +37,7 @@ export const createSshLoginMappings = async ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
if (allowedPrincipals.usernames.length > 0) {
|
if (allowedPrincipals.usernames && allowedPrincipals.usernames.length > 0) {
|
||||||
const users = await userDAL.find(
|
const users = await userDAL.find(
|
||||||
{
|
{
|
||||||
$in: {
|
$in: {
|
||||||
@@ -74,6 +76,41 @@ export const createSshLoginMappings = async ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (allowedPrincipals.groups && allowedPrincipals.groups.length > 0) {
|
||||||
|
const projectGroups = await groupDAL.findGroupsByProjectId(projectId);
|
||||||
|
const groups = projectGroups.filter((g) => allowedPrincipals.groups?.includes(g.slug));
|
||||||
|
|
||||||
|
if (groups.length !== allowedPrincipals.groups?.length) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid group slugs: ${allowedPrincipals.groups
|
||||||
|
.filter((g) => !projectGroups.some((pg) => pg.slug === g))
|
||||||
|
.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const group of groups) {
|
||||||
|
// check that each group has access to the SSH project and have read access to hosts
|
||||||
|
const hasPermission = await permissionService.checkGroupProjectPermission({
|
||||||
|
groupId: group.id,
|
||||||
|
projectId,
|
||||||
|
checkPermissions: [ProjectPermissionSshHostActions.Read, ProjectPermissionSub.SshHosts]
|
||||||
|
});
|
||||||
|
if (!hasPermission) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Group ${group.slug} does not have access to the SSH project`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await sshHostLoginUserMappingDAL.insertMany(
|
||||||
|
groups.map((group) => ({
|
||||||
|
sshHostLoginUserId: sshHostLoginUser.id,
|
||||||
|
groupId: group.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,24 @@ export const sanitizedSshHost = SshHostsSchema.pick({
|
|||||||
|
|
||||||
export const loginMappingSchema = z.object({
|
export const loginMappingSchema = z.object({
|
||||||
loginUser: z.string().trim(),
|
loginUser: z.string().trim(),
|
||||||
allowedPrincipals: z.object({
|
allowedPrincipals: z
|
||||||
usernames: z.array(z.string().trim()).transform((usernames) => Array.from(new Set(usernames)))
|
.object({
|
||||||
})
|
usernames: z
|
||||||
|
.array(z.string().trim())
|
||||||
|
.transform((usernames) => Array.from(new Set(usernames)))
|
||||||
|
.optional(),
|
||||||
|
groups: z
|
||||||
|
.array(z.string().trim())
|
||||||
|
.transform((groups) => Array.from(new Set(groups)))
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
return (data.usernames && data.usernames.length > 0) || (data.groups && data.groups.length > 0);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "At least one username or group must be provided",
|
||||||
|
path: ["allowedPrincipals"]
|
||||||
|
}
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||||
|
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
@@ -19,6 +20,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|||||||
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal";
|
||||||
import {
|
import {
|
||||||
convertActorToPrincipals,
|
convertActorToPrincipals,
|
||||||
createSshCert,
|
createSshCert,
|
||||||
@@ -39,12 +41,14 @@ import {
|
|||||||
|
|
||||||
type TSshHostServiceFactoryDep = {
|
type TSshHostServiceFactoryDep = {
|
||||||
userDAL: Pick<TUserDALFactory, "findById" | "find">;
|
userDAL: Pick<TUserDALFactory, "findById" | "find">;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "find">;
|
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||||
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
|
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
|
||||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findOne">;
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findOne">;
|
||||||
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
|
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
|
||||||
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
|
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
|
||||||
|
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findGroupMembershipsByUserIdInOrg">;
|
||||||
sshHostDAL: Pick<
|
sshHostDAL: Pick<
|
||||||
TSshHostDALFactory,
|
TSshHostDALFactory,
|
||||||
| "transaction"
|
| "transaction"
|
||||||
@@ -58,7 +62,10 @@ type TSshHostServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
|
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
|
||||||
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
|
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
permissionService: Pick<
|
||||||
|
TPermissionServiceFactory,
|
||||||
|
"getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission"
|
||||||
|
>;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,6 +73,8 @@ export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
|
|||||||
|
|
||||||
export const sshHostServiceFactory = ({
|
export const sshHostServiceFactory = ({
|
||||||
userDAL,
|
userDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
groupDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectSshConfigDAL,
|
projectSshConfigDAL,
|
||||||
sshCertificateAuthorityDAL,
|
sshCertificateAuthorityDAL,
|
||||||
@@ -208,6 +217,7 @@ export const sshHostServiceFactory = ({
|
|||||||
loginMappings,
|
loginMappings,
|
||||||
sshHostLoginUserDAL,
|
sshHostLoginUserDAL,
|
||||||
sshHostLoginUserMappingDAL,
|
sshHostLoginUserMappingDAL,
|
||||||
|
groupDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -278,6 +288,7 @@ export const sshHostServiceFactory = ({
|
|||||||
loginMappings,
|
loginMappings,
|
||||||
sshHostLoginUserDAL,
|
sshHostLoginUserDAL,
|
||||||
sshHostLoginUserMappingDAL,
|
sshHostLoginUserMappingDAL,
|
||||||
|
groupDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectId: host.projectId,
|
projectId: host.projectId,
|
||||||
@@ -387,10 +398,14 @@ export const sshHostServiceFactory = ({
|
|||||||
userDAL
|
userDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actorId, actorOrgId);
|
||||||
|
const userGroupSlugs = userGroups.map((g) => g.groupSlug);
|
||||||
|
|
||||||
const mapping = host.loginMappings.find(
|
const mapping = host.loginMappings.find(
|
||||||
(m) =>
|
(m) =>
|
||||||
m.loginUser === loginUser &&
|
m.loginUser === loginUser &&
|
||||||
m.allowedPrincipals.usernames.some((allowed) => internalPrincipals.includes(allowed))
|
(m.allowedPrincipals.usernames?.some((allowed) => internalPrincipals.includes(allowed)) ||
|
||||||
|
m.allowedPrincipals.groups?.some((allowed) => userGroupSlugs.includes(allowed)))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mapping) {
|
if (!mapping) {
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ import { TProjectPermission } from "@app/lib/types";
|
|||||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
|
|
||||||
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TLoginMapping = {
|
export type TLoginMapping = {
|
||||||
loginUser: string;
|
loginUser: string;
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: string[];
|
usernames?: string[];
|
||||||
|
groups?: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,7 +66,8 @@ type BaseCreateSshLoginMappingsDTO = {
|
|||||||
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction">;
|
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction">;
|
||||||
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
||||||
userDAL: Pick<TUserDALFactory, "find">;
|
userDAL: Pick<TUserDALFactory, "find">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getUserProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getUserProjectPermission" | "checkGroupProjectPermission">;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorOrgId: string;
|
actorOrgId: string;
|
||||||
|
|||||||
@@ -1478,7 +1478,7 @@ export const SSH_HOSTS = {
|
|||||||
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||||
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
||||||
loginMappings:
|
loginMappings:
|
||||||
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project.",
|
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users or groups slugs in the Infisical SSH project.",
|
||||||
userSshCaId:
|
userSshCaId:
|
||||||
"The ID of the SSH CA to use for user certificates. If not specified, the default user SSH CA will be used if it exists.",
|
"The ID of the SSH CA to use for user certificates. If not specified, the default user SSH CA will be used if it exists.",
|
||||||
hostSshCaId:
|
hostSshCaId:
|
||||||
@@ -1493,7 +1493,7 @@ export const SSH_HOSTS = {
|
|||||||
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||||
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
||||||
loginMappings:
|
loginMappings:
|
||||||
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project."
|
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users or groups slugs in the Infisical SSH project."
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
sshHostId: "The ID of the SSH host to delete."
|
sshHostId: "The ID of the SSH host to delete."
|
||||||
|
|||||||
@@ -870,6 +870,8 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const sshHostService = sshHostServiceFactory({
|
const sshHostService = sshHostServiceFactory({
|
||||||
userDAL,
|
userDAL,
|
||||||
|
groupDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectSshConfigDAL,
|
projectSshConfigDAL,
|
||||||
sshCertificateAuthorityDAL,
|
sshCertificateAuthorityDAL,
|
||||||
@@ -892,7 +894,8 @@ export const registerRoutes = async (
|
|||||||
sshHostLoginUserMappingDAL,
|
sshHostLoginUserMappingDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
groupDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ export enum LoginMappingSource {
|
|||||||
export type TLoginMapping = {
|
export type TLoginMapping = {
|
||||||
loginUser: string;
|
loginUser: string;
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: string[];
|
usernames?: string[];
|
||||||
|
groups?: string[];
|
||||||
};
|
};
|
||||||
source: LoginMappingSource;
|
source: LoginMappingSource;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ export const workspaceKeys = {
|
|||||||
type ? ["workspaces", { type }] : (["workspaces"] as const),
|
type ? ["workspaces", { type }] : (["workspaces"] as const),
|
||||||
getWorkspaceAuditLogs: (workspaceId: string) =>
|
getWorkspaceAuditLogs: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-audit-logs"] as const,
|
[{ workspaceId }, "workspace-audit-logs"] as const,
|
||||||
getWorkspaceUsers: (workspaceId: string, includeGroupMembers?: boolean, roles?: string[]) =>
|
getWorkspaceUsers: (
|
||||||
[{ workspaceId, includeGroupMembers, roles }, "workspace-users"] as const,
|
workspaceId: string,
|
||||||
|
includeGroupMembers: boolean = false,
|
||||||
|
roles: string[] = []
|
||||||
|
) => [{ workspaceId, includeGroupMembers, roles }, "workspace-users"] as const,
|
||||||
getWorkspaceUserDetails: (workspaceId: string, membershipId: string) =>
|
getWorkspaceUserDetails: (workspaceId: string, membershipId: string) =>
|
||||||
[{ workspaceId, membershipId }, "workspace-user-details"] as const,
|
[{ workspaceId, membershipId }, "workspace-user-details"] as const,
|
||||||
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
||||||
|
|||||||
@@ -1,33 +1,37 @@
|
|||||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
import {
|
||||||
import { zodValidator } from "@tanstack/zod-adapter";
|
createFileRoute,
|
||||||
import { z } from "zod";
|
linkOptions,
|
||||||
|
stripSearchParams,
|
||||||
|
} from '@tanstack/react-router'
|
||||||
|
import { zodValidator } from '@tanstack/zod-adapter'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { SettingsPage } from "./SettingsPage";
|
import { SettingsPage } from './SettingsPage'
|
||||||
|
|
||||||
const SettingsPageQueryParams = z.object({
|
const SettingsPageQueryParams = z.object({
|
||||||
selectedTab: z.string().catch("")
|
selectedTab: z.string().catch(''),
|
||||||
});
|
})
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
|
'/_authenticate/_inject-org-details/_org-layout/organization/settings/',
|
||||||
)({
|
)({
|
||||||
component: SettingsPage,
|
component: SettingsPage,
|
||||||
validateSearch: zodValidator(SettingsPageQueryParams),
|
validateSearch: zodValidator(SettingsPageQueryParams),
|
||||||
search: {
|
search: {
|
||||||
middlewares: [stripSearchParams({ selectedTab: "" })]
|
middlewares: [stripSearchParams({ selectedTab: '' })],
|
||||||
},
|
},
|
||||||
context: () => ({
|
context: () => ({
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
label: "Home",
|
label: 'Home',
|
||||||
icon: () => <FontAwesomeIcon icon={faHome} />,
|
icon: () => <FontAwesomeIcon icon={faHome} />,
|
||||||
link: linkOptions({ to: "/" })
|
link: linkOptions({ to: '/' }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Settings"
|
label: 'Settings',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
})
|
}),
|
||||||
});
|
})
|
||||||
|
|||||||
+1
-1
@@ -125,7 +125,7 @@ export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props)
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
className="mb-0 flex-grow"
|
className="mb-0 flex-grow"
|
||||||
>
|
>
|
||||||
<Input {...field} />
|
<Input {...field} onChange={(e) => field.onChange(e.target.value.trim())} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
useCreateSshHostGroup,
|
useCreateSshHostGroup,
|
||||||
useGetSshHostGroupById,
|
useGetSshHostGroupById,
|
||||||
useGetWorkspaceUsers,
|
useGetWorkspaceUsers,
|
||||||
|
useListWorkspaceGroups,
|
||||||
useListWorkspaceSshHostGroups,
|
useListWorkspaceSshHostGroups,
|
||||||
useUpdateSshHostGroup
|
useUpdateSshHostGroup
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
@@ -38,7 +39,14 @@ const schema = z
|
|||||||
loginMappings: z
|
loginMappings: z
|
||||||
.object({
|
.object({
|
||||||
loginUser: z.string().trim().min(1),
|
loginUser: z.string().trim().min(1),
|
||||||
allowedPrincipals: z.array(z.string().trim()).default([])
|
allowedPrincipals: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.enum(["user", "group"]),
|
||||||
|
value: z.string().trim().min(1)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.default([])
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([])
|
.default([])
|
||||||
@@ -49,9 +57,10 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
|
|
||||||
export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const projectId = currentWorkspace?.id || "";
|
const projectId = currentWorkspace.id;
|
||||||
const { data: sshHostGroups } = useListWorkspaceSshHostGroups(currentWorkspace.id);
|
const { data: sshHostGroups } = useListWorkspaceSshHostGroups(projectId);
|
||||||
const { data: members = [] } = useGetWorkspaceUsers(projectId);
|
const { data: members = [] } = useGetWorkspaceUsers(projectId);
|
||||||
|
const { data: groups = [] } = useListWorkspaceGroups(projectId);
|
||||||
const [expandedMappings, setExpandedMappings] = useState<Record<number, boolean>>({});
|
const [expandedMappings, setExpandedMappings] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
const { data: sshHostGroup } = useGetSshHostGroupById(
|
const { data: sshHostGroup } = useGetSshHostGroupById(
|
||||||
@@ -87,7 +96,16 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
name: sshHostGroup.name,
|
name: sshHostGroup.name,
|
||||||
loginMappings: sshHostGroup.loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
loginMappings: sshHostGroup.loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: allowedPrincipals.usernames
|
allowedPrincipals: [
|
||||||
|
...(allowedPrincipals.usernames || []).map((username) => ({
|
||||||
|
type: "user" as const,
|
||||||
|
value: username
|
||||||
|
})),
|
||||||
|
...(allowedPrincipals.groups || []).map((group) => ({
|
||||||
|
type: "group" as const,
|
||||||
|
value: group
|
||||||
|
}))
|
||||||
|
]
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,27 +136,35 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transformedLoginMappings = loginMappings.map(({ loginUser, allowedPrincipals }) => {
|
||||||
|
const usernames = allowedPrincipals
|
||||||
|
.filter((p) => p.type === "user" && p.value)
|
||||||
|
.map((p) => p.value);
|
||||||
|
|
||||||
|
const groupNames = allowedPrincipals
|
||||||
|
.filter((p) => p.type === "group" && p.value)
|
||||||
|
.map((p) => p.value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames,
|
||||||
|
groups: groupNames
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (sshHostGroup) {
|
if (sshHostGroup) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
sshHostGroupId: sshHostGroup.id,
|
sshHostGroupId: sshHostGroup.id,
|
||||||
name,
|
name,
|
||||||
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
loginMappings: transformedLoginMappings
|
||||||
loginUser,
|
|
||||||
allowedPrincipals: {
|
|
||||||
usernames: allowedPrincipals
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createMutateAsync({
|
await createMutateAsync({
|
||||||
projectId,
|
projectId,
|
||||||
name,
|
name,
|
||||||
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
loginMappings: transformedLoginMappings
|
||||||
loginUser,
|
|
||||||
allowedPrincipals: {
|
|
||||||
usernames: allowedPrincipals
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +191,15 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isPrincipalDuplicate = (
|
||||||
|
mappingIndex: number,
|
||||||
|
principalType: string,
|
||||||
|
principalValue: string
|
||||||
|
) => {
|
||||||
|
const principals = getValues(`loginMappings.${mappingIndex}.allowedPrincipals`) || [];
|
||||||
|
return principals.some((p) => p.type === principalType && p.value === principalValue);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.sshHostGroup?.isOpen}
|
isOpen={popUp?.sshHostGroup?.isOpen}
|
||||||
@@ -203,7 +238,10 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newIndex = loginMappingsFormFields.fields.length;
|
const newIndex = loginMappingsFormFields.fields.length;
|
||||||
loginMappingsFormFields.append({ loginUser: "", allowedPrincipals: [""] });
|
loginMappingsFormFields.append({
|
||||||
|
loginUser: "",
|
||||||
|
allowedPrincipals: [{ type: "user", value: "" }]
|
||||||
|
});
|
||||||
setExpandedMappings((prev) => ({
|
setExpandedMappings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[newIndex]: true
|
[newIndex]: true
|
||||||
@@ -299,7 +337,10 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const current = getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
|
const current = getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
|
||||||
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
|
setValue(`loginMappings.${i}.allowedPrincipals`, [
|
||||||
|
...current,
|
||||||
|
{ type: "user", value: "" }
|
||||||
|
]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Principal
|
Add Principal
|
||||||
@@ -310,40 +351,69 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
name={`loginMappings.${i}.allowedPrincipals`}
|
name={`loginMappings.${i}.allowedPrincipals`}
|
||||||
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
|
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
{(value.length === 0 ? [""] : value).map(
|
{value.map((principal, principalIndex) => (
|
||||||
(principal: string, principalIndex: number) => (
|
<div
|
||||||
<div
|
key={`principal-${i + 1}-${principalIndex + 1}-${principal.type}`}
|
||||||
key={`${metadataFieldId}-principal-${principal}`}
|
className="flex items-center space-x-2"
|
||||||
className="flex items-center space-x-2"
|
>
|
||||||
>
|
<div className="mr-2">
|
||||||
<div className="flex-1">
|
<Select
|
||||||
<Select
|
className="w-24"
|
||||||
value={principal}
|
value={principal.type}
|
||||||
onValueChange={(newValue) => {
|
onValueChange={(newType) => {
|
||||||
if (value.includes(newValue)) {
|
const newPrincipals = [...value];
|
||||||
createNotification({
|
newPrincipals[principalIndex] = {
|
||||||
text: "This principal is already added",
|
type: newType as "user" | "group",
|
||||||
type: "error"
|
value: ""
|
||||||
});
|
};
|
||||||
return;
|
onChange(newPrincipals);
|
||||||
}
|
}}
|
||||||
const newPrincipals = [...value];
|
>
|
||||||
newPrincipals[principalIndex] = newValue;
|
<SelectItem value="user">User</SelectItem>
|
||||||
onChange(newPrincipals);
|
<SelectItem value="group">Group</SelectItem>
|
||||||
}}
|
</Select>
|
||||||
placeholder="Select a member"
|
</div>
|
||||||
className="w-full"
|
<div className="flex-1">
|
||||||
>
|
<Select
|
||||||
{members.map((member) => (
|
value={principal.value}
|
||||||
<SelectItem
|
onValueChange={(newValue) => {
|
||||||
key={member.user.id}
|
if (isPrincipalDuplicate(i, principal.type, newValue)) {
|
||||||
value={member.user.username}
|
createNotification({
|
||||||
>
|
text: `This ${principal.type} is already added`,
|
||||||
{member.user.username}
|
type: "error"
|
||||||
</SelectItem>
|
});
|
||||||
))}
|
return;
|
||||||
</Select>
|
}
|
||||||
</div>
|
const newPrincipals = [...value];
|
||||||
|
newPrincipals[principalIndex] = {
|
||||||
|
type: principal.type as "user" | "group",
|
||||||
|
value: newValue
|
||||||
|
};
|
||||||
|
onChange(newPrincipals);
|
||||||
|
}}
|
||||||
|
placeholder={`Select a ${principal.type}`}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{principal.type === "user"
|
||||||
|
? members.map((member) => (
|
||||||
|
<SelectItem
|
||||||
|
key={member.user.id}
|
||||||
|
value={member.user.username}
|
||||||
|
>
|
||||||
|
{member.user.username}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
: groups.map((group) => (
|
||||||
|
<SelectItem
|
||||||
|
key={group.group.slug}
|
||||||
|
value={group.group.slug}
|
||||||
|
>
|
||||||
|
{group.group.slug}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-10 justify-center">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
ariaLabel="delete principal"
|
ariaLabel="delete principal"
|
||||||
@@ -353,14 +423,14 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const newPrincipals = value.filter(
|
const newPrincipals = value.filter(
|
||||||
(_, idx) => idx !== principalIndex
|
(_, idx) => idx !== principalIndex
|
||||||
);
|
);
|
||||||
onChange(newPrincipals);
|
onChange(newPrincipals.length ? newPrincipals : []);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
)}
|
))}
|
||||||
{error && <span className="text-sm text-red-500">{error.message}</span>}
|
{error && <span className="text-sm text-red-500">{error.message}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
@@ -56,22 +56,38 @@ export const SshHostGroupsSection = () => {
|
|||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Host Groups</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Host Groups</p>
|
||||||
<ProjectPermissionCan
|
<div className="flex justify-end">
|
||||||
I={ProjectPermissionActions.Create}
|
<a
|
||||||
a={ProjectPermissionSub.SshHostGroups}
|
target="_blank"
|
||||||
>
|
rel="noopener noreferrer"
|
||||||
{(isAllowed) => (
|
href="https://infisical.com/docs/documentation/platform/ssh/host-groups"
|
||||||
<Button
|
>
|
||||||
colorSchema="primary"
|
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||||
type="button"
|
Documentation{" "}
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
<FontAwesomeIcon
|
||||||
onClick={() => handleAddSshHostGroupModal()}
|
icon={faArrowUpRightFromSquare}
|
||||||
isDisabled={!isAllowed}
|
className="mb-[0.06rem] ml-1 text-xs"
|
||||||
>
|
/>
|
||||||
Add Group
|
</span>
|
||||||
</Button>
|
</a>
|
||||||
)}
|
<ProjectPermissionCan
|
||||||
</ProjectPermissionCan>
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={ProjectPermissionSub.SshHostGroups}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="button"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handleAddSshHostGroupModal()}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
className="ml-4"
|
||||||
|
>
|
||||||
|
Add Group
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SshHostGroupsTable handlePopUpOpen={handlePopUpOpen} />
|
<SshHostGroupsTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
<SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import { faEllipsis, faPencil, faServer, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faEllipsis,
|
||||||
|
faPencil,
|
||||||
|
faServer,
|
||||||
|
faTrash,
|
||||||
|
faUser,
|
||||||
|
faUsers
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -77,9 +85,40 @@ export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
group.loginMappings.map(({ loginUser, allowedPrincipals }) => (
|
group.loginMappings.map(({ loginUser, allowedPrincipals }) => (
|
||||||
<div key={`${group.id}-${loginUser}`} className="mb-2">
|
<div key={`${group.id}-${loginUser}`} className="mb-2">
|
||||||
<div className="text-mineshaft-200">{loginUser}</div>
|
<div className="text-mineshaft-200">{loginUser}</div>
|
||||||
{allowedPrincipals.usernames.map((username) => (
|
{allowedPrincipals.usernames?.map((username) => (
|
||||||
<div key={`${group.id}-${loginUser}-${username}`} className="ml-4">
|
<div
|
||||||
└─ {username}
|
key={`${loginUser}-${username}`}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-gray-400">└─</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faUser}
|
||||||
|
className="text-xs text-yellow/80"
|
||||||
|
/>
|
||||||
|
<span>{username}</span>
|
||||||
|
<Badge variant="primary">user</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{allowedPrincipals.groups?.map((allowedGroup) => (
|
||||||
|
<div
|
||||||
|
key={`${loginUser}-${allowedGroup}`}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-gray-400">└─</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faUsers}
|
||||||
|
className="text-xs text-green/80"
|
||||||
|
/>
|
||||||
|
<span>{allowedGroup}</span>
|
||||||
|
<Badge variant="success">group</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
useCreateSshHost,
|
useCreateSshHost,
|
||||||
useGetSshHostById,
|
useGetSshHostById,
|
||||||
useGetWorkspaceUsers,
|
useGetWorkspaceUsers,
|
||||||
|
useListWorkspaceGroups,
|
||||||
useListWorkspaceSshHosts,
|
useListWorkspaceSshHosts,
|
||||||
useUpdateSshHost
|
useUpdateSshHost
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
@@ -49,7 +50,14 @@ const schema = z
|
|||||||
loginMappings: z
|
loginMappings: z
|
||||||
.object({
|
.object({
|
||||||
loginUser: z.string().trim().min(1),
|
loginUser: z.string().trim().min(1),
|
||||||
allowedPrincipals: z.array(z.string().trim()).default([]),
|
allowedPrincipals: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.enum(["user", "group"]),
|
||||||
|
value: z.string().trim().min(1)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.default([]),
|
||||||
source: z.nativeEnum(LoginMappingSource)
|
source: z.nativeEnum(LoginMappingSource)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
@@ -64,6 +72,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const projectId = currentWorkspace?.id || "";
|
const projectId = currentWorkspace?.id || "";
|
||||||
const { data: sshHosts } = useListWorkspaceSshHosts(currentWorkspace.id);
|
const { data: sshHosts } = useListWorkspaceSshHosts(currentWorkspace.id);
|
||||||
const { data: members = [] } = useGetWorkspaceUsers(projectId);
|
const { data: members = [] } = useGetWorkspaceUsers(projectId);
|
||||||
|
const { data: groups = [] } = useListWorkspaceGroups(projectId);
|
||||||
const [expandedMappings, setExpandedMappings] = useState<Record<number, boolean>>({});
|
const [expandedMappings, setExpandedMappings] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
const { data: sshHost } = useGetSshHostById(
|
const { data: sshHost } = useGetSshHostById(
|
||||||
@@ -103,7 +112,16 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
userCertTtl: sshHost.userCertTtl,
|
userCertTtl: sshHost.userCertTtl,
|
||||||
loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals, source }) => ({
|
loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals, source }) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: allowedPrincipals.usernames,
|
allowedPrincipals: [
|
||||||
|
...(allowedPrincipals.usernames || []).map((username) => ({
|
||||||
|
type: "user" as const,
|
||||||
|
value: username
|
||||||
|
})),
|
||||||
|
...(allowedPrincipals.groups || []).map((group) => ({
|
||||||
|
type: "group" as const,
|
||||||
|
value: group
|
||||||
|
}))
|
||||||
|
],
|
||||||
source
|
source
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
@@ -159,18 +177,31 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transformedLoginMappings = hostLoginMappings.map(({ loginUser, allowedPrincipals }) => {
|
||||||
|
const usernames = allowedPrincipals
|
||||||
|
.filter((p) => p.type === "user" && p.value)
|
||||||
|
.map((p) => p.value);
|
||||||
|
|
||||||
|
const groupNames = allowedPrincipals
|
||||||
|
.filter((p) => p.type === "group" && p.value)
|
||||||
|
.map((p) => p.value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames,
|
||||||
|
groups: groupNames
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (sshHost) {
|
if (sshHost) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
sshHostId: sshHost.id,
|
sshHostId: sshHost.id,
|
||||||
hostname,
|
hostname,
|
||||||
alias: trimmedAlias,
|
alias: trimmedAlias,
|
||||||
userCertTtl,
|
userCertTtl,
|
||||||
loginMappings: hostLoginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
loginMappings: transformedLoginMappings
|
||||||
loginUser,
|
|
||||||
allowedPrincipals: {
|
|
||||||
usernames: allowedPrincipals
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createMutateAsync({
|
await createMutateAsync({
|
||||||
@@ -178,12 +209,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
hostname,
|
hostname,
|
||||||
alias: trimmedAlias,
|
alias: trimmedAlias,
|
||||||
userCertTtl,
|
userCertTtl,
|
||||||
loginMappings: hostLoginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
loginMappings: transformedLoginMappings
|
||||||
loginUser,
|
|
||||||
allowedPrincipals: {
|
|
||||||
usernames: allowedPrincipals
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +236,15 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isPrincipalDuplicate = (
|
||||||
|
mappingIndex: number,
|
||||||
|
principalType: string,
|
||||||
|
principalValue: string
|
||||||
|
) => {
|
||||||
|
const principals = getValues(`loginMappings.${mappingIndex}.allowedPrincipals`) || [];
|
||||||
|
return principals.some((p) => p.type === principalType && p.value === principalValue);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.sshHost?.isOpen}
|
isOpen={popUp?.sshHost?.isOpen}
|
||||||
@@ -275,7 +310,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const newIndex = loginMappingsFormFields.fields.length;
|
const newIndex = loginMappingsFormFields.fields.length;
|
||||||
loginMappingsFormFields.append({
|
loginMappingsFormFields.append({
|
||||||
loginUser: "",
|
loginUser: "",
|
||||||
allowedPrincipals: [""],
|
allowedPrincipals: [],
|
||||||
source: LoginMappingSource.HOST
|
source: LoginMappingSource.HOST
|
||||||
});
|
});
|
||||||
setExpandedMappings((prev) => ({
|
setExpandedMappings((prev) => ({
|
||||||
@@ -397,7 +432,10 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
const current =
|
const current =
|
||||||
getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
|
getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
|
||||||
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
|
setValue(`loginMappings.${i}.allowedPrincipals`, [
|
||||||
|
...current,
|
||||||
|
{ type: "user", value: "" }
|
||||||
|
]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Principal
|
Add Principal
|
||||||
@@ -409,50 +447,82 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
name={`loginMappings.${i}.allowedPrincipals`}
|
name={`loginMappings.${i}.allowedPrincipals`}
|
||||||
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
|
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
{(value.length === 0 ? [""] : value).map(
|
{value.map((principal, principalIndex) => (
|
||||||
(principal: string, principalIndex: number) => (
|
<div
|
||||||
<div
|
key={`principal-${i + 1}-${principalIndex + 1}-${principal.type}`}
|
||||||
key={`${metadataFieldId}-principal-${principal}`}
|
className="flex items-center space-x-2"
|
||||||
className="flex items-center space-x-2"
|
>
|
||||||
>
|
<div className="mr-2">
|
||||||
<div className="flex-1">
|
<Select
|
||||||
<Select
|
className="w-24"
|
||||||
value={principal}
|
value={principal.type}
|
||||||
onValueChange={(newValue) => {
|
onValueChange={(newType) => {
|
||||||
if (
|
const newPrincipals = [...value];
|
||||||
loginMappingsFormFields.fields[i].source ===
|
newPrincipals[principalIndex] = {
|
||||||
LoginMappingSource.HOST_GROUP
|
type: newType as "user" | "group",
|
||||||
)
|
value: ""
|
||||||
return;
|
};
|
||||||
|
onChange(newPrincipals);
|
||||||
if (value.includes(newValue)) {
|
}}
|
||||||
createNotification({
|
isDisabled={
|
||||||
text: "This principal is already added",
|
loginMappingsFormFields.fields[i].source ===
|
||||||
type: "error"
|
LoginMappingSource.HOST_GROUP
|
||||||
});
|
}
|
||||||
return;
|
>
|
||||||
}
|
<SelectItem value="user">User</SelectItem>
|
||||||
const newPrincipals = [...value];
|
<SelectItem value="group">Group</SelectItem>
|
||||||
newPrincipals[principalIndex] = newValue;
|
</Select>
|
||||||
onChange(newPrincipals);
|
</div>
|
||||||
}}
|
<div className="flex-1">
|
||||||
placeholder="Select a member"
|
<Select
|
||||||
className="w-full"
|
value={principal.value}
|
||||||
isDisabled={
|
onValueChange={(newValue) => {
|
||||||
|
if (
|
||||||
loginMappingsFormFields.fields[i].source ===
|
loginMappingsFormFields.fields[i].source ===
|
||||||
LoginMappingSource.HOST_GROUP
|
LoginMappingSource.HOST_GROUP
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
if (isPrincipalDuplicate(i, principal.type, newValue)) {
|
||||||
|
createNotification({
|
||||||
|
text: `This ${principal.type} is already added`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
>
|
const newPrincipals = [...value];
|
||||||
{members.map((member) => (
|
newPrincipals[principalIndex] = {
|
||||||
<SelectItem
|
type: principal.type,
|
||||||
key={member.user.id}
|
value: newValue
|
||||||
value={member.user.username}
|
};
|
||||||
>
|
onChange(newPrincipals);
|
||||||
{member.user.username}
|
}}
|
||||||
</SelectItem>
|
placeholder={`Select a ${principal.type}`}
|
||||||
))}
|
className="w-full"
|
||||||
</Select>
|
isDisabled={
|
||||||
</div>
|
loginMappingsFormFields.fields[i].source ===
|
||||||
|
LoginMappingSource.HOST_GROUP
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{principal.type === "user"
|
||||||
|
? members.map((member) => (
|
||||||
|
<SelectItem
|
||||||
|
key={member.user.id}
|
||||||
|
value={member.user.username}
|
||||||
|
>
|
||||||
|
{member.user.username}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
: groups.map((group) => (
|
||||||
|
<SelectItem
|
||||||
|
key={group.group.slug}
|
||||||
|
value={group.group.slug}
|
||||||
|
>
|
||||||
|
{group.group.slug}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-10 justify-center">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
ariaLabel="delete principal"
|
ariaLabel="delete principal"
|
||||||
@@ -468,7 +538,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const newPrincipals = value.filter(
|
const newPrincipals = value.filter(
|
||||||
(_, idx) => idx !== principalIndex
|
(_, idx) => idx !== principalIndex
|
||||||
);
|
);
|
||||||
onChange(newPrincipals);
|
onChange([...newPrincipals]);
|
||||||
}}
|
}}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
loginMappingsFormFields.fields[i].source ===
|
loginMappingsFormFields.fields[i].source ===
|
||||||
@@ -478,8 +548,8 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<FontAwesomeIcon icon={faTrash} />
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
)}
|
))}
|
||||||
{error && <span className="text-sm text-red-500">{error.message}</span>}
|
{error && <span className="text-sm text-red-500">{error.message}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const SshHostsSection = () => {
|
|||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Hosts</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Hosts</p>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex justify-end">
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import {
|
|||||||
faEllipsis,
|
faEllipsis,
|
||||||
faPencil,
|
faPencil,
|
||||||
faServer,
|
faServer,
|
||||||
faTrash
|
faTrash,
|
||||||
|
faUser,
|
||||||
|
faUsers
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import FileSaver from "file-saver";
|
import FileSaver from "file-saver";
|
||||||
@@ -12,6 +14,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -101,43 +104,53 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
|
|
||||||
const hostLoginUserToPrincipals = hostMappings.reduce(
|
const hostLoginUserToPrincipals = hostMappings.reduce(
|
||||||
(acc, { loginUser, allowedPrincipals }) => {
|
(acc, { loginUser, allowedPrincipals }) => {
|
||||||
acc[loginUser] = new Set(allowedPrincipals.usernames);
|
acc[loginUser] = {
|
||||||
|
users: new Set(allowedPrincipals.usernames),
|
||||||
|
groups: new Set(allowedPrincipals.groups)
|
||||||
|
};
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, Set<string>>
|
{} as Record<string, { users: Set<string>; groups: Set<string> }>
|
||||||
);
|
);
|
||||||
|
|
||||||
const entriesFromHost = hostMappings.map(
|
const entriesFromHost = hostMappings.map(
|
||||||
({ loginUser, allowedPrincipals }) => ({
|
({ loginUser, allowedPrincipals }) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
source: LoginMappingSource.HOST,
|
source: LoginMappingSource.HOST,
|
||||||
usernames: allowedPrincipals.usernames
|
users: allowedPrincipals.usernames,
|
||||||
|
groups: allowedPrincipals.groups
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const entriesFromGroup = groupMappings
|
const entriesFromGroup = groupMappings
|
||||||
.map(({ loginUser, allowedPrincipals }) => {
|
.map(({ loginUser, allowedPrincipals }) => {
|
||||||
const existing = hostLoginUserToPrincipals[loginUser] || new Set();
|
const existing = hostLoginUserToPrincipals[loginUser] || {};
|
||||||
const filteredUsernames = allowedPrincipals.usernames.filter(
|
const filteredUsernames = allowedPrincipals.usernames?.filter(
|
||||||
(u) => !existing.has(u)
|
(u) => !existing.users?.has(u)
|
||||||
);
|
);
|
||||||
return filteredUsernames.length > 0
|
const filteredGroups = allowedPrincipals.groups?.filter(
|
||||||
|
(g) => !existing.groups?.has(g)
|
||||||
|
);
|
||||||
|
return ((filteredGroups?.length || filteredUsernames?.length) ?? 0) >
|
||||||
|
0
|
||||||
? {
|
? {
|
||||||
loginUser,
|
loginUser,
|
||||||
source: LoginMappingSource.HOST_GROUP,
|
source: LoginMappingSource.HOST_GROUP,
|
||||||
usernames: filteredUsernames
|
users: filteredUsernames,
|
||||||
|
groups: filteredGroups
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
})
|
})
|
||||||
.filter(Boolean) as {
|
.filter(Boolean) as {
|
||||||
loginUser: string;
|
loginUser: string;
|
||||||
source: LoginMappingSource;
|
source: LoginMappingSource;
|
||||||
usernames: string[];
|
users: string[];
|
||||||
|
groups: string[];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
return [...entriesFromHost, ...entriesFromGroup]
|
return [...entriesFromHost, ...entriesFromGroup]
|
||||||
.sort((a, b) => a.loginUser.localeCompare(b.loginUser))
|
.sort((a, b) => a.loginUser.localeCompare(b.loginUser))
|
||||||
.map(({ loginUser, usernames, source }) => (
|
.map(({ loginUser, users, groups, source }) => (
|
||||||
<div key={`${host.id}-${loginUser}-${source}`} className="mb-2">
|
<div key={`${host.id}-${loginUser}-${source}`} className="mb-2">
|
||||||
<div className="text-mineshaft-200">
|
<div className="text-mineshaft-200">
|
||||||
{loginUser}
|
{loginUser}
|
||||||
@@ -147,12 +160,40 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{usernames.map((username) => (
|
{users?.map((username) => (
|
||||||
<div
|
<div
|
||||||
key={`${host.id}-${loginUser}-${source}-${username}`}
|
key={`${host.id}-${loginUser}-${source}-${username}`}
|
||||||
className="ml-4"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
└─ {username}
|
<div className="flex items-center">
|
||||||
|
<span className="text-gray-400">└─</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faUser}
|
||||||
|
className="text-xs text-yellow/80"
|
||||||
|
/>
|
||||||
|
<span>{username}</span>
|
||||||
|
<Badge variant="primary">user</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{groups?.map((group) => (
|
||||||
|
<div
|
||||||
|
key={`${host.id}-${loginUser}-${source}-${group}`}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-gray-400">└─</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faUsers}
|
||||||
|
className="text-xs text-green/80"
|
||||||
|
/>
|
||||||
|
<span>{group}</span>
|
||||||
|
<Badge variant="success">group</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user