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
15 changes: 0 additions & 15 deletions api/routes/nsadm.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const (
AddNamespaceMemberURL = "/namespaces/:tenant/members"
RemoveNamespaceMemberURL = "/namespaces/:tenant/members/:uid"
EditNamespaceMemberURL = "/namespaces/:tenant/members/:uid"
GetSessionRecordURL = "/users/security"
EditSessionRecordStatusURL = "/users/security/:tenant"
EditDeviceAutoAcceptURL = "/namespaces/device-auto-accept/:tenant"
)
Expand Down Expand Up @@ -249,17 +248,3 @@ func (h *Handler) EditSessionRecordStatus(c gateway.Context) error {

return c.NoContent(http.StatusOK)
}

func (h *Handler) GetSessionRecord(c gateway.Context) error {
var tenant string
if v := c.Tenant(); v != nil {
tenant = v.ID
}

status, err := h.service.GetSessionRecord(c.Ctx(), tenant)
if err != nil {
return err
}

return c.JSON(http.StatusOK, status)
}
93 changes: 29 additions & 64 deletions api/routes/nsadm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,53 +244,6 @@ func TestDeleteNamespace(t *testing.T) {
mock.AssertExpectations(t)
}

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

cases := []struct {
name string
tenant string
requiredMocks func()
expectedStatus int
}{
{
name: "fails when try to get a session record of a non-existing session",
tenant: "tenant",
requiredMocks: func() {
mock.On("GetSessionRecord", gomock.Anything, "tenant").Return(false, svc.ErrNotFound).Once()
},
expectedStatus: http.StatusNotFound,
},
{
name: "success when try to get a session record of a existing session",
tenant: "tenant",
requiredMocks: func() {
mock.On("GetSessionRecord", gomock.Anything, "tenant").Return(true, nil).Once()
},
expectedStatus: http.StatusOK,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
tc.requiredMocks()

req := httptest.NewRequest(http.MethodGet, "/api/users/security", nil)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Role", authorizer.RoleOwner.String())
req.Header.Set("X-Tenant-ID", tc.tenant)
rec := httptest.NewRecorder()

e := NewRouter(mock)
e.ServeHTTP(rec, req)

assert.Equal(t, tc.expectedStatus, rec.Result().StatusCode)
})
}

mock.AssertExpectations(t)
}

func TestEditNamespace(t *testing.T) {
svcMock := new(mocks.Service)

Expand Down Expand Up @@ -513,6 +466,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 +636,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)
}
9 changes: 4 additions & 5 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,18 +178,17 @@ 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))
publicAPI.PUT(EditDeviceAutoAcceptURL, gateway.Handler(handler.EditDeviceAutoAccept), routesmiddleware.RequiresTenant(ParamNamespaceTenant), routesmiddleware.RequiresPermission(authorizer.NamespaceDeviceAutoAccept))

Expand Down
28 changes: 0 additions & 28 deletions api/services/mocks/services.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 0 additions & 16 deletions api/services/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ type NamespaceService interface {
GetNamespace(ctx context.Context, tenantID string) (*models.Namespace, error)
DeleteNamespace(ctx context.Context, tenantID string) error
EditSessionRecordStatus(ctx context.Context, sessionRecord bool, tenantID string) error
GetSessionRecord(ctx context.Context, tenantID string) (bool, error)
EditDeviceAutoAccept(ctx context.Context, deviceAutoAccept bool, tenantID string) error
}

Expand Down Expand Up @@ -248,18 +247,3 @@ func (s *service) EditDeviceAutoAccept(ctx context.Context, deviceAutoAccept boo

return nil
}

// GetSessionRecord gets the session record data.
//
// It receives a context, used to "control" the request flow, the tenant ID from models.Namespace.
//
// GetSessionRecord returns a boolean indicating the session record status and an error. When error is not nil,
// the boolean is false.
func (s *service) GetSessionRecord(ctx context.Context, tenantID string) (bool, error) {
n, err := s.store.NamespaceResolve(ctx, store.NamespaceTenantIDResolver, tenantID)
if err != nil {
return false, NewErrNamespaceNotFound(tenantID, err)
}

return n.Settings.SessionRecord, nil
}
68 changes: 0 additions & 68 deletions api/services/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1499,71 +1499,3 @@ func TestDeleteNamespace(t *testing.T) {

storeMock.AssertExpectations(t)
}

func TestGetSessionRecord(t *testing.T) {
storeMock := new(storemock.Store)

type Expected struct {
status bool
err error
}

cases := []struct {
name string
tenantID string
mocks func(context.Context)
expected Expected
}{
{
name: "fails when namespace not found",
tenantID: "a736a52b-5777-4f92-b0b8-e359bf484713",
mocks: func(ctx context.Context) {
storeMock.On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "a736a52b-5777-4f92-b0b8-e359bf484713").Return(nil, store.ErrNoDocuments).Once()
},
expected: Expected{false, NewErrNamespaceNotFound("a736a52b-5777-4f92-b0b8-e359bf484713", store.ErrNoDocuments)},
},
{
name: "fails when store namespace resolve fails",
tenantID: "a736a52b-5777-4f92-b0b8-e359bf484713",
mocks: func(ctx context.Context) {
storeMock.
On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "a736a52b-5777-4f92-b0b8-e359bf484713").
Return(nil, errors.New("error")).
Once()
},
expected: Expected{false, NewErrNamespaceNotFound("a736a52b-5777-4f92-b0b8-e359bf484713", errors.New("error"))},
},
{
name: "succeeds",
tenantID: "a736a52b-5777-4f92-b0b8-e359bf484713",
mocks: func(ctx context.Context) {
storeMock.
On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "a736a52b-5777-4f92-b0b8-e359bf484713").
Return(
&models.Namespace{
Name: "group1",
Owner: "hash1",
TenantID: "a736a52b-5777-4f92-b0b8-e359bf484713",
Settings: &models.NamespaceSettings{SessionRecord: false},
},
nil,
).
Once()
},
expected: Expected{false, nil},
},
}

s := NewService(store.Store(storeMock), privateKey, publicKey, storecache.NewNullCache(), clientMock)

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
tc.mocks(ctx)
status, err := s.GetSessionRecord(ctx, tc.tenantID)
assert.Equal(t, tc.expected, Expected{status, err})
})
}

storeMock.AssertExpectations(t)
}
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