Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 143 additions & 39 deletions api/resource.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,10 @@ paths:
tags:
- Actions
summary: List actions at resource server level
description: Returns all actions defined at the resource server level
description: |
Returns all actions defined at the resource server level. Use the optional `kind` query
parameter to filter to a specific MCP primitive type. Omitting `kind` returns all actions
regardless of kind (the default, backward-compatible behavior).
parameters:
- in: path
name: rsId
Expand All @@ -1135,6 +1138,16 @@ paths:
type: string
format: uuid
description: Resource server ID
- in: query
name: kind
required: false
schema:
type: string
enum: [tool, resource]
description: |
Filter actions by MCP primitive kind. Only `tool` and `resource` are valid values;
any other value (including future kinds) results in a 400 response. Omit the parameter
to return all actions regardless of kind.
- $ref: '#/components/parameters/limitQueryParam'
- $ref: '#/components/parameters/offsetQueryParam'
responses:
Expand All @@ -1144,24 +1157,47 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ActionListResponse'
example:
totalResults: 5
startIndex: 1
count: 2
actions:
- id: "9c6d3g0g-7e8b-4d82-b454-4d2ccg87gh5e"
name: "Create Booking"
description: "Create a new booking"
handle: "create"
permission: "create"
- id: "ad7e4h1h-8f9c-4e93-c565-5e3ddf98hi6f"
name: "List Bookings"
description: "List all bookings"
handle: "list"
permission: "list"
links:
- href: "resource-servers/3fa85f64-5717-4562-b3fc-2c963f66afa6/actions?offset=2&limit=2"
rel: "next"
examples:
unfiltered:
summary: All actions (no kind filter)
value:
totalResults: 5
startIndex: 1
count: 2
actions:
- id: "9c6d3g0g-7e8b-4d82-b454-4d2ccg87gh5e"
name: "Create Booking"
description: "Create a new booking"
handle: "create"
permission: "create"
- id: "ad7e4h1h-8f9c-4e93-c565-5e3ddf98hi6f"
Comment on lines +1168 to +1173

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use schema-valid UUIDs in the new examples.

The new id example values contain non-hex characters (g, h, i, j), so they do not satisfy the documented format: uuid. Some OpenAPI tooling validates examples and will flag these payloads as invalid.

Also applies to: 1188-1194, 1718-1723, 1738-1738

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/resource.yaml` around lines 1168 - 1173, Update the example UUID values
in the resource schema so they are valid UUIDs composed only of hex characters
and hyphens, since the current examples in the booking/resource definitions use
invalid characters and conflict with the documented uuid format. Replace the
affected example ids in the resource examples block and the other referenced
example sections with schema-valid UUIDs, keeping the surrounding fields and
structure unchanged.

name: "List Bookings"
description: "List all bookings"
handle: "list"
permission: "list"
links:
- href: "resource-servers/3fa85f64-5717-4562-b3fc-2c963f66afa6/actions?offset=2&limit=2"
rel: "next"
kind-tool-filter:
summary: MCP tools only (kind=tool)
value:
totalResults: 2
startIndex: 1
count: 2
actions:
- id: "9c6d3g0g-7e8b-4d82-b454-4d2ccg87gh5e"
name: "Create User"
description: "Creates a new user account"
handle: "create_user"
permission: "booking-mcp:create_user"
kind: "tool"
- id: "ad7e4h1h-8f9c-4e93-c565-5e3ddf98hi6f"
name: "Delete User"
description: "Deletes a user account"
handle: "delete_user"
permission: "booking-mcp:delete_user"
kind: "tool"
links: []
"400":
description: Bad request
content:
Expand Down Expand Up @@ -1199,6 +1235,16 @@ paths:
description:
key: "error.resourceservice.result_limit_exceeded_in_composite_mode_description"
defaultValue: "The total number of records exceeds the maximum limit in composite mode"
invalid-kind:
summary: Invalid kind parameter value
value:
code: "RES-1001"
message:
key: "error.resourceservice.invalid_request_format"
defaultValue: "Invalid request format"
description:
key: "error.resourceservice.invalid_request_format_description"
defaultValue: "The request body is malformed or contains invalid data"
Comment on lines +1238 to +1247

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the invalid-kind error examples.

These are GET query-validation failures, but the example description says the request body is malformed. That makes the contract misleading for clients diagnosing a bad kind parameter.

Also applies to: 1782-1791

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/resource.yaml` around lines 1238 - 1247, Update the invalid-kind example
in the resource validation examples so it matches a GET query-parameter failure,
not a malformed request body. In the relevant example blocks for the
invalid-kind response, change the description text to refer to an invalid query
parameter or query validation issue, and keep the error key/value structure
consistent with the surrounding resource validation examples. Use the
invalid-kind example entry to locate and mirror the same fix in both places.

"404":
description: Resource server not found
content:
Expand Down Expand Up @@ -1623,7 +1669,10 @@ paths:
tags:
- Actions
summary: List actions at resource level
description: Returns all actions defined for a specific resource
description: |
Returns all actions defined for a specific resource. Use the optional `kind` query
parameter to filter to a specific MCP primitive type. Omitting `kind` returns all actions
regardless of kind (the default, backward-compatible behavior).
parameters:
- in: path
name: rsId
Expand All @@ -1639,6 +1688,16 @@ paths:
type: string
format: uuid
description: Resource ID
- in: query
name: kind
required: false
schema:
type: string
enum: [tool, resource]
description: |
Filter actions by MCP primitive kind. Only `tool` and `resource` are valid values;
any other value (including future kinds) results in a 400 response. Omit the parameter
to return all actions regardless of kind.
- $ref: '#/components/parameters/limitQueryParam'
- $ref: '#/components/parameters/offsetQueryParam'
responses:
Expand All @@ -1648,24 +1707,41 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ActionListResponse'
example:
totalResults: 6
startIndex: 1
count: 2
actions:
- id: "be8f5i2i-9g0d-5fa4-d676-6f4eeg09ij7g"
name: "Create Reservation"
description: "Create a new reservation"
handle: "create"
permission: "reservations:create"
- id: "cf9g6j3j-0h1e-6gb5-e787-7g5ffh10jk8h"
name: "Read Reservation"
description: "Read reservation details"
handle: "read"
permission: "reservations:read"
links:
- href: "resource-servers/3fa85f64-5717-4562-b3fc-2c963f66afa6/resources/7a4b1f8e-5c69-4b60-9232-2b0aaf65ef3c/actions?offset=2&limit=2"
rel: "next"
examples:
unfiltered:
summary: All actions (no kind filter)
value:
totalResults: 6
startIndex: 1
count: 2
actions:
- id: "be8f5i2i-9g0d-5fa4-d676-6f4eeg09ij7g"
name: "Create Reservation"
description: "Create a new reservation"
handle: "create"
permission: "reservations:create"
- id: "cf9g6j3j-0h1e-6gb5-e787-7g5ffh10jk8h"
name: "Read Reservation"
description: "Read reservation details"
handle: "read"
permission: "reservations:read"
links:
- href: "resource-servers/3fa85f64-5717-4562-b3fc-2c963f66afa6/resources/7a4b1f8e-5c69-4b60-9232-2b0aaf65ef3c/actions?offset=2&limit=2"
rel: "next"
kind-resource-filter:
summary: MCP resources only (kind=resource)
value:
totalResults: 1
startIndex: 1
count: 1
actions:
- id: "cf9g6j3j-0h1e-6gb5-e787-7g5ffh10jk8h"
name: "User List"
description: "Read-only list of users"
handle: "user_list"
permission: "booking-mcp:user-mgmt:user_list"
kind: "resource"
links: []
"400":
description: Bad request
content:
Expand Down Expand Up @@ -1703,6 +1779,16 @@ paths:
description:
key: "error.resourceservice.result_limit_exceeded_in_composite_mode_description"
defaultValue: "The total number of records exceeds the maximum limit in composite mode"
invalid-kind:
summary: Invalid kind parameter value
value:
code: "RES-1001"
message:
key: "error.resourceservice.invalid_request_format"
defaultValue: "Invalid request format"
description:
key: "error.resourceservice.invalid_request_format_description"
defaultValue: "The request body is malformed or contains invalid data"
"404":
description: Resource or resource server not found
content:
Expand Down Expand Up @@ -2384,6 +2470,15 @@ components:
permission:
type: string
description: Derived permission string based on handle hierarchy
kind:
type: string
enum: [tool, resource]
description: >
MCP primitive kind. "tool" represents a callable MCP tool (model-controlled); "resource"
represents a read-only MCP resource (application-controlled). Always present for type=MCP
actions (defaults to "tool" when omitted on create). For API and CUSTOM resource server
actions it is present only when a kind was provided on create, otherwise omitted.
Immutable after creation.

CreateActionRequest:
type: object
Expand All @@ -2394,10 +2489,19 @@ components:
description: Display name of the action
handle:
type: string
description: Immutable and unique under resource server or resource. Identifier used for permission derivation
maxLength: 100
description: Immutable and unique under resource server or resource. Identifier used for permission derivation. Maximum length is 100 characters.
description:
type: string
description: Optional description of the action
kind:
type: string
enum: [tool, resource]
description: >
MCP primitive kind. Optional for all resource server types; when provided it must be
"tool" or "resource". For type=MCP it defaults to "tool" when omitted. For API and
CUSTOM resource servers it is optional and, when omitted, stays empty. Immutable after
creation; omit on update requests (kind cannot be changed).

UpdateActionRequest:
type: object
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/authzen/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ func (s *authzenService) getAllActions(
actions := make([]providers.Action, 0)
for offset := 0; ; {
actionList, svcErr := s.resourceService.GetActionList(
ctx, resourceServerID, resourceID, serverconst.MaxPageSize, offset)
ctx, resourceServerID, resourceID, "", serverconst.MaxPageSize, offset)
if svcErr != nil {
return nil, svcErr
}
Expand Down
23 changes: 15 additions & 8 deletions backend/internal/authzen/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,8 @@ func (s *ServiceTestSuite) TestSearchActionsReturnsAuthorizedActions() {
s.mockResourceServerHandle("booking")
bookingResourceID := testBookingResourceID
invoiceResourceID := "invoice1"
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil), mock.Anything, 0).
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil),
providers.ActionKind(""), mock.Anything, 0).
Return(&resource.ActionList{
Actions: []providers.Action{
{Handle: "read", Permission: "booking:booking:read"},
Expand All @@ -732,13 +733,15 @@ func (s *ServiceTestSuite) TestSearchActionsReturnsAuthorizedActions() {
{ID: invoiceResourceID},
},
}, nil)
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, &bookingResourceID, mock.Anything, 0).
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, &bookingResourceID,
providers.ActionKind(""), mock.Anything, 0).
Return(&resource.ActionList{
Actions: []providers.Action{
{Handle: "delete", Permission: "booking:booking:delete"},
},
}, nil)
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, &invoiceResourceID, mock.Anything, 0).
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, &invoiceResourceID,
providers.ActionKind(""), mock.Anything, 0).
Return(&resource.ActionList{
Actions: []providers.Action{
{Handle: "approve", Permission: "invoice:invoice:approve"},
Expand Down Expand Up @@ -779,7 +782,8 @@ func (s *ServiceTestSuite) TestSearchActionsPaginatesResourceServerActions() {

s.entityProviderMock.On("GetTransitiveEntityGroups", "user1").Return([]providers.EntityGroup{}, nil)
s.mockResourceServerHandle("booking")
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil), serverconst.MaxPageSize, 0).
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil),
providers.ActionKind(""), serverconst.MaxPageSize, 0).
Return(&resource.ActionList{
TotalResults: serverconst.MaxPageSize + 1,
Count: serverconst.MaxPageSize,
Expand All @@ -788,7 +792,7 @@ func (s *ServiceTestSuite) TestSearchActionsPaginatesResourceServerActions() {
},
}, nil)
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil),
serverconst.MaxPageSize, serverconst.MaxPageSize).
providers.ActionKind(""), serverconst.MaxPageSize, serverconst.MaxPageSize).
Return(&resource.ActionList{
TotalResults: serverconst.MaxPageSize + 1,
Count: 1,
Expand Down Expand Up @@ -829,7 +833,8 @@ func (s *ServiceTestSuite) TestSearchActionsReturnsEmptyResultsWhenDenied() {

s.entityProviderMock.On("GetTransitiveEntityGroups", "user1").Return([]providers.EntityGroup{}, nil)
s.mockResourceServerHandle("booking")
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil), mock.Anything, 0).
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil),
providers.ActionKind(""), mock.Anything, 0).
Return(&resource.ActionList{
Actions: []providers.Action{
{Handle: "read", Permission: "booking:booking:read"},
Expand Down Expand Up @@ -906,7 +911,8 @@ func (s *ServiceTestSuite) TestSearchActionsResourceServiceError() {

s.entityProviderMock.On("GetTransitiveEntityGroups", "user1").Return([]providers.EntityGroup{}, nil)
s.mockResourceServerHandle("booking")
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil), mock.Anything, 0).
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil),
providers.ActionKind(""), mock.Anything, 0).
Return((*resource.ActionList)(nil), &tidcommon.InternalServerError)

resp, svcErr := s.service.SearchActions(context.Background(), req)
Expand All @@ -924,7 +930,8 @@ func (s *ServiceTestSuite) TestSearchActionsAuthorizationServiceError() {

s.entityProviderMock.On("GetTransitiveEntityGroups", "user1").Return([]providers.EntityGroup{}, nil)
s.mockResourceServerHandle("booking")
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil), mock.Anything, 0).
s.resourceMock.On("GetActionList", mock.Anything, testResourceServerID, (*string)(nil),
providers.ActionKind(""), mock.Anything, 0).
Return(&resource.ActionList{
Actions: []providers.Action{
{Handle: "read", Permission: "booking:booking:read"},
Expand Down
Loading
Loading