mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
fix(terraform): stop sending blocked on litellm_user updates (#45410)
* fix(terraform): stop sending blocked on litellm_user updates /user/update writes blocked into the user table upsert, but blocked only exists as a column on keys, so every litellm_user update failed with a 400 from Prisma. Updates no longer send it. Create sends it only when set to true, since /user/new applies it to the auto-created key blocked only applies at creation; litellm_key_block covers changes after that * fix(terraform): always send blocked on litellm_user create /user/new fills an omitted blocked from default_internal_user_params, so leaving it out on create let a proxy-side blocked=true default block the auto-created key. Create sends the configured value again, including false, while updates still never send it * chore(terraform): drop comment on litellm_user create blocked * fix(terraform): keep litellm_user blocked undeprecated Create sends blocked again, so it still controls the auto-created key. Whether to deprecate it in favor of litellm_key_block is left to the provider maintainers; the docs describe it as creation-only instead
This commit is contained in:
parent
c397b57f2b
commit
4e44b382fd
4 changed files with 83 additions and 5 deletions
|
|
@ -49,6 +49,7 @@ longer signal it.
|
|||
|
||||
### Fixed
|
||||
|
||||
- **user**: `litellm_user` updates no longer send `blocked`, which `/user/update` writes into the user table where no such column exists, so every update failed with a 400 (`Could not find field at upsertOneLiteLLM_UserTable.create.blocked`). Create still sends `blocked`, including `false`, so a `default_internal_user_params.blocked` on the proxy cannot block the auto-created key. Changing `blocked` after creation has no effect, since the proxy has no user-level block; use `litellm_key_block` instead
|
||||
- **model**: `litellm_model` refresh now reads the `{"data": [...]}` envelope `/model/info` returns, so `model_info` fields changed outside Terraform show up as drift instead of silently keeping the previous state
|
||||
- **key**: An update that changes `team_id` and fails because the key was already cascade-deleted along with its previous team now recovers by recreating the key under the new team, instead of aborting the apply. The key's absence is confirmed against the proxy first, so an unrelated failure still errors out, and a `team_id` change between two teams that both still exist stays a plain in-place update
|
||||
- **credential**: create now reports a `credential_name` collision as a clear error naming the `terraform import` command that adopts the existing credential, instead of surfacing the proxy's raw 500 with a Prisma `Unique constraint failed` message. New `adopt_existing` argument (default `false`) opts into taking the existing credential over during create, which makes `apply` idempotent again once state loses track of a credential that still exists on the proxy. Requires a proxy that answers 409 on the collision; older proxies are still detected by their 500 message
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ resource "litellm_user" "alice" {
|
|||
- `permissions` (Optional) - Map of permission values for the user
|
||||
- `model_max_budget` (Optional) - JSON string of per-model budget config, e.g. `jsonencode({"gpt-4o" = {max_budget = 10.0}})`
|
||||
- `guardrails` (Optional) - List of guardrails applied to the user's requests
|
||||
- `blocked` (Optional, Default `false`) - Whether the user is blocked from making requests
|
||||
- `blocked` (Optional, Default `false`) - Whether to block the key auto-created with the user. Only sent on creation, so changing it afterwards has no effect. The proxy has no user-level block; use `litellm_key_block` to block keys
|
||||
|
||||
## Attribute Reference
|
||||
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ func resourceLiteLLMUser() *schema.Resource {
|
|||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
Default: false,
|
||||
Description: "Whether the user is blocked from making requests",
|
||||
Description: "Whether to block the key auto-created with the user. Only sent on creation; use litellm_key_block to change it afterwards",
|
||||
},
|
||||
"key": {
|
||||
Type: schema.TypeString,
|
||||
|
|
@ -195,6 +195,7 @@ func resourceLiteLLMUserCreate(d *schema.ResourceData, m interface{}) error {
|
|||
}
|
||||
userData["auto_create_key"] = d.Get("auto_create_key").(bool)
|
||||
userData["send_invite_email"] = d.Get("send_invite_email").(bool)
|
||||
userData["blocked"] = d.Get("blocked").(bool)
|
||||
|
||||
log.Printf("[DEBUG] Create user request payload: %+v", userData)
|
||||
|
||||
|
|
@ -337,9 +338,7 @@ func resourceLiteLLMUserDelete(d *schema.ResourceData, m interface{}) error {
|
|||
}
|
||||
|
||||
func buildUserData(d *schema.ResourceData) map[string]interface{} {
|
||||
userData := map[string]interface{}{
|
||||
"blocked": d.Get("blocked").(bool),
|
||||
}
|
||||
userData := map[string]interface{}{}
|
||||
|
||||
for _, key := range []string{
|
||||
"user_email", "user_alias", "user_role", "teams", "models", "max_budget",
|
||||
|
|
|
|||
|
|
@ -239,3 +239,81 @@ func TestResourceUserDelete_SendsUserIDs(t *testing.T) {
|
|||
t.Fatalf("expected ID to be cleared after delete, got %q", d.Id())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceUserUpdate_NeverSendsBlocked(t *testing.T) {
|
||||
for name, config := range map[string]map[string]interface{}{
|
||||
"unset": {"user_role": "internal_user"},
|
||||
"true": {"user_role": "internal_user", "blocked": true},
|
||||
"false": {"user_role": "internal_user", "blocked": false},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/user/update":
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("failed to decode update payload: %v", err)
|
||||
}
|
||||
if _, ok := payload["blocked"]; ok {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"error": "Could not find field at upsertOneLiteLLM_UserTable.create.blocked"}`))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(`{"user_id": "u-7"}`))
|
||||
case "/user/info":
|
||||
w.Write(userInfoBody("u-7", map[string]interface{}{"user_role": "internal_user"}))
|
||||
default:
|
||||
t.Errorf("unexpected request to %s", r.URL.Path)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
d := schema.TestResourceDataRaw(t, resourceLiteLLMUser().Schema, config)
|
||||
d.SetId("u-7")
|
||||
|
||||
if err := resourceLiteLLMUserUpdate(d, NewClient(srv.URL, "test-key", true)); err != nil {
|
||||
t.Fatalf("update failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceUserCreate_AlwaysSendsBlocked(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
config map[string]interface{}
|
||||
wantBlocked interface{}
|
||||
}{
|
||||
"unset": {config: map[string]interface{}{}, wantBlocked: false},
|
||||
"false": {config: map[string]interface{}{"blocked": false}, wantBlocked: false},
|
||||
"true": {config: map[string]interface{}{"blocked": true}, wantBlocked: true},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var createPayload map[string]interface{}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/user/new":
|
||||
if err := json.NewDecoder(r.Body).Decode(&createPayload); err != nil {
|
||||
t.Fatalf("failed to decode create payload: %v", err)
|
||||
}
|
||||
w.Write([]byte(`{"user_id": "u-1", "key": "sk-generated"}`))
|
||||
case "/user/info":
|
||||
w.Write(userInfoBody("u-1", map[string]interface{}{"user_role": "internal_user"}))
|
||||
default:
|
||||
t.Errorf("unexpected request to %s", r.URL.Path)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
d := schema.TestResourceDataRaw(t, resourceLiteLLMUser().Schema, tc.config)
|
||||
|
||||
if err := resourceLiteLLMUserCreate(d, NewClient(srv.URL, "test-key", true)); err != nil {
|
||||
t.Fatalf("create failed: %v", err)
|
||||
}
|
||||
if got := createPayload["blocked"]; got != tc.wantBlocked {
|
||||
t.Errorf("expected blocked %v in create payload, got %v", tc.wantBlocked, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue