Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

cleanup: remove registration token fallback from garden socket auth

Garden socket now only accepts Boruta OAuth tokens. Removes:
- AccessToken-based authentication path from GardenSocket
- Query param token fallback (extract_token_from_params)
- migrate_agent_sid from Garden.Storage (all gardens on 0.8.0+)

Updates all channel tests to use Boruta-authenticated connections
via a new create_garden_with_oauth helper.

Also removes the channel-based garden registration test since
registration now happens exclusively via the API.

sow-136, sow-148

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

+95 -149
+9 -35
apps/sower/lib/sower_web/garden_socket.ex
··· 1 1 defmodule SowerWeb.GardenSocket do 2 - import Sower.Authorization 3 2 require Logger 4 3 use Phoenix.Socket 5 4 6 5 channel("garden:*", SowerWeb.GardenChannel) 7 6 8 7 @impl Phoenix.Socket 9 - def connect(params, socket, connect_info) do 10 - case extract_token(params, connect_info) do 8 + def connect(_params, socket, connect_info) do 9 + case extract_token(connect_info) do 11 10 {:ok, token} -> 12 11 case authenticate_token(token) do 13 - {:ok, access_token} -> 12 + {:ok, context} -> 14 13 socket = 15 14 socket 16 - |> assign(:access_token, access_token) 15 + |> assign(:access_token, context) 17 16 |> assign(:conn_sid, SowerClient.Sid.generate("conn")) 18 17 19 18 {:ok, socket} ··· 32 31 @impl Phoenix.Socket 33 32 def id(_socket), do: nil 34 33 35 - defp extract_token(params, %{x_headers: headers}) do 34 + defp extract_token(%{x_headers: headers}) do 36 35 case List.keyfind(headers, "x-auth-token", 0) do 37 - {_, token} -> {:ok, token} 38 - nil -> extract_token_from_params(params) 36 + {_, "boruta:" <> _ = token} -> {:ok, token} 37 + {_, _} -> :error 38 + nil -> :error 39 39 end 40 40 end 41 41 42 - defp extract_token(params, _connect_info), do: extract_token_from_params(params) 43 - 44 - # Fallback for query param (pre-0.8.0 garden compat) 45 - defp extract_token_from_params(%{"token" => token}), do: {:ok, token} 46 - defp extract_token_from_params(_params), do: :error 42 + defp extract_token(_connect_info), do: :error 47 43 48 44 defp authenticate_token("boruta:" <> boruta_token) do 49 45 case authorize_boruta_token(boruta_token) do ··· 68 64 69 65 {:error, reason} -> 70 66 {:error, reason} 71 - end 72 - end 73 - 74 - defp authenticate_token(base64_token) do 75 - with {:ok, decoded} <- Base.decode64(base64_token), 76 - {:ok, access_token} <- Sower.Accounts.AccessToken.authenticate(decoded) do 77 - if access_token |> can() |> create?(Sower.Orchestration.Garden) do 78 - {:ok, access_token} 79 - else 80 - Logger.error( 81 - msg: "Access token is not authorized to be a garden", 82 - access_token_sid: access_token.sid 83 - ) 84 - 85 - {:error, :unauthorized} 86 - end 87 - else 88 - :error -> 89 - {:error, :invalid_token} 90 - 91 - {:error, error} -> 92 - {:error, error} 93 67 end 94 68 end 95 69
-30
apps/sower/test/sower_web/channels/garden_channel_handle_in_test.exs
··· 24 24 assert_reply ref, :ok, reply, 1_000 25 25 assert reply.sid == garden.sid 26 26 end 27 - 28 - test "registers a new garden when garden_sid is nil" do 29 - %{socket: socket} = connect_and_join_garden() 30 - 31 - local_sid = SowerClient.Sid.generate("lc_grdn") 32 - 33 - {_private_pem, public_pem} = 34 - JOSE.JWK.generate_key({:rsa, 2048}) 35 - |> then(fn jwk -> 36 - {_, priv} = JOSE.JWK.to_pem(jwk) 37 - {_, pub} = jwk |> JOSE.JWK.to_public() |> JOSE.JWK.to_pem() 38 - {priv, pub} 39 - end) 40 - 41 - ref = 42 - push(socket, "garden:hello", %{ 43 - "local_sid" => local_sid, 44 - "name" => "new-garden", 45 - "public_key" => public_pem 46 - }) 47 - 48 - # Registration includes Boruta client creation with public key 49 - assert_reply ref, :ok, reply, 5_000 50 - assert is_binary(reply.sid) 51 - assert is_map(reply.oauth_credentials) 52 - assert is_binary(reply.oauth_credentials.client_id) 53 - refute Map.has_key?(reply.oauth_credentials, :client_secret) 54 - refute Map.has_key?(reply.oauth_credentials, :refresh_token) 55 - refute Map.has_key?(reply.oauth_credentials, :access_token) 56 - end 57 27 end 58 28 59 29 describe "deployment:request" do
+27 -63
apps/sower/test/sower_web/channels/garden_channel_test.exs
··· 4 4 import ExUnit.CaptureLog 5 5 6 6 describe "connect/3" do 7 - test "authenticates via x-auth-token header" do 7 + test "authenticates via boruta token in x-auth-token header" do 8 8 user = user_fixture() 9 9 Sower.Repo.put_org_id(user.org_id) 10 10 11 - {:ok, access_token} = 12 - Sower.Accounts.AccessToken.create(%{ 13 - "description" => "test", 14 - "user_id" => user.id, 15 - "org_id" => user.org_id, 16 - "permissions" => [%{"role" => "garden:register"}] 17 - }) 18 - 19 - encoded_token = Base.encode64(access_token.token) 11 + %{boruta_token: boruta_token} = create_garden_with_oauth() 20 12 21 13 {:ok, _socket} = 22 14 connect(SowerWeb.GardenSocket, %{}, 23 - connect_info: %{x_headers: [{"x-auth-token", encoded_token}]} 15 + connect_info: %{x_headers: [{"x-auth-token", "boruta:#{boruta_token}"}]} 24 16 ) 25 17 end 26 18 27 - test "falls back to query param token for backward compat" do 19 + test "rejects non-boruta token" do 28 20 user = user_fixture() 29 21 Sower.Repo.put_org_id(user.org_id) 30 22 ··· 38 30 39 31 encoded_token = Base.encode64(access_token.token) 40 32 41 - {:ok, _socket} = 42 - connect(SowerWeb.GardenSocket, %{"token" => encoded_token}) 43 - end 44 - 45 - test "rejects invalid base64 token" do 46 33 capture_log(fn -> 47 34 assert {:error, :unauthorized} = 48 - connect(SowerWeb.GardenSocket, %{"token" => "not-valid-base64!!!"}) 35 + connect(SowerWeb.GardenSocket, %{}, 36 + connect_info: %{x_headers: [{"x-auth-token", encoded_token}]} 37 + ) 49 38 end) 50 39 end 51 40 52 41 test "rejects connection with no token" do 53 42 capture_log(fn -> 54 43 assert {:error, :unauthorized} = 55 - connect(SowerWeb.GardenSocket, %{}) 44 + connect(SowerWeb.GardenSocket, %{}, connect_info: %{x_headers: []}) 56 45 end) 57 46 end 58 47 end ··· 68 57 user = user_fixture() 69 58 Sower.Repo.put_org_id(user.org_id) 70 59 71 - {:ok, access_token} = 72 - Sower.Accounts.AccessToken.create(%{ 73 - "description" => "test", 74 - "user_id" => user.id, 75 - "org_id" => user.org_id, 76 - "permissions" => [%{"role" => "garden:register"}] 77 - }) 78 - 79 - encoded_token = Base.encode64(access_token.token) 60 + %{boruta_token: boruta_token} = create_garden_with_oauth() 80 61 81 - {:ok, socket} = connect(SowerWeb.GardenSocket, %{"token" => encoded_token}) 62 + {:ok, socket} = 63 + connect(SowerWeb.GardenSocket, %{}, 64 + connect_info: %{x_headers: [{"x-auth-token", "boruta:#{boruta_token}"}]} 65 + ) 82 66 83 67 assert {:error, %{reason: "unauthorized"}} = 84 68 subscribe_and_join( ··· 89 73 ) 90 74 end 91 75 92 - test "rejects join when local_sid does not match" do 76 + test "rejects join for a different garden than the authenticated one" do 93 77 user = user_fixture() 94 78 Sower.Repo.put_org_id(user.org_id) 95 79 96 - {:ok, access_token} = 97 - Sower.Accounts.AccessToken.create(%{ 98 - "description" => "test", 99 - "user_id" => user.id, 100 - "org_id" => user.org_id, 101 - "permissions" => [%{"role" => "garden:register"}] 102 - }) 80 + %{boruta_token: boruta_token} = create_garden_with_oauth() 81 + other_garden = garden_fixture(%{sid: SowerClient.Sid.generate("grdn")}) 103 82 104 - garden = 105 - garden_fixture(%{ 106 - sid: SowerClient.Sid.generate("grdn"), 107 - local_sid: SowerClient.Sid.generate("lc_grdn") 108 - }) 109 - 110 - encoded_token = Base.encode64(access_token.token) 111 - 112 - {:ok, socket} = connect(SowerWeb.GardenSocket, %{"token" => encoded_token}) 83 + {:ok, socket} = 84 + connect(SowerWeb.GardenSocket, %{}, 85 + connect_info: %{x_headers: [{"x-auth-token", "boruta:#{boruta_token}"}]} 86 + ) 113 87 114 88 assert {:error, %{reason: "unauthorized"}} = 115 89 subscribe_and_join( 116 90 socket, 117 91 SowerWeb.GardenChannel, 118 - "garden:#{garden.sid}", 119 - %{"local_sid" => "wrong_local_sid"} 92 + "garden:#{other_garden.sid}", 93 + %{} 120 94 ) 121 95 end 122 96 end ··· 126 100 user = user_fixture() 127 101 Sower.Repo.put_org_id(user.org_id) 128 102 129 - {:ok, access_token} = 130 - Sower.Accounts.AccessToken.create(%{ 131 - "description" => "test", 132 - "user_id" => user.id, 133 - "org_id" => user.org_id, 134 - "permissions" => [%{"role" => "garden:register"}] 135 - }) 136 - 137 - garden = 138 - garden_fixture(%{ 139 - sid: SowerClient.Sid.generate("grdn"), 140 - local_sid: SowerClient.Sid.generate("lc_grdn") 141 - }) 103 + %{garden: garden, boruta_token: boruta_token} = create_garden_with_oauth() 142 104 143 105 seed = seed_fixture(%{name: "replay-seed", seed_type: "nixos"}) 144 106 ··· 168 130 deployed_at: DateTime.utc_now() |> DateTime.truncate(:second) 169 131 }) 170 132 171 - encoded_token = Base.encode64(access_token.token) 172 - {:ok, socket} = connect(SowerWeb.GardenSocket, %{"token" => encoded_token}) 133 + {:ok, socket} = 134 + connect(SowerWeb.GardenSocket, %{}, 135 + connect_info: %{x_headers: [{"x-auth-token", "boruta:#{boruta_token}"}]} 136 + ) 173 137 174 138 {:ok, _reply, _socket} = 175 139 subscribe_and_join(
+59 -21
apps/sower/test/support/channel_case.ex
··· 31 31 :ok 32 32 end 33 33 34 + def generate_keypair do 35 + JOSE.JWK.generate_key({:rsa, 2048}) 36 + |> then(fn jwk -> 37 + {_, priv} = JOSE.JWK.to_pem(jwk) 38 + {_, pub} = jwk |> JOSE.JWK.to_public() |> JOSE.JWK.to_pem() 39 + {priv, pub} 40 + end) 41 + end 42 + 43 + def create_garden_with_oauth(attrs \\ %{}) do 44 + {private_pem, public_pem} = generate_keypair() 45 + 46 + garden = 47 + Sower.OrchestrationFixtures.garden_fixture( 48 + Map.merge( 49 + %{ 50 + sid: SowerClient.Sid.generate("grdn"), 51 + local_sid: SowerClient.Sid.generate("lc_grdn") 52 + }, 53 + attrs 54 + ) 55 + ) 56 + 57 + {:ok, client} = Sower.GardenAuth.create_client(garden.sid, public_pem) 58 + 59 + {:ok, garden} = 60 + Sower.Orchestration.Garden.update_garden(garden, %{oauth_client_id: client.id}) 61 + 62 + assertion = build_client_assertion(client.id, private_pem) 63 + {:ok, %{access_token: boruta_token}} = Sower.GardenAuth.issue(assertion) 64 + 65 + %{garden: garden, boruta_token: boruta_token} 66 + end 67 + 68 + defp build_client_assertion(client_id, private_key_pem) do 69 + now = System.system_time(:second) 70 + 71 + claims = %{ 72 + "iss" => client_id, 73 + "sub" => client_id, 74 + "aud" => "sower", 75 + "iat" => now, 76 + "exp" => now + 60 77 + } 78 + 79 + jwk = JOSE.JWK.from_pem(private_key_pem) 80 + jws = %{"alg" => "RS512"} 81 + {_, token} = JOSE.JWS.compact(JOSE.JWT.sign(jwk, jws, claims)) 82 + token 83 + end 84 + 34 85 defmacro connect_and_join_garden(attrs \\ Macro.escape(%{})) do 35 86 quote do 36 87 user = user_fixture() 37 88 Sower.Repo.put_org_id(user.org_id) 38 89 39 - {:ok, access_token} = 40 - Sower.Accounts.AccessToken.create(%{ 41 - "description" => "test", 42 - "user_id" => user.id, 43 - "org_id" => user.org_id, 44 - "permissions" => [%{"role" => "garden:register"}] 45 - }) 90 + %{garden: garden, boruta_token: boruta_token} = 91 + SowerWeb.ChannelCase.create_garden_with_oauth(unquote(attrs)) 46 92 47 - garden = 48 - garden_fixture( 49 - Map.merge( 50 - %{ 51 - sid: SowerClient.Sid.generate("grdn"), 52 - local_sid: SowerClient.Sid.generate("lc_grdn") 53 - }, 54 - unquote(attrs) 55 - ) 93 + {:ok, socket} = 94 + connect(SowerWeb.GardenSocket, %{}, 95 + connect_info: %{ 96 + x_headers: [{"x-auth-token", "boruta:#{boruta_token}"}] 97 + } 56 98 ) 57 - 58 - encoded_token = Base.encode64(access_token.token) 59 - 60 - {:ok, socket} = connect(SowerWeb.GardenSocket, %{"token" => encoded_token}) 61 99 62 100 {:ok, _reply, socket} = 63 101 subscribe_and_join( ··· 67 105 %{"local_sid" => garden.local_sid} 68 106 ) 69 107 70 - %{socket: socket, garden: garden, user: user, access_token: access_token} 108 + %{socket: socket, garden: garden, user: user} 71 109 end 72 110 end 73 111 end