Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

cleanup: remove rekey flow, fall through to re-registration instead

The rekey flow only handled a corrupted state (garden exists but OAuth
client doesn't) that isn't a normal operational scenario. Simplify the
client-side fallback: when reauthentication fails, clear credentials
and re-register as a new garden.

Removes: rekey API endpoint, GardenRekey schema, rekey_garden/2,
Registration.rekey/3, and all client-side rekey logic.

sow-157

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+11 -228
+11 -80
apps/garden/lib/garden/socket.ex
··· 222 222 } 223 223 when is_binary(token) and is_binary(private_key_pem) -> 224 224 if token_expired?(issued_at, expires_in) do 225 - try_reauthenticate_or_rekey(storage) 225 + try_reauthenticate_or_reregister(storage) 226 226 else 227 227 Logger.debug(msg: "Using stored Boruta access token") 228 228 {:ok, "boruta:#{token}"} ··· 233 233 private_key_pem: private_key_pem 234 234 } 235 235 when is_binary(client_id) and is_binary(private_key_pem) -> 236 - try_reauthenticate_or_rekey(storage) 237 - 238 - %{garden_sid: garden_sid} when is_binary(garden_sid) -> 239 - Logger.warning( 240 - msg: "Garden is registered but has no usable credentials, attempting rekey", 241 - garden_sid: garden_sid 242 - ) 243 - 244 - try_http_rekey(storage) 236 + try_reauthenticate_or_reregister(storage) 245 237 246 238 _ -> 247 239 try_http_registration(storage) 248 240 end 249 241 end 250 242 251 - defp try_reauthenticate_or_rekey(%{garden_sid: garden_sid} = storage) 252 - when is_binary(garden_sid) do 243 + defp try_reauthenticate_or_reregister(storage) do 253 244 case try_reauthenticate(storage) do 254 245 {:ok, _} = ok -> 255 246 ok 256 247 257 248 {:error, reason} -> 258 249 Logger.warning( 259 - msg: "Reauthentication failed, attempting rekey", 260 - garden_sid: garden_sid, 250 + msg: "Reauthentication failed, clearing credentials and re-registering", 251 + garden_sid: get_in(storage, [:garden_sid]), 261 252 reason: inspect(reason) 262 253 ) 263 254 264 - try_http_rekey(storage) 255 + storage = 256 + storage 257 + |> Map.delete(:garden_sid) 258 + |> Map.delete(:oauth_credentials) 259 + 260 + try_http_registration(storage) 265 261 end 266 262 end 267 - 268 - defp try_reauthenticate_or_rekey(storage), do: try_reauthenticate(storage) 269 263 270 264 defp token_expired?(issued_at, expires_in) 271 265 when is_integer(issued_at) and is_integer(expires_in) do ··· 301 295 end 302 296 303 297 defp try_reauthenticate(_), do: {:error, :no_credentials} 304 - 305 - defp try_http_rekey(storage) do 306 - config = Garden.Config.get() 307 - {public_key_pem, storage} = Garden.Auth.ensure_keypair(storage) 308 - 309 - req = 310 - Req.new( 311 - base_url: "#{config.endpoint}/api/v1", 312 - auth: {:bearer, config.access_token}, 313 - retry: false 314 - ) 315 - 316 - case SowerClient.Registration.rekey(req, storage.garden_sid, public_key_pem) do 317 - {:ok, %{client_id: client_id}} -> 318 - Logger.info( 319 - msg: "Re-keyed via HTTP", 320 - garden_sid: storage.garden_sid, 321 - client_id: client_id 322 - ) 323 - 324 - oauth_creds = %{client_id: client_id} 325 - storage = Map.put(storage, :oauth_credentials, oauth_creds) 326 - 327 - case Garden.Auth.request_token(client_id, storage.private_key_pem) do 328 - {:ok, token_response} -> 329 - updated_creds = 330 - Map.merge(oauth_creds, %{ 331 - access_token: token_response.access_token, 332 - expires_in: token_response.expires_in, 333 - token_issued_at: System.system_time(:second) 334 - }) 335 - 336 - storage |> Map.put(:oauth_credentials, updated_creds) |> Storage.write() 337 - {:ok, "boruta:#{token_response.access_token}"} 338 - 339 - {:error, reason} -> 340 - Storage.write(storage) 341 - {:error, {:token_request_failed, reason}} 342 - end 343 - 344 - {:error, :garden_not_found} -> 345 - Logger.warning( 346 - msg: "Garden no longer exists on server, re-registering", 347 - garden_sid: storage.garden_sid 348 - ) 349 - 350 - storage = 351 - storage 352 - |> Map.delete(:garden_sid) 353 - |> Map.delete(:oauth_credentials) 354 - 355 - try_http_registration(storage) 356 - 357 - {:error, reason} -> 358 - Logger.error( 359 - msg: "Rekey failed, garden may need operator intervention", 360 - garden_sid: storage.garden_sid, 361 - reason: inspect(reason) 362 - ) 363 - 364 - {:error, {:rekey_failed, reason}} 365 - end 366 - end 367 298 368 299 defp try_http_registration(storage) do 369 300 config = Garden.Config.get()
-23
apps/sower/lib/sower/orchestration/garden.ex
··· 261 261 |> Repo.update() 262 262 end 263 263 264 - def rekey_garden(%__MODULE__{} = garden, public_key_pem) do 265 - with :ok <- delete_existing_client(garden), 266 - {:ok, client} <- Sower.GardenAuth.create_client(garden.sid, public_key_pem), 267 - {:ok, garden} <- update_garden(garden, %{oauth_client_id: client.id}) do 268 - Logger.info( 269 - msg: "Garden re-keyed", 270 - garden_sid: garden.sid, 271 - new_client_id: client.id 272 - ) 273 - 274 - {:ok, garden, %{client_id: client.id}} 275 - else 276 - {:error, reason} -> 277 - Logger.error( 278 - msg: "Failed to re-key garden", 279 - garden_sid: garden.sid, 280 - error: inspect(reason) 281 - ) 282 - 283 - {:error, reason} 284 - end 285 - end 286 - 287 264 defp delete_existing_client(%__MODULE__{oauth_client_id: nil}), do: :ok 288 265 289 266 defp delete_existing_client(%__MODULE__{oauth_client_id: client_id}) do
-73
apps/sower/lib/sower_web/controllers/api/garden_controller.ex
··· 66 66 conn |> put_status(:unauthorized) |> render(:error, error: "unauthorized") 67 67 end 68 68 end 69 - 70 - operation(:rekey, 71 - operation_id: "RekeyGarden", 72 - summary: "Re-key a garden's OAuth client", 73 - parameters: [ 74 - sid: [ 75 - in: :path, 76 - type: :string, 77 - description: "Garden SID", 78 - required: true 79 - ] 80 - ], 81 - request_body: {"Garden rekey params", "application/json", SowerClient.GardenRekey}, 82 - responses: %{ 83 - ok: 84 - {"Garden rekey response", "application/json", 85 - %Schema{ 86 - type: :object, 87 - properties: %{ 88 - sid: %Schema{type: :string, description: "Garden SID"}, 89 - oauth_credentials: SowerClient.Auth.OAuthCredentials 90 - }, 91 - required: [:sid, :oauth_credentials] 92 - }}, 93 - unauthorized: 94 - {"Unauthorized", "application/json", 95 - %Schema{type: :object, properties: %{error: %Schema{type: :string}}}}, 96 - not_found: 97 - {"Not found", "application/json", 98 - %Schema{type: :object, properties: %{error: %Schema{type: :string}}}}, 99 - unprocessable_entity: 100 - {"Rekey error", "application/json", 101 - %Schema{type: :object, properties: %{error: %Schema{type: :string}}}} 102 - } 103 - ) 104 - 105 - def rekey( 106 - %Plug.Conn{ 107 - body_params: %SowerClient.GardenRekey{ 108 - public_key: public_key 109 - } 110 - } = conn, 111 - %{sid: sid} 112 - ) do 113 - access_token = conn.assigns.access_token 114 - 115 - if can(access_token) 116 - |> create?(%Sower.Orchestration.Garden{org_id: access_token.org_id}) do 117 - case Sower.Orchestration.Garden.get_garden_sid(sid) do 118 - nil -> 119 - conn 120 - |> put_status(:not_found) 121 - |> render(:error, error: "garden not found") 122 - 123 - garden -> 124 - case Sower.Orchestration.Garden.rekey_garden(garden, public_key) do 125 - {:ok, garden, %{client_id: client_id}} -> 126 - conn 127 - |> put_status(:ok) 128 - |> render(:register, garden: garden, client_id: client_id) 129 - 130 - {:error, reason} -> 131 - Logger.error(msg: "Garden rekey failed", error: inspect(reason)) 132 - 133 - conn 134 - |> put_status(:unprocessable_entity) 135 - |> render(:error, error: "rekey failed") 136 - end 137 - end 138 - else 139 - conn |> put_status(:unauthorized) |> render(:error, error: "unauthorized") 140 - end 141 - end 142 69 end
-1
apps/sower/lib/sower_web/router.ex
··· 113 113 get "/auth/verify", AuthController, :verify 114 114 115 115 post "/gardens/register", GardenController, :register 116 - post "/gardens/:sid/rekey", GardenController, :rekey 117 116 118 117 get "/nix/caches", Nix.CacheController, :list 119 118 get "/seeds", SeedController, :list
-1
apps/sower_client/lib/sower_client.ex
··· 26 26 SowerClient.GardenHello, 27 27 SowerClient.GardenRegistration, 28 28 SowerClient.AgentHello, 29 - SowerClient.GardenRekey, 30 29 SowerClient.Auth.OAuthCredentials, 31 30 SowerClient.Auth.TokenInfo, 32 31 SowerClient.Orchestration.GardenSeedGeneration,
-16
apps/sower_client/lib/sower_client/garden_rekey.ex
··· 1 - defmodule SowerClient.GardenRekey do 2 - use SowerClient.Schema 3 - 4 - OpenApiSpex.schema(%{ 5 - title: "GardenRekey", 6 - description: "Request to re-key a garden's OAuth client", 7 - type: :object, 8 - properties: %{ 9 - public_key: %Schema{ 10 - type: :string, 11 - description: "PEM-encoded RSA public key for private_key_jwt authentication" 12 - } 13 - }, 14 - required: [:public_key] 15 - }) 16 - end
-34
apps/sower_client/lib/sower_client/registration.ex
··· 32 32 err 33 33 end 34 34 end 35 - 36 - def rekey(%Req.Request{} = req, garden_sid, public_key_pem) do 37 - case Req.post(req, 38 - url: "/gardens/#{garden_sid}/rekey", 39 - json: %{ 40 - public_key: public_key_pem 41 - } 42 - ) do 43 - {:ok, %{status: 200, body: body}} -> 44 - {:ok, 45 - %{ 46 - garden_sid: body["sid"], 47 - client_id: body["oauth_credentials"]["client_id"] 48 - }} 49 - 50 - {:ok, %{status: 401}} -> 51 - {:error, :unauthorized} 52 - 53 - {:ok, %{status: 404}} -> 54 - {:error, :garden_not_found} 55 - 56 - {:ok, %{body: %{"error" => error}}} -> 57 - {:error, error} 58 - 59 - {:ok, response} -> 60 - {:error, response} 61 - 62 - {:error, %Req.TransportError{reason: reason}} -> 63 - {:error, {:connection_error, reason}} 64 - 65 - {:error, _} = err -> 66 - err 67 - end 68 - end 69 35 end