mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: enforce per-user limits on user_secrets (#25588)
Add a Postgres trigger and matching codersdk constants that cap each user's secrets in four dimensions: count (50), total stored value bytes (200 KiB), env-injected stored value bytes (24 KiB), and env name length (256 bytes). Without these caps a user could overflow the 4 MiB DRPC agent manifest, the ~32 KiB Windows process env block, or Linux/macOS ARG_MAX at workspace start. The trigger is the source of truth on aggregates; the handler maps its check_violation error into a 400 that names the per-user budget in stored (post-encryption) bytes. A handler test exercises off-by-one at each cap across POST and PATCH, plus per-user budget isolation. Generated with help from Coder Agents.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -23,6 +24,13 @@ const (
|
||||
userSecretValueField = "value"
|
||||
userSecretEnvNameField = "env_name"
|
||||
userSecretFilePathField = "file_path"
|
||||
|
||||
// These names are raised by the enforce_user_secrets_per_user_limits
|
||||
// trigger with USING CONSTRAINT. They are not table CHECK
|
||||
// constraints, so dbgen does not emit them in check_constraint.go.
|
||||
userSecretsCountLimitConstraint database.CheckConstraint = "user_secrets_per_user_count_limit"
|
||||
userSecretsTotalBytesLimitConstraint database.CheckConstraint = "user_secrets_per_user_total_bytes_limit"
|
||||
userSecretsEnvBytesLimitConstraint database.CheckConstraint = "user_secrets_per_user_env_bytes_limit"
|
||||
)
|
||||
|
||||
// @Summary Create a new user secret
|
||||
@@ -74,6 +82,10 @@ func (api *API) postUserSecret(rw http.ResponseWriter, r *http.Request) {
|
||||
writeUserSecretValidationErrors(ctx, rw, http.StatusConflict, validations)
|
||||
return
|
||||
}
|
||||
if resp, ok := userSecretLimitResponse(err); ok {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, resp)
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error creating secret.",
|
||||
Detail: err.Error(),
|
||||
@@ -246,6 +258,10 @@ func (api *API) patchUserSecret(rw http.ResponseWriter, r *http.Request) {
|
||||
writeUserSecretValidationErrors(ctx, rw, http.StatusConflict, validations)
|
||||
return
|
||||
}
|
||||
if resp, ok := userSecretLimitResponse(err); ok {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, resp)
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error updating secret.",
|
||||
Detail: err.Error(),
|
||||
@@ -346,6 +362,44 @@ func appendUserSecretValidationError(validations []codersdk.ValidationError, fie
|
||||
})
|
||||
}
|
||||
|
||||
// userSecretLimitResponse maps a per-user-limits trigger violation
|
||||
// (raised by enforce_user_secrets_per_user_limits) to a 400. Returns
|
||||
// ok=false if err is not such a violation. See
|
||||
// codersdk.MaxUserSecretsPerUserCount for the rationale behind the caps.
|
||||
func userSecretLimitResponse(err error) (codersdk.Response, bool) {
|
||||
switch {
|
||||
case database.IsCheckViolation(err, userSecretsCountLimitConstraint):
|
||||
return codersdk.Response{
|
||||
Message: "User secrets limit reached.",
|
||||
Detail: fmt.Sprintf(
|
||||
"Each user can have at most %d secrets.",
|
||||
codersdk.MaxUserSecretsPerUserCount,
|
||||
),
|
||||
}, true
|
||||
case database.IsCheckViolation(err, userSecretsTotalBytesLimitConstraint):
|
||||
return codersdk.Response{
|
||||
Message: "User secrets value-bytes limit reached.",
|
||||
Detail: fmt.Sprintf(
|
||||
"Stored bytes of your secret values exceed the per-user "+
|
||||
"budget (%d bytes after encryption, if applicable). "+
|
||||
"Reduce the size or number of your secrets.",
|
||||
codersdk.MaxUserSecretsTotalValueBytes,
|
||||
),
|
||||
}, true
|
||||
case database.IsCheckViolation(err, userSecretsEnvBytesLimitConstraint):
|
||||
return codersdk.Response{
|
||||
Message: "Environment-injected user secrets bytes limit reached.",
|
||||
Detail: fmt.Sprintf(
|
||||
"Stored bytes of env-injected secret values exceed the "+
|
||||
"per-user budget (%d bytes after encryption, if applicable). "+
|
||||
"Clear env_name on large secrets or use file_path instead.",
|
||||
codersdk.MaxUserSecretValueBytes,
|
||||
),
|
||||
}, true
|
||||
}
|
||||
return codersdk.Response{}, false
|
||||
}
|
||||
|
||||
func userSecretConflictValidationErrors(err error) []codersdk.ValidationError {
|
||||
switch {
|
||||
case database.IsUniqueViolation(err, database.UniqueUserSecretsUserNameIndex):
|
||||
|
||||
Reference in New Issue
Block a user