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
4 changes: 2 additions & 2 deletions cmd/api/handlers/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ func (h *HandlersApi) NodePostureHandler(w http.ResponseWriter, r *http.Request)
apiErrorResponse(w, "error getting environment", http.StatusNotFound, err)
return
}
if !h.Users.CheckPermissions(user, users.UserLevel, env.UUID) {
if !h.Users.CheckPermissions(user, users.AdminLevel, env.UUID) {
apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt by %s", user))
return
}
Expand Down Expand Up @@ -845,7 +845,7 @@ func (h *HandlersApi) NodePostureScoreHandler(w http.ResponseWriter, r *http.Req
apiErrorResponse(w, "error getting environment", http.StatusNotFound, err)
return
}
if !h.Users.CheckPermissions(user, users.UserLevel, env.UUID) {
if !h.Users.CheckPermissions(user, users.AdminLevel, env.UUID) {
apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt by %s", user))
return
}
Expand Down
48 changes: 48 additions & 0 deletions cmd/api/handlers/nodes_posture_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package handlers

import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/jmpsec/osctrl/pkg/nodes"
"github.com/jmpsec/osctrl/pkg/posture"
"github.com/jmpsec/osctrl/pkg/tags"
"github.com/jmpsec/osctrl/pkg/users"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -157,3 +161,47 @@ func TestProjectNodeHealthUsesPostureRisk(t *testing.T) {
t.Fatalf("expected at_risk health, got %+v", view.Health)
}
}

func TestNodePostureHandlersRequireAdminLevel(t *testing.T) {
_, h, env, node := setupConsoleHandlers(t)
if err := h.Users.CreatePermission(users.UserPermission{
Username: "bob",
AccessType: int(users.UserLevel),
AccessValue: true,
Environment: env.UUID,
EnvironmentID: env.ID,
}); err != nil {
t.Fatalf("create user-level permission: %v", err)
}

for _, tc := range []struct {
name string
handler http.HandlerFunc
path string
}{
{
name: "posture",
handler: h.NodePostureHandler,
path: "/api/v1/nodes/env/node/NODE-UUID/posture",
},
{
name: "posture score",
handler: h.NodePostureScoreHandler,
path: "/api/v1/nodes/env/node/NODE-UUID/posture/score",
},
} {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
req.SetPathValue("env", env.Name)
req.SetPathValue("uuid", node.UUID)
req = req.WithContext(context.WithValue(req.Context(), ContextKey(contextAPI), ContextValue{ctxUser: "bob"}))
rr := httptest.NewRecorder()

tc.handler(rr, req)

if rr.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d; body=%s", rr.Code, http.StatusForbidden, rr.Body.String())
}
})
}
}
4 changes: 2 additions & 2 deletions cmd/api/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ const mcpInternalHost = "http://osctrl-api.internal"
// client would hit, so authentication, the per-endpoint permission checks,
// and audit logging all run exactly as they do for any other caller. The MCP
// layer therefore contains no authorization logic of its own — which matters
// because osctrl's read permissions are not uniform (reading a node needs
// AdminLevel, its posture only UserLevel, queries QueryLevel), and restating
// because osctrl's read permissions are not uniform (node detail and posture
// need AdminLevel, queries need QueryLevel), and restating
// that policy anywhere else would over-grant the moment the two drift.
type loopbackTransport struct {
handler http.Handler
Expand Down
Loading