Skip to content

Commit 1325f21

Browse files
committed
chore: add external group view
1 parent 5605ca7 commit 1325f21

3 files changed

Lines changed: 127 additions & 4 deletions

File tree

rbac/objects.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var dbResourceObjMap = map[string]string{
1616
"config_access_unwrapped": policy.ObjectApplication,
1717
"config_access_summary": policy.ObjectApplication,
1818
"config_access_summary_by_config": policy.ObjectApplication,
19+
"config_access_summary_by_group": policy.ObjectApplication,
1920
"config_access_summary_by_user": policy.ObjectApplication,
2021
"external_group_summary": policy.ObjectCatalog,
2122
"rpc/config_access_filter_options": policy.ObjectApplication,

tests/config_access_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package tests
22

33
import (
4+
"encoding/json"
45
"time"
56

67
"github.com/google/uuid"
@@ -56,6 +57,82 @@ var _ = Describe("Config Access Summary View", Ordered, func() {
5657
Expect(emptyGroupRow.ExternalUserID).To(Equal(uuid.Nil))
5758
})
5859

60+
It("should separate user and group access summaries", func() {
61+
var nullUserCount int64
62+
Expect(DefaultContext.DB().
63+
Table("config_access_summary_by_user").
64+
Where("external_user_id IS NULL").
65+
Count(&nullUserCount).Error).ToNot(HaveOccurred())
66+
Expect(nullUserCount).To(BeZero())
67+
68+
type groupAccessSummary struct {
69+
ExternalGroupID uuid.UUID
70+
AccessCount int64
71+
DistinctUsers int64
72+
DistinctRoles int64
73+
DistinctConfigs int64
74+
}
75+
76+
groupIDs := []uuid.UUID{
77+
dummy.MissionControlAdminsGroup.ID,
78+
dummy.MissionControlReadersGroup.ID,
79+
dummy.MissionControlEmptyGroup.ID,
80+
}
81+
var groupSummaries []groupAccessSummary
82+
Expect(DefaultContext.DB().
83+
Table("config_access_summary_by_group").
84+
Where("external_group_id IN ?", groupIDs).
85+
Find(&groupSummaries).Error).ToNot(HaveOccurred())
86+
Expect(groupSummaries).To(HaveLen(len(groupIDs)))
87+
88+
byID := make(map[uuid.UUID]groupAccessSummary, len(groupSummaries))
89+
for _, summary := range groupSummaries {
90+
byID[summary.ExternalGroupID] = summary
91+
}
92+
93+
Expect(byID[dummy.MissionControlAdminsGroup.ID]).To(Equal(groupAccessSummary{
94+
ExternalGroupID: dummy.MissionControlAdminsGroup.ID,
95+
AccessCount: 2,
96+
DistinctUsers: 2,
97+
DistinctConfigs: 1,
98+
}))
99+
Expect(byID[dummy.MissionControlReadersGroup.ID]).To(Equal(groupAccessSummary{
100+
ExternalGroupID: dummy.MissionControlReadersGroup.ID,
101+
AccessCount: 2,
102+
DistinctUsers: 2,
103+
DistinctConfigs: 1,
104+
}))
105+
Expect(byID[dummy.MissionControlEmptyGroup.ID]).To(Equal(groupAccessSummary{
106+
ExternalGroupID: dummy.MissionControlEmptyGroup.ID,
107+
AccessCount: 1,
108+
DistinctConfigs: 1,
109+
}))
110+
})
111+
112+
It("should keep users and groups in separate filter facets", func() {
113+
var options struct {
114+
Users []struct {
115+
ExternalUserID uuid.UUID `json:"external_user_id"`
116+
} `json:"users"`
117+
Groups []struct {
118+
ExternalGroupID uuid.UUID `json:"external_group_id"`
119+
} `json:"groups"`
120+
}
121+
122+
var raw string
123+
Expect(DefaultContext.DB().
124+
Raw("SELECT config_access_filter_options()::text").
125+
Scan(&raw).Error).ToNot(HaveOccurred())
126+
Expect(json.Unmarshal([]byte(raw), &options)).To(Succeed())
127+
128+
for _, user := range options.Users {
129+
Expect(user.ExternalUserID).ToNot(Equal(uuid.Nil))
130+
}
131+
Expect(options.Groups).To(ContainElement(struct {
132+
ExternalGroupID uuid.UUID `json:"external_group_id"`
133+
}{ExternalGroupID: dummy.MissionControlEmptyGroup.ID}))
134+
})
135+
59136
It("should not fan out rows for users with multiple access log entries", func() {
60137
// JohnDoe has one access log entry; John's direct grant should still yield a single row.
61138
var johnDirectRows []models.ConfigAccessSummary

views/038_config_access.sql

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ SELECT
7676

7777
-- config_access_summary
7878
DROP VIEW IF EXISTS config_access_summary_by_user;
79+
DROP VIEW IF EXISTS config_access_summary_by_group;
7980
DROP VIEW IF EXISTS config_access_summary_by_config;
8081
DROP VIEW IF EXISTS config_access_summary;
8182

@@ -122,22 +123,45 @@ SELECT
122123
COUNT(DISTINCT config_access_summary."role") as distinct_roles,
123124
COUNT(DISTINCT config_access_summary.config_id) as distinct_configs,
124125
MAX(config_access_summary.last_signed_in_at) as last_signed_in_at,
125-
MAX(config_access_summary.created_at) as latest_grant
126+
MAX(config_access_summary.created_at) as latest_grant,
127+
ARRAY_AGG(DISTINCT config_access_summary.config_type ORDER BY config_access_summary.config_type)
128+
FILTER (WHERE config_access_summary.config_type IS NOT NULL) as config_types
126129
FROM config_access_summary
130+
WHERE config_access_summary.external_user_id IS NOT NULL
127131
GROUP BY config_access_summary.external_user_id, config_access_summary."user", config_access_summary.email;
128132

133+
-- config_access_summary_by_group
134+
CREATE VIEW config_access_summary_by_group AS
135+
SELECT
136+
config_access_summary.external_group_id as external_group_id,
137+
external_groups.name as "group",
138+
COUNT(*) as access_count,
139+
COUNT(DISTINCT config_access_summary.external_user_id) as distinct_users,
140+
COUNT(DISTINCT config_access_summary."role") as distinct_roles,
141+
COUNT(DISTINCT config_access_summary.config_id) as distinct_configs,
142+
MAX(config_access_summary.last_signed_in_at) as last_signed_in_at,
143+
MAX(config_access_summary.created_at) as latest_grant,
144+
ARRAY_AGG(DISTINCT config_access_summary.config_type ORDER BY config_access_summary.config_type)
145+
FILTER (WHERE config_access_summary.config_type IS NOT NULL) as config_types
146+
FROM config_access_summary
147+
JOIN external_groups ON config_access_summary.external_group_id = external_groups.id
148+
WHERE config_access_summary.external_group_id IS NOT NULL
149+
GROUP BY config_access_summary.external_group_id, external_groups.name;
150+
129151
-- config_access_filter_options
130152
-- Returns distinct values for all filter dropdowns in a single call.
131153
-- Each facet excludes its own filter parameter so that selecting a value
132154
-- in one dropdown does not remove it from its own option list (faceted search).
133-
DROP FUNCTION IF EXISTS config_access_filter_options;
155+
DROP FUNCTION IF EXISTS config_access_filter_options(uuid, text, uuid, text, text);
156+
DROP FUNCTION IF EXISTS config_access_filter_options(uuid, text, uuid, text, text, uuid);
134157

135158
CREATE OR REPLACE FUNCTION config_access_filter_options(
136159
p_config_id uuid DEFAULT NULL,
137160
p_config_type text DEFAULT NULL,
138161
p_user_id uuid DEFAULT NULL,
139162
p_role text DEFAULT NULL,
140-
p_user_type text DEFAULT NULL
163+
p_user_type text DEFAULT NULL,
164+
p_group_id uuid DEFAULT NULL
141165
) RETURNS jsonb AS $$
142166
SELECT jsonb_build_object(
143167
'catalogs', COALESCE((
@@ -147,6 +171,7 @@ SELECT jsonb_build_object(
147171
FROM config_access_summary
148172
WHERE (p_config_type IS NULL OR config_type = p_config_type)
149173
AND (p_user_id IS NULL OR external_user_id = p_user_id)
174+
AND (p_group_id IS NULL OR external_group_id = p_group_id)
150175
AND (p_role IS NULL OR "role" = p_role)
151176
AND (p_user_type IS NULL OR user_type = p_user_type)
152177
ORDER BY config_name
@@ -158,14 +183,32 @@ SELECT jsonb_build_object(
158183
FROM (
159184
SELECT DISTINCT external_user_id, "user", email
160185
FROM config_access_summary
161-
WHERE (p_config_id IS NULL OR config_id = p_config_id)
186+
WHERE external_user_id IS NOT NULL
187+
AND (p_config_id IS NULL OR config_id = p_config_id)
162188
AND (p_config_type IS NULL OR config_type = p_config_type)
189+
AND (p_group_id IS NULL OR external_group_id = p_group_id)
163190
AND (p_role IS NULL OR "role" = p_role)
164191
AND (p_user_type IS NULL OR user_type = p_user_type)
165192
ORDER BY "user"
166193
) sub
167194
), '[]'::jsonb),
168195

196+
'groups', COALESCE((
197+
SELECT jsonb_agg(to_jsonb(sub))
198+
FROM (
199+
SELECT DISTINCT config_access_summary.external_group_id, external_groups.name AS "group"
200+
FROM config_access_summary
201+
JOIN external_groups ON config_access_summary.external_group_id = external_groups.id
202+
WHERE config_access_summary.external_group_id IS NOT NULL
203+
AND (p_config_id IS NULL OR config_id = p_config_id)
204+
AND (p_config_type IS NULL OR config_type = p_config_type)
205+
AND (p_user_id IS NULL OR external_user_id = p_user_id)
206+
AND (p_role IS NULL OR "role" = p_role)
207+
AND (p_user_type IS NULL OR user_type = p_user_type)
208+
ORDER BY "group"
209+
) sub
210+
), '[]'::jsonb),
211+
169212
'roles', COALESCE((
170213
SELECT jsonb_agg(to_jsonb(sub))
171214
FROM (
@@ -175,6 +218,7 @@ SELECT jsonb_build_object(
175218
AND (p_config_id IS NULL OR config_id = p_config_id)
176219
AND (p_config_type IS NULL OR config_type = p_config_type)
177220
AND (p_user_id IS NULL OR external_user_id = p_user_id)
221+
AND (p_group_id IS NULL OR external_group_id = p_group_id)
178222
AND (p_user_type IS NULL OR user_type = p_user_type)
179223
ORDER BY "role"
180224
) sub
@@ -189,6 +233,7 @@ SELECT jsonb_build_object(
189233
AND (p_config_id IS NULL OR config_id = p_config_id)
190234
AND (p_config_type IS NULL OR config_type = p_config_type)
191235
AND (p_user_id IS NULL OR external_user_id = p_user_id)
236+
AND (p_group_id IS NULL OR external_group_id = p_group_id)
192237
AND (p_role IS NULL OR "role" = p_role)
193238
ORDER BY user_type
194239
) sub

0 commit comments

Comments
 (0)