Skip to content

Commit 7b013f5

Browse files
Redesign the agent edit page
1 parent 6e45290 commit 7b013f5

92 files changed

Lines changed: 6660 additions & 2649 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/agent.yaml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,76 @@ paths:
13251325
"500":
13261326
description: Internal server error
13271327

1328+
/agents/{id}/roles:
1329+
get:
1330+
tags:
1331+
- Agents
1332+
summary: List roles assigned to the agent
1333+
description: |
1334+
Returns a paginated list of role names assigned to the agent, both directly and through
1335+
group membership. Use the group and role APIs to manage the underlying assignments.
1336+
parameters:
1337+
- in: path
1338+
name: id
1339+
required: true
1340+
schema:
1341+
type: string
1342+
format: uuid
1343+
description: "The unique identifier of the agent"
1344+
example: "9a475e1e-b0cb-4b29-8df5-2e5b24fb0ed3"
1345+
- $ref: '#/components/parameters/limitQueryParam'
1346+
- $ref: '#/components/parameters/offsetQueryParam'
1347+
responses:
1348+
"200":
1349+
description: List of roles assigned to the agent
1350+
content:
1351+
application/json:
1352+
schema:
1353+
$ref: '#/components/schemas/AgentRoleListResponse'
1354+
example:
1355+
totalResults: 2
1356+
startIndex: 1
1357+
count: 2
1358+
roles:
1359+
- "agent-operator"
1360+
- "billing-viewer"
1361+
links: []
1362+
"400":
1363+
description: Bad request
1364+
content:
1365+
application/json:
1366+
schema:
1367+
$ref: '#/components/schemas/Error'
1368+
examples:
1369+
missing-id:
1370+
summary: Missing agent ID
1371+
value:
1372+
code: "AGT-1008"
1373+
message:
1374+
key: "error.agentservice.missing_agent_id"
1375+
defaultValue: "Missing agent ID"
1376+
description:
1377+
key: "error.agentservice.missing_agent_id_description"
1378+
defaultValue: "The agent ID is required"
1379+
"404":
1380+
description: Agent not found
1381+
content:
1382+
application/json:
1383+
schema:
1384+
$ref: '#/components/schemas/Error'
1385+
example:
1386+
code: "AGT-1004"
1387+
message:
1388+
key: "error.agentservice.agent_not_found"
1389+
defaultValue: "Agent not found"
1390+
description:
1391+
key: "error.agentservice.agent_not_found_description"
1392+
defaultValue: "The agent with the specified id does not exist"
1393+
"401":
1394+
$ref: '#/components/responses/Unauthorized'
1395+
"500":
1396+
description: Internal server error
1397+
13281398
/agent-types:
13291399
get:
13301400
tags:
@@ -2237,6 +2307,28 @@ components:
22372307
items:
22382308
$ref: '#/components/schemas/Link'
22392309

2310+
AgentRoleListResponse:
2311+
type: object
2312+
properties:
2313+
totalResults:
2314+
type: integer
2315+
example: 2
2316+
startIndex:
2317+
type: integer
2318+
example: 1
2319+
count:
2320+
type: integer
2321+
example: 2
2322+
roles:
2323+
type: array
2324+
items:
2325+
type: string
2326+
example: ["agent-operator", "billing-viewer"]
2327+
links:
2328+
type: array
2329+
items:
2330+
$ref: '#/components/schemas/Link'
2331+
22402332
AssertionConfig:
22412333
type: object
22422334
description: |

backend/cmd/server/servicemanager.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,8 +409,8 @@ func registerServices(mux *http.ServeMux, cacheManager cache.CacheManagerInterfa
409409
}
410410
exporters = append(exporters, applicationExporter)
411411

412-
agentService, agentExporter, err := agent.Initialize(
413-
mux, entityService, inboundClientService, ouService)
412+
agentService, agentExporter, err := agent.Initialize(mux, entityService, inboundClientService, ouService,
413+
roleService)
414414
if err != nil {
415415
logger.Fatal(ctx, "Failed to initialize AgentService", log.Error(err))
416416
}

backend/internal/agent/handler.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,28 @@ func (h *agentHandler) HandleAgentGroupsRequest(w http.ResponseWriter, r *http.R
193193
sysutils.WriteSuccessResponse(ctx, w, http.StatusOK, resp)
194194
}
195195

196+
// HandleAgentRolesRequest handles GET /agents/{id}/roles.
197+
func (h *agentHandler) HandleAgentRolesRequest(w http.ResponseWriter, r *http.Request) {
198+
ctx := r.Context()
199+
id := r.PathValue("id")
200+
if id == "" {
201+
writeServiceError(ctx, w, &ErrorMissingAgentID)
202+
return
203+
}
204+
limit, offset, svcErr := parsePaginationParams(r.URL.Query())
205+
if svcErr != nil {
206+
writeServiceError(ctx, w, svcErr)
207+
return
208+
}
209+
210+
resp, svcErr := h.service.GetAgentRoles(ctx, id, limit, offset)
211+
if svcErr != nil {
212+
writeServiceError(ctx, w, svcErr)
213+
return
214+
}
215+
sysutils.WriteSuccessResponse(ctx, w, http.StatusOK, resp)
216+
}
217+
196218
// parsePaginationParams parses limit and offset query parameters.
197219
func parsePaginationParams(query url.Values) (int, int, *tidcommon.ServiceError) {
198220
limit := 0

backend/internal/agent/handler_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ type InlineStubAgentService struct {
5252
OnGetAgentGroups func(
5353
ctx context.Context, id string, limit, offset int,
5454
) (*model.AgentGroupListResponse, *tidcommon.ServiceError)
55+
OnGetAgentRoles func(
56+
ctx context.Context, id string, limit, offset int,
57+
) (*model.AgentRoleListResponse, *tidcommon.ServiceError)
5558
}
5659

5760
func (s *InlineStubAgentService) CreateAgent(
@@ -104,6 +107,14 @@ func (s *InlineStubAgentService) GetAgentGroups(
104107
return &model.AgentGroupListResponse{}, nil
105108
}
106109

110+
func (s *InlineStubAgentService) GetAgentRoles(
111+
ctx context.Context, id string, limit, offset int) (*model.AgentRoleListResponse, *tidcommon.ServiceError) {
112+
if s.OnGetAgentRoles != nil {
113+
return s.OnGetAgentRoles(ctx, id, limit, offset)
114+
}
115+
return &model.AgentRoleListResponse{}, nil
116+
}
117+
107118
func (s *InlineStubAgentService) ValidateAgent(
108119
ctx context.Context, agent *model.Agent, flowID string,
109120
) (string, string, inboundmodel.InboundClient, *tidcommon.ServiceError) {
@@ -351,3 +362,56 @@ func TestHandleAgentGroupsRequest_ServiceError(t *testing.T) {
351362
assert.Equal(t, http.StatusNotFound, w.Code)
352363
assert.Contains(t, w.Body.String(), ErrorAgentNotFound.Code)
353364
}
365+
366+
func TestHandleAgentRolesRequest_Success(t *testing.T) {
367+
stubService := &InlineStubAgentService{}
368+
handler := newAgentHandler(stubService)
369+
req := httptest.NewRequest(http.MethodGet, "/agents/agent-123/roles", nil)
370+
req.SetPathValue("id", "agent-123")
371+
w := httptest.NewRecorder()
372+
373+
handler.HandleAgentRolesRequest(w, req)
374+
assert.Equal(t, http.StatusOK, w.Code)
375+
}
376+
377+
func TestHandleAgentRolesRequest_MissingID(t *testing.T) {
378+
stubService := &InlineStubAgentService{}
379+
handler := newAgentHandler(stubService)
380+
req := httptest.NewRequest(http.MethodGet, "/agents//roles", nil)
381+
req.SetPathValue("id", "")
382+
w := httptest.NewRecorder()
383+
384+
handler.HandleAgentRolesRequest(w, req)
385+
assert.Equal(t, http.StatusBadRequest, w.Code)
386+
assert.Contains(t, w.Body.String(), ErrorMissingAgentID.Code)
387+
}
388+
389+
func TestHandleAgentRolesRequest_InvalidLimit(t *testing.T) {
390+
stubService := &InlineStubAgentService{}
391+
handler := newAgentHandler(stubService)
392+
req := httptest.NewRequest(http.MethodGet, "/agents/agent1/roles?limit=abc", nil)
393+
req.SetPathValue("id", "agent1")
394+
w := httptest.NewRecorder()
395+
396+
handler.HandleAgentRolesRequest(w, req)
397+
assert.Equal(t, http.StatusBadRequest, w.Code)
398+
assert.Contains(t, w.Body.String(), ErrorInvalidLimit.Code)
399+
}
400+
401+
func TestHandleAgentRolesRequest_ServiceError(t *testing.T) {
402+
stubService := &InlineStubAgentService{
403+
OnGetAgentRoles: func(
404+
ctx context.Context, id string, limit, offset int,
405+
) (*model.AgentRoleListResponse, *tidcommon.ServiceError) {
406+
return nil, &ErrorAgentNotFound
407+
},
408+
}
409+
handler := newAgentHandler(stubService)
410+
req := httptest.NewRequest(http.MethodGet, "/agents/agent1/roles", nil)
411+
req.SetPathValue("id", "agent1")
412+
w := httptest.NewRecorder()
413+
414+
handler.HandleAgentRolesRequest(w, req)
415+
assert.Equal(t, http.StatusNotFound, w.Code)
416+
assert.Contains(t, w.Body.String(), ErrorAgentNotFound.Code)
417+
}

backend/internal/agent/init.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/thunder-id/thunderid/internal/entity"
2626
"github.com/thunder-id/thunderid/internal/inboundclient"
2727
oupkg "github.com/thunder-id/thunderid/internal/ou"
28+
"github.com/thunder-id/thunderid/internal/role"
2829
serverconst "github.com/thunder-id/thunderid/internal/system/constants"
2930
declarativeresource "github.com/thunder-id/thunderid/internal/system/declarative_resource"
3031
"github.com/thunder-id/thunderid/internal/system/middleware"
@@ -36,8 +37,9 @@ func Initialize(
3637
entityService entity.EntityServiceInterface,
3738
inboundClientService inboundclient.InboundClientServiceInterface,
3839
ouService oupkg.OrganizationUnitServiceInterface,
40+
roleService role.RoleServiceInterface,
3941
) (AgentServiceInterface, declarativeresource.ResourceExporter, error) {
40-
service := newAgentService(entityService, inboundClientService, ouService)
42+
service := newAgentService(entityService, inboundClientService, ouService, roleService)
4143

4244
storeMode := getAgentStoreMode()
4345
if storeMode == serverconst.StoreModeComposite || storeMode == serverconst.StoreModeDeclarative {
@@ -93,4 +95,6 @@ func registerRoutes(mux *http.ServeMux, h *agentHandler) {
9395
}
9496
mux.HandleFunc(middleware.WithCORS("GET /agents/{id}/groups",
9597
h.HandleAgentGroupsRequest, groupsOpts))
98+
mux.HandleFunc(middleware.WithCORS("GET /agents/{id}/roles",
99+
h.HandleAgentRolesRequest, groupsOpts))
96100
}

backend/internal/agent/init_test.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/thunder-id/thunderid/tests/mocks/entitymock"
3434
"github.com/thunder-id/thunderid/tests/mocks/inboundclientmock"
3535
"github.com/thunder-id/thunderid/tests/mocks/oumock"
36+
"github.com/thunder-id/thunderid/tests/mocks/rolemock"
3637
)
3738

3839
func setupAgentConfig(t *testing.T, agentStore string, declarativeEnabled bool) {
@@ -100,12 +101,13 @@ func (suite *InitializeTestSuite) TestInitialize_DeclarativeMode_EntityLoadError
100101
mockEntity := entitymock.NewEntityServiceInterfaceMock(suite.T())
101102
mockInbound := inboundclientmock.NewInboundClientServiceInterfaceMock(suite.T())
102103
mockOU := oumock.NewOrganizationUnitServiceInterfaceMock(suite.T())
104+
mockRole := rolemock.NewRoleServiceInterfaceMock(suite.T())
103105

104106
mockEntity.On("LoadDeclarativeResources", mock.Anything).
105107
Return(errors.New("entity load error")).Once()
106108

107109
mux := http.NewServeMux()
108-
svc, exporter, err := Initialize(mux, mockEntity, mockInbound, mockOU)
110+
svc, exporter, err := Initialize(mux, mockEntity, mockInbound, mockOU, mockRole)
109111

110112
suite.Error(err)
111113
suite.Equal("entity load error", err.Error())
@@ -125,13 +127,14 @@ func (suite *InitializeTestSuite) TestInitialize_InboundLoadError_AllDeclarative
125127
mockEntity := entitymock.NewEntityServiceInterfaceMock(suite.T())
126128
mockInbound := inboundclientmock.NewInboundClientServiceInterfaceMock(suite.T())
127129
mockOU := oumock.NewOrganizationUnitServiceInterfaceMock(suite.T())
130+
mockRole := rolemock.NewRoleServiceInterfaceMock(suite.T())
128131

129132
mockEntity.On("LoadDeclarativeResources", mock.Anything).Return(nil).Once()
130133
mockInbound.On("LoadDeclarativeResources", mock.Anything, mock.Anything).
131134
Return(errors.New("inbound load error")).Once()
132135

133136
mux := http.NewServeMux()
134-
svc, exporter, err := Initialize(mux, mockEntity, mockInbound, mockOU)
137+
svc, exporter, err := Initialize(mux, mockEntity, mockInbound, mockOU, mockRole)
135138

136139
suite.Error(err)
137140
suite.Equal("inbound load error", err.Error())
@@ -149,12 +152,13 @@ func (suite *InitializeTestSuite) TestInitialize_DeclarativeMode_Success() {
149152
mockEntity := entitymock.NewEntityServiceInterfaceMock(suite.T())
150153
mockInbound := inboundclientmock.NewInboundClientServiceInterfaceMock(suite.T())
151154
mockOU := oumock.NewOrganizationUnitServiceInterfaceMock(suite.T())
155+
mockRole := rolemock.NewRoleServiceInterfaceMock(suite.T())
152156

153157
mockEntity.On("LoadDeclarativeResources", mock.Anything).Return(nil).Once()
154158
mockInbound.On("LoadDeclarativeResources", mock.Anything, mock.Anything).Return(nil).Once()
155159

156160
mux := http.NewServeMux()
157-
svc, exporter, err := Initialize(mux, mockEntity, mockInbound, mockOU)
161+
svc, exporter, err := Initialize(mux, mockEntity, mockInbound, mockOU, mockRole)
158162

159163
suite.NoError(err)
160164
suite.NotNil(svc)
@@ -169,12 +173,13 @@ func (suite *InitializeTestSuite) TestInitialize_CompositeMode_EntityLoadError()
169173
mockEntity := entitymock.NewEntityServiceInterfaceMock(suite.T())
170174
mockInbound := inboundclientmock.NewInboundClientServiceInterfaceMock(suite.T())
171175
mockOU := oumock.NewOrganizationUnitServiceInterfaceMock(suite.T())
176+
mockRole := rolemock.NewRoleServiceInterfaceMock(suite.T())
172177

173178
mockEntity.On("LoadDeclarativeResources", mock.Anything).
174179
Return(errors.New("entity composite load error")).Once()
175180

176181
mux := http.NewServeMux()
177-
svc, exporter, err := Initialize(mux, mockEntity, mockInbound, mockOU)
182+
svc, exporter, err := Initialize(mux, mockEntity, mockInbound, mockOU, mockRole)
178183

179184
suite.Error(err)
180185
suite.Equal("entity composite load error", err.Error())
@@ -189,9 +194,10 @@ func (suite *InitializeTestSuite) TestInitialize_MutableMode_SkipsDeclarativeLoa
189194
mockEntity := entitymock.NewEntityServiceInterfaceMock(suite.T())
190195
mockInbound := inboundclientmock.NewInboundClientServiceInterfaceMock(suite.T())
191196
mockOU := oumock.NewOrganizationUnitServiceInterfaceMock(suite.T())
197+
mockRole := rolemock.NewRoleServiceInterfaceMock(suite.T())
192198

193199
mux := http.NewServeMux()
194-
svc, exporter, err := Initialize(mux, mockEntity, mockInbound, mockOU)
200+
svc, exporter, err := Initialize(mux, mockEntity, mockInbound, mockOU, mockRole)
195201

196202
suite.NoError(err)
197203
suite.NotNil(svc)

backend/internal/agent/model/agent.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,14 @@ type AgentGroupListResponse struct {
162162
Groups []AgentGroup `json:"groups"`
163163
Links []utils.Link `json:"links"`
164164
}
165+
166+
// AgentRoleListResponse is the paginated response for an agent's assigned roles (direct and
167+
// group-inherited). Roles are represented by name only, matching what the underlying role
168+
// lookup returns.
169+
type AgentRoleListResponse struct {
170+
TotalResults int `json:"totalResults"`
171+
StartIndex int `json:"startIndex"`
172+
Count int `json:"count"`
173+
Roles []string `json:"roles"`
174+
Links []utils.Link `json:"links"`
175+
}

0 commit comments

Comments
 (0)