From aeeb9f9ee9c2d124f5f67f4526e711344fe0e286 Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Sat, 15 Aug 2026 17:41:59 +0000 Subject: [PATCH] [management] Refuse a usage limit a one-off setup key cannot honour GenerateSetupKey pins a one-off key at a single use and ignores whatever usage_limit the request asked for. A caller that asks for five gets a key that works once, is told the creation succeeded, and finds out only by using it. Terraform found this the hard way: the provider sends the usage limit it planned, the server stores a different one, the next refresh writes the server's value into state, and from then on every plan sees a change on an attribute that forces replacement. Adding a group to a one-off setup key destroyed the key and issued a new secret in its place. Values above 1 are now refused with a message naming the reusable type, which is what a caller asking for more than one use wants. 0 is still accepted: usage_limit is a required field with no null in it, so a caller with nothing to say about the limit has no way to say that except by sending 0, and refusing it would break every such client. --- .../handlers/setup_keys/setupkeys_handler.go | 13 +++++++ .../setup_keys/setupkeys_handler_test.go | 34 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler.go b/management/server/http/handlers/setup_keys/setupkeys_handler.go index d267b6eea..bb498f46b 100644 --- a/management/server/http/handlers/setup_keys/setupkeys_handler.go +++ b/management/server/http/handlers/setup_keys/setupkeys_handler.go @@ -64,6 +64,19 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) { return } + // A one-off key can be used once, and GenerateSetupKey pins its usage limit + // at 1 whatever the request says. Silently overriding a caller that asked + // for a different number leaves them holding a key that does not do what + // they configured, and no way to find out except by using it. Only values + // above 1 are refused: usage_limit is a required field with no null, so 0 + // cannot be told apart from a caller that has nothing to say about it. + if types.SetupKeyType(req.Type) == types.SetupKeyOneOff && req.UsageLimit > 1 { + util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, + "usage_limit %d is not valid for a one-off setup key, which can be used once; use type reusable for a key that can be used more than once", + req.UsageLimit), w) + return + } + expiresIn := time.Duration(req.ExpiresIn) * time.Second if expiresIn < 0 { diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go index b137b6dd1..a9cfd4bd3 100644 --- a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go +++ b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go @@ -134,6 +134,40 @@ func TestSetupKeysHandlers(t *testing.T) { expectedBody: true, expectedSetupKey: expectedNewKey, }, + { + // A one-off key is used once. Asking for more used to be accepted + // and then quietly reduced to 1. + name: "Create One-Off Setup Key With Conflicting Usage Limit", + requestType: http.MethodPost, + requestPath: "/api/setup-keys", + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"one-off\",\"expires_in\":86400,\"usage_limit\":5}", newSetupKeyName))), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + // 0 is what a caller sends when it has nothing to say about the + // usage limit, since the field is required and has no null, so it + // has to keep working. + name: "Create One-Off Setup Key Without Usage Limit", + requestType: http.MethodPost, + requestPath: "/api/setup-keys", + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"one-off\",\"expires_in\":86400,\"usage_limit\":0}", newSetupKeyName))), + expectedStatus: http.StatusOK, + expectedBody: false, + }, + { + // Only one-off keys are constrained; a reusable key means what it + // says. + name: "Create Reusable Setup Key With Usage Limit", + requestType: http.MethodPost, + requestPath: "/api/setup-keys", + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"reusable\",\"expires_in\":86400,\"usage_limit\":5}", newSetupKeyName))), + expectedStatus: http.StatusOK, + expectedBody: false, + }, { name: "Update Setup Key", requestType: http.MethodPut,