Skip to content
Merged
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
15 changes: 15 additions & 0 deletions api/routes/api-key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,21 @@ func TestListAPIKey(t *testing.T) {
requiredMocks func()
expected Expected
}{
{
description: "fails with api key",
headers: map[string]string{
"Content-Type": "application/json",
"X-API-KEY": "b2f7cc0e-d933-4aad-9ab2-b557f2f2554f",
"X-Tenant-ID": "00000000-0000-4000-0000-000000000000",
"X-Role": "owner",
},
query: func() string {
return ""
},
requiredMocks: func() {
},
expected: Expected{body: nil, status: http.StatusForbidden},
},
{
description: "success",
headers: map[string]string{
Expand Down
46 changes: 29 additions & 17 deletions api/routes/nsadm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,16 @@ func TestHandler_LeaveNamespace(t *testing.T) {
requiredMocks func()
expected int
}{
{
description: "fails with api key",
tenantID: "00000000-0000-4000-0000-000000000000",
headers: map[string]string{
"X-API-KEY": "b2f7cc0e-d933-4aad-9ab2-b557f2f2554f",
"X-Tenant-ID": "00000000-0000-4000-0000-000000000000",
},
requiredMocks: func() {},
expected: http.StatusForbidden,
},
{
description: "fails to leave the namespace",
tenantID: "00000000-0000-4000-0000-000000000000",
Expand Down Expand Up @@ -673,29 +683,31 @@ func TestNamespaceCrossTenantAccess(t *testing.T) {
}
}

// TestListNamespacesAPIKeyScope ensures a caller without a user ID (API key
// path) cannot enumerate namespaces across tenants: the list is scoped to the
// caller's own tenant. Covers the regression described in GHSA-vwx9-7qcf-gg7f.
func TestListNamespacesAPIKeyScope(t *testing.T) {
const callerTenant = "00000000-0000-4000-0000-000000000000"

func TestGetNamespaceListBlocksAPIKey(t *testing.T) {
mock := new(mocks.Service)
scopedNS := &models.Namespace{TenantID: callerTenant, Name: "dev"}
mock.
On("ListNamespaces", gomock.Anything, gomock.MatchedBy(func(req *requests.NamespaceList) bool {
return req.UserID == "" && req.TenantID == callerTenant && !req.IsAdmin
})).
Return([]models.Namespace{*scopedNS}, 1, nil).
Once()

req := httptest.NewRequest(http.MethodGet, "/api/namespaces", nil)
req.Header.Set("X-Role", authorizer.RoleOwner.String())
req.Header.Set("X-Tenant-ID", callerTenant)
// No X-ID set: this simulates an API key caller.
req.Header.Set("X-API-KEY", "b2f7cc0e-d933-4aad-9ab2-b557f2f2554f")
req.Header.Set("X-Tenant-ID", "00000000-0000-4000-0000-000000000000")

rec := httptest.NewRecorder()
NewRouter(mock).ServeHTTP(rec, req)

assert.Equal(t, http.StatusForbidden, rec.Result().StatusCode)
mock.AssertExpectations(t)
}

func TestCreateNamespaceBlocksAPIKey(t *testing.T) {
mock := new(mocks.Service)

req := httptest.NewRequest(http.MethodPost, "/api/namespaces", strings.NewReader(`{}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-KEY", "b2f7cc0e-d933-4aad-9ab2-b557f2f2554f")
req.Header.Set("X-Tenant-ID", "00000000-0000-4000-0000-000000000000")

rec := httptest.NewRecorder()
NewRouter(mock).ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
assert.Equal(t, http.StatusForbidden, rec.Result().StatusCode)
mock.AssertExpectations(t)
}
8 changes: 4 additions & 4 deletions api/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func NewRouter(service services.Service, opts ...Option) *echo.Echo {
publicAPI.POST(AuthPublicKeyURL, gateway.Handler(handler.AuthPublicKey))

publicAPI.POST(CreateAPIKeyURL, gateway.Handler(handler.CreateAPIKey), routesmiddleware.BlockAPIKey, routesmiddleware.RequiresPermission(authorizer.APIKeyCreate))
publicAPI.GET(ListAPIKeysURL, gateway.Handler(handler.ListAPIKeys))
publicAPI.GET(ListAPIKeysURL, gateway.Handler(handler.ListAPIKeys), routesmiddleware.BlockAPIKey)
publicAPI.PATCH(UpdateAPIKeyURL, gateway.Handler(handler.UpdateAPIKey), routesmiddleware.BlockAPIKey, routesmiddleware.RequiresPermission(authorizer.APIKeyUpdate))
publicAPI.DELETE(DeleteAPIKeyURL, gateway.Handler(handler.DeleteAPIKey), routesmiddleware.BlockAPIKey, routesmiddleware.RequiresPermission(authorizer.APIKeyDelete))

Expand Down Expand Up @@ -178,16 +178,16 @@ func NewRouter(service services.Service, opts ...Option) *echo.Echo {
publicAPI.PUT(UpdatePublicKeyURL, gateway.Handler(handler.UpdatePublicKey), routesmiddleware.DecodeParam(ParamPublicKeyFingerprint), routesmiddleware.RequiresPermission(authorizer.PublicKeyEdit))
publicAPI.DELETE(DeletePublicKeyURL, gateway.Handler(handler.DeletePublicKey), routesmiddleware.DecodeParam(ParamPublicKeyFingerprint), routesmiddleware.RequiresPermission(authorizer.PublicKeyRemove))

publicAPI.POST(CreateNamespaceURL, gateway.Handler(handler.CreateNamespace))
publicAPI.POST(CreateNamespaceURL, gateway.Handler(handler.CreateNamespace), routesmiddleware.BlockAPIKey)
publicAPI.GET(GetNamespaceURL, gateway.Handler(handler.GetNamespace), routesmiddleware.RequiresTenant(ParamNamespaceTenant))
publicAPI.GET(ListNamespaceURL, gateway.Handler(handler.GetNamespaceList))
publicAPI.GET(ListNamespaceURL, gateway.Handler(handler.GetNamespaceList), routesmiddleware.BlockAPIKey)
publicAPI.PUT(EditNamespaceURL, gateway.Handler(handler.EditNamespace), routesmiddleware.RequiresTenant(ParamNamespaceTenant), routesmiddleware.RequiresPermission(authorizer.NamespaceUpdate))
publicAPI.DELETE(DeleteNamespaceURL, gateway.Handler(handler.DeleteNamespace), routesmiddleware.RequiresTenant(ParamNamespaceTenant), routesmiddleware.RequiresPermission(authorizer.NamespaceDelete))

publicAPI.POST(AddNamespaceMemberURL, gateway.Handler(handler.AddNamespaceMember), routesmiddleware.RequiresPermission(authorizer.NamespaceAddMember))
publicAPI.PATCH(EditNamespaceMemberURL, gateway.Handler(handler.EditNamespaceMember), routesmiddleware.RequiresPermission(authorizer.NamespaceEditMember))
publicAPI.DELETE(RemoveNamespaceMemberURL, gateway.Handler(handler.RemoveNamespaceMember), routesmiddleware.RequiresPermission(authorizer.NamespaceRemoveMember))
publicAPI.DELETE(LeaveNamespaceURL, gateway.Handler(handler.LeaveNamespace))
publicAPI.DELETE(LeaveNamespaceURL, gateway.Handler(handler.LeaveNamespace), routesmiddleware.BlockAPIKey)

publicAPI.GET(GetSessionRecordURL, gateway.Handler(handler.GetSessionRecord))
publicAPI.PUT(EditSessionRecordStatusURL, gateway.Handler(handler.EditSessionRecordStatus), routesmiddleware.RequiresTenant(ParamNamespaceTenant), routesmiddleware.RequiresPermission(authorizer.NamespaceEnableSessionRecord))
Expand Down
4 changes: 0 additions & 4 deletions openapi/spec/paths/api@namespaces.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
get:
operationId: getNamespaces
x-internal: true
summary: Get namespaces list
description: Returns a list of namespaces.
tags:
- community
- namespaces
security:
- jwt: []
- api-key: []
parameters:
- name: filter
description: |
Expand Down Expand Up @@ -62,15 +60,13 @@ get:
$ref: ../components/responses/500.yaml
post:
operationId: createNamespace
x-internal: true
summary: Create namespace
description: Create a namespace.
tags:
- community
- namespaces
security:
- jwt: []
- api-key: []
requestBody:
content:
application/json:
Expand Down
2 changes: 0 additions & 2 deletions openapi/spec/paths/api@namespaces@api-key.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,13 @@ post:
$ref: ../components/responses/500.yaml
get:
operationId: apiKeyList
x-internal: true
summary: List API Keys
tags:
- api-keys
- community
- namespaces
security:
- jwt: []
- api-key: []
parameters:
- $ref: ../components/parameters/query/pageQuery.yaml
- $ref: ../components/parameters/query/perPageQuery.yaml
Expand Down
2 changes: 0 additions & 2 deletions openapi/spec/paths/api@namespaces@{tenant}@members.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
delete:
operationId: leaveNamespace
x-internal: true
summary: Leave Namespace
description: |
Allows the authenticated user to leave the specified namespace. Owners
Expand All @@ -13,7 +12,6 @@ delete:
- namespaces
security:
- jwt: []
- api-key: []
parameters:
- $ref: ../components/parameters/path/namespaceTenantIDPath.yaml
responses:
Expand Down
Loading