Skip to content

Commit fcce9a3

Browse files
committed
Add endpoints to retrieve agent roles and groups
1 parent 541f160 commit fcce9a3

10 files changed

Lines changed: 1087 additions & 27 deletions

File tree

agent-manager-service/api/agent_routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,6 @@ func registerAgentRoutes(rr *middleware.RouteRegistrar, ctrl controllers.AgentCo
5353
rr.HandleFuncWithValidationAndAuthz("DELETE /orgs/{orgName}/projects/{projName}/agents/{agentName}/identities", rbac.AgentUpdate, ctrl.RevokeAgentIdentitySecret)
5454
rr.HandleFuncWithValidationAndAuthz("GET /orgs/{orgName}/projects/{projName}/agents/{agentName}/identities/secrets", rbac.AgentUpdate, ctrl.GetAgentCredentials)
5555
rr.HandleFuncWithValidationAndAuthz("DELETE /orgs/{orgName}/projects/{projName}/agents/{agentName}/identities/secrets", rbac.AgentUpdate, ctrl.ClaimAgentIdentitySecret)
56+
rr.HandleFuncWithValidationAndAuthz("GET /orgs/{orgName}/projects/{projName}/agents/{agentName}/roles", rbac.AgentUpdate, ctrl.GetAgentRoles)
57+
rr.HandleFuncWithValidationAndAuthz("GET /orgs/{orgName}/projects/{projName}/agents/{agentName}/groups", rbac.AgentUpdate, ctrl.GetAgentGroups)
5658
}

agent-manager-service/clients/clientmocks/env_identity_client_mock.go

Lines changed: 106 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

agent-manager-service/clients/thundersvc/identity_client.go

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ type IdentityClient interface {
4242
GetUserRoles(ctx context.Context, userID string) ([]ThunderRole, error)
4343
InviteUser(ctx context.Context, email string, ouID string) (string, error)
4444

45+
// Agents (identity, not the AMP agent resource)
46+
// GetAgentRoles returns the roles assigned to an agent identity, by
47+
// fanning out over every role and checking its assignees — Thunder has no
48+
// reverse-lookup endpoint for this (mirrors GetUserRoles/GetGroupRoles).
49+
GetAgentRoles(ctx context.Context, agentID string) ([]ThunderRole, error)
50+
// GetAgentGroups returns the groups an agent identity belongs to, by
51+
// fanning out over every group in ouID and checking its members —
52+
// Thunder has no reverse-lookup endpoint for this.
53+
GetAgentGroups(ctx context.Context, ouID, agentID string) ([]ThunderGroup, error)
54+
4555
// Groups
4656
ListGroups(ctx context.Context, ouID string, offset, limit int) ([]ThunderGroup, int, error)
4757
ListGroupsByOUId(ctx context.Context, ouID string, offset, limit int) ([]ThunderGroup, int, error)
@@ -482,14 +492,20 @@ func (c *thunderClient) listRoleAssignmentEntries(ctx context.Context, roleID st
482492
return resp.Assignments, nil
483493
}
484494

485-
func (c *thunderClient) GetGroupRoles(ctx context.Context, groupID string) ([]ThunderRole, error) {
495+
// rolesForAssignee returns every role that has an assignment entry matching
496+
// assigneeType/assigneeID (e.g. "group"/groupID, "user"/userID, "agent"/
497+
// agentID). Thunder has no reverse-lookup endpoint for "roles assigned to
498+
// this assignee", so this pages through every role in the instance and
499+
// checks its assignment entries client-side. Shared by GetGroupRoles,
500+
// GetUserRoles, and GetAgentRoles.
501+
func (c *thunderClient) rolesForAssignee(ctx context.Context, assigneeType, assigneeID string) ([]ThunderRole, error) {
486502
const pageSize = 50
487503
var allRoles []ThunderRole
488504
offset := 0
489505
for {
490506
page, total, err := c.ListRoles(ctx, "", offset, pageSize)
491507
if err != nil {
492-
return nil, fmt.Errorf("thunder get group roles (list): %w", err)
508+
return nil, fmt.Errorf("thunder get %s roles (list): %w", assigneeType, err)
493509
}
494510
allRoles = append(allRoles, page...)
495511
offset += len(page)
@@ -498,52 +514,83 @@ func (c *thunderClient) GetGroupRoles(ctx context.Context, groupID string) ([]Th
498514
}
499515
}
500516

501-
var groupRoles []ThunderRole
517+
var assigneeRoles []ThunderRole
502518
for _, role := range allRoles {
503519
entries, err := c.listRoleAssignmentEntries(ctx, role.ID)
504520
if err != nil {
505-
return nil, fmt.Errorf("thunder get group roles (assignments for role %s): %w", role.ID, err)
521+
return nil, fmt.Errorf("thunder get %s roles (assignments for role %s): %w", assigneeType, role.ID, err)
506522
}
507523
for _, e := range entries {
508-
if e.Type == "group" && e.ID == groupID {
509-
groupRoles = append(groupRoles, role)
524+
if e.Type == assigneeType && e.ID == assigneeID {
525+
assigneeRoles = append(assigneeRoles, role)
510526
break
511527
}
512528
}
513529
}
514-
return groupRoles, nil
530+
return assigneeRoles, nil
531+
}
532+
533+
func (c *thunderClient) GetGroupRoles(ctx context.Context, groupID string) ([]ThunderRole, error) {
534+
return c.rolesForAssignee(ctx, "group", groupID)
515535
}
516536

517537
func (c *thunderClient) GetUserRoles(ctx context.Context, userID string) ([]ThunderRole, error) {
538+
return c.rolesForAssignee(ctx, "user", userID)
539+
}
540+
541+
// GetAgentRoles returns the roles assigned to an agent identity. Same
542+
// fan-out-and-filter approach as GetUserRoles/GetGroupRoles, since Thunder has
543+
// no reverse-lookup endpoint for "roles assigned to this assignee".
544+
func (c *thunderClient) GetAgentRoles(ctx context.Context, agentID string) ([]ThunderRole, error) {
545+
return c.rolesForAssignee(ctx, "agent", agentID)
546+
}
547+
548+
// GetAgentGroups returns the groups an agent identity belongs to, by fanning
549+
// out over every group in ouID and checking its member entries — Thunder has
550+
// no reverse-lookup endpoint for "groups this assignee belongs to".
551+
func (c *thunderClient) GetAgentGroups(ctx context.Context, ouID, agentID string) ([]ThunderGroup, error) {
518552
const pageSize = 50
519-
var allRoles []ThunderRole
553+
var allGroups []ThunderGroup
520554
offset := 0
521555
for {
522-
page, total, err := c.ListRoles(ctx, "", offset, pageSize)
556+
page, total, err := c.ListGroupsByOUId(ctx, ouID, offset, pageSize)
523557
if err != nil {
524-
return nil, fmt.Errorf("thunder get user roles (list): %w", err)
558+
return nil, fmt.Errorf("thunder get agent groups (list): %w", err)
525559
}
526-
allRoles = append(allRoles, page...)
560+
allGroups = append(allGroups, page...)
527561
offset += len(page)
528562
if offset >= total || len(page) == 0 {
529563
break
530564
}
531565
}
532566

533-
var userRoles []ThunderRole
534-
for _, role := range allRoles {
535-
entries, err := c.listRoleAssignmentEntries(ctx, role.ID)
536-
if err != nil {
537-
return nil, fmt.Errorf("thunder get user roles (assignments for role %s): %w", role.ID, err)
538-
}
539-
for _, e := range entries {
540-
if e.Type == "user" && e.ID == userID {
541-
userRoles = append(userRoles, role)
567+
var agentGroups []ThunderGroup
568+
for _, group := range allGroups {
569+
const memberPageSize = 100
570+
memberOffset := 0
571+
for {
572+
members, total, err := c.ListGroupMemberEntries(ctx, group.ID, memberOffset, memberPageSize)
573+
if err != nil {
574+
return nil, fmt.Errorf("thunder get agent groups (members for group %s): %w", group.ID, err)
575+
}
576+
matched := false
577+
for _, m := range members {
578+
if m.Type == "agent" && m.ID == agentID {
579+
matched = true
580+
break
581+
}
582+
}
583+
if matched {
584+
agentGroups = append(agentGroups, group)
585+
break
586+
}
587+
memberOffset += len(members)
588+
if memberOffset >= total || len(members) == 0 {
542589
break
543590
}
544591
}
545592
}
546-
return userRoles, nil
593+
return agentGroups, nil
547594
}
548595

549596
// --- Roles ---

agent-manager-service/controllers/agent_controller.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ type AgentController interface {
6464
RevokeAgentIdentitySecret(w http.ResponseWriter, r *http.Request)
6565
ProvisionAgentIdentity(w http.ResponseWriter, r *http.Request)
6666
GetAgentCredentials(w http.ResponseWriter, r *http.Request)
67+
GetAgentRoles(w http.ResponseWriter, r *http.Request)
68+
GetAgentGroups(w http.ResponseWriter, r *http.Request)
6769
}
6870

6971
type agentController struct {
@@ -1269,3 +1271,81 @@ func (c *agentController) GetAgentCredentials(w http.ResponseWriter, r *http.Req
12691271
log.Info("GetAgentCredentials: completed", "orgName", orgName, "agentName", agentName, "envID", envID)
12701272
utils.WriteSuccessResponse(w, http.StatusOK, resp)
12711273
}
1274+
1275+
// GetAgentRoles handles
1276+
// GET /orgs/{orgName}/projects/{projName}/agents/{agentName}/roles?environment={envID}
1277+
//
1278+
// Returns the Thunder roles assigned to the agent's AgentID in one
1279+
// environment. An agent's AgentID (and its role assignments) is per
1280+
// environment, so `environment` is required — there is no single answer
1281+
// across every environment the agent is deployed to.
1282+
func (c *agentController) GetAgentRoles(w http.ResponseWriter, r *http.Request) {
1283+
ctx := r.Context()
1284+
log := logger.GetLogger(ctx)
1285+
1286+
orgName := r.PathValue(utils.PathParamOrgName)
1287+
ouID := middleware.OUIDFromRequest(r)
1288+
projName := r.PathValue(utils.PathParamProjName)
1289+
agentName := r.PathValue(utils.PathParamAgentName)
1290+
envID := r.URL.Query().Get("environment")
1291+
if envID == "" {
1292+
utils.WriteErrorResponse(w, http.StatusBadRequest, "environment query parameter is required")
1293+
return
1294+
}
1295+
1296+
log.Info("GetAgentRoles: starting", "orgName", orgName, "agentName", agentName, "envID", envID)
1297+
1298+
roles, err := c.agentService.GetAgentRoles(ctx, ouID, projName, agentName, envID)
1299+
if err != nil {
1300+
if errors.Is(err, utils.ErrAgentIdentityNotProvisioned) {
1301+
log.Warn("GetAgentRoles: identity not yet provisioned", "orgName", orgName, "agentName", agentName, "envID", envID)
1302+
utils.WriteErrorResponse(w, http.StatusNotFound, "Agent identity not yet provisioned for this environment")
1303+
return
1304+
}
1305+
log.Error("GetAgentRoles: failed to get agent roles", "orgName", orgName, "agentName", agentName, "envID", envID, "error", err)
1306+
handleCommonErrors(w, err, "Failed to get agent roles")
1307+
return
1308+
}
1309+
1310+
log.Info("GetAgentRoles: completed", "orgName", orgName, "agentName", agentName, "envID", envID)
1311+
utils.WriteSuccessResponse(w, http.StatusOK, map[string]any{"roles": roles})
1312+
}
1313+
1314+
// GetAgentGroups handles
1315+
// GET /orgs/{orgName}/projects/{projName}/agents/{agentName}/groups?environment={envID}
1316+
//
1317+
// Returns the Thunder groups the agent's AgentID belongs to in one
1318+
// environment. An agent's AgentID (and its group memberships) is per
1319+
// environment, so `environment` is required — there is no single answer
1320+
// across every environment the agent is deployed to.
1321+
func (c *agentController) GetAgentGroups(w http.ResponseWriter, r *http.Request) {
1322+
ctx := r.Context()
1323+
log := logger.GetLogger(ctx)
1324+
1325+
orgName := r.PathValue(utils.PathParamOrgName)
1326+
ouID := middleware.OUIDFromRequest(r)
1327+
projName := r.PathValue(utils.PathParamProjName)
1328+
agentName := r.PathValue(utils.PathParamAgentName)
1329+
envID := r.URL.Query().Get("environment")
1330+
if envID == "" {
1331+
utils.WriteErrorResponse(w, http.StatusBadRequest, "environment query parameter is required")
1332+
return
1333+
}
1334+
1335+
log.Info("GetAgentGroups: starting", "orgName", orgName, "agentName", agentName, "envID", envID)
1336+
1337+
groups, err := c.agentService.GetAgentGroups(ctx, ouID, projName, agentName, envID)
1338+
if err != nil {
1339+
if errors.Is(err, utils.ErrAgentIdentityNotProvisioned) {
1340+
log.Warn("GetAgentGroups: identity not yet provisioned", "orgName", orgName, "agentName", agentName, "envID", envID)
1341+
utils.WriteErrorResponse(w, http.StatusNotFound, "Agent identity not yet provisioned for this environment")
1342+
return
1343+
}
1344+
log.Error("GetAgentGroups: failed to get agent groups", "orgName", orgName, "agentName", agentName, "envID", envID, "error", err)
1345+
handleCommonErrors(w, err, "Failed to get agent groups")
1346+
return
1347+
}
1348+
1349+
log.Info("GetAgentGroups: completed", "orgName", orgName, "agentName", agentName, "envID", envID)
1350+
utils.WriteSuccessResponse(w, http.StatusOK, map[string]any{"groups": groups})
1351+
}

0 commit comments

Comments
 (0)