@valentinkolb/cloud 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +69 -0
- package/public/logo.svg +1 -0
- package/scripts/build.ts +113 -0
- package/scripts/preload.ts +73 -0
- package/src/_internal/define-app.ts +399 -0
- package/src/_internal/heartbeat.ts +33 -0
- package/src/_internal/registry.ts +100 -0
- package/src/_internal/runtime-context.ts +38 -0
- package/src/api/accounts-entities.ts +134 -0
- package/src/api/admin-lifecycle.ts +210 -0
- package/src/api/auth/schemas.ts +28 -0
- package/src/api/auth.ts +230 -0
- package/src/api/index.ts +66 -0
- package/src/api/me.ts +206 -0
- package/src/api/search/schemas.ts +43 -0
- package/src/api/search.ts +130 -0
- package/src/clients/core.ts +19 -0
- package/src/config/env.ts +23 -0
- package/src/config/index.ts +6 -0
- package/src/config/ssr.ts +58 -0
- package/src/contracts/app.ts +140 -0
- package/src/contracts/index.ts +5 -0
- package/src/contracts/profile.ts +67 -0
- package/src/contracts/registry.ts +50 -0
- package/src/contracts/settings-types.ts +84 -0
- package/src/contracts/shared.ts +258 -0
- package/src/contracts/widgets.ts +121 -0
- package/src/index.ts +6 -0
- package/src/server/api/index.ts +1 -0
- package/src/server/api/respond.ts +55 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/index.ts +62 -0
- package/src/server/middleware/auth.ts +168 -0
- package/src/server/middleware/index.ts +7 -0
- package/src/server/middleware/middleware.ts +47 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +126 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/validator.ts +35 -0
- package/src/server/services/access.ts +294 -0
- package/src/server/services/freeipa/client.ts +100 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +78 -0
- package/src/server/services/freeipa/tls.ts +48 -0
- package/src/server/services/freeipa/util.ts +60 -0
- package/src/server/services/geo.ts +154 -0
- package/src/server/services/index.ts +28 -0
- package/src/server/services/services.ts +13 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +907 -0
- package/src/services/account-lifecycle/scheduler.ts +347 -0
- package/src/services/account-model.ts +21 -0
- package/src/services/accounts/app.ts +966 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +45 -0
- package/src/services/accounts/entities.ts +529 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +246 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +64 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +491 -0
- package/src/services/accounts/model.ts +135 -0
- package/src/services/accounts/switching.ts +117 -0
- package/src/services/accounts/users.ts +714 -0
- package/src/services/auth-flows/index.ts +6 -0
- package/src/services/auth-flows/ipa.ts +128 -0
- package/src/services/auth-flows/magic-link.ts +119 -0
- package/src/services/freeipa-config.ts +89 -0
- package/src/services/index.ts +46 -0
- package/src/services/ipa/auth.ts +122 -0
- package/src/services/ipa/groups.ts +684 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/profile.ts +90 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/sync.ts +740 -0
- package/src/services/ipa/users.ts +794 -0
- package/src/services/logging/index.ts +294 -0
- package/src/services/notifications/email.ts +123 -0
- package/src/services/notifications/index.ts +413 -0
- package/src/services/postgres.ts +51 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +13 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +255 -0
- package/src/services/session/index.ts +137 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +101 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +824 -0
- package/src/services/settings/index.ts +203 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +179 -0
- package/src/services/settings/templates.ts +10 -0
- package/src/services/weather/forecast.ts +287 -0
- package/src/services/weather/geo.ts +110 -0
- package/src/services/weather/index.ts +99 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +125 -0
- package/src/services/weather/migrate.ts +22 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/shared/account-display.ts +17 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/icons.ts +109 -0
- package/src/shared/index.ts +10 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +58 -0
- package/src/shared/markdown/extensions/images.ts +43 -0
- package/src/shared/markdown/extensions/info-blocks.ts +93 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +34 -0
- package/src/shared/markdown/extensions/tables.ts +88 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/index.ts +97 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/ssr/AdminLayout.tsx +42 -0
- package/src/ssr/AdminSidebar.tsx +95 -0
- package/src/ssr/Footer.island.tsx +62 -0
- package/src/ssr/GlobalSearchDialog.tsx +389 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +106 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +42 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +99 -0
- package/src/ssr/Layout.tsx +326 -0
- package/src/ssr/MoreAppsDropdown.island.tsx +61 -0
- package/src/ssr/NavMenu.island.tsx +108 -0
- package/src/ssr/ThemeToggleRail.island.tsx +27 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/SearchBar.island.tsx +77 -0
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/runtime.ts +22 -0
- package/src/styles/base-popover.css +28 -0
- package/src/styles/effects.css +65 -0
- package/src/styles/global.css +133 -0
- package/src/styles/input.css +54 -0
- package/src/styles/tokens.css +35 -0
- package/src/styles/utilities-buttons.css +125 -0
- package/src/styles/utilities-feedback.css +65 -0
- package/src/styles/utilities-layout.css +122 -0
- package/src/styles/utilities-navigation.css +196 -0
- package/src/types/ambient.d.ts +8 -0
- package/src/ui/admin-settings.tsx +148 -0
- package/src/ui/dialog-core.ts +146 -0
- package/src/ui/filter/FilterChip.tsx +196 -0
- package/src/ui/filter/index.ts +2 -0
- package/src/ui/index.ts +19 -0
- package/src/ui/input/Checkbox.tsx +55 -0
- package/src/ui/input/ColorInput.tsx +122 -0
- package/src/ui/input/DateTimeInput.tsx +86 -0
- package/src/ui/input/ImageInput.tsx +170 -0
- package/src/ui/input/NumberInput.tsx +113 -0
- package/src/ui/input/PinInput.tsx +169 -0
- package/src/ui/input/SegmentedControl.tsx +99 -0
- package/src/ui/input/Select.tsx +288 -0
- package/src/ui/input/SelectChip.tsx +61 -0
- package/src/ui/input/Slider.tsx +118 -0
- package/src/ui/input/Switch.tsx +62 -0
- package/src/ui/input/TagsInput.tsx +115 -0
- package/src/ui/input/TextInput.tsx +160 -0
- package/src/ui/input/index.ts +13 -0
- package/src/ui/input/types.ts +42 -0
- package/src/ui/input/util.tsx +105 -0
- package/src/ui/ipa/Avatar.tsx +28 -0
- package/src/ui/ipa/GroupView.tsx +36 -0
- package/src/ui/ipa/LoginBtn.tsx +16 -0
- package/src/ui/ipa/UserView.tsx +58 -0
- package/src/ui/ipa/index.ts +4 -0
- package/src/ui/misc/ContextMenu.tsx +211 -0
- package/src/ui/misc/CopyButton.tsx +28 -0
- package/src/ui/misc/Dropdown.tsx +194 -0
- package/src/ui/misc/EntitySearch.tsx +213 -0
- package/src/ui/misc/Lightbox.tsx +194 -0
- package/src/ui/misc/LinkCard.tsx +34 -0
- package/src/ui/misc/LogEntriesTable.tsx +61 -0
- package/src/ui/misc/MarkdownView.tsx +65 -0
- package/src/ui/misc/Pagination.tsx +51 -0
- package/src/ui/misc/PermissionEditor.tsx +379 -0
- package/src/ui/misc/ProgressBar.tsx +47 -0
- package/src/ui/misc/RemoveBtn.tsx +27 -0
- package/src/ui/misc/StatCell.tsx +90 -0
- package/src/ui/misc/index.ts +18 -0
- package/src/ui/navigation.ts +32 -0
- package/src/ui/prompts.tsx +854 -0
- package/src/ui/sidebar.tsx +468 -0
- package/src/ui/widgets/Widget.tsx +62 -0
- package/src/ui/widgets/WidgetCard.tsx +19 -0
- package/src/ui/widgets/WidgetHero.tsx +39 -0
- package/src/ui/widgets/WidgetList.tsx +84 -0
- package/src/ui/widgets/WidgetPills.tsx +68 -0
- package/src/ui/widgets/WidgetStat.tsx +67 -0
- package/src/ui/widgets/WidgetStatus.tsx +62 -0
- package/src/ui/widgets/index.ts +9 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import type { BaseGroup, GroupMember, MutationResult } from "../../contracts/shared";
|
|
3
|
+
import { freeipa } from "../../server/services";
|
|
4
|
+
import { updateProfileForAffectedUsers, updateUserIpaProfile } from "./profile";
|
|
5
|
+
import { toPgUuidArray } from "../postgres";
|
|
6
|
+
import { getIpaUrl, ensureFreeIpaMutationAvailable } from "./guard";
|
|
7
|
+
|
|
8
|
+
type DbRow = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
type IpaGroupRow = {
|
|
11
|
+
id: string;
|
|
12
|
+
cn: string;
|
|
13
|
+
name: string;
|
|
14
|
+
provider: "ipa" | "local";
|
|
15
|
+
description: string | null;
|
|
16
|
+
gidNumber: number | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const toBaseGroup = (row: IpaGroupRow): BaseGroup => ({
|
|
20
|
+
id: row.id,
|
|
21
|
+
provider: row.provider,
|
|
22
|
+
name: row.name,
|
|
23
|
+
description: row.description,
|
|
24
|
+
gidnumber: row.gidNumber,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const getIpaGroupById = async (id: string): Promise<IpaGroupRow | null> => {
|
|
28
|
+
const [row] = await sql<DbRow[]>`
|
|
29
|
+
SELECT id, cn, name, provider, description, gid_number
|
|
30
|
+
FROM auth.groups
|
|
31
|
+
WHERE id = ${id} AND provider = 'ipa'
|
|
32
|
+
`;
|
|
33
|
+
if (!row) return null;
|
|
34
|
+
return {
|
|
35
|
+
id: row.id as string,
|
|
36
|
+
cn: row.cn as string,
|
|
37
|
+
name: row.name as string,
|
|
38
|
+
provider: row.provider as "ipa" | "local",
|
|
39
|
+
description: row.description as string | null,
|
|
40
|
+
gidNumber: row.gid_number as number | null,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getIpaGroupIdByCn = async (cn: string): Promise<string | null> => {
|
|
45
|
+
const [row] = await sql<DbRow[]>`SELECT id FROM auth.groups WHERE cn = ${cn} AND provider = 'ipa'`;
|
|
46
|
+
return (row?.id as string | undefined) ?? null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ipaMutationError = (response: Awaited<ReturnType<typeof freeipa.client.call>>): MutationResult<never> => ({
|
|
50
|
+
ok: false,
|
|
51
|
+
error: response.error?.message ?? "FreeIPA request failed",
|
|
52
|
+
status: response.error ? freeipa.util.mapIpaErrorCode(response.error.code) : 500,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const get = async (params: { id: string }): Promise<BaseGroup | null> => {
|
|
56
|
+
const row = await getIpaGroupById(params.id);
|
|
57
|
+
return row ? toBaseGroup(row) : null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const list = async (params: {
|
|
61
|
+
ids?: string[];
|
|
62
|
+
userId?: string;
|
|
63
|
+
search?: string;
|
|
64
|
+
page?: number;
|
|
65
|
+
perPage?: number;
|
|
66
|
+
}): Promise<{
|
|
67
|
+
groups: BaseGroup[];
|
|
68
|
+
total: number;
|
|
69
|
+
pagination: {
|
|
70
|
+
page: number;
|
|
71
|
+
perPage: number;
|
|
72
|
+
totalPages: number;
|
|
73
|
+
hasNext: boolean;
|
|
74
|
+
};
|
|
75
|
+
}> => {
|
|
76
|
+
const page = params.page ?? 1;
|
|
77
|
+
const perPage = params.perPage ?? 100;
|
|
78
|
+
const offset = (page - 1) * perPage;
|
|
79
|
+
const search = params.search ? `%${freeipa.util.escapeLike(params.search.toLowerCase())}%` : null;
|
|
80
|
+
const ids = params.ids;
|
|
81
|
+
|
|
82
|
+
if (ids && ids.length === 0) {
|
|
83
|
+
return {
|
|
84
|
+
groups: [],
|
|
85
|
+
total: 0,
|
|
86
|
+
pagination: { page, perPage, totalPages: 0, hasNext: false },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const conditions = [sql`g.provider = 'ipa'`];
|
|
91
|
+
if (ids) conditions.push(sql`g.id = ANY(${toPgUuidArray(ids)}::uuid[])`);
|
|
92
|
+
if (search) conditions.push(sql`(LOWER(g.name) LIKE ${search} ESCAPE '\\' OR LOWER(g.description) LIKE ${search} ESCAPE '\\')`);
|
|
93
|
+
if (params.userId) {
|
|
94
|
+
conditions.push(sql`(
|
|
95
|
+
g.id IN (
|
|
96
|
+
SELECT ug.group_id
|
|
97
|
+
FROM auth.user_groups_v2 ug
|
|
98
|
+
JOIN auth.groups g_filter ON g_filter.id = ug.group_id
|
|
99
|
+
WHERE ug.user_id = ${params.userId} AND g_filter.provider = 'ipa'
|
|
100
|
+
)
|
|
101
|
+
OR g.id IN (
|
|
102
|
+
WITH RECURSIVE user_all_groups AS (
|
|
103
|
+
SELECT ug.group_id
|
|
104
|
+
FROM auth.user_groups_v2 ug
|
|
105
|
+
JOIN auth.groups g_filter ON g_filter.id = ug.group_id
|
|
106
|
+
WHERE ug.user_id = ${params.userId} AND g_filter.provider = 'ipa'
|
|
107
|
+
UNION
|
|
108
|
+
SELECT gg.parent_group_id
|
|
109
|
+
FROM auth.group_groups_v2 gg
|
|
110
|
+
JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
|
|
111
|
+
JOIN user_all_groups ag ON gg.child_group_id = ag.group_id
|
|
112
|
+
WHERE g_parent.provider = 'ipa'
|
|
113
|
+
)
|
|
114
|
+
SELECT DISTINCT g_manage.id
|
|
115
|
+
FROM auth.groups g_manage
|
|
116
|
+
LEFT JOIN auth.group_manager_users_v2 gmu ON gmu.group_id = g_manage.id AND gmu.user_id = ${params.userId}
|
|
117
|
+
LEFT JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = g_manage.id
|
|
118
|
+
LEFT JOIN user_all_groups ug ON ug.group_id = gmg.manager_group_id
|
|
119
|
+
WHERE g_manage.provider = 'ipa' AND (gmu.user_id IS NOT NULL OR ug.group_id IS NOT NULL)
|
|
120
|
+
)
|
|
121
|
+
)`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const where = conditions.reduce((acc, condition) => sql`${acc} AND ${condition}`);
|
|
125
|
+
|
|
126
|
+
const [countRow] = await sql<DbRow[]>`SELECT COUNT(*)::int AS count FROM auth.groups g WHERE ${where}`;
|
|
127
|
+
const total = Number(countRow?.count ?? 0);
|
|
128
|
+
const totalPages = Math.ceil(total / perPage);
|
|
129
|
+
|
|
130
|
+
const rows = await sql<DbRow[]>`
|
|
131
|
+
SELECT g.id, g.provider, g.name, g.description, g.gid_number
|
|
132
|
+
FROM auth.groups g
|
|
133
|
+
WHERE ${where}
|
|
134
|
+
ORDER BY g.name
|
|
135
|
+
LIMIT ${perPage} OFFSET ${offset}
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
groups: rows.map((row) => ({
|
|
140
|
+
id: row.id as string,
|
|
141
|
+
provider: row.provider as "ipa" | "local",
|
|
142
|
+
name: row.name as string,
|
|
143
|
+
description: row.description as string | null,
|
|
144
|
+
gidnumber: row.gid_number as number | null,
|
|
145
|
+
})),
|
|
146
|
+
total,
|
|
147
|
+
pagination: { page, perPage, totalPages, hasNext: page < totalPages },
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const getMembers = async (params: { id: string; type?: "user" | "group"; recursive?: boolean }): Promise<GroupMember[]> => {
|
|
152
|
+
const group = await getIpaGroupById(params.id);
|
|
153
|
+
if (!group) return [];
|
|
154
|
+
|
|
155
|
+
const members: GroupMember[] = [];
|
|
156
|
+
|
|
157
|
+
if (!params.type || params.type === "user") {
|
|
158
|
+
const userRows = params.recursive
|
|
159
|
+
? await sql<DbRow[]>`
|
|
160
|
+
WITH RECURSIVE child_groups AS (
|
|
161
|
+
SELECT ${group.id}::uuid AS group_id
|
|
162
|
+
UNION
|
|
163
|
+
SELECT gg.child_group_id
|
|
164
|
+
FROM auth.group_groups_v2 gg
|
|
165
|
+
JOIN auth.groups g_child ON g_child.id = gg.child_group_id
|
|
166
|
+
JOIN child_groups cg ON gg.parent_group_id = cg.group_id
|
|
167
|
+
WHERE g_child.provider = 'ipa'
|
|
168
|
+
)
|
|
169
|
+
SELECT DISTINCT u.id, u.uid, u.display_name
|
|
170
|
+
FROM auth.user_groups_v2 ug
|
|
171
|
+
JOIN child_groups cg ON ug.group_id = cg.group_id
|
|
172
|
+
JOIN auth.users u ON u.id = ug.user_id
|
|
173
|
+
WHERE u.provider = 'ipa'
|
|
174
|
+
ORDER BY u.uid
|
|
175
|
+
`
|
|
176
|
+
: await sql<DbRow[]>`
|
|
177
|
+
SELECT u.id, u.uid, u.display_name
|
|
178
|
+
FROM auth.user_groups_v2 ug
|
|
179
|
+
JOIN auth.users u ON u.id = ug.user_id
|
|
180
|
+
WHERE ug.group_id = ${group.id} AND u.provider = 'ipa'
|
|
181
|
+
ORDER BY u.uid
|
|
182
|
+
`;
|
|
183
|
+
|
|
184
|
+
for (const row of userRows) {
|
|
185
|
+
members.push({ type: "user", id: row.id as string, displayName: (row.display_name as string | null) ?? (row.uid as string) });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!params.type || params.type === "group") {
|
|
190
|
+
const groupRows = params.recursive
|
|
191
|
+
? await sql<DbRow[]>`
|
|
192
|
+
WITH RECURSIVE child_groups AS (
|
|
193
|
+
SELECT gg.child_group_id AS group_id
|
|
194
|
+
FROM auth.group_groups_v2 gg
|
|
195
|
+
JOIN auth.groups g_child ON g_child.id = gg.child_group_id
|
|
196
|
+
WHERE gg.parent_group_id = ${group.id} AND g_child.provider = 'ipa'
|
|
197
|
+
UNION
|
|
198
|
+
SELECT gg.child_group_id AS group_id
|
|
199
|
+
FROM auth.group_groups_v2 gg
|
|
200
|
+
JOIN auth.groups g_child ON g_child.id = gg.child_group_id
|
|
201
|
+
JOIN child_groups cg ON gg.parent_group_id = cg.group_id
|
|
202
|
+
WHERE g_child.provider = 'ipa'
|
|
203
|
+
)
|
|
204
|
+
SELECT DISTINCT g.id, g.name, g.description
|
|
205
|
+
FROM child_groups cg
|
|
206
|
+
JOIN auth.groups g ON g.id = cg.group_id
|
|
207
|
+
WHERE g.provider = 'ipa'
|
|
208
|
+
ORDER BY g.name
|
|
209
|
+
`
|
|
210
|
+
: await sql<DbRow[]>`
|
|
211
|
+
SELECT g.id, g.name, g.description
|
|
212
|
+
FROM auth.group_groups_v2 gg
|
|
213
|
+
JOIN auth.groups g ON g.id = gg.child_group_id
|
|
214
|
+
WHERE gg.parent_group_id = ${group.id} AND g.provider = 'ipa'
|
|
215
|
+
ORDER BY g.name
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
for (const row of groupRows) {
|
|
219
|
+
members.push({ type: "group", id: row.id as string, displayName: row.name as string });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return members;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const getManagers = async (params: { id: string; type?: "user" | "group"; recursive?: boolean }): Promise<GroupMember[]> => {
|
|
227
|
+
const group = await getIpaGroupById(params.id);
|
|
228
|
+
if (!group) return [];
|
|
229
|
+
|
|
230
|
+
const managers: GroupMember[] = [];
|
|
231
|
+
|
|
232
|
+
if (!params.type || params.type === "user") {
|
|
233
|
+
const userRows = params.recursive
|
|
234
|
+
? await sql<DbRow[]>`
|
|
235
|
+
WITH RECURSIVE manager_groups AS (
|
|
236
|
+
SELECT gmg.manager_group_id AS group_id
|
|
237
|
+
FROM auth.group_manager_groups_v2 gmg
|
|
238
|
+
JOIN auth.groups g_manager ON g_manager.id = gmg.manager_group_id
|
|
239
|
+
WHERE gmg.group_id = ${group.id} AND g_manager.provider = 'ipa'
|
|
240
|
+
UNION
|
|
241
|
+
SELECT gg.parent_group_id AS group_id
|
|
242
|
+
FROM auth.group_groups_v2 gg
|
|
243
|
+
JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
|
|
244
|
+
JOIN manager_groups mg ON gg.child_group_id = mg.group_id
|
|
245
|
+
WHERE g_parent.provider = 'ipa'
|
|
246
|
+
)
|
|
247
|
+
SELECT DISTINCT u.id, u.uid, u.display_name
|
|
248
|
+
FROM (
|
|
249
|
+
SELECT gmu.user_id
|
|
250
|
+
FROM auth.group_manager_users_v2 gmu
|
|
251
|
+
JOIN auth.users u_direct ON u_direct.id = gmu.user_id
|
|
252
|
+
WHERE gmu.group_id = ${group.id} AND u_direct.provider = 'ipa'
|
|
253
|
+
UNION
|
|
254
|
+
SELECT ug.user_id
|
|
255
|
+
FROM auth.user_groups_v2 ug
|
|
256
|
+
JOIN auth.users u_member ON u_member.id = ug.user_id
|
|
257
|
+
JOIN manager_groups mg ON ug.group_id = mg.group_id
|
|
258
|
+
WHERE u_member.provider = 'ipa'
|
|
259
|
+
) all_managers
|
|
260
|
+
JOIN auth.users u ON u.id = all_managers.user_id
|
|
261
|
+
ORDER BY u.uid
|
|
262
|
+
`
|
|
263
|
+
: await sql<DbRow[]>`
|
|
264
|
+
SELECT u.id, u.uid, u.display_name
|
|
265
|
+
FROM auth.group_manager_users_v2 gmu
|
|
266
|
+
JOIN auth.users u ON u.id = gmu.user_id
|
|
267
|
+
WHERE gmu.group_id = ${group.id} AND u.provider = 'ipa'
|
|
268
|
+
ORDER BY u.uid
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
for (const row of userRows) {
|
|
272
|
+
managers.push({ type: "user", id: row.id as string, displayName: (row.display_name as string | null) ?? (row.uid as string) });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!params.type || params.type === "group") {
|
|
277
|
+
const groupRows = await sql<DbRow[]>`
|
|
278
|
+
SELECT g.id, g.name, g.description
|
|
279
|
+
FROM auth.group_manager_groups_v2 gmg
|
|
280
|
+
JOIN auth.groups g ON g.id = gmg.manager_group_id
|
|
281
|
+
WHERE gmg.group_id = ${group.id} AND g.provider = 'ipa'
|
|
282
|
+
ORDER BY g.name
|
|
283
|
+
`;
|
|
284
|
+
|
|
285
|
+
for (const row of groupRows) {
|
|
286
|
+
managers.push({ type: "group", id: row.id as string, displayName: row.name as string });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return managers;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export const getParents = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
|
|
294
|
+
const group = await getIpaGroupById(params.id);
|
|
295
|
+
if (!group) return [];
|
|
296
|
+
|
|
297
|
+
const rows = params.recursive
|
|
298
|
+
? await sql<DbRow[]>`
|
|
299
|
+
WITH RECURSIVE parent_groups AS (
|
|
300
|
+
SELECT gg.parent_group_id AS group_id
|
|
301
|
+
FROM auth.group_groups_v2 gg
|
|
302
|
+
JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
|
|
303
|
+
WHERE gg.child_group_id = ${group.id} AND g_parent.provider = 'ipa'
|
|
304
|
+
UNION
|
|
305
|
+
SELECT gg.parent_group_id AS group_id
|
|
306
|
+
FROM auth.group_groups_v2 gg
|
|
307
|
+
JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
|
|
308
|
+
JOIN parent_groups pg ON gg.child_group_id = pg.group_id
|
|
309
|
+
WHERE g_parent.provider = 'ipa'
|
|
310
|
+
)
|
|
311
|
+
SELECT DISTINCT group_id AS parent_group_id
|
|
312
|
+
FROM parent_groups
|
|
313
|
+
`
|
|
314
|
+
: await sql<DbRow[]>`
|
|
315
|
+
SELECT gg.parent_group_id
|
|
316
|
+
FROM auth.group_groups_v2 gg
|
|
317
|
+
JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
|
|
318
|
+
WHERE gg.child_group_id = ${group.id} AND g_parent.provider = 'ipa'
|
|
319
|
+
`;
|
|
320
|
+
|
|
321
|
+
return rows.map((row) => row.parent_group_id as string);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export const getManagedGroups = async (params: { id: string }): Promise<string[]> => {
|
|
325
|
+
const group = await getIpaGroupById(params.id);
|
|
326
|
+
if (!group) return [];
|
|
327
|
+
|
|
328
|
+
const rows = await sql<DbRow[]>`
|
|
329
|
+
SELECT gmg.group_id
|
|
330
|
+
FROM auth.group_manager_groups_v2 gmg
|
|
331
|
+
JOIN auth.groups g ON g.id = gmg.group_id
|
|
332
|
+
WHERE gmg.manager_group_id = ${group.id} AND g.provider = 'ipa'
|
|
333
|
+
ORDER BY g.name
|
|
334
|
+
`;
|
|
335
|
+
|
|
336
|
+
return rows.map((row) => row.group_id as string);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export const add = async (params: {
|
|
340
|
+
ipaSession: string;
|
|
341
|
+
cn: string;
|
|
342
|
+
description?: string;
|
|
343
|
+
posix?: boolean;
|
|
344
|
+
}): Promise<MutationResult<BaseGroup>> => {
|
|
345
|
+
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
346
|
+
if (unavailable) return unavailable;
|
|
347
|
+
const options: Record<string, unknown> = { nonposix: params.posix ? false : true };
|
|
348
|
+
if (params.description) options.description = params.description;
|
|
349
|
+
|
|
350
|
+
const response = await freeipa.client.call({ url: await getIpaUrl(), ipaSession: params.ipaSession, method: "group_add", args: [params.cn], options });
|
|
351
|
+
if (response.error) return ipaMutationError(response);
|
|
352
|
+
|
|
353
|
+
const gidnumber = freeipa.util.num((response.result?.result as Record<string, unknown> | undefined)?.gidnumber);
|
|
354
|
+
const [row] = await sql<DbRow[]>`
|
|
355
|
+
INSERT INTO auth.groups (id, cn, name, provider, description, gid_number, synced_at)
|
|
356
|
+
VALUES (gen_random_uuid(), ${params.cn}, ${params.cn}, 'ipa', ${params.description ?? null}, ${gidnumber}, now())
|
|
357
|
+
ON CONFLICT (provider, name) DO UPDATE
|
|
358
|
+
SET cn = EXCLUDED.cn,
|
|
359
|
+
description = EXCLUDED.description,
|
|
360
|
+
gid_number = EXCLUDED.gid_number,
|
|
361
|
+
synced_at = now()
|
|
362
|
+
RETURNING id, provider, name, description, gid_number
|
|
363
|
+
`;
|
|
364
|
+
if (!row) return { ok: false, error: "Failed to persist IPA group mirror", status: 500 };
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
ok: true,
|
|
368
|
+
data: {
|
|
369
|
+
id: row.id as string,
|
|
370
|
+
provider: row.provider as "ipa" | "local",
|
|
371
|
+
name: row.name as string,
|
|
372
|
+
description: row.description as string | null,
|
|
373
|
+
gidnumber: row.gid_number as number | null,
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
export const update = async (params: { ipaSession: string; id: string; description: string }): Promise<MutationResult<void>> => {
|
|
379
|
+
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
380
|
+
if (unavailable) return unavailable;
|
|
381
|
+
const group = await getIpaGroupById(params.id);
|
|
382
|
+
if (!group) return { ok: false, error: "IPA group not found", status: 404 };
|
|
383
|
+
|
|
384
|
+
const response = await freeipa.client.call({
|
|
385
|
+
url: await getIpaUrl(),
|
|
386
|
+
ipaSession: params.ipaSession,
|
|
387
|
+
method: "group_mod",
|
|
388
|
+
args: [group.cn],
|
|
389
|
+
options: { description: params.description },
|
|
390
|
+
});
|
|
391
|
+
if (response.error) return ipaMutationError(response);
|
|
392
|
+
|
|
393
|
+
await sql`UPDATE auth.groups SET description = ${params.description}, synced_at = now() WHERE id = ${group.id}`;
|
|
394
|
+
return { ok: true, data: undefined };
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
export const del = async (params: { ipaSession: string; id: string }): Promise<MutationResult<void>> => {
|
|
398
|
+
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
399
|
+
if (unavailable) return unavailable;
|
|
400
|
+
const group = await getIpaGroupById(params.id);
|
|
401
|
+
if (!group) return { ok: false, error: "IPA group not found", status: 404 };
|
|
402
|
+
|
|
403
|
+
const response = await freeipa.client.call({ url: await getIpaUrl(), ipaSession: params.ipaSession, method: "group_del", args: [group.cn], options: {} });
|
|
404
|
+
if (response.error) return ipaMutationError(response);
|
|
405
|
+
|
|
406
|
+
await sql`DELETE FROM auth.groups WHERE id = ${group.id}`;
|
|
407
|
+
return { ok: true, data: undefined };
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export const makePosix = async (params: { ipaSession: string; id: string }): Promise<MutationResult<{ gidnumber: number | null }>> => {
|
|
411
|
+
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
412
|
+
if (unavailable) return unavailable;
|
|
413
|
+
const group = await getIpaGroupById(params.id);
|
|
414
|
+
if (!group) return { ok: false, error: "IPA group not found", status: 404 };
|
|
415
|
+
|
|
416
|
+
const response = await freeipa.client.call({ url: await getIpaUrl(), ipaSession: params.ipaSession, method: "group_mod", args: [group.cn], options: { posix: true } });
|
|
417
|
+
if (response.error) return ipaMutationError(response);
|
|
418
|
+
|
|
419
|
+
const gidnumber = freeipa.util.num((response.result?.result as Record<string, unknown> | undefined)?.gidnumber);
|
|
420
|
+
await sql`UPDATE auth.groups SET gid_number = ${gidnumber}, synced_at = now() WHERE id = ${group.id}`;
|
|
421
|
+
return { ok: true, data: { gidnumber } };
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
export const addMember = async (params: { ipaSession: string; id: string; user?: string; group?: string }): Promise<MutationResult<void>> => {
|
|
425
|
+
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
426
|
+
if (unavailable) return unavailable;
|
|
427
|
+
const group = await getIpaGroupById(params.id);
|
|
428
|
+
if (!group) return { ok: false, error: "IPA group not found", status: 404 };
|
|
429
|
+
|
|
430
|
+
let userUid: string | undefined;
|
|
431
|
+
if (params.user) {
|
|
432
|
+
const [userRow] = await sql<DbRow[]>`SELECT uid FROM auth.users WHERE id = ${params.user} AND provider = 'ipa'`;
|
|
433
|
+
if (!userRow) return { ok: false, error: "IPA user not found", status: 404 };
|
|
434
|
+
const [existing] = await sql<DbRow[]>`
|
|
435
|
+
SELECT 1
|
|
436
|
+
FROM auth.user_groups_v2
|
|
437
|
+
WHERE user_id = ${params.user}::uuid
|
|
438
|
+
AND group_id = ${group.id}::uuid
|
|
439
|
+
LIMIT 1
|
|
440
|
+
`;
|
|
441
|
+
if (existing) return { ok: false, error: "User is already a direct member of this group", status: 409 };
|
|
442
|
+
userUid = userRow.uid as string;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let childGroup: IpaGroupRow | null = null;
|
|
446
|
+
if (params.group) {
|
|
447
|
+
childGroup = await getIpaGroupById(params.group);
|
|
448
|
+
if (!childGroup) return { ok: false, error: "IPA group not found", status: 404 };
|
|
449
|
+
const [existing] = await sql<DbRow[]>`
|
|
450
|
+
SELECT 1
|
|
451
|
+
FROM auth.group_groups_v2
|
|
452
|
+
WHERE parent_group_id = ${group.id}::uuid
|
|
453
|
+
AND child_group_id = ${childGroup.id}::uuid
|
|
454
|
+
LIMIT 1
|
|
455
|
+
`;
|
|
456
|
+
if (existing) return { ok: false, error: "Group is already a direct member of this group", status: 409 };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const options: Record<string, unknown> = {};
|
|
460
|
+
if (userUid) options.user = userUid;
|
|
461
|
+
if (childGroup) options.group = childGroup.cn;
|
|
462
|
+
|
|
463
|
+
const response = await freeipa.client.call({
|
|
464
|
+
url: await getIpaUrl(),
|
|
465
|
+
ipaSession: params.ipaSession,
|
|
466
|
+
method: "group_add_member",
|
|
467
|
+
args: [group.cn],
|
|
468
|
+
options,
|
|
469
|
+
});
|
|
470
|
+
if (response.error) return ipaMutationError(response);
|
|
471
|
+
|
|
472
|
+
const result = response.result?.result as Record<string, unknown> | undefined;
|
|
473
|
+
const memberFailed = (result?.failed as Record<string, unknown> | undefined)?.member as Record<string, unknown> | undefined;
|
|
474
|
+
if (userUid && Array.isArray(memberFailed?.user) && memberFailed.user.length > 0) {
|
|
475
|
+
return { ok: false, error: (memberFailed.user[0] as [string, string])[1] || "Failed to add user to group", status: 400 };
|
|
476
|
+
}
|
|
477
|
+
if (childGroup && Array.isArray(memberFailed?.group) && memberFailed.group.length > 0) {
|
|
478
|
+
return { ok: false, error: (memberFailed.group[0] as [string, string])[1] || "Failed to add group to group", status: 400 };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (params.user) {
|
|
482
|
+
await sql`INSERT INTO auth.user_groups_v2 (user_id, group_id) VALUES (${params.user}, ${group.id}) ON CONFLICT DO NOTHING`;
|
|
483
|
+
await updateUserIpaProfile(params.user);
|
|
484
|
+
}
|
|
485
|
+
if (childGroup) {
|
|
486
|
+
await sql`INSERT INTO auth.group_groups_v2 (parent_group_id, child_group_id) VALUES (${group.id}, ${childGroup.id}) ON CONFLICT DO NOTHING`;
|
|
487
|
+
await updateProfileForAffectedUsers(childGroup.id);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return { ok: true, data: undefined };
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
export const removeMember = async (params: { ipaSession: string; id: string; user?: string; group?: string }): Promise<MutationResult<void>> => {
|
|
494
|
+
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
495
|
+
if (unavailable) return unavailable;
|
|
496
|
+
const group = await getIpaGroupById(params.id);
|
|
497
|
+
if (!group) return { ok: false, error: "IPA group not found", status: 404 };
|
|
498
|
+
|
|
499
|
+
let userUid: string | undefined;
|
|
500
|
+
if (params.user) {
|
|
501
|
+
const [userRow] = await sql<DbRow[]>`SELECT uid FROM auth.users WHERE id = ${params.user} AND provider = 'ipa'`;
|
|
502
|
+
if (!userRow) return { ok: false, error: "IPA user not found", status: 404 };
|
|
503
|
+
userUid = userRow.uid as string;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let childGroup: IpaGroupRow | null = null;
|
|
507
|
+
if (params.group) {
|
|
508
|
+
childGroup = await getIpaGroupById(params.group);
|
|
509
|
+
if (!childGroup) return { ok: false, error: "IPA group not found", status: 404 };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const options: Record<string, unknown> = {};
|
|
513
|
+
if (userUid) options.user = userUid;
|
|
514
|
+
if (childGroup) options.group = childGroup.cn;
|
|
515
|
+
|
|
516
|
+
const response = await freeipa.client.call({
|
|
517
|
+
url: await getIpaUrl(),
|
|
518
|
+
ipaSession: params.ipaSession,
|
|
519
|
+
method: "group_remove_member",
|
|
520
|
+
args: [group.cn],
|
|
521
|
+
options,
|
|
522
|
+
});
|
|
523
|
+
if (response.error) return ipaMutationError(response);
|
|
524
|
+
|
|
525
|
+
const result = response.result?.result as Record<string, unknown> | undefined;
|
|
526
|
+
const memberFailed = (result?.failed as Record<string, unknown> | undefined)?.member as Record<string, unknown> | undefined;
|
|
527
|
+
if (userUid && Array.isArray(memberFailed?.user) && memberFailed.user.length > 0) {
|
|
528
|
+
return { ok: false, error: (memberFailed.user[0] as [string, string])[1] || "Failed to remove user from group", status: 400 };
|
|
529
|
+
}
|
|
530
|
+
if (childGroup && Array.isArray(memberFailed?.group) && memberFailed.group.length > 0) {
|
|
531
|
+
return { ok: false, error: (memberFailed.group[0] as [string, string])[1] || "Failed to remove group from group", status: 400 };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (params.user) {
|
|
535
|
+
await sql`DELETE FROM auth.user_groups_v2 WHERE user_id = ${params.user} AND group_id = ${group.id}`;
|
|
536
|
+
await updateUserIpaProfile(params.user);
|
|
537
|
+
}
|
|
538
|
+
if (childGroup) {
|
|
539
|
+
const affectedUsers = await sql<DbRow[]>`
|
|
540
|
+
WITH RECURSIVE child_groups AS (
|
|
541
|
+
SELECT ${childGroup.id}::uuid AS group_id
|
|
542
|
+
UNION
|
|
543
|
+
SELECT gg.child_group_id
|
|
544
|
+
FROM auth.group_groups_v2 gg
|
|
545
|
+
JOIN child_groups cg ON gg.parent_group_id = cg.group_id
|
|
546
|
+
)
|
|
547
|
+
SELECT DISTINCT ug.user_id
|
|
548
|
+
FROM auth.user_groups_v2 ug
|
|
549
|
+
JOIN child_groups cg ON ug.group_id = cg.group_id
|
|
550
|
+
`;
|
|
551
|
+
|
|
552
|
+
await sql`DELETE FROM auth.group_groups_v2 WHERE parent_group_id = ${group.id} AND child_group_id = ${childGroup.id}`;
|
|
553
|
+
for (const row of affectedUsers) {
|
|
554
|
+
await updateUserIpaProfile(row.user_id as string);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return { ok: true, data: undefined };
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
export const addManager = async (params: { ipaSession: string; id: string; user?: string; group?: string }): Promise<MutationResult<void>> => {
|
|
562
|
+
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
563
|
+
if (unavailable) return unavailable;
|
|
564
|
+
const group = await getIpaGroupById(params.id);
|
|
565
|
+
if (!group) return { ok: false, error: "IPA group not found", status: 404 };
|
|
566
|
+
|
|
567
|
+
let userUid: string | undefined;
|
|
568
|
+
if (params.user) {
|
|
569
|
+
const [userRow] = await sql<DbRow[]>`SELECT uid FROM auth.users WHERE id = ${params.user} AND provider = 'ipa'`;
|
|
570
|
+
if (!userRow) return { ok: false, error: "IPA user not found", status: 404 };
|
|
571
|
+
const [existing] = await sql<DbRow[]>`
|
|
572
|
+
SELECT 1
|
|
573
|
+
FROM auth.group_manager_users_v2
|
|
574
|
+
WHERE group_id = ${group.id}::uuid
|
|
575
|
+
AND user_id = ${params.user}::uuid
|
|
576
|
+
LIMIT 1
|
|
577
|
+
`;
|
|
578
|
+
if (existing) return { ok: false, error: "User is already a direct manager of this group", status: 409 };
|
|
579
|
+
userUid = userRow.uid as string;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let managerGroup: IpaGroupRow | null = null;
|
|
583
|
+
if (params.group) {
|
|
584
|
+
managerGroup = await getIpaGroupById(params.group);
|
|
585
|
+
if (!managerGroup) return { ok: false, error: "IPA group not found", status: 404 };
|
|
586
|
+
const [existing] = await sql<DbRow[]>`
|
|
587
|
+
SELECT 1
|
|
588
|
+
FROM auth.group_manager_groups_v2
|
|
589
|
+
WHERE group_id = ${group.id}::uuid
|
|
590
|
+
AND manager_group_id = ${managerGroup.id}::uuid
|
|
591
|
+
LIMIT 1
|
|
592
|
+
`;
|
|
593
|
+
if (existing) return { ok: false, error: "Group is already a direct manager of this group", status: 409 };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const options: Record<string, unknown> = {};
|
|
597
|
+
if (userUid) options.user = userUid;
|
|
598
|
+
if (managerGroup) options.group = managerGroup.cn;
|
|
599
|
+
|
|
600
|
+
const response = await freeipa.client.call({
|
|
601
|
+
url: await getIpaUrl(),
|
|
602
|
+
ipaSession: params.ipaSession,
|
|
603
|
+
method: "group_add_member_manager",
|
|
604
|
+
args: [group.cn],
|
|
605
|
+
options,
|
|
606
|
+
});
|
|
607
|
+
if (response.error) return ipaMutationError(response);
|
|
608
|
+
|
|
609
|
+
const result = response.result?.result as Record<string, unknown> | undefined;
|
|
610
|
+
const managerFailed = (result?.failed as Record<string, unknown> | undefined)?.membermanager as Record<string, unknown> | undefined;
|
|
611
|
+
if (userUid && Array.isArray(managerFailed?.user) && managerFailed.user.length > 0) {
|
|
612
|
+
return { ok: false, error: (managerFailed.user[0] as [string, string])[1] || "Failed to add user as manager", status: 400 };
|
|
613
|
+
}
|
|
614
|
+
if (managerGroup && Array.isArray(managerFailed?.group) && managerFailed.group.length > 0) {
|
|
615
|
+
return { ok: false, error: (managerFailed.group[0] as [string, string])[1] || "Failed to add group as manager", status: 400 };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (params.user) {
|
|
619
|
+
await sql`INSERT INTO auth.group_manager_users_v2 (group_id, user_id) VALUES (${group.id}, ${params.user}) ON CONFLICT DO NOTHING`;
|
|
620
|
+
}
|
|
621
|
+
if (managerGroup) {
|
|
622
|
+
await sql`INSERT INTO auth.group_manager_groups_v2 (group_id, manager_group_id) VALUES (${group.id}, ${managerGroup.id}) ON CONFLICT DO NOTHING`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return { ok: true, data: undefined };
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
export const removeManager = async (params: { ipaSession: string; id: string; user?: string; group?: string }): Promise<MutationResult<void>> => {
|
|
629
|
+
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
630
|
+
if (unavailable) return unavailable;
|
|
631
|
+
const group = await getIpaGroupById(params.id);
|
|
632
|
+
if (!group) return { ok: false, error: "IPA group not found", status: 404 };
|
|
633
|
+
|
|
634
|
+
let userUid: string | undefined;
|
|
635
|
+
if (params.user) {
|
|
636
|
+
const [userRow] = await sql<DbRow[]>`SELECT uid FROM auth.users WHERE id = ${params.user} AND provider = 'ipa'`;
|
|
637
|
+
if (!userRow) return { ok: false, error: "IPA user not found", status: 404 };
|
|
638
|
+
userUid = userRow.uid as string;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
let managerGroup: IpaGroupRow | null = null;
|
|
642
|
+
if (params.group) {
|
|
643
|
+
managerGroup = await getIpaGroupById(params.group);
|
|
644
|
+
if (!managerGroup) return { ok: false, error: "IPA group not found", status: 404 };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const options: Record<string, unknown> = {};
|
|
648
|
+
if (userUid) options.user = userUid;
|
|
649
|
+
if (managerGroup) options.group = managerGroup.cn;
|
|
650
|
+
|
|
651
|
+
const response = await freeipa.client.call({
|
|
652
|
+
url: await getIpaUrl(),
|
|
653
|
+
ipaSession: params.ipaSession,
|
|
654
|
+
method: "group_remove_member_manager",
|
|
655
|
+
args: [group.cn],
|
|
656
|
+
options,
|
|
657
|
+
});
|
|
658
|
+
if (response.error) return ipaMutationError(response);
|
|
659
|
+
|
|
660
|
+
const result = response.result?.result as Record<string, unknown> | undefined;
|
|
661
|
+
const managerFailed = (result?.failed as Record<string, unknown> | undefined)?.membermanager as Record<string, unknown> | undefined;
|
|
662
|
+
if (userUid && Array.isArray(managerFailed?.user) && managerFailed.user.length > 0) {
|
|
663
|
+
return { ok: false, error: (managerFailed.user[0] as [string, string])[1] || "Failed to remove user as manager", status: 400 };
|
|
664
|
+
}
|
|
665
|
+
if (managerGroup && Array.isArray(managerFailed?.group) && managerFailed.group.length > 0) {
|
|
666
|
+
return { ok: false, error: (managerFailed.group[0] as [string, string])[1] || "Failed to remove group as manager", status: 400 };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (params.user) {
|
|
670
|
+
await sql`DELETE FROM auth.group_manager_users_v2 WHERE group_id = ${group.id} AND user_id = ${params.user}`;
|
|
671
|
+
}
|
|
672
|
+
if (managerGroup) {
|
|
673
|
+
await sql`DELETE FROM auth.group_manager_groups_v2 WHERE group_id = ${group.id} AND manager_group_id = ${managerGroup.id}`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return { ok: true, data: undefined };
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
export const getManagedGroupsByName = async (params: { id: string }): Promise<string[]> => {
|
|
680
|
+
const ids = await getManagedGroups(params);
|
|
681
|
+
if (ids.length === 0) return [];
|
|
682
|
+
const rows = await sql<DbRow[]>`SELECT name FROM auth.groups WHERE id = ANY(${toPgUuidArray(ids)}::uuid[]) ORDER BY name`;
|
|
683
|
+
return rows.map((row) => row.name as string);
|
|
684
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MutationResult } from "../../contracts/shared";
|
|
2
|
+
import { getFreeIpaConfig } from "../freeipa-config";
|
|
3
|
+
|
|
4
|
+
type MutationError = Extract<MutationResult<unknown>, { ok: false }>;
|
|
5
|
+
|
|
6
|
+
export const getIpaUrl = async (): Promise<string> => (await getFreeIpaConfig()).url;
|
|
7
|
+
|
|
8
|
+
export const ensureFreeIpaMutationAvailable = async (): Promise<MutationError | null> => {
|
|
9
|
+
const config = await getFreeIpaConfig();
|
|
10
|
+
if (!config.enabled) {
|
|
11
|
+
return { ok: false, error: "FreeIPA is disabled.", status: 400 };
|
|
12
|
+
}
|
|
13
|
+
if (!config.configured) {
|
|
14
|
+
return { ok: false, error: "FreeIPA is enabled but not fully configured.", status: 500 };
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
};
|