Compare commits

..

1 Commits

Author SHA1 Message Date
vikrantgupta25
8b2a5ecb14 fix(user): return 400 instead of 501 for protected user states
ErrIfRoot, ErrIfDeleted and ErrIfPending used TypeUnsupported, which maps
to 501. That code means the server lacks the functionality, which is how
the noop providers use it for edition gating. These guards are business
rules present in every build, so 501 was never accurate.

They now use TypeInvalidInput, matching ErrIfNotPending which already
did. No status code expresses "permanently rejected for every caller",
so the meaning stays in the machine readable error code that each guard
already carries.

Assisted-by: Claude Fable 5
2026-08-10 14:41:21 +05:30
6 changed files with 67 additions and 35 deletions

View File

@@ -81,24 +81,16 @@ devenv-clickhouse-clean: ## Clean all ClickHouse data from filesystem
##############################################################
# go commands
##############################################################
SQLITE_PATH ?= signoz.db
HTTP_HOST_PORT ?= 0.0.0.0:8080
PRIVATE_HOST_PORT ?= 0.0.0.0:8085
OPAMP_WS_ENDPOINT ?= 0.0.0.0:4320
.PHONY: go-run-enterprise
go-run-enterprise: ## Runs the enterprise go backend server
@SIGNOZ_INSTRUMENTATION_LOGS_LEVEL=debug \
SIGNOZ_SQLSTORE_SQLITE_PATH=$(SQLITE_PATH) \
SIGNOZ_SQLSTORE_SQLITE_PATH=signoz.db \
SIGNOZ_WEB_ENABLED=false \
SIGNOZ_TOKENIZER_JWT_SECRET=secret \
SIGNOZ_ALERTMANAGER_PROVIDER=signoz \
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER=cluster \
SIGNOZ_HTTP_HOST_PORT=$(HTTP_HOST_PORT) \
SIGNOZ_PRIVATE_HOST_PORT=$(PRIVATE_HOST_PORT) \
SIGNOZ_OPAMP_WS_ENDPOINT=$(OPAMP_WS_ENDPOINT) \
go run -race \
$(GO_BUILD_CONTEXT_ENTERPRISE)/*.go server
@@ -109,30 +101,16 @@ go-test: ## Runs go unit tests
.PHONY: go-run-community
go-run-community: ## Runs the community go backend server
@SIGNOZ_INSTRUMENTATION_LOGS_LEVEL=debug \
SIGNOZ_SQLSTORE_SQLITE_PATH=$(SQLITE_PATH) \
SIGNOZ_SQLSTORE_SQLITE_PATH=signoz.db \
SIGNOZ_WEB_ENABLED=false \
SIGNOZ_TOKENIZER_JWT_SECRET=secret \
SIGNOZ_ALERTMANAGER_PROVIDER=signoz \
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER=cluster \
SIGNOZ_HTTP_HOST_PORT=$(HTTP_HOST_PORT) \
SIGNOZ_PRIVATE_HOST_PORT=$(PRIVATE_HOST_PORT) \
SIGNOZ_OPAMP_WS_ENDPOINT=$(OPAMP_WS_ENDPOINT) \
go run -race \
$(GO_BUILD_CONTEXT_COMMUNITY)/*.go server
.PHONY: go-stop
go-stop: ## Stops the go backend server listening on HTTP_HOST_PORT
@PORT=$(lastword $(subst :, ,$(HTTP_HOST_PORT))); \
PIDS=$$(lsof -ti tcp:$$PORT); \
if [ -n "$$PIDS" ]; then \
kill $$PIDS; \
echo "Stopped signoz server on port $$PORT (pid $$PIDS)"; \
else \
echo "No signoz server running on port $$PORT"; \
fi
.PHONY: go-build-community $(GO_BUILD_ARCHS_COMMUNITY)
go-build-community: ## Builds the go backend server for community
go-build-community: $(GO_BUILD_ARCHS_COMMUNITY)

View File

@@ -23421,6 +23421,12 @@ paths:
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
@@ -23767,6 +23773,12 @@ paths:
responses:
"200":
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
@@ -23819,6 +23831,12 @@ paths:
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
@@ -23906,6 +23924,12 @@ paths:
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:

View File

@@ -122,7 +122,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
})).Methods(http.MethodPut).GetError(); err != nil {
@@ -173,7 +173,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
@@ -343,7 +343,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
@@ -360,7 +360,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {

View File

@@ -11,10 +11,10 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
HTTPHostPort = GetOrDefaultEnv("SIGNOZ_HTTP_HOST_PORT", "0.0.0.0:8080") // Address to serve http (query service)
PrivateHostPort = GetOrDefaultEnv("SIGNOZ_PRIVATE_HOST_PORT", "0.0.0.0:8085") // Address to server internal services like alert manager
OpAmpWsEndpoint = GetOrDefaultEnv("SIGNOZ_OPAMP_WS_ENDPOINT", "0.0.0.0:4320") // address for opamp websocket
const (
HTTPHostPort = "0.0.0.0:8080" // Address to serve http (query service)
PrivateHostPort = "0.0.0.0:8085" // Address to server internal services like alert manager
OpAmpWsEndpoint = "0.0.0.0:4320" // address for opamp websocket
)
const MaxAllowedPointsInTimeSeries = 300

View File

@@ -170,7 +170,7 @@ func (u *User) UpdateEmail(email valuer.Email) {
// enrich the error with the specific operation using errors.WithAdditionalf.
func (u *User) ErrIfRoot() error {
if u.IsRoot {
return errors.New(errors.TypeUnsupported, ErrCodeRootUserOperationUnsupported, "this operation is not supported for the root user")
return errors.New(errors.TypeInvalidInput, ErrCodeRootUserOperationUnsupported, "this operation is not supported for the root user")
}
return nil
}
@@ -179,7 +179,7 @@ func (u *User) ErrIfRoot() error {
// This error can be enriched with specific operation by the called using errors.WithAdditionalf.
func (u *User) ErrIfDeleted() error {
if u.Status == UserStatusDeleted {
return errors.New(errors.TypeUnsupported, ErrCodeUserStatusDeleted, "unsupported operation for deleted user")
return errors.New(errors.TypeInvalidInput, ErrCodeUserStatusDeleted, "unsupported operation for deleted user")
}
return nil
}
@@ -188,7 +188,7 @@ func (u *User) ErrIfDeleted() error {
// This error can be enriched with specific operation by the called using errors.WithAdditionalf.
func (u *User) ErrIfPending() error {
if u.Status == UserStatusPendingInvite {
return errors.New(errors.TypeUnsupported, ErrCodeUserStatusPendingInvite, "unsupported operation for pending user")
return errors.New(errors.TypeInvalidInput, ErrCodeUserStatusPendingInvite, "unsupported operation for pending user")
}
return nil
}

View File

@@ -55,3 +55,33 @@ def test_impersonated_user_is_admin(signoz: types.SigNoz) -> None:
)
assert root_detail.status_code == HTTPStatus.OK
assert_user_has_role(root_detail.json()["data"], "signoz-admin")
def test_mutating_root_user_is_rejected(signoz: types.SigNoz) -> None:
"""
The root user is permanently protected, so mutating it is rejected with
400 and a machine readable code. ErrIfRoot runs before the self-delete
guard, so this holds even when the caller is the impersonated root
identity itself.
"""
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/users"),
timeout=2,
)
assert response.status_code == HTTPStatus.OK
root_user = next(
(u for u in response.json()["data"] if u.get("isRoot") is True),
None,
)
assert root_user is not None
response = requests.delete(
signoz.self.host_configs["8080"].get(f"/api/v2/users/{root_user['id']}"),
timeout=2,
)
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
error = response.json()["error"]
assert error["type"] == "invalid-input"
assert error["code"] == "root_user_operation_unsupported"