Skip to content

Commit d106441

Browse files
committed
feat(system): emit playbook config access
Use duty RBAC subject access search from the system scraper to derive config access for active users and playbook actions.\n\nThe scraper now emits roles for mcp:run, playbook:run, playbook:approve and playbook:cancel, and only creates config_access rows for playbooks a user is allowed to access.
1 parent 06d2e5b commit d106441

1 file changed

Lines changed: 85 additions & 13 deletions

File tree

scrapers/system/system.go

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package system
22

33
import (
44
"fmt"
5+
"strings"
56
"time"
67

78
"github.com/flanksource/config-db/api"
89
v1 "github.com/flanksource/config-db/api/v1"
910
"github.com/flanksource/duty/models"
1011
"github.com/flanksource/duty/query"
12+
"github.com/flanksource/duty/rbac"
1113
"github.com/flanksource/duty/rbac/policy"
1214
"github.com/google/uuid"
1315
"github.com/lib/pq"
@@ -171,6 +173,13 @@ func scrapeAccessEntities(ctx api.ScrapeContext, scraperID uuid.UUID) v1.ScrapeR
171173

172174
result.ExternalRoles = scrapePlaybookRoles(scraperID)
173175

176+
access, errAccess := scrapePlaybookAccess(ctx, scraperID, users)
177+
if errAccess != nil {
178+
result = result.SetError(errAccess)
179+
return result
180+
}
181+
result.ConfigAccess = access
182+
174183
return result
175184
}
176185

@@ -221,22 +230,85 @@ func scrapeTeams(ctx api.ScrapeContext, scraperID uuid.UUID) ([]models.ExternalG
221230
}
222231

223232
func scrapePlaybookRoles(scraperID uuid.UUID) []models.ExternalRole {
224-
return []models.ExternalRole{
225-
{
226-
Name: policy.ActionPlaybookRun,
227-
Tenant: "mission-control",
228-
RoleType: "playbook-action",
229-
Aliases: pq.StringArray{"role:" + policy.ActionPlaybookRun},
230-
ScraperID: &scraperID,
231-
CreatedAt: time.Now(),
232-
},
233-
{
234-
Name: policy.ActionPlaybookApprove,
233+
actions := []string{
234+
policy.ActionMCPRun,
235+
policy.ActionPlaybookRun,
236+
policy.ActionPlaybookApprove,
237+
policy.ActionPlaybookCancel,
238+
}
239+
240+
roles := make([]models.ExternalRole, 0, len(actions))
241+
for _, action := range actions {
242+
roles = append(roles, models.ExternalRole{
243+
Name: action,
235244
Tenant: "mission-control",
236245
RoleType: "playbook-action",
237-
Aliases: pq.StringArray{"role:" + policy.ActionPlaybookApprove},
246+
Aliases: pq.StringArray{"role:" + action},
238247
ScraperID: &scraperID,
239248
CreatedAt: time.Now(),
240-
},
249+
})
250+
}
251+
252+
return roles
253+
}
254+
255+
func scrapePlaybookAccess(ctx api.ScrapeContext, scraperID uuid.UUID, users []models.ExternalUser) ([]v1.ExternalConfigAccess, error) {
256+
actions := []string{
257+
policy.ActionMCPRun,
258+
policy.ActionPlaybookRun,
259+
policy.ActionPlaybookApprove,
260+
policy.ActionPlaybookCancel,
261+
}
262+
source := "mission-control-rbac"
263+
access := make([]v1.ExternalConfigAccess, 0)
264+
265+
for _, user := range users {
266+
personID := personIDFromAliases(user.Aliases)
267+
if personID == "" {
268+
continue
269+
}
270+
271+
for _, action := range actions {
272+
response, err := rbac.RunSubjectAccessSearch(ctx.DutyContext(), rbac.SubjectAccessSearchRequest{
273+
Subject: personID,
274+
Action: action,
275+
ResourceTypes: []string{"playbook"},
276+
})
277+
if err != nil {
278+
return nil, fmt.Errorf("error running playbook access search for user %s action %s: %w", personID, action, err)
279+
}
280+
281+
for _, result := range response.Results {
282+
if result.ResourceType != "playbook" {
283+
continue
284+
}
285+
286+
playbookID, err := uuid.Parse(result.ID)
287+
if err != nil {
288+
return nil, fmt.Errorf("invalid playbook id from access search %q: %w", result.ID, err)
289+
}
290+
291+
access = append(access, v1.ExternalConfigAccess{
292+
ConfigID: playbookID,
293+
ExternalUserAliases: []string{"people:" + personID},
294+
ExternalRoleAliases: []string{"role:" + action},
295+
ScraperID: &scraperID,
296+
Source: &source,
297+
CreatedAt: time.Now(),
298+
ConfigExternalID: v1.ExternalID{ConfigID: playbookID.String(), ConfigType: "MissionControl::Playbook"},
299+
})
300+
}
301+
}
302+
}
303+
304+
return access, nil
305+
}
306+
307+
func personIDFromAliases(aliases pq.StringArray) string {
308+
for _, alias := range aliases {
309+
if strings.HasPrefix(alias, "people:") {
310+
return strings.TrimPrefix(alias, "people:")
311+
}
241312
}
313+
return ""
242314
}

0 commit comments

Comments
 (0)