@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,22 @@
|
|
|
1
|
+
import type { Role, UserProfile, UserProvider } from "../../contracts/shared";
|
|
2
|
+
|
|
3
|
+
export const buildRoles = (params: {
|
|
4
|
+
provider: UserProvider;
|
|
5
|
+
profile: UserProfile;
|
|
6
|
+
memberofGroup: string[];
|
|
7
|
+
manages: string[];
|
|
8
|
+
admin?: boolean;
|
|
9
|
+
}): Role[] => {
|
|
10
|
+
const { provider, profile, manages } = params;
|
|
11
|
+
const roles = new Set<Role>();
|
|
12
|
+
|
|
13
|
+
roles.add(profile);
|
|
14
|
+
roles.add(provider);
|
|
15
|
+
roles.add(`${provider}/${profile}` as Extract<Role, "ipa/user" | "ipa/guest" | "local/user" | "local/guest">);
|
|
16
|
+
|
|
17
|
+
if (profile === "guest") return [...roles];
|
|
18
|
+
|
|
19
|
+
if (params.admin) roles.add("admin");
|
|
20
|
+
if (manages.length > 0) roles.add("group-manager");
|
|
21
|
+
return [...roles];
|
|
22
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BaseGroup, UserProvider } from "../../contracts/shared";
|
|
2
|
+
|
|
3
|
+
type DbRow = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export const buildBaseGroup = (row: DbRow): BaseGroup => ({
|
|
6
|
+
id: row.id as string,
|
|
7
|
+
provider: row.provider as UserProvider,
|
|
8
|
+
name: row.name as string,
|
|
9
|
+
description: (row.description as string | null | undefined) ?? null,
|
|
10
|
+
gidnumber: (row.gid_number as number | null | undefined) ?? null,
|
|
11
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BaseUser, UserProfile, UserProvider } from "../../contracts/shared";
|
|
2
|
+
import { buildRoles } from "./authz";
|
|
3
|
+
|
|
4
|
+
type DbRow = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export const resolveProviderProfile = (row: DbRow): { provider: UserProvider; profile: UserProfile } => ({
|
|
7
|
+
provider: (row.provider as UserProvider | null | undefined) ?? "local",
|
|
8
|
+
profile: (row.profile as UserProfile | null | undefined) ?? "guest",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const resolveBaseUserDisplayName = (row: DbRow): string => {
|
|
12
|
+
const displayName = (row.display_name as string | null | undefined) ?? "";
|
|
13
|
+
const mail = (row.mail as string | null | undefined) ?? "";
|
|
14
|
+
const uid = (row.uid as string | null | undefined) ?? "";
|
|
15
|
+
return displayName || mail || uid;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a BaseUser from a DB row. `admin` is taken from `row.effective_admin`
|
|
20
|
+
* when present (list queries pre-compute it by joining IPA-admin group
|
|
21
|
+
* membership), otherwise from `row.admin` for local users. The previous
|
|
22
|
+
* implementation routed through `resolveEffectiveAdminState` with an empty
|
|
23
|
+
* `memberofGroup` list, which silently dropped the admin role for IPA users.
|
|
24
|
+
*/
|
|
25
|
+
export const buildBaseUser = (row: DbRow): BaseUser => {
|
|
26
|
+
const { provider, profile } = resolveProviderProfile(row);
|
|
27
|
+
const effectiveAdmin = row.effective_admin !== undefined ? Boolean(row.effective_admin) : Boolean(row.admin);
|
|
28
|
+
return {
|
|
29
|
+
id: row.id as string,
|
|
30
|
+
uid: row.uid as string,
|
|
31
|
+
roles: buildRoles({
|
|
32
|
+
provider,
|
|
33
|
+
profile,
|
|
34
|
+
memberofGroup: [],
|
|
35
|
+
manages: [],
|
|
36
|
+
admin: effectiveAdmin,
|
|
37
|
+
}),
|
|
38
|
+
provider,
|
|
39
|
+
profile,
|
|
40
|
+
givenname: (row.given_name as string) ?? "",
|
|
41
|
+
sn: (row.sn as string) ?? "",
|
|
42
|
+
displayName: resolveBaseUserDisplayName(row),
|
|
43
|
+
mail: (row.mail as string) ?? null,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import type {
|
|
3
|
+
EntityKind,
|
|
4
|
+
EntityListItem,
|
|
5
|
+
UserProfile,
|
|
6
|
+
UserProvider,
|
|
7
|
+
} from "../../contracts/shared";
|
|
8
|
+
import { getFreeIpaConfig } from "../freeipa-config";
|
|
9
|
+
import { escapeLikePattern, toPgTextArray, toPgUuidArray } from "../postgres";
|
|
10
|
+
import { buildBaseGroup } from "./base-group";
|
|
11
|
+
import { buildBaseUser } from "./base-user";
|
|
12
|
+
import { buildManagedGroupScopeCondition } from "./group-sql";
|
|
13
|
+
|
|
14
|
+
type DbRow = Record<string, unknown>;
|
|
15
|
+
type SqlFragment = any;
|
|
16
|
+
|
|
17
|
+
export type EntityListParams = {
|
|
18
|
+
search?: string;
|
|
19
|
+
kinds?: EntityKind[];
|
|
20
|
+
provider?: UserProvider;
|
|
21
|
+
profile?: UserProfile;
|
|
22
|
+
excludeUserIds?: string[];
|
|
23
|
+
excludeGroupIds?: string[];
|
|
24
|
+
userMemberOfGroupIds?: string[];
|
|
25
|
+
memberOfGroupId?: string;
|
|
26
|
+
managerOfGroupId?: string;
|
|
27
|
+
parentGroupId?: string;
|
|
28
|
+
managedByUserId?: string;
|
|
29
|
+
recursive?: boolean;
|
|
30
|
+
page?: number;
|
|
31
|
+
perPage?: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type EntityQuerySpec = {
|
|
35
|
+
recursive: boolean;
|
|
36
|
+
prelude: SqlFragment;
|
|
37
|
+
userFrom: SqlFragment;
|
|
38
|
+
userDirectExpr: SqlFragment;
|
|
39
|
+
userWhere: SqlFragment;
|
|
40
|
+
groupFrom: SqlFragment;
|
|
41
|
+
groupDirectExpr: SqlFragment;
|
|
42
|
+
groupWhere: SqlFragment;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const buildNoRelationSpec = (): EntityQuerySpec => ({
|
|
46
|
+
recursive: false,
|
|
47
|
+
prelude: sql`scope_seed AS (SELECT 1 AS seed)`,
|
|
48
|
+
userFrom: sql`FROM auth.users u`,
|
|
49
|
+
userDirectExpr: sql`NULL::boolean`,
|
|
50
|
+
userWhere: sql`TRUE`,
|
|
51
|
+
groupFrom: sql`FROM auth.groups g`,
|
|
52
|
+
groupDirectExpr: sql`NULL::boolean`,
|
|
53
|
+
groupWhere: sql`TRUE`,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const buildMemberOfGroupSpec = (groupId: string, recursive: boolean): EntityQuerySpec => {
|
|
57
|
+
if (recursive) {
|
|
58
|
+
return {
|
|
59
|
+
recursive: true,
|
|
60
|
+
prelude: sql`
|
|
61
|
+
target_group AS (
|
|
62
|
+
SELECT id, provider
|
|
63
|
+
FROM auth.groups
|
|
64
|
+
WHERE id = ${groupId}::uuid
|
|
65
|
+
),
|
|
66
|
+
member_group_tree AS (
|
|
67
|
+
SELECT gg.child_group_id AS group_id
|
|
68
|
+
FROM target_group tg
|
|
69
|
+
JOIN auth.group_groups_v2 gg ON gg.parent_group_id = tg.id
|
|
70
|
+
JOIN auth.groups g_child ON g_child.id = gg.child_group_id
|
|
71
|
+
WHERE g_child.provider = tg.provider
|
|
72
|
+
UNION
|
|
73
|
+
SELECT gg.child_group_id AS group_id
|
|
74
|
+
FROM auth.group_groups_v2 gg
|
|
75
|
+
JOIN auth.groups g_child ON g_child.id = gg.child_group_id
|
|
76
|
+
JOIN member_group_tree tree ON gg.parent_group_id = tree.group_id
|
|
77
|
+
JOIN target_group tg ON TRUE
|
|
78
|
+
WHERE g_child.provider = tg.provider
|
|
79
|
+
),
|
|
80
|
+
member_user_rel AS (
|
|
81
|
+
SELECT ug.user_id AS entity_id, TRUE AS direct
|
|
82
|
+
FROM auth.user_groups_v2 ug
|
|
83
|
+
JOIN target_group tg ON ug.group_id = tg.id
|
|
84
|
+
JOIN auth.users u ON u.id = ug.user_id
|
|
85
|
+
WHERE tg.provider <> 'ipa' OR u.provider = 'ipa'
|
|
86
|
+
UNION ALL
|
|
87
|
+
SELECT ug.user_id AS entity_id, FALSE AS direct
|
|
88
|
+
FROM auth.user_groups_v2 ug
|
|
89
|
+
JOIN member_group_tree tree ON ug.group_id = tree.group_id
|
|
90
|
+
JOIN target_group tg ON TRUE
|
|
91
|
+
JOIN auth.users u ON u.id = ug.user_id
|
|
92
|
+
WHERE tg.provider <> 'ipa' OR u.provider = 'ipa'
|
|
93
|
+
),
|
|
94
|
+
member_group_rel AS (
|
|
95
|
+
SELECT gg.child_group_id AS entity_id, TRUE AS direct
|
|
96
|
+
FROM target_group tg
|
|
97
|
+
JOIN auth.group_groups_v2 gg ON gg.parent_group_id = tg.id
|
|
98
|
+
JOIN auth.groups g ON g.id = gg.child_group_id
|
|
99
|
+
WHERE g.provider = tg.provider
|
|
100
|
+
UNION ALL
|
|
101
|
+
SELECT tree.group_id AS entity_id, FALSE AS direct
|
|
102
|
+
FROM member_group_tree tree
|
|
103
|
+
),
|
|
104
|
+
relation_rows AS (
|
|
105
|
+
SELECT 'user'::text AS kind, entity_id, BOOL_OR(direct) AS direct
|
|
106
|
+
FROM member_user_rel
|
|
107
|
+
GROUP BY entity_id
|
|
108
|
+
UNION ALL
|
|
109
|
+
SELECT 'group'::text AS kind, entity_id, BOOL_OR(direct) AS direct
|
|
110
|
+
FROM member_group_rel
|
|
111
|
+
GROUP BY entity_id
|
|
112
|
+
)
|
|
113
|
+
`,
|
|
114
|
+
userFrom: sql`FROM auth.users u JOIN relation_rows rr ON rr.kind = 'user' AND rr.entity_id = u.id`,
|
|
115
|
+
userDirectExpr: sql`rr.direct`,
|
|
116
|
+
userWhere: sql`TRUE`,
|
|
117
|
+
groupFrom: sql`FROM auth.groups g JOIN relation_rows rr ON rr.kind = 'group' AND rr.entity_id = g.id`,
|
|
118
|
+
groupDirectExpr: sql`rr.direct`,
|
|
119
|
+
groupWhere: sql`TRUE`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
recursive: false,
|
|
125
|
+
prelude: sql`
|
|
126
|
+
target_group AS (
|
|
127
|
+
SELECT id, provider
|
|
128
|
+
FROM auth.groups
|
|
129
|
+
WHERE id = ${groupId}::uuid
|
|
130
|
+
),
|
|
131
|
+
relation_rows AS (
|
|
132
|
+
SELECT 'user'::text AS kind, ug.user_id AS entity_id, TRUE AS direct
|
|
133
|
+
FROM auth.user_groups_v2 ug
|
|
134
|
+
JOIN target_group tg ON ug.group_id = tg.id
|
|
135
|
+
JOIN auth.users u ON u.id = ug.user_id
|
|
136
|
+
WHERE tg.provider <> 'ipa' OR u.provider = 'ipa'
|
|
137
|
+
UNION ALL
|
|
138
|
+
SELECT 'group'::text AS kind, gg.child_group_id AS entity_id, TRUE AS direct
|
|
139
|
+
FROM target_group tg
|
|
140
|
+
JOIN auth.group_groups_v2 gg ON gg.parent_group_id = tg.id
|
|
141
|
+
JOIN auth.groups g ON g.id = gg.child_group_id
|
|
142
|
+
WHERE g.provider = tg.provider
|
|
143
|
+
)
|
|
144
|
+
`,
|
|
145
|
+
userFrom: sql`FROM auth.users u JOIN relation_rows rr ON rr.kind = 'user' AND rr.entity_id = u.id`,
|
|
146
|
+
userDirectExpr: sql`rr.direct`,
|
|
147
|
+
userWhere: sql`TRUE`,
|
|
148
|
+
groupFrom: sql`FROM auth.groups g JOIN relation_rows rr ON rr.kind = 'group' AND rr.entity_id = g.id`,
|
|
149
|
+
groupDirectExpr: sql`rr.direct`,
|
|
150
|
+
groupWhere: sql`TRUE`,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const buildManagerOfGroupSpec = (groupId: string, recursive: boolean): EntityQuerySpec => {
|
|
155
|
+
if (recursive) {
|
|
156
|
+
return {
|
|
157
|
+
recursive: true,
|
|
158
|
+
prelude: sql`
|
|
159
|
+
target_group AS (
|
|
160
|
+
SELECT id, provider
|
|
161
|
+
FROM auth.groups
|
|
162
|
+
WHERE id = ${groupId}::uuid
|
|
163
|
+
),
|
|
164
|
+
manager_group_tree AS (
|
|
165
|
+
SELECT gmg.manager_group_id AS group_id
|
|
166
|
+
FROM target_group tg
|
|
167
|
+
JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = tg.id
|
|
168
|
+
JOIN auth.groups g_manager ON g_manager.id = gmg.manager_group_id
|
|
169
|
+
WHERE g_manager.provider = tg.provider
|
|
170
|
+
UNION
|
|
171
|
+
SELECT gg.parent_group_id AS group_id
|
|
172
|
+
FROM auth.group_groups_v2 gg
|
|
173
|
+
JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
|
|
174
|
+
JOIN manager_group_tree tree ON gg.child_group_id = tree.group_id
|
|
175
|
+
JOIN target_group tg ON TRUE
|
|
176
|
+
WHERE g_parent.provider = tg.provider
|
|
177
|
+
),
|
|
178
|
+
manager_user_rel AS (
|
|
179
|
+
SELECT gmu.user_id AS entity_id, TRUE AS direct
|
|
180
|
+
FROM auth.group_manager_users_v2 gmu
|
|
181
|
+
JOIN target_group tg ON gmu.group_id = tg.id
|
|
182
|
+
JOIN auth.users u ON u.id = gmu.user_id
|
|
183
|
+
WHERE tg.provider <> 'ipa' OR u.provider = 'ipa'
|
|
184
|
+
UNION ALL
|
|
185
|
+
SELECT ug.user_id AS entity_id, FALSE AS direct
|
|
186
|
+
FROM auth.user_groups_v2 ug
|
|
187
|
+
JOIN manager_group_tree tree ON ug.group_id = tree.group_id
|
|
188
|
+
JOIN target_group tg ON TRUE
|
|
189
|
+
JOIN auth.users u ON u.id = ug.user_id
|
|
190
|
+
WHERE tg.provider <> 'ipa' OR u.provider = 'ipa'
|
|
191
|
+
),
|
|
192
|
+
manager_group_rel AS (
|
|
193
|
+
SELECT gmg.manager_group_id AS entity_id, TRUE AS direct
|
|
194
|
+
FROM target_group tg
|
|
195
|
+
JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = tg.id
|
|
196
|
+
JOIN auth.groups g ON g.id = gmg.manager_group_id
|
|
197
|
+
WHERE g.provider = tg.provider
|
|
198
|
+
UNION ALL
|
|
199
|
+
SELECT tree.group_id AS entity_id, FALSE AS direct
|
|
200
|
+
FROM manager_group_tree tree
|
|
201
|
+
),
|
|
202
|
+
relation_rows AS (
|
|
203
|
+
SELECT 'user'::text AS kind, entity_id, BOOL_OR(direct) AS direct
|
|
204
|
+
FROM manager_user_rel
|
|
205
|
+
GROUP BY entity_id
|
|
206
|
+
UNION ALL
|
|
207
|
+
SELECT 'group'::text AS kind, entity_id, BOOL_OR(direct) AS direct
|
|
208
|
+
FROM manager_group_rel
|
|
209
|
+
GROUP BY entity_id
|
|
210
|
+
)
|
|
211
|
+
`,
|
|
212
|
+
userFrom: sql`FROM auth.users u JOIN relation_rows rr ON rr.kind = 'user' AND rr.entity_id = u.id`,
|
|
213
|
+
userDirectExpr: sql`rr.direct`,
|
|
214
|
+
userWhere: sql`TRUE`,
|
|
215
|
+
groupFrom: sql`FROM auth.groups g JOIN relation_rows rr ON rr.kind = 'group' AND rr.entity_id = g.id`,
|
|
216
|
+
groupDirectExpr: sql`rr.direct`,
|
|
217
|
+
groupWhere: sql`TRUE`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
recursive: false,
|
|
223
|
+
prelude: sql`
|
|
224
|
+
target_group AS (
|
|
225
|
+
SELECT id, provider
|
|
226
|
+
FROM auth.groups
|
|
227
|
+
WHERE id = ${groupId}::uuid
|
|
228
|
+
),
|
|
229
|
+
relation_rows AS (
|
|
230
|
+
SELECT 'user'::text AS kind, gmu.user_id AS entity_id, TRUE AS direct
|
|
231
|
+
FROM auth.group_manager_users_v2 gmu
|
|
232
|
+
JOIN target_group tg ON gmu.group_id = tg.id
|
|
233
|
+
JOIN auth.users u ON u.id = gmu.user_id
|
|
234
|
+
WHERE tg.provider <> 'ipa' OR u.provider = 'ipa'
|
|
235
|
+
UNION ALL
|
|
236
|
+
SELECT 'group'::text AS kind, gmg.manager_group_id AS entity_id, TRUE AS direct
|
|
237
|
+
FROM target_group tg
|
|
238
|
+
JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = tg.id
|
|
239
|
+
JOIN auth.groups g ON g.id = gmg.manager_group_id
|
|
240
|
+
WHERE g.provider = tg.provider
|
|
241
|
+
)
|
|
242
|
+
`,
|
|
243
|
+
userFrom: sql`FROM auth.users u JOIN relation_rows rr ON rr.kind = 'user' AND rr.entity_id = u.id`,
|
|
244
|
+
userDirectExpr: sql`rr.direct`,
|
|
245
|
+
userWhere: sql`TRUE`,
|
|
246
|
+
groupFrom: sql`FROM auth.groups g JOIN relation_rows rr ON rr.kind = 'group' AND rr.entity_id = g.id`,
|
|
247
|
+
groupDirectExpr: sql`rr.direct`,
|
|
248
|
+
groupWhere: sql`TRUE`,
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const buildParentGroupSpec = (groupId: string, recursive: boolean): EntityQuerySpec => {
|
|
253
|
+
if (recursive) {
|
|
254
|
+
return {
|
|
255
|
+
recursive: true,
|
|
256
|
+
prelude: sql`
|
|
257
|
+
target_group AS (
|
|
258
|
+
SELECT id, provider
|
|
259
|
+
FROM auth.groups
|
|
260
|
+
WHERE id = ${groupId}::uuid
|
|
261
|
+
),
|
|
262
|
+
parent_group_tree AS (
|
|
263
|
+
SELECT gg.parent_group_id AS group_id
|
|
264
|
+
FROM target_group tg
|
|
265
|
+
JOIN auth.group_groups_v2 gg ON gg.child_group_id = tg.id
|
|
266
|
+
JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
|
|
267
|
+
WHERE g_parent.provider = tg.provider
|
|
268
|
+
UNION
|
|
269
|
+
SELECT gg.parent_group_id AS group_id
|
|
270
|
+
FROM auth.group_groups_v2 gg
|
|
271
|
+
JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
|
|
272
|
+
JOIN parent_group_tree tree ON gg.child_group_id = tree.group_id
|
|
273
|
+
JOIN target_group tg ON TRUE
|
|
274
|
+
WHERE g_parent.provider = tg.provider
|
|
275
|
+
),
|
|
276
|
+
parent_group_rel AS (
|
|
277
|
+
SELECT gg.parent_group_id AS entity_id, TRUE AS direct
|
|
278
|
+
FROM target_group tg
|
|
279
|
+
JOIN auth.group_groups_v2 gg ON gg.child_group_id = tg.id
|
|
280
|
+
JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
|
|
281
|
+
WHERE g_parent.provider = tg.provider
|
|
282
|
+
UNION ALL
|
|
283
|
+
SELECT tree.group_id AS entity_id, FALSE AS direct
|
|
284
|
+
FROM parent_group_tree tree
|
|
285
|
+
),
|
|
286
|
+
relation_rows AS (
|
|
287
|
+
SELECT 'group'::text AS kind, entity_id, BOOL_OR(direct) AS direct
|
|
288
|
+
FROM parent_group_rel
|
|
289
|
+
GROUP BY entity_id
|
|
290
|
+
)
|
|
291
|
+
`,
|
|
292
|
+
userFrom: sql`FROM auth.users u JOIN relation_rows rr ON rr.kind = 'user' AND rr.entity_id = u.id`,
|
|
293
|
+
userDirectExpr: sql`rr.direct`,
|
|
294
|
+
userWhere: sql`FALSE`,
|
|
295
|
+
groupFrom: sql`FROM auth.groups g JOIN relation_rows rr ON rr.kind = 'group' AND rr.entity_id = g.id`,
|
|
296
|
+
groupDirectExpr: sql`rr.direct`,
|
|
297
|
+
groupWhere: sql`TRUE`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
recursive: false,
|
|
303
|
+
prelude: sql`
|
|
304
|
+
target_group AS (
|
|
305
|
+
SELECT id, provider
|
|
306
|
+
FROM auth.groups
|
|
307
|
+
WHERE id = ${groupId}::uuid
|
|
308
|
+
),
|
|
309
|
+
relation_rows AS (
|
|
310
|
+
SELECT 'group'::text AS kind, gg.parent_group_id AS entity_id, TRUE AS direct
|
|
311
|
+
FROM target_group tg
|
|
312
|
+
JOIN auth.group_groups_v2 gg ON gg.child_group_id = tg.id
|
|
313
|
+
JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
|
|
314
|
+
WHERE g_parent.provider = tg.provider
|
|
315
|
+
)
|
|
316
|
+
`,
|
|
317
|
+
userFrom: sql`FROM auth.users u JOIN relation_rows rr ON rr.kind = 'user' AND rr.entity_id = u.id`,
|
|
318
|
+
userDirectExpr: sql`rr.direct`,
|
|
319
|
+
userWhere: sql`FALSE`,
|
|
320
|
+
groupFrom: sql`FROM auth.groups g JOIN relation_rows rr ON rr.kind = 'group' AND rr.entity_id = g.id`,
|
|
321
|
+
groupDirectExpr: sql`rr.direct`,
|
|
322
|
+
groupWhere: sql`TRUE`,
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const buildManagedByUserSpec = (userId: string, recursive: boolean): EntityQuerySpec => {
|
|
327
|
+
const directCondition = sql`
|
|
328
|
+
(
|
|
329
|
+
g.id IN (
|
|
330
|
+
SELECT DISTINCT g_manage.id
|
|
331
|
+
FROM auth.groups g_manage
|
|
332
|
+
LEFT JOIN auth.group_manager_users_v2 gmu ON gmu.group_id = g_manage.id AND gmu.user_id = ${userId}::uuid
|
|
333
|
+
LEFT JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = g_manage.id
|
|
334
|
+
LEFT JOIN auth.user_groups_v2 ug ON ug.group_id = gmg.manager_group_id AND ug.user_id = ${userId}::uuid
|
|
335
|
+
LEFT JOIN auth.groups g_manager ON g_manager.id = gmg.manager_group_id
|
|
336
|
+
WHERE g_manage.provider = g.provider
|
|
337
|
+
AND (gmu.user_id IS NOT NULL OR (ug.user_id IS NOT NULL AND g_manager.provider = g_manage.provider))
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
`;
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
recursive: false,
|
|
344
|
+
prelude: sql`scope_seed AS (SELECT 1 AS seed)`,
|
|
345
|
+
userFrom: sql`FROM auth.users u`,
|
|
346
|
+
userDirectExpr: sql`NULL::boolean`,
|
|
347
|
+
userWhere: sql`FALSE`,
|
|
348
|
+
groupFrom: sql`FROM auth.groups g`,
|
|
349
|
+
groupDirectExpr: sql`NULL::boolean`,
|
|
350
|
+
groupWhere: recursive
|
|
351
|
+
? buildManagedGroupScopeCondition({ userId, groupProvider: sql`g.provider` })
|
|
352
|
+
: directCondition,
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const buildQuerySpec = (params: EntityListParams): EntityQuerySpec => {
|
|
357
|
+
const relationFilters = [
|
|
358
|
+
params.memberOfGroupId,
|
|
359
|
+
params.managerOfGroupId,
|
|
360
|
+
params.parentGroupId,
|
|
361
|
+
params.managedByUserId,
|
|
362
|
+
].filter(Boolean);
|
|
363
|
+
|
|
364
|
+
if (relationFilters.length > 1) {
|
|
365
|
+
throw new Error("Only one relation filter can be used at a time.");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (params.memberOfGroupId) return buildMemberOfGroupSpec(params.memberOfGroupId, Boolean(params.recursive));
|
|
369
|
+
if (params.managerOfGroupId) return buildManagerOfGroupSpec(params.managerOfGroupId, Boolean(params.recursive));
|
|
370
|
+
if (params.parentGroupId) return buildParentGroupSpec(params.parentGroupId, Boolean(params.recursive));
|
|
371
|
+
if (params.managedByUserId) return buildManagedByUserSpec(params.managedByUserId, params.recursive !== false);
|
|
372
|
+
return buildNoRelationSpec();
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const mapEntityRow = (row: DbRow): EntityListItem => {
|
|
376
|
+
const direct = typeof row.direct === "boolean" ? row.direct : undefined;
|
|
377
|
+
|
|
378
|
+
if (row.kind === "user") {
|
|
379
|
+
return {
|
|
380
|
+
kind: "user",
|
|
381
|
+
user: buildBaseUser(row),
|
|
382
|
+
relation: direct === undefined ? undefined : { direct },
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
kind: "group",
|
|
388
|
+
group: buildBaseGroup(row),
|
|
389
|
+
relation: direct === undefined ? undefined : { direct },
|
|
390
|
+
};
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export const list = async (params: EntityListParams): Promise<{
|
|
394
|
+
items: EntityListItem[];
|
|
395
|
+
total: number;
|
|
396
|
+
pagination: { page: number; perPage: number; totalPages: number; hasNext: boolean };
|
|
397
|
+
}> => {
|
|
398
|
+
const page = params.page ?? 1;
|
|
399
|
+
const perPage = params.perPage ?? 100;
|
|
400
|
+
const offset = (page - 1) * perPage;
|
|
401
|
+
const pattern = params.search ? `%${escapeLikePattern(params.search.trim().toLowerCase())}%` : null;
|
|
402
|
+
const groupsAdmin = (await getFreeIpaConfig()).groupsAdmin;
|
|
403
|
+
const groupsAdminLiteral = toPgTextArray(groupsAdmin);
|
|
404
|
+
const spec = buildQuerySpec(params);
|
|
405
|
+
const kindsCondition =
|
|
406
|
+
(params.kinds?.length ?? 0) === 0 ? sql`TRUE` : sql`kind = ANY(${toPgTextArray(params.kinds ?? [])}::text[])`;
|
|
407
|
+
const excludeUserCondition =
|
|
408
|
+
(params.excludeUserIds?.length ?? 0) === 0
|
|
409
|
+
? sql`TRUE`
|
|
410
|
+
: sql`(kind <> 'user' OR id <> ALL(${toPgUuidArray(params.excludeUserIds ?? [])}::uuid[]))`;
|
|
411
|
+
const excludeGroupCondition =
|
|
412
|
+
(params.excludeGroupIds?.length ?? 0) === 0
|
|
413
|
+
? sql`TRUE`
|
|
414
|
+
: sql`(kind <> 'group' OR id <> ALL(${toPgUuidArray(params.excludeGroupIds ?? [])}::uuid[]))`;
|
|
415
|
+
const userMemberOfGroupCondition =
|
|
416
|
+
(params.userMemberOfGroupIds?.length ?? 0) === 0
|
|
417
|
+
? sql`TRUE`
|
|
418
|
+
: sql`(kind <> 'user' OR EXISTS (
|
|
419
|
+
SELECT 1
|
|
420
|
+
FROM auth.user_groups_v2 ug
|
|
421
|
+
WHERE ug.user_id = id
|
|
422
|
+
AND ug.group_id = ANY(${toPgUuidArray(params.userMemberOfGroupIds ?? [])}::uuid[])
|
|
423
|
+
))`;
|
|
424
|
+
|
|
425
|
+
const where = sql`
|
|
426
|
+
${kindsCondition}
|
|
427
|
+
AND (${params.provider ?? null}::text IS NULL OR provider = ${params.provider ?? null})
|
|
428
|
+
AND (${params.profile ?? null}::text IS NULL OR kind = 'group' OR profile = ${params.profile ?? null})
|
|
429
|
+
AND ${excludeUserCondition}
|
|
430
|
+
AND ${excludeGroupCondition}
|
|
431
|
+
AND ${userMemberOfGroupCondition}
|
|
432
|
+
AND (
|
|
433
|
+
${pattern}::text IS NULL
|
|
434
|
+
OR (
|
|
435
|
+
kind = 'user' AND (
|
|
436
|
+
LOWER(uid) LIKE ${pattern} ESCAPE '\\'
|
|
437
|
+
OR LOWER(COALESCE(display_name, '')) LIKE ${pattern} ESCAPE '\\'
|
|
438
|
+
OR LOWER(COALESCE(given_name, '')) LIKE ${pattern} ESCAPE '\\'
|
|
439
|
+
OR LOWER(COALESCE(sn, '')) LIKE ${pattern} ESCAPE '\\'
|
|
440
|
+
OR LOWER(COALESCE(mail, '')) LIKE ${pattern} ESCAPE '\\'
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
OR (
|
|
444
|
+
kind = 'group' AND (
|
|
445
|
+
LOWER(name) LIKE ${pattern} ESCAPE '\\'
|
|
446
|
+
OR LOWER(COALESCE(description, '')) LIKE ${pattern} ESCAPE '\\'
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
`;
|
|
451
|
+
|
|
452
|
+
const rows = await sql<DbRow[]>`
|
|
453
|
+
WITH ${spec.recursive ? sql`RECURSIVE` : sql``}
|
|
454
|
+
${spec.prelude},
|
|
455
|
+
user_rows AS (
|
|
456
|
+
SELECT
|
|
457
|
+
'user'::text AS kind,
|
|
458
|
+
${spec.userDirectExpr} AS direct,
|
|
459
|
+
u.id,
|
|
460
|
+
u.provider,
|
|
461
|
+
u.profile,
|
|
462
|
+
u.uid,
|
|
463
|
+
u.given_name,
|
|
464
|
+
u.sn,
|
|
465
|
+
u.display_name,
|
|
466
|
+
u.mail,
|
|
467
|
+
NULL::text AS name,
|
|
468
|
+
NULL::text AS description,
|
|
469
|
+
NULL::int AS gid_number,
|
|
470
|
+
CASE
|
|
471
|
+
WHEN u.provider = 'local' THEN u.admin
|
|
472
|
+
ELSE EXISTS(
|
|
473
|
+
SELECT 1
|
|
474
|
+
FROM auth.user_groups_v2 ug_admin
|
|
475
|
+
JOIN auth.groups g_admin ON g_admin.id = ug_admin.group_id
|
|
476
|
+
WHERE ug_admin.user_id = u.id
|
|
477
|
+
AND g_admin.provider = 'ipa'
|
|
478
|
+
AND g_admin.name = ANY(${groupsAdminLiteral}::text[])
|
|
479
|
+
)
|
|
480
|
+
END AS effective_admin,
|
|
481
|
+
LOWER(COALESCE(NULLIF(u.display_name, ''), NULLIF(u.mail, ''), u.uid)) AS sort_label
|
|
482
|
+
${spec.userFrom}
|
|
483
|
+
WHERE ${spec.userWhere}
|
|
484
|
+
),
|
|
485
|
+
group_rows AS (
|
|
486
|
+
SELECT
|
|
487
|
+
'group'::text AS kind,
|
|
488
|
+
${spec.groupDirectExpr} AS direct,
|
|
489
|
+
g.id,
|
|
490
|
+
g.provider,
|
|
491
|
+
NULL::text AS profile,
|
|
492
|
+
NULL::text AS uid,
|
|
493
|
+
NULL::text AS given_name,
|
|
494
|
+
NULL::text AS sn,
|
|
495
|
+
NULL::text AS display_name,
|
|
496
|
+
NULL::text AS mail,
|
|
497
|
+
g.name,
|
|
498
|
+
g.description,
|
|
499
|
+
g.gid_number,
|
|
500
|
+
NULL::boolean AS effective_admin,
|
|
501
|
+
LOWER(g.name) AS sort_label
|
|
502
|
+
${spec.groupFrom}
|
|
503
|
+
WHERE ${spec.groupWhere}
|
|
504
|
+
),
|
|
505
|
+
entity_rows AS (
|
|
506
|
+
SELECT * FROM user_rows
|
|
507
|
+
UNION ALL
|
|
508
|
+
SELECT * FROM group_rows
|
|
509
|
+
)
|
|
510
|
+
SELECT *, COUNT(*) OVER() AS total
|
|
511
|
+
FROM entity_rows
|
|
512
|
+
WHERE ${where}
|
|
513
|
+
ORDER BY sort_label, kind, id
|
|
514
|
+
LIMIT ${perPage}
|
|
515
|
+
OFFSET ${offset}
|
|
516
|
+
`;
|
|
517
|
+
|
|
518
|
+
const total = rows.length > 0 ? Number((rows[0] as Record<string, unknown>).total) : 0;
|
|
519
|
+
return {
|
|
520
|
+
items: rows.map(mapEntityRow),
|
|
521
|
+
total,
|
|
522
|
+
pagination: {
|
|
523
|
+
page,
|
|
524
|
+
perPage,
|
|
525
|
+
totalPages: Math.ceil(total / perPage),
|
|
526
|
+
hasNext: page * perPage < total,
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
};
|