diff --git a/.sqlx/query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json b/.sqlx/query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json deleted file mode 100644 index 032ed79111..0000000000 --- a/.sqlx/query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO wireguard_network_device (device_id, wireguard_network_id, wireguard_ips, is_authorized, authorized_at, preshared_key) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT ON CONSTRAINT device_network DO UPDATE SET wireguard_ips = $3, is_authorized = $4", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "InetArray", - "Bool", - "Timestamp", - "Text" - ] - }, - "nullable": [] - }, - "hash": "09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842" -} diff --git a/.sqlx/query-0f1e325349ad365965d6fb7b1e09951c8ddab38dd1ba41e9a3f657273574988f.json b/.sqlx/query-0f1e325349ad365965d6fb7b1e09951c8ddab38dd1ba41e9a3f657273574988f.json new file mode 100644 index 0000000000..2590d55e75 --- /dev/null +++ b/.sqlx/query-0f1e325349ad365965d6fb7b1e09951c8ddab38dd1ba41e9a3f657273574988f.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.wireguard_pubkey pubkey, ARRAY( SELECT host(ip) FROM unnest(wnd.wireguard_ips) AS ip ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND d.configured AND u.is_active ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "allowed_ips!: Vec", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + null + ] + }, + "hash": "0f1e325349ad365965d6fb7b1e09951c8ddab38dd1ba41e9a3f657273574988f" +} diff --git a/.sqlx/query-153fa42e3a61b24b7a264d9ef236841ab88a9361a52129ef199768e900dd12ad.json b/.sqlx/query-153fa42e3a61b24b7a264d9ef236841ab88a9361a52129ef199768e900dd12ad.json new file mode 100644 index 0000000000..356dbc859a --- /dev/null +++ b/.sqlx/query-153fa42e3a61b24b7a264d9ef236841ab88a9361a52129ef199768e900dd12ad.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO wireguard_network_device (device_id, wireguard_network_id, wireguard_ips) VALUES ($1, $2, $3) ON CONFLICT ON CONSTRAINT device_network DO UPDATE SET wireguard_ips = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "InetArray" + ] + }, + "nullable": [] + }, + "hash": "153fa42e3a61b24b7a264d9ef236841ab88a9361a52129ef199768e900dd12ad" +} diff --git a/.sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json b/.sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json deleted file mode 100644 index f8ce8a0666..0000000000 --- a/.sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE wireguard_network_device SET wireguard_ips = $3, is_authorized = $4, authorized_at = $5, preshared_key = $6 WHERE device_id = $1 AND wireguard_network_id = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "InetArray", - "Bool", - "Timestamp", - "Text" - ] - }, - "nullable": [] - }, - "hash": "20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933" -} diff --git a/.sqlx/query-dd0190b514cf61ba7c444d14f9c47bda3869b74fcb598ec6f2cf8352424f87d6.json b/.sqlx/query-32b5d4d820a72da19cbd3bb1a33e17c9555d0350d03679d9cf3b7ccc6451c7ae.json similarity index 83% rename from .sqlx/query-dd0190b514cf61ba7c444d14f9c47bda3869b74fcb598ec6f2cf8352424f87d6.json rename to .sqlx/query-32b5d4d820a72da19cbd3bb1a33e17c9555d0350d03679d9cf3b7ccc6451c7ae.json index 238f7541aa..35150a6a35 100644 --- a/.sqlx/query-dd0190b514cf61ba7c444d14f9c47bda3869b74fcb598ec6f2cf8352424f87d6.json +++ b/.sqlx/query-32b5d4d820a72da19cbd3bb1a33e17c9555d0350d03679d9cf3b7ccc6451c7ae.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"vpn_client_session\" (\"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\",\"state\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", + "query": "INSERT INTO \"vpn_client_session\" (\"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\",\"state\",\"preshared_key\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id", "describe": { "columns": [ { @@ -42,12 +42,13 @@ ] } } - } + }, + "Text" ] }, "nullable": [ false ] }, - "hash": "dd0190b514cf61ba7c444d14f9c47bda3869b74fcb598ec6f2cf8352424f87d6" + "hash": "32b5d4d820a72da19cbd3bb1a33e17c9555d0350d03679d9cf3b7ccc6451c7ae" } diff --git a/.sqlx/query-f64a22a8141030a41aa9b85275158a3a75e5ebfa0f14c84b1aefc8582156eb03.json b/.sqlx/query-3ea2a5fbb1ec0dc86448b6145cf8b5f8fa8c6ab81da002d0d1ddb5445e7c6d31.json similarity index 91% rename from .sqlx/query-f64a22a8141030a41aa9b85275158a3a75e5ebfa0f14c84b1aefc8582156eb03.json rename to .sqlx/query-3ea2a5fbb1ec0dc86448b6145cf8b5f8fa8c6ab81da002d0d1ddb5445e7c6d31.json index 72e7fb3e2e..cb1765ce7d 100644 --- a/.sqlx/query-f64a22a8141030a41aa9b85275158a3a75e5ebfa0f14c84b1aefc8582156eb03.json +++ b/.sqlx/query-3ea2a5fbb1ec0dc86448b6145cf8b5f8fa8c6ab81da002d0d1ddb5445e7c6d31.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT n.id network_id, n.name network_name, n.endpoint gateway_endpoint, wnd.wireguard_ips \"device_wireguard_ips: Vec\", vs.endpoint \"device_endpoint?\", vs.latest_handshake \"latest_handshake?\", vs.state \"state?: VpnClientSessionState\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN LATERAL ( SELECT id, state, location_id, endpoint, latest_handshake FROM vpn_client_session LEFT JOIN LATERAL ( SELECT session_id, endpoint, latest_handshake FROM vpn_session_stats WHERE session_id = vpn_client_session.id ORDER BY collected_at DESC LIMIT 1 ) vss ON vss.session_id = vpn_client_session.id WHERE location_id = n.id and device_id = $1 ORDER BY created_at DESC LIMIT 1 ) vs ON vs.location_id = n.id WHERE wnd.device_id = $1", + "query": "SELECT n.id network_id, n.name network_name, n.endpoint gateway_endpoint, wnd.wireguard_ips \"device_wireguard_ips: Vec\", vs.endpoint \"device_endpoint?\", vs.latest_handshake \"latest_handshake?\", vs.state \"state?: VpnClientSessionState\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN LATERAL ( SELECT id, state, location_id, endpoint, latest_handshake FROM vpn_client_session LEFT JOIN LATERAL ( SELECT session_id, endpoint, latest_handshake FROM vpn_session_stats WHERE session_id = vpn_client_session.id ORDER BY collected_at DESC LIMIT 1 ) vss ON vss.session_id = vpn_client_session.id WHERE location_id = n.id and device_id = $1 ORDER BY created_at DESC, id DESC LIMIT 1 ) vs ON vs.location_id = n.id WHERE wnd.device_id = $1", "describe": { "columns": [ { @@ -65,5 +65,5 @@ false ] }, - "hash": "f64a22a8141030a41aa9b85275158a3a75e5ebfa0f14c84b1aefc8582156eb03" + "hash": "3ea2a5fbb1ec0dc86448b6145cf8b5f8fa8c6ab81da002d0d1ddb5445e7c6d31" } diff --git a/.sqlx/query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json b/.sqlx/query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json deleted file mode 100644 index 0ba89d9c7a..0000000000 --- a/.sqlx/query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "device_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "wireguard_network_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "wireguard_ips: Vec", - "type_info": "InetArray" - }, - { - "ordinal": 3, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_authorized", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "authorized_at", - "type_info": "Timestamp" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - true - ] - }, - "hash": "469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac" -} diff --git a/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json b/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json index 0e36c94cdb..8a6776d365 100644 --- a/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json +++ b/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json @@ -82,7 +82,7 @@ false, false, true, - false, + true, false, false, false, diff --git a/.sqlx/query-73965013a68538139aadcf0c2346e315923c12c54b9446c7b18e4b4fb1791f42.json b/.sqlx/query-4b05abebeafeda2f88fff48f6d9d45938371b3f39822c4ff68a1a9515767e0ad.json similarity index 85% rename from .sqlx/query-73965013a68538139aadcf0c2346e315923c12c54b9446c7b18e4b4fb1791f42.json rename to .sqlx/query-4b05abebeafeda2f88fff48f6d9d45938371b3f39822c4ff68a1a9515767e0ad.json index bd6db45ee8..cdbe9ae673 100644 --- a/.sqlx/query-73965013a68538139aadcf0c2346e315923c12c54b9446c7b18e4b4fb1791f42.json +++ b/.sqlx/query-4b05abebeafeda2f88fff48f6d9d45938371b3f39822c4ff68a1a9515767e0ad.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\" \"mfa_method?: _\",\"state\" \"state: _\" FROM \"vpn_client_session\" WHERE id = $1", + "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\" \"mfa_method?: _\",\"state\" \"state: _\",\"preshared_key\" FROM \"vpn_client_session\" WHERE id = $1", "describe": { "columns": [ { @@ -71,6 +71,11 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -87,8 +92,9 @@ true, true, true, - false + false, + true ] }, - "hash": "73965013a68538139aadcf0c2346e315923c12c54b9446c7b18e4b4fb1791f42" + "hash": "4b05abebeafeda2f88fff48f6d9d45938371b3f39822c4ff68a1a9515767e0ad" } diff --git a/.sqlx/query-54fada56be8b91633550c77f7259703bcc3163f4935898d0988a6045c29e7dd8.json b/.sqlx/query-54fada56be8b91633550c77f7259703bcc3163f4935898d0988a6045c29e7dd8.json deleted file mode 100644 index 8ef5321bb0..0000000000 --- a/.sqlx/query-54fada56be8b91633550c77f7259703bcc3163f4935898d0988a6045c29e7dd8.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey pubkey, preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND (is_authorized OR NOT $2) AND d.configured AND u.is_active ORDER BY d.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "pubkey", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "allowed_ips!: Vec", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Int8", - "Bool" - ] - }, - "nullable": [ - false, - true, - null - ] - }, - "hash": "54fada56be8b91633550c77f7259703bcc3163f4935898d0988a6045c29e7dd8" -} diff --git a/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json b/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json index d62e957d33..548a89c193 100644 --- a/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json +++ b/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json @@ -80,7 +80,7 @@ false, false, true, - false, + true, false, false, false, diff --git a/.sqlx/query-2bc56fca85ec693d2d53fe08e53143fef55fb878076487fc97738f120573820f.json b/.sqlx/query-5a856149fa68d294e5a15ceacdfc5da77a32a11145804dff5692df7c3d74ce7b.json similarity index 81% rename from .sqlx/query-2bc56fca85ec693d2d53fe08e53143fef55fb878076487fc97738f120573820f.json rename to .sqlx/query-5a856149fa68d294e5a15ceacdfc5da77a32a11145804dff5692df7c3d74ce7b.json index e12c06233c..9698c37940 100644 --- a/.sqlx/query-2bc56fca85ec693d2d53fe08e53143fef55fb878076487fc97738f120573820f.json +++ b/.sqlx/query-5a856149fa68d294e5a15ceacdfc5da77a32a11145804dff5692df7c3d74ce7b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key FROM vpn_client_session WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC", "describe": { "columns": [ { @@ -71,10 +71,16 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { "Left": [ + "Int8", "Int8" ] }, @@ -87,8 +93,9 @@ true, true, true, - false + false, + true ] }, - "hash": "2bc56fca85ec693d2d53fe08e53143fef55fb878076487fc97738f120573820f" + "hash": "5a856149fa68d294e5a15ceacdfc5da77a32a11145804dff5692df7c3d74ce7b" } diff --git a/.sqlx/query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json b/.sqlx/query-6363977f3e290a8ee991c9442ab9633fafa49b98d2fd958fcafc66c4cd624ec0.json similarity index 50% rename from .sqlx/query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json rename to .sqlx/query-6363977f3e290a8ee991c9442ab9633fafa49b98d2fd958fcafc66c4cd624ec0.json index a3db757922..4f920f9865 100644 --- a/.sqlx/query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json +++ b/.sqlx/query-6363977f3e290a8ee991c9442ab9633fafa49b98d2fd958fcafc66c4cd624ec0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1", "describe": { "columns": [ { @@ -17,21 +17,6 @@ "ordinal": 2, "name": "wireguard_ips: Vec", "type_info": "InetArray" - }, - { - "ordinal": 3, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_authorized", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "authorized_at", - "type_info": "Timestamp" } ], "parameters": { @@ -42,11 +27,8 @@ "nullable": [ false, false, - false, - true, - false, - true + false ] }, - "hash": "748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475" + "hash": "6363977f3e290a8ee991c9442ab9633fafa49b98d2fd958fcafc66c4cd624ec0" } diff --git a/.sqlx/query-86b6f0f850b4b8436379a9da38c0280867b8f432fc4bd52ba7194a5895540b4e.json b/.sqlx/query-675fe2562e81e9886d488ce639a8c6a9b3fa7d140b30bfee7657e3eacf6de6aa.json similarity index 85% rename from .sqlx/query-86b6f0f850b4b8436379a9da38c0280867b8f432fc4bd52ba7194a5895540b4e.json rename to .sqlx/query-675fe2562e81e9886d488ce639a8c6a9b3fa7d140b30bfee7657e3eacf6de6aa.json index ac7a8d312f..5583f37d4e 100644 --- a/.sqlx/query-86b6f0f850b4b8436379a9da38c0280867b8f432fc4bd52ba7194a5895540b4e.json +++ b/.sqlx/query-675fe2562e81e9886d488ce639a8c6a9b3fa7d140b30bfee7657e3eacf6de6aa.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\" \"mfa_method?: _\",\"state\" \"state: _\" FROM \"vpn_client_session\"", + "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\" \"mfa_method?: _\",\"state\" \"state: _\",\"preshared_key\" FROM \"vpn_client_session\"", "describe": { "columns": [ { @@ -71,6 +71,11 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -85,8 +90,9 @@ true, true, true, - false + false, + true ] }, - "hash": "86b6f0f850b4b8436379a9da38c0280867b8f432fc4bd52ba7194a5895540b4e" + "hash": "675fe2562e81e9886d488ce639a8c6a9b3fa7d140b30bfee7657e3eacf6de6aa" } diff --git a/.sqlx/query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json b/.sqlx/query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json deleted file mode 100644 index 5df6af55de..0000000000 --- a/.sqlx/query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT wireguard_network_id network_id, wireguard_ips \"device_wireguard_ips: Vec\", preshared_key, is_authorized FROM wireguard_network_device WHERE device_id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "network_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "device_wireguard_ips: Vec", - "type_info": "InetArray" - }, - { - "ordinal": 2, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "is_authorized", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - true, - false - ] - }, - "hash": "72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd" -} diff --git a/.sqlx/query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json b/.sqlx/query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json deleted file mode 100644 index f56db2e413..0000000000 --- a/.sqlx/query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1 AND device_id IN (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "device_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "wireguard_network_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "wireguard_ips: Vec", - "type_info": "InetArray" - }, - { - "ordinal": 3, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_authorized", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "authorized_at", - "type_info": "Timestamp" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - true - ] - }, - "hash": "812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087" -} diff --git a/.sqlx/query-8aa6ed2a7f4069cd6e0e0176ce4724039e1a281dd4d3122b5bb8e1cb499070b0.json b/.sqlx/query-8aa6ed2a7f4069cd6e0e0176ce4724039e1a281dd4d3122b5bb8e1cb499070b0.json new file mode 100644 index 0000000000..96542cccbd --- /dev/null +++ b/.sqlx/query-8aa6ed2a7f4069cd6e0e0176ce4724039e1a281dd4d3122b5bb8e1cb499070b0.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE wireguard_network_device SET wireguard_ips = $3 WHERE device_id = $1 AND wireguard_network_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "InetArray" + ] + }, + "nullable": [] + }, + "hash": "8aa6ed2a7f4069cd6e0e0176ce4724039e1a281dd4d3122b5bb8e1cb499070b0" +} diff --git a/.sqlx/query-8b9ede93cc39f26e6006bd26eb6d3e7a35e12060a60ddc70ba3cc135c89db642.json b/.sqlx/query-8b9ede93cc39f26e6006bd26eb6d3e7a35e12060a60ddc70ba3cc135c89db642.json new file mode 100644 index 0000000000..126c4d8db2 --- /dev/null +++ b/.sqlx/query-8b9ede93cc39f26e6006bd26eb6d3e7a35e12060a60ddc70ba3cc135c89db642.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wnd.wireguard_network_id network_id, wnd.wireguard_ips \"device_wireguard_ips: Vec\", CASE WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN NULL::text ELSE active_session.preshared_key END \"preshared_key?\", CASE WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN TRUE ELSE active_session.preshared_key IS NOT NULL END \"is_authorized!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wnd.device_id = $1 ORDER BY wnd.wireguard_network_id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "network_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "device_wireguard_ips: Vec", + "type_info": "InetArray" + }, + { + "ordinal": 2, + "name": "preshared_key?", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "is_authorized!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + null, + null + ] + }, + "hash": "8b9ede93cc39f26e6006bd26eb6d3e7a35e12060a60ddc70ba3cc135c89db642" +} diff --git a/.sqlx/query-51dffd9c1ae018de2ade5fde93ae829ad4a5825707e8b2f55b86801039cd784e.json b/.sqlx/query-973c64873ac510cc407d43708efaaa1f93553237be1c6767564c2499b9d8f67d.json similarity index 86% rename from .sqlx/query-51dffd9c1ae018de2ade5fde93ae829ad4a5825707e8b2f55b86801039cd784e.json rename to .sqlx/query-973c64873ac510cc407d43708efaaa1f93553237be1c6767564c2499b9d8f67d.json index 0dc3ac7eed..304137add8 100644 --- a/.sqlx/query-51dffd9c1ae018de2ade5fde93ae829ad4a5825707e8b2f55b86801039cd784e.json +++ b/.sqlx/query-973c64873ac510cc407d43708efaaa1f93553237be1c6767564c2499b9d8f67d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"vpn_client_session\" SET \"location_id\" = $2,\"user_id\" = $3,\"device_id\" = $4,\"created_at\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7,\"mfa_method\" = $8,\"state\" = $9 WHERE id = $1", + "query": "UPDATE \"vpn_client_session\" SET \"location_id\" = $2,\"user_id\" = $3,\"device_id\" = $4,\"created_at\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7,\"mfa_method\" = $8,\"state\" = $9,\"preshared_key\" = $10 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -37,10 +37,11 @@ ] } } - } + }, + "Text" ] }, "nullable": [] }, - "hash": "51dffd9c1ae018de2ade5fde93ae829ad4a5825707e8b2f55b86801039cd784e" + "hash": "973c64873ac510cc407d43708efaaa1f93553237be1c6767564c2499b9d8f67d" } diff --git a/.sqlx/query-45e1c056d0868dd8072abd5cdab00c42e268149b730b7e4f2add5cbdbe7843c8.json b/.sqlx/query-9cd4fb6b8bb2f231d136f7e02bb9e0f094c419e60e3f8d8bba86f9df5b7d4c9f.json similarity index 83% rename from .sqlx/query-45e1c056d0868dd8072abd5cdab00c42e268149b730b7e4f2add5cbdbe7843c8.json rename to .sqlx/query-9cd4fb6b8bb2f231d136f7e02bb9e0f094c419e60e3f8d8bba86f9df5b7d4c9f.json index 5ead8ddc67..d5be1a7441 100644 --- a/.sqlx/query-45e1c056d0868dd8072abd5cdab00c42e268149b730b7e4f2add5cbdbe7843c8.json +++ b/.sqlx/query-9cd4fb6b8bb2f231d136f7e02bb9e0f094c419e60e3f8d8bba86f9df5b7d4c9f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected')", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key FROM vpn_client_session WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", "describe": { "columns": [ { @@ -71,11 +71,15 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { "Left": [ - "Int8", "Int8" ] }, @@ -88,8 +92,9 @@ true, true, true, - false + false, + true ] }, - "hash": "45e1c056d0868dd8072abd5cdab00c42e268149b730b7e4f2add5cbdbe7843c8" + "hash": "9cd4fb6b8bb2f231d136f7e02bb9e0f094c419e60e3f8d8bba86f9df5b7d4c9f" } diff --git a/.sqlx/query-743a5ec9c0ef4e1f92465ad287dc211dedd7b66f89cdf11b36e4a5d7306258be.json b/.sqlx/query-a2a31a9e9d53d830f658131eab155413d1dd6ce5a24b87b9fc4060dd1ae704be.json similarity index 77% rename from .sqlx/query-743a5ec9c0ef4e1f92465ad287dc211dedd7b66f89cdf11b36e4a5d7306258be.json rename to .sqlx/query-a2a31a9e9d53d830f658131eab155413d1dd6ce5a24b87b9fc4060dd1ae704be.json index b98cbb6ba2..f5b56480af 100644 --- a/.sqlx/query-743a5ec9c0ef4e1f92465ad287dc211dedd7b66f89cdf11b36e4a5d7306258be.json +++ b/.sqlx/query-a2a31a9e9d53d830f658131eab155413d1dd6ce5a24b87b9fc4060dd1ae704be.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT s.id, location_id, user_id, device_id, created_at, s.connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" FROM vpn_client_session s LEFT JOIN LATERAL ( SELECT latest_handshake FROM vpn_session_stats WHERE session_id = s.id ORDER BY latest_handshake DESC LIMIT 1 ) ss ON true WHERE location_id = $1 AND state = 'connected' AND (NOW() - ss.latest_handshake) > $2 * interval '1 second'", + "query": "SELECT s.id, location_id, user_id, device_id, created_at, s.connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key FROM vpn_client_session s LEFT JOIN LATERAL ( SELECT latest_handshake FROM vpn_session_stats WHERE session_id = s.id ORDER BY latest_handshake DESC LIMIT 1 ) ss ON true WHERE location_id = $1 AND state = 'connected' AND (NOW() - ss.latest_handshake) > $2 * interval '1 second'", "describe": { "columns": [ { @@ -71,6 +71,11 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -88,8 +93,9 @@ true, true, true, - false + false, + true ] }, - "hash": "743a5ec9c0ef4e1f92465ad287dc211dedd7b66f89cdf11b36e4a5d7306258be" + "hash": "a2a31a9e9d53d830f658131eab155413d1dd6ce5a24b87b9fc4060dd1ae704be" } diff --git a/.sqlx/query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json b/.sqlx/query-b1c28cf7a7c919c5951dfc050a44528dccc0eaa54d3adbaa7e4a336c992a974d.json similarity index 50% rename from .sqlx/query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json rename to .sqlx/query-b1c28cf7a7c919c5951dfc050a44528dccc0eaa54d3adbaa7e4a336c992a974d.json index aebfd05555..1147544502 100644 --- a/.sqlx/query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json +++ b/.sqlx/query-b1c28cf7a7c919c5951dfc050a44528dccc0eaa54d3adbaa7e4a336c992a974d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", "describe": { "columns": [ { @@ -17,21 +17,6 @@ "ordinal": 2, "name": "wireguard_ips: Vec", "type_info": "InetArray" - }, - { - "ordinal": 3, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_authorized", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "authorized_at", - "type_info": "Timestamp" } ], "parameters": { @@ -43,11 +28,8 @@ "nullable": [ false, false, - false, - true, - false, - true + false ] }, - "hash": "f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3" + "hash": "b1c28cf7a7c919c5951dfc050a44528dccc0eaa54d3adbaa7e4a336c992a974d" } diff --git a/.sqlx/query-cfd57ee3fa1f81e46368f8ee1488bf72f6b253c79b8429d1c0dfa9a935393afd.json b/.sqlx/query-cfd57ee3fa1f81e46368f8ee1488bf72f6b253c79b8429d1c0dfa9a935393afd.json new file mode 100644 index 0000000000..80f736a1b0 --- /dev/null +++ b/.sqlx/query-cfd57ee3fa1f81e46368f8ee1488bf72f6b253c79b8429d1c0dfa9a935393afd.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.wireguard_pubkey pubkey, active_session.preshared_key \"preshared_key!\", ARRAY( SELECT host(ip) FROM unnest(wnd.wireguard_ips) AS ip ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id JOIN LATERAL ( SELECT preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') AND preshared_key IS NOT NULL ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wireguard_network_id = $1 AND d.configured AND u.is_active ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "preshared_key!", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "allowed_ips!: Vec", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + null + ] + }, + "hash": "cfd57ee3fa1f81e46368f8ee1488bf72f6b253c79b8429d1c0dfa9a935393afd" +} diff --git a/.sqlx/query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json b/.sqlx/query-d3bfcc8f183cf03b369c7defab29d6051e67990518cfcf9f6d1889548b3ab9b4.json similarity index 50% rename from .sqlx/query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json rename to .sqlx/query-d3bfcc8f183cf03b369c7defab29d6051e67990518cfcf9f6d1889548b3ab9b4.json index 4341335409..365066c36b 100644 --- a/.sqlx/query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json +++ b/.sqlx/query-d3bfcc8f183cf03b369c7defab29d6051e67990518cfcf9f6d1889548b3ab9b4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device WHERE wireguard_network_id = $1 AND device_id IN (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", "describe": { "columns": [ { @@ -17,36 +17,19 @@ "ordinal": 2, "name": "wireguard_ips: Vec", "type_info": "InetArray" - }, - { - "ordinal": 3, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_authorized", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "authorized_at", - "type_info": "Timestamp" } ], "parameters": { "Left": [ + "Int8", "Int8" ] }, "nullable": [ false, false, - false, - true, - false, - true + false ] }, - "hash": "98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f" + "hash": "d3bfcc8f183cf03b369c7defab29d6051e67990518cfcf9f6d1889548b3ab9b4" } diff --git a/.sqlx/query-2700bf01e6a2afbe3ac2a4b686f715ad1262d5eb8cdf76cf4234b6a4971e6769.json b/.sqlx/query-d86d5f9cb508b1840f0de3c40a993ee77d9cb3c80d8028d99bcc08ccd4c78dd0.json similarity index 82% rename from .sqlx/query-2700bf01e6a2afbe3ac2a4b686f715ad1262d5eb8cdf76cf4234b6a4971e6769.json rename to .sqlx/query-d86d5f9cb508b1840f0de3c40a993ee77d9cb3c80d8028d99bcc08ccd4c78dd0.json index efcd4c5101..f18173e22b 100644 --- a/.sqlx/query-2700bf01e6a2afbe3ac2a4b686f715ad1262d5eb8cdf76cf4234b6a4971e6769.json +++ b/.sqlx/query-d86d5f9cb508b1840f0de3c40a993ee77d9cb3c80d8028d99bcc08ccd4c78dd0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND state = 'new' AND (NOW() - created_at) > $2 * interval '1 second'", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key FROM vpn_client_session WHERE location_id = $1 AND state = 'new' AND (NOW() - created_at) > $2 * interval '1 second'", "describe": { "columns": [ { @@ -71,6 +71,11 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -88,8 +93,9 @@ true, true, true, - false + false, + true ] }, - "hash": "2700bf01e6a2afbe3ac2a4b686f715ad1262d5eb8cdf76cf4234b6a4971e6769" + "hash": "d86d5f9cb508b1840f0de3c40a993ee77d9cb3c80d8028d99bcc08ccd4c78dd0" } diff --git a/.sqlx/query-e87af2b4e3fd79709a28381e04690aee96054585a87e6673a5efa275795dc060.json b/.sqlx/query-e87af2b4e3fd79709a28381e04690aee96054585a87e6673a5efa275795dc060.json new file mode 100644 index 0000000000..2eebd02c59 --- /dev/null +++ b/.sqlx/query-e87af2b4e3fd79709a28381e04690aee96054585a87e6673a5efa275795dc060.json @@ -0,0 +1,101 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key FROM vpn_client_session WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "location_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "mfa_method: VpnClientMfaMethod", + "type_info": { + "Custom": { + "name": "vpn_client_mfa_method", + "kind": { + "Enum": [ + "totp", + "email", + "oidc", + "biometric", + "mobileapprove" + ] + } + } + } + }, + { + "ordinal": 8, + "name": "state: VpnClientSessionState", + "type_info": { + "Custom": { + "name": "vpn_client_session_state", + "kind": { + "Enum": [ + "new", + "connected", + "disconnected" + ] + } + } + } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + true, + false, + true + ] + }, + "hash": "e87af2b4e3fd79709a28381e04690aee96054585a87e6673a5efa275795dc060" +} diff --git a/.sqlx/query-eec3959fe7c4cbfc4ec5215f1664ac34a15e6026f3d571bff9266065b04c14c0.json b/.sqlx/query-eec3959fe7c4cbfc4ec5215f1664ac34a15e6026f3d571bff9266065b04c14c0.json new file mode 100644 index 0000000000..15053df8d7 --- /dev/null +++ b/.sqlx/query-eec3959fe7c4cbfc4ec5215f1664ac34a15e6026f3d571bff9266065b04c14c0.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device WHERE device_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "wireguard_network_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "wireguard_ips: Vec", + "type_info": "InetArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "eec3959fe7c4cbfc4ec5215f1664ac34a15e6026f3d571bff9266065b04c14c0" +} diff --git a/.sqlx/query-fed5b29a44329968a6f134990261a3ab49a5a2f9fd81eed714678ec272718ea4.json b/.sqlx/query-fed5b29a44329968a6f134990261a3ab49a5a2f9fd81eed714678ec272718ea4.json new file mode 100644 index 0000000000..3851a84ee2 --- /dev/null +++ b/.sqlx/query-fed5b29a44329968a6f134990261a3ab49a5a2f9fd81eed714678ec272718ea4.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device WHERE wireguard_network_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "wireguard_network_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "wireguard_ips: Vec", + "type_info": "InetArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "fed5b29a44329968a6f134990261a3ab49a5a2f9fd81eed714678ec272718ea4" +} diff --git a/.trivyignore.yaml b/.trivyignore.yaml index bb760342b8..bdb80882a1 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -11,3 +11,6 @@ vulnerabilities: - id: CVE-2025-7709 expired_at: 2026-03-30 statement: "Not yet fixed in Debian" + - id: CVE-2026-32763 + expired_at: 2026-03-26 + statement: "Waiting for upstream patch in paraglide" diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index a3ccb525ab..218e5f7596 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -23,7 +23,7 @@ use crate::{ models::{ ModelError, WireguardNetwork, user::User, - vpn_client_session::VpnClientSessionState, + vpn_client_session::{VpnClientSession, VpnClientSessionState}, wireguard::{ LocationMfaMode, NetworkAddressError, ServiceLocationMode, WireguardNetworkError, }, @@ -152,6 +152,25 @@ pub struct DeviceNetworkInfo { pub is_authorized: bool, } +impl DeviceNetworkInfo { + #[must_use] + pub fn from_authorized_mfa_session( + network_id: Id, + device_wireguard_ips: I, + preshared_key: String, + ) -> Self + where + I: Into>, + { + Self { + network_id, + device_wireguard_ips: device_wireguard_ips.into(), + preshared_key: Some(preshared_key), + is_authorized: true, + } + } +} + impl DeviceInfo { pub async fn from_device<'e, E>(executor: E, device: Device) -> Result where @@ -160,11 +179,29 @@ impl DeviceInfo { debug!("Generating device info for {device}"); let network_info = query_as!( DeviceNetworkInfo, - "SELECT wireguard_network_id network_id, \ - wireguard_ips \"device_wireguard_ips: Vec\", \ - preshared_key, is_authorized \ - FROM wireguard_network_device \ - WHERE device_id = $1", + "SELECT wnd.wireguard_network_id network_id, \ + wnd.wireguard_ips \"device_wireguard_ips: Vec\", \ + CASE \ + WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN NULL::text \ + ELSE active_session.preshared_key \ + END \"preshared_key?\", \ + CASE \ + WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN TRUE \ + ELSE active_session.preshared_key IS NOT NULL \ + END \"is_authorized!\" \ + FROM wireguard_network_device wnd \ + JOIN wireguard_network n ON n.id = wnd.wireguard_network_id \ + LEFT JOIN LATERAL ( \ + SELECT id, preshared_key \ + FROM vpn_client_session \ + WHERE location_id = wnd.wireguard_network_id \ + AND device_id = wnd.device_id \ + AND state IN ('new', 'connected') \ + ORDER BY created_at DESC, id DESC \ + LIMIT 1 \ + ) active_session ON true \ + WHERE wnd.device_id = $1 \ + ORDER BY wnd.wireguard_network_id ASC", device.id ) .fetch_all(executor) @@ -218,7 +255,7 @@ impl UserDevice { LIMIT 1 \ ) vss ON vss.session_id = vpn_client_session.id \ WHERE location_id = n.id and device_id = $1 \ - ORDER BY created_at DESC \ + ORDER BY created_at DESC, id DESC \ LIMIT 1 \ ) vs ON vs.location_id = n.id \ WHERE wnd.device_id = $1", @@ -274,9 +311,6 @@ pub struct WireguardNetworkDevice { pub wireguard_network_id: Id, pub wireguard_ips: Vec, pub device_id: Id, - pub preshared_key: Option, - pub is_authorized: bool, - pub authorized_at: Option, } #[derive(Debug, Deserialize, Serialize, ToSchema)] @@ -293,6 +327,56 @@ pub struct ModifyDevice { } impl WireguardNetworkDevice { + async fn latest_active_session<'e, E>( + executor: E, + network: &WireguardNetwork, + device_id: Id, + ) -> sqlx::Result>> + where + E: PgExecutor<'e>, + { + if !network.mfa_enabled() { + return Ok(None); + } + + VpnClientSession::try_get_active_session(executor, network.id, device_id).await + } + + #[must_use] + pub fn to_device_network_info( + &self, + network: &WireguardNetwork, + active_session: Option<&VpnClientSession>, + ) -> DeviceNetworkInfo { + let (preshared_key, is_authorized) = if !network.mfa_enabled() { + (None, true) + } else { + let preshared_key = active_session.and_then(|session| session.preshared_key.clone()); + let is_authorized = preshared_key.is_some(); + (preshared_key, is_authorized) + }; + + DeviceNetworkInfo { + network_id: network.id, + device_wireguard_ips: self.wireguard_ips.clone(), + preshared_key, + is_authorized, + } + } + + pub async fn to_device_network_info_runtime<'e, E>( + &self, + executor: E, + network: &WireguardNetwork, + ) -> sqlx::Result + where + E: PgExecutor<'e>, + { + let active_session = Self::latest_active_session(executor, network, self.device_id).await?; + + Ok(self.to_device_network_info(network, active_session.as_ref())) + } + #[must_use] pub fn new(network_id: Id, device_id: Id, wireguard_ips: I) -> Self where @@ -302,9 +386,6 @@ impl WireguardNetworkDevice { wireguard_network_id: network_id, wireguard_ips: wireguard_ips.into(), device_id, - preshared_key: None, - is_authorized: false, - authorized_at: None, } } @@ -322,17 +403,13 @@ impl WireguardNetworkDevice { { query!( "INSERT INTO wireguard_network_device \ - (device_id, wireguard_network_id, wireguard_ips, is_authorized, authorized_at, \ - preshared_key) \ - VALUES ($1, $2, $3, $4, $5, $6) \ + (device_id, wireguard_network_id, wireguard_ips) \ + VALUES ($1, $2, $3) \ ON CONFLICT ON CONSTRAINT device_network \ - DO UPDATE SET wireguard_ips = $3, is_authorized = $4", + DO UPDATE SET wireguard_ips = $3", self.device_id, self.wireguard_network_id, &self.ips_as_network(), - self.is_authorized, - self.authorized_at, - self.preshared_key, ) .execute(executor) .await?; @@ -346,14 +423,11 @@ impl WireguardNetworkDevice { { query!( "UPDATE wireguard_network_device \ - SET wireguard_ips = $3, is_authorized = $4, authorized_at = $5, preshared_key = $6 \ + SET wireguard_ips = $3 \ WHERE device_id = $1 AND wireguard_network_id = $2", self.device_id, self.wireguard_network_id, &self.ips_as_network(), - self.is_authorized, - self.authorized_at, - self.preshared_key, ) .execute(executor) .await?; @@ -388,8 +462,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ips \"wireguard_ips: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device \ WHERE device_id = $1 AND wireguard_network_id = $2", device_id, @@ -410,8 +483,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ips \"wireguard_ips: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device \ WHERE device_id = $1 ORDER BY id LIMIT 1", device_id @@ -432,8 +504,7 @@ impl WireguardNetworkDevice { let result = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ips \"wireguard_ips: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device WHERE device_id = $1", device_id ) @@ -454,8 +525,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ips \"wireguard_ips: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device \ WHERE wireguard_network_id = $1", network_id @@ -480,8 +550,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ips \"wireguard_ips: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device \ WHERE wireguard_network_id = $1 AND device_id IN \ (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", @@ -686,12 +755,9 @@ impl Device { WireguardNetworkDevice::find(&mut *transaction, self.id, network.id) .await? .ok_or_else(|| DeviceError::Unexpected("Device not found in network".into()))?; - let device_network_info = DeviceNetworkInfo { - network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips.clone(), - preshared_key: wireguard_network_device.preshared_key.clone(), - is_authorized: wireguard_network_device.is_authorized, - }; + let device_network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *transaction, network) + .await?; let config = Self::create_config(network, &wireguard_network_device); let device_config = DeviceConfig { @@ -720,12 +786,9 @@ impl Device { let wireguard_network_device = self .assign_network_ips(&mut *transaction, network, ip) .await?; - let device_network_info = DeviceNetworkInfo { - network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips.clone(), - preshared_key: wireguard_network_device.preshared_key.clone(), - is_authorized: wireguard_network_device.is_authorized, - }; + let device_network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *transaction, network) + .await?; let config = Self::create_config(network, &wireguard_network_device); let device_config = DeviceConfig { @@ -797,12 +860,9 @@ impl Device { self.name, self.user_id ); - let device_network_info = DeviceNetworkInfo { - network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips.clone(), - preshared_key: wireguard_network_device.preshared_key.clone(), - is_authorized: wireguard_network_device.is_authorized, - }; + let device_network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *conn, &network) + .await?; network_info.push(device_network_info); let config = Self::create_config(&network, &wireguard_network_device); @@ -1043,7 +1103,7 @@ mod test { use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; - use crate::db::setup_pool; + use crate::db::{models::vpn_client_session::VpnClientMfaMethod, setup_pool}; impl Device { /// Create new device and assign IP in a given network @@ -1405,6 +1465,334 @@ mod test { assert_ok!(Device::validate_pubkey(valid_test_key)); } + #[sqlx::test] + async fn test_runtime_mfa_state_marks_mfa_session_without_preshared_key_as_unauthorized( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let network = WireguardNetwork::new( + "runtime-mfa-network".into(), + 51820, + "vpn.example.com".into(), + None, + Vec::::new(), + false, + false, + false, + LocationMfaMode::Internal, + ServiceLocationMode::Disabled, + ) + .try_set_address("10.1.1.1/24") + .unwrap() + .save(&pool) + .await + .unwrap(); + + let wireguard_network_device = WireguardNetworkDevice { + wireguard_network_id: network.id, + wireguard_ips: vec![IpAddr::from_str("10.1.1.2").unwrap()], + device_id: 1, + }; + let active_session = VpnClientSession { + id: 1, + location_id: network.id, + user_id: 1, + device_id: wireguard_network_device.device_id, + created_at: Utc::now().naive_utc(), + connected_at: None, + disconnected_at: None, + mfa_method: Some(VpnClientMfaMethod::Totp), + state: VpnClientSessionState::New, + preshared_key: None, + }; + + let network_info = + wireguard_network_device.to_device_network_info(&network, Some(&active_session)); + + assert_eq!(network_info.preshared_key, None); + assert!(!network_info.is_authorized); + } + + #[sqlx::test] + async fn test_runtime_mfa_state_keeps_session_preshared_key_for_authorized_runtime_reads( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let network = WireguardNetwork::new( + "runtime-mfa-network".into(), + 51820, + "vpn.example.com".into(), + None, + Vec::::new(), + false, + false, + false, + LocationMfaMode::Internal, + ServiceLocationMode::Disabled, + ) + .try_set_address("10.1.1.1/24") + .unwrap() + .save(&pool) + .await + .unwrap(); + + let wireguard_network_device = WireguardNetworkDevice { + wireguard_network_id: network.id, + wireguard_ips: vec![IpAddr::from_str("10.1.1.2").unwrap()], + device_id: 1, + }; + let active_session = VpnClientSession { + id: 1, + location_id: network.id, + user_id: 1, + device_id: wireguard_network_device.device_id, + created_at: Utc::now().naive_utc(), + connected_at: Some(Utc::now().naive_utc()), + disconnected_at: None, + mfa_method: Some(VpnClientMfaMethod::Totp), + state: VpnClientSessionState::Connected, + preshared_key: Some("runtime-session-psk".into()), + }; + + let network_info = + wireguard_network_device.to_device_network_info(&network, Some(&active_session)); + + assert_eq!( + network_info.preshared_key, + Some("runtime-session-psk".into()) + ); + assert!(network_info.is_authorized); + } + + #[sqlx::test] + async fn test_device_info_marks_mfa_session_without_preshared_key_as_unauthorized( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password"), + "Tester", + "Test", + "test@test.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device".into(), + "pubkey".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let network = WireguardNetwork::new( + "device-info-network".into(), + 51820, + "vpn.example.com".into(), + None, + Vec::::new(), + false, + false, + false, + LocationMfaMode::Internal, + ServiceLocationMode::Disabled, + ) + .try_set_address("10.1.1.1/24") + .unwrap(); + let network = network.save(&pool).await.unwrap(); + + let wireguard_network_device = WireguardNetworkDevice::new( + network.id, + device.id, + [IpAddr::from_str("10.1.1.2").unwrap()], + ); + wireguard_network_device.insert(&pool).await.unwrap(); + + let session = VpnClientSession::new( + network.id, + user.id, + device.id, + None, + Some(VpnClientMfaMethod::Totp), + ); + session.save(&pool).await.unwrap(); + + let device_info = DeviceInfo::from_device(&pool, device).await.unwrap(); + let network_info = device_info + .network_info + .into_iter() + .find(|info| info.network_id == network.id) + .unwrap(); + + assert!(!network_info.is_authorized); + assert_eq!(network_info.preshared_key, None); + } + + #[sqlx::test] + async fn test_device_info_keeps_mfa_session_preshared_key_for_authorized_full_sync_reads( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password"), + "Tester", + "Test", + "test@test.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device".into(), + "pubkey".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let network = WireguardNetwork::new( + "device-info-network".into(), + 51820, + "vpn.example.com".into(), + None, + Vec::::new(), + false, + false, + false, + LocationMfaMode::Internal, + ServiceLocationMode::Disabled, + ) + .try_set_address("10.1.1.1/24") + .unwrap() + .save(&pool) + .await + .unwrap(); + + let wireguard_network_device = WireguardNetworkDevice::new( + network.id, + device.id, + [IpAddr::from_str("10.1.1.2").unwrap()], + ); + wireguard_network_device.insert(&pool).await.unwrap(); + + let mut session = VpnClientSession::new( + network.id, + user.id, + device.id, + Some(Utc::now().naive_utc()), + Some(VpnClientMfaMethod::Totp), + ); + session.preshared_key = Some("device-info-session-psk".into()); + session.save(&pool).await.unwrap(); + + let device_info = DeviceInfo::from_device(&pool, device).await.unwrap(); + let network_info = device_info + .network_info + .into_iter() + .find(|info| info.network_id == network.id) + .unwrap(); + + assert!(network_info.is_authorized); + assert_eq!( + network_info.preshared_key, + Some("device-info-session-psk".into()) + ); + } + + #[sqlx::test] + async fn test_device_info_keeps_non_mfa_location_authorized_without_exposing_session_preshared_key( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password"), + "Tester", + "Test", + "test@test.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device".into(), + "pubkey".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let network = WireguardNetwork::new( + "device-info-network".into(), + 51820, + "vpn.example.com".into(), + None, + Vec::::new(), + false, + false, + false, + LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, + ) + .try_set_address("10.1.1.1/24") + .unwrap() + .save(&pool) + .await + .unwrap(); + + let wireguard_network_device = WireguardNetworkDevice::new( + network.id, + device.id, + [IpAddr::from_str("10.1.1.2").unwrap()], + ); + wireguard_network_device.insert(&pool).await.unwrap(); + + let mut session = VpnClientSession::new(network.id, user.id, device.id, None, None); + session.preshared_key = Some("legacy-session-psk".into()); + session.save(&pool).await.unwrap(); + + let device_info = DeviceInfo::from_device(&pool, device).await.unwrap(); + let network_info = device_info + .network_info + .into_iter() + .find(|info| info.network_id == network.id) + .unwrap(); + + assert!(network_info.is_authorized); + assert_eq!(network_info.preshared_key, None); + } + #[sqlx::test] fn test_all_for_network_and_user(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs index bfb95095d2..6ca6dcc886 100644 --- a/crates/defguard_common/src/db/models/vpn_client_session.rs +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -41,6 +41,7 @@ pub struct VpnClientSession { pub mfa_method: Option, #[model(enum)] pub state: VpnClientSessionState, + pub preshared_key: Option, } impl VpnClientSession { @@ -69,6 +70,7 @@ impl VpnClientSession { disconnected_at: None, mfa_method, state, + preshared_key: None, } } } @@ -85,9 +87,11 @@ impl VpnClientSession { query_as!( Self, "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, \ - mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" \ + mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key \ FROM vpn_client_session \ - WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected')", + WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') \ + ORDER BY created_at DESC, id DESC \ + LIMIT 1", location_id, device_id ) @@ -121,7 +125,7 @@ impl VpnClientSession { query_as!( Self, "SELECT s.id, location_id, user_id, device_id, created_at, s.connected_at, disconnected_at, \ - mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" \ + mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key \ FROM vpn_client_session s \ LEFT JOIN LATERAL ( \ SELECT latest_handshake \ @@ -145,7 +149,7 @@ impl VpnClientSession { query_as!( Self, "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, \ - mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" \ + mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key \ FROM vpn_client_session \ WHERE location_id = $1 AND state = 'new' \ AND (NOW() - created_at) > $2 * interval '1 second'", @@ -163,9 +167,10 @@ impl VpnClientSession { query_as!( Self, "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, \ - mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" \ + mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key \ FROM vpn_client_session \ - WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected')", + WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') \ + ORDER BY created_at DESC, id DESC", location_id, device_id, ).fetch_all(executor).await diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index d61d012d5d..fdcf0cabbd 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -1410,7 +1410,7 @@ impl WireguardNetwork { VpnClientSession, "SELECT id, location_id, user_id, device_id, created_at, connected_at, \ disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", \ - state \"state: VpnClientSessionState\" \ + state \"state: VpnClientSessionState\", preshared_key \ FROM vpn_client_session \ WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", self.id, diff --git a/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs index c7093101f3..47ae9b0db2 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs @@ -67,9 +67,6 @@ async fn setup_user_and_device( device_id: device.id, wireguard_network_id: location.id, wireguard_ips, - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(pool).await.unwrap(); } diff --git a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs index 258c459552..6a78e4b09e 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -130,9 +130,6 @@ async fn create_test_users_and_devices( device_id: device.id, wireguard_network_id: location.id, wireguard_ips, - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(pool).await.unwrap(); } @@ -290,9 +287,6 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO user.id as u8, device_num as u8, ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -389,9 +383,6 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO device_id, wireguard_network_id: location.id, wireguard_ips: vec![ip], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -717,9 +708,6 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO user.id as u16, device_num as u16, ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -816,9 +804,6 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO device_id, wireguard_network_id: location.id, wireguard_ips: vec![ip], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -1175,9 +1160,6 @@ async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: P device_num as u16, )), ], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -1283,9 +1265,6 @@ async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: P device_id, wireguard_network_id: location.id, wireguard_ips: ips, - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -2184,9 +2163,6 @@ async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgCon user.id as u8, device_num as u8, ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); let network_device = WireguardNetworkDevice { @@ -2202,9 +2178,6 @@ async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgCon user.id as u16, device_num as u16, ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); let network_device = WireguardNetworkDevice { @@ -2223,9 +2196,6 @@ async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgCon device_num as u16, )), ], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index beb66fd8a1..204b836717 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -12,7 +12,7 @@ use defguard_common::{ Id, models::{ Device, Settings, WireguardNetwork, - device::{DeviceInfo, WireguardNetworkDevice}, + device::{DeviceInfo, DeviceNetworkInfo}, wireguard::ServiceLocationMode, }, }, @@ -229,7 +229,7 @@ pub enum GatewayEvent { DeviceDeleted(DeviceInfo), FirewallConfigChanged(Id, FirewallConfig), FirewallDisabled(Id), - MfaSessionAuthorized(Id, Device, WireguardNetworkDevice), + MfaSessionAuthorized(Id, Device, DeviceNetworkInfo), MfaSessionDisconnected(Id, Device), } diff --git a/crates/defguard_core/src/grpc/proxy/client_mfa.rs b/crates/defguard_core/src/grpc/proxy/client_mfa.rs index 865713675b..e379eed6c1 100644 --- a/crates/defguard_core/src/grpc/proxy/client_mfa.rs +++ b/crates/defguard_core/src/grpc/proxy/client_mfa.rs @@ -11,7 +11,7 @@ use defguard_common::{ Id, models::{ BiometricAuth, BiometricChallenge, Device, User, WireguardNetwork, - device::WireguardNetworkDevice, + device::{DeviceNetworkInfo, WireguardNetworkDevice}, vpn_client_session::{VpnClientMfaMethod, VpnClientSession, VpnClientSessionState}, wireguard::LocationMfaMode, }, @@ -79,6 +79,17 @@ pub struct ClientMfaServer { } impl ClientMfaServer { + fn build_mfa_authorized_gateway_network_info( + network_device: WireguardNetworkDevice, + preshared_key: String, + ) -> DeviceNetworkInfo { + DeviceNetworkInfo::from_authorized_mfa_session( + network_device.wireguard_network_id, + network_device.wireguard_ips, + preshared_key, + ) + } + #[must_use] pub fn new( pool: PgPool, @@ -656,48 +667,39 @@ impl ClientMfaServer { })?; // fetch device config for the location - let Ok(Some(mut network_device)) = + let Ok(Some(network_device)) = WireguardNetworkDevice::find(&mut *transaction, device.id, location.id).await else { error!("Failed to fetch network config for device {device} and location {location}"); return Err(Status::internal("unexpected error")); }; + // generate PSK + let key = WireguardNetwork::genkey(); + // create new VPN client session let vpn_client_session = self.create_new_mfa_session( - &mut transaction, + &mut transaction, &location, &user, &device, method.into(), + key.public.clone(), ) - .await + .await .map_err(|err| { error!("Failed to create new VPN client session for device {device} in location {location}: {err}"); Status::internal("unexpected error") })?; debug!("Created new VPN client session: {vpn_client_session:?}"); - // generate PSK - let key = WireguardNetwork::genkey(); - network_device.preshared_key = Some(key.public.clone()); - - // authorize device for given location - network_device.is_authorized = true; - network_device.authorized_at = Some(Utc::now().naive_utc()); - - // save updated network config - network_device - .update(&mut *transaction) - .await - .map_err(|err| { - error!("Failed to update device network config {network_device:?}: {err}"); - Status::internal("unexpected error") - })?; + let gateway_network_info = + Self::build_mfa_authorized_gateway_network_info(network_device, key.public.clone()); // send gateway event debug!("Sending `peer_create` message to gateway"); - let event = GatewayEvent::MfaSessionAuthorized(location.id, device.clone(), network_device); + let event = + GatewayEvent::MfaSessionAuthorized(location.id, device.clone(), gateway_network_info); self.wireguard_tx.send(event).map_err(|err| { error!("Error sending WireGuard event: {err}"); Status::internal("unexpected error") @@ -762,6 +764,7 @@ impl ClientMfaServer { user: &User, device: &Device, mfa_method: VpnClientMfaMethod, + preshared_key: String, ) -> Result, Status> { debug!( "Creating new VPN session for device {device} of user {user} in location {location} after successful MFA authorization." @@ -788,11 +791,13 @@ impl ClientMfaServer { } // create new MFA session - VpnClientSession::new(location.id, user.id, device.id, None, Some(mfa_method)).save(conn).await - .map_err(|err| { - error!("Failed to create new VPN client session for device {device} in location {location}: {err}"); - Status::internal("unexpected error") - }) + let mut session = + VpnClientSession::new(location.id, user.id, device.id, None, Some(mfa_method)); + session.preshared_key = Some(preshared_key); + session.save(conn).await.map_err(|err| { + error!("Failed to create new VPN client session for device {device} in location {location}: {err}"); + Status::internal("unexpected error") + }) } /// Update session state as disconnected and send relevant gateway update @@ -816,30 +821,6 @@ impl ClientMfaServer { Status::internal("unexpected error") })?; - // FIXME: remove once MFA-related data is no longer stored here - // update device network config - if let Some(mut device_network_info) = WireguardNetworkDevice::find( - &mut *conn, - device.id, - location.id, - ) - .await - .map_err(|err| { - error!( - "Failed to fetch WireGuard config for device {device} in location {location}: {err}" - ); - Status::internal("unexpected error") - })? { - device_network_info.is_authorized = false; - device_network_info.preshared_key = None; - device_network_info.update(&mut *conn).await.map_err(|err| { - error!( - "Failed to update WireGuard config for device {device} in location {location}: {err}" - ); - Status::internal("unexpected error") - })?; - } - // gateway update is only needed to remove peer for MFA sessions // this is needed to remove peers for both Connected and New sessions if is_mfa_session { @@ -884,15 +865,32 @@ mod tests { sync::{Arc, RwLock}, }; + use chrono::Utc; use defguard_common::db::{ - models::{DeviceType, device::WireguardNetworkDevice, wireguard::ServiceLocationMode}, + Id, + models::{ + Device, DeviceType, User, WireguardNetwork, + device::WireguardNetworkDevice, + vpn_client_session::{VpnClientMfaMethod, VpnClientSession, VpnClientSessionState}, + wireguard::{LocationMfaMode, ServiceLocationMode}, + }, setup_pool, }; use ipnetwork::IpNetwork; - use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - use tokio::sync::{broadcast, mpsc::unbounded_channel, oneshot}; + use sqlx::{ + PgPool, + postgres::{PgConnectOptions, PgPoolOptions}, + }; + use tokio::sync::{broadcast, mpsc, oneshot}; - use super::*; + use super::{ClientLoginSession, ClientMfaServer}; + use crate::{ + events::{BidiStreamEvent, BidiStreamEventType, DesktopClientMfaEvent}, + grpc::GatewayEvent, + }; + + const REPLACEMENT_MFA_PRESHARED_KEY: &str = "replacement-mfa-psk"; + const NEW_MFA_PRESHARED_KEY: &str = "new-psk"; #[sqlx::test] async fn test_replacing_connected_mfa_session_emits_mfa_disconnect_event( @@ -925,6 +923,7 @@ mod tests { &user, &device, VpnClientMfaMethod::Totp, + REPLACEMENT_MFA_PRESHARED_KEY.to_string(), ) .await .expect("should replace connected MFA session"); @@ -999,6 +998,7 @@ mod tests { &user, &device, VpnClientMfaMethod::Totp, + REPLACEMENT_MFA_PRESHARED_KEY.to_string(), ) .await .expect("should replace new MFA session"); @@ -1057,6 +1057,7 @@ mod tests { &user, &device, VpnClientMfaMethod::Totp, + REPLACEMENT_MFA_PRESHARED_KEY.to_string(), ) .await .expect("should replace connected non-MFA session"); @@ -1102,7 +1103,7 @@ mod tests { tokio::sync::broadcast::Receiver, ) { let (wireguard_tx, wireguard_rx) = broadcast::channel(8); - let (bidi_event_tx, bidi_event_rx) = unbounded_channel(); + let (bidi_event_tx, bidi_event_rx) = mpsc::unbounded_channel(); let remote_mfa_responses: Arc>>> = Arc::default(); let sessions: Arc>> = Arc::default(); @@ -1148,6 +1149,90 @@ mod tests { .expect("failed to create device") } + #[sqlx::test] + async fn test_create_new_mfa_session_disconnects_previous_active_session( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + let location = create_mfa_location(&pool).await; + let user = create_user(&pool).await; + let device = create_device(&pool, user.id).await; + attach_device_to_location(&pool, location.id, device.id).await; + + let mut previous_session = VpnClientSession::new( + location.id, + user.id, + device.id, + Some(Utc::now().naive_utc()), + Some(VpnClientMfaMethod::Totp), + ); + previous_session.preshared_key = Some("old-psk".to_string()); + previous_session.state = VpnClientSessionState::Connected; + let previous_session = previous_session + .save(&pool) + .await + .expect("failed to create previous active MFA session"); + + let (gateway_tx, mut gateway_rx) = broadcast::channel(4); + let (bidi_event_tx, _bidi_event_rx) = mpsc::unbounded_channel(); + let server = ClientMfaServer::new( + pool.clone(), + gateway_tx, + bidi_event_tx, + Arc::new(RwLock::new( + HashMap::>::new(), + )), + Arc::new(RwLock::new(HashMap::::new())), + ); + let mut conn = pool + .acquire() + .await + .expect("failed to acquire database connection"); + + let new_session = server + .create_new_mfa_session( + &mut conn, + &location, + &user, + &device, + VpnClientMfaMethod::Totp, + NEW_MFA_PRESHARED_KEY.to_string(), + ) + .await + .expect("failed to create replacement MFA session"); + + let previous_session = VpnClientSession::find_by_id(&pool, previous_session.id) + .await + .expect("failed to reload previous session") + .expect("expected previous session to exist"); + assert_eq!(previous_session.state, VpnClientSessionState::Disconnected); + assert!(previous_session.disconnected_at.is_some()); + + let active_sessions = VpnClientSession::get_all_active_device_sessions_in_location( + &pool, + location.id, + device.id, + ) + .await + .expect("failed to fetch active sessions"); + assert_eq!(active_sessions.len(), 1); + assert_eq!(active_sessions[0].id, new_session.id); + assert_eq!( + active_sessions[0].preshared_key.as_deref(), + Some(NEW_MFA_PRESHARED_KEY) + ); + + match gateway_rx.try_recv() { + Ok(GatewayEvent::MfaSessionDisconnected(location_id, disconnected_device)) => { + assert_eq!(location_id, location.id); + assert_eq!(disconnected_device.id, device.id); + } + Ok(other) => panic!("unexpected gateway event: {other:?}"), + Err(error) => panic!("expected MFA disconnect gateway event, got {error:?}"), + } + } + async fn create_mfa_location(pool: &PgPool) -> WireguardNetwork { WireguardNetwork::new( "client-mfa-location".to_string(), diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index 214c209307..ba3e39451d 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -9,7 +9,7 @@ use defguard_common::{ db::{ Id, models::{ - Device, DeviceConfig, DeviceNetworkInfo, DeviceType, WireguardNetwork, + Device, DeviceConfig, DeviceType, WireguardNetwork, device::{AddDevice, DeviceInfo, ModifyDevice, WireguardNetworkDevice}, wireguard::{LocationMfaMode, MappedDevice, ServiceLocationMode}, }, @@ -1027,12 +1027,9 @@ pub(crate) async fn modify_device( let wireguard_network_device = WireguardNetworkDevice::find(&appstate.pool, device.id, network.id).await?; if let Some(wireguard_network_device) = wireguard_network_device { - let device_network_info = DeviceNetworkInfo { - network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips, - preshared_key: wireguard_network_device.preshared_key, - is_authorized: wireguard_network_device.is_authorized, - }; + let device_network_info = wireguard_network_device + .to_device_network_info_runtime(&appstate.pool, network) + .await?; network_info.push(device_network_info); } } diff --git a/crates/defguard_core/src/location_management/allowed_peers.rs b/crates/defguard_core/src/location_management/allowed_peers.rs index bce9fa761e..88e5bc1c14 100644 --- a/crates/defguard_core/src/location_management/allowed_peers.rs +++ b/crates/defguard_core/src/location_management/allowed_peers.rs @@ -10,6 +10,7 @@ use crate::grpc::should_prevent_service_location_usage; /// which enables enforcing peer disconnect in MFA-protected networks. /// /// If the location is a service location, only returns peers if enterprise features are enabled. +/// MFA-enabled locations only return peers backed by an active session with a runtime preshared key. pub async fn get_location_allowed_peers<'e, E>( location: &WireguardNetwork, executor: E, @@ -27,56 +28,87 @@ where return Ok(Vec::new()); } + if !location.mfa_enabled() { + let rows = query!( + "SELECT d.wireguard_pubkey pubkey, \ + ARRAY( \ + SELECT host(ip) \ + FROM unnest(wnd.wireguard_ips) AS ip \ + ) \"allowed_ips!: Vec\" \ + FROM wireguard_network_device wnd \ + JOIN device d ON wnd.device_id = d.id \ + JOIN \"user\" u ON d.user_id = u.id \ + WHERE wireguard_network_id = $1 \ + AND d.configured \ + AND u.is_active \ + ORDER BY d.id ASC", + location.id, + ) + .fetch_all(executor) + .await?; + + return Ok(rows + .into_iter() + .map(|row| Peer { + pubkey: row.pubkey, + allowed_ips: row.allowed_ips, + preshared_key: None, + keepalive_interval: Some(location.keepalive_interval.cast_unsigned()), + }) + .collect()); + } + let rows = query!( - "SELECT d.wireguard_pubkey pubkey, preshared_key, \ - -- TODO possible to not use ARRAY-unnest here? - ARRAY( - SELECT host(ip) - FROM unnest(wnd.wireguard_ips) AS ip + "SELECT d.wireguard_pubkey pubkey, \ + active_session.preshared_key \"preshared_key!\", \ + ARRAY( \ + SELECT host(ip) \ + FROM unnest(wnd.wireguard_ips) AS ip \ ) \"allowed_ips!: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ JOIN \"user\" u ON d.user_id = u.id \ - WHERE wireguard_network_id = $1 AND (is_authorized OR NOT $2) \ - AND d.configured AND u.is_active \ + JOIN LATERAL ( \ + SELECT preshared_key \ + FROM vpn_client_session \ + WHERE location_id = wnd.wireguard_network_id \ + AND device_id = wnd.device_id \ + AND state IN ('new', 'connected') \ + AND preshared_key IS NOT NULL \ + ORDER BY created_at DESC, id DESC \ + LIMIT 1 \ + ) active_session ON true \ + WHERE wireguard_network_id = $1 \ + AND d.configured \ + AND u.is_active \ ORDER BY d.id ASC", location.id, - location.mfa_enabled() ) .fetch_all(executor) .await?; - // keepalive has to be added manually because Postgres - // doesn't support unsigned integers - let result = rows + Ok(rows .into_iter() .map(|row| Peer { pubkey: row.pubkey, allowed_ips: row.allowed_ips, - // Don't send preshared key if MFA is not enabled, it can't be used and may - // cause issues with clients connecting if they expect no preshared key - // e.g. when you disable MFA on a location - preshared_key: if location.mfa_enabled() { - row.preshared_key - } else { - None - }, + preshared_key: Some(row.preshared_key), keepalive_interval: Some(location.keepalive_interval.cast_unsigned()), }) - .collect(); - - Ok(result) + .collect()) } #[cfg(test)] mod test { use std::{net::IpAddr, str::FromStr}; + use chrono::Utc; use defguard_common::db::{ models::{ Device, DeviceType, WireguardNetwork, device::WireguardNetworkDevice, user::User, + vpn_client_session::VpnClientSession, wireguard::{LocationMfaMode, ServiceLocationMode}, }, setup_pool, @@ -220,4 +252,211 @@ mod test { ); assert_eq!(peers_alwayson[0].pubkey, "pubkey3"); } + + #[sqlx::test] + async fn test_get_location_allowed_peers_skips_active_mfa_session_without_preshared_key( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password123"), + "Test", + "User", + "test@example.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device1".into(), + "pubkey1".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let mut network = WireguardNetwork::default() + .try_set_address("10.4.1.1/24") + .unwrap(); + network.name = "mfa-location".to_string(); + network.service_location_mode = ServiceLocationMode::Disabled; + network.location_mfa_mode = LocationMfaMode::Internal; + let network = network.save(&pool).await.unwrap(); + + let network_device = WireguardNetworkDevice::new( + network.id, + device.id, + vec![IpAddr::from_str("10.4.1.2").unwrap()], + ); + network_device.insert(&pool).await.unwrap(); + + VpnClientSession::new(network.id, user.id, device.id, None, None) + .save(&pool) + .await + .unwrap(); + + let peers = get_location_allowed_peers(&network, &pool).await.unwrap(); + + assert!(peers.is_empty()); + } + + #[sqlx::test] + async fn test_get_location_allowed_peers_keeps_non_mfa_peer_without_session_lookup_dependency( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password123"), + "Test", + "User", + "test@example.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device1".into(), + "pubkey1".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let mut network = WireguardNetwork::default() + .try_set_address("10.5.1.1/24") + .unwrap(); + network.name = "non-mfa-location".to_string(); + network.service_location_mode = ServiceLocationMode::Disabled; + network.location_mfa_mode = LocationMfaMode::Disabled; + let network = network.save(&pool).await.unwrap(); + + let network_device = WireguardNetworkDevice::new( + network.id, + device.id, + vec![IpAddr::from_str("10.5.1.2").unwrap()], + ); + network_device.insert(&pool).await.unwrap(); + + let peers = get_location_allowed_peers(&network, &pool).await.unwrap(); + + assert_eq!(peers.len(), 1); + assert_eq!(peers[0].pubkey, "pubkey1"); + assert_eq!(peers[0].preshared_key, None); + } + + #[sqlx::test] + async fn test_get_location_allowed_peers_includes_active_mfa_peers_with_session_preshared_key( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password123"), + "Test", + "User", + "test@example.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let new_device = Device::new( + "device-new".into(), + "pubkey-new".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let connected_device = Device::new( + "device-connected".into(), + "pubkey-connected".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let mut network = WireguardNetwork::default() + .try_set_address("10.6.1.1/24") + .unwrap(); + network.name = "mfa-location-with-session-psk".to_string(); + network.service_location_mode = ServiceLocationMode::Disabled; + network.location_mfa_mode = LocationMfaMode::Internal; + let network = network.save(&pool).await.unwrap(); + + WireguardNetworkDevice::new( + network.id, + new_device.id, + vec![IpAddr::from_str("10.6.1.2").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + WireguardNetworkDevice::new( + network.id, + connected_device.id, + vec![IpAddr::from_str("10.6.1.3").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + let mut new_session = VpnClientSession::new(network.id, user.id, new_device.id, None, None); + new_session.preshared_key = Some("new-session-psk".into()); + new_session.save(&pool).await.unwrap(); + + let mut connected_session = VpnClientSession::new( + network.id, + user.id, + connected_device.id, + Some(Utc::now().naive_utc()), + None, + ); + connected_session.preshared_key = Some("connected-session-psk".into()); + connected_session.save(&pool).await.unwrap(); + + let peers = get_location_allowed_peers(&network, &pool).await.unwrap(); + + assert_eq!(peers.len(), 2); + assert_eq!( + peers + .iter() + .map(|peer| (peer.pubkey.as_str(), peer.preshared_key.as_deref())) + .collect::>(), + vec![ + ("pubkey-new", Some("new-session-psk")), + ("pubkey-connected", Some("connected-session-psk")), + ] + ); + } } diff --git a/crates/defguard_core/src/location_management/mod.rs b/crates/defguard_core/src/location_management/mod.rs index 1355cce3c5..6d3eb3cd06 100644 --- a/crates/defguard_core/src/location_management/mod.rs +++ b/crates/defguard_core/src/location_management/mod.rs @@ -5,8 +5,7 @@ use defguard_common::{ db::{ Id, models::{ - Device, DeviceNetworkInfo, DeviceType, ModelError, WireguardNetwork, - WireguardNetworkError, + Device, DeviceType, ModelError, WireguardNetwork, WireguardNetworkError, device::{DeviceInfo, WireguardNetworkDevice}, user::User, wireguard::MappedDevice, @@ -186,14 +185,12 @@ pub async fn process_device_access_changes( ) .await?; used_ips.extend(wireguard_network_device.wireguard_ips.iter().copied()); + let network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *transaction, location) + .await?; events.push(GatewayEvent::DeviceModified(DeviceInfo { device, - network_info: vec![DeviceNetworkInfo { - network_id: location.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips, - preshared_key: wireguard_network_device.preshared_key, - is_authorized: wireguard_network_device.is_authorized, - }], + network_info: vec![network_info], })); } // Device is no longer allowed @@ -208,14 +205,10 @@ pub async fn process_device_access_changes( if let Some(device) = Device::find_by_id(&mut *transaction, device_network_config.device_id).await? { + let network_info = device_network_config.to_device_network_info(location, None); events.push(GatewayEvent::DeviceDeleted(DeviceInfo { device, - network_info: vec![DeviceNetworkInfo { - network_id: location.id, - device_wireguard_ips: device_network_config.wireguard_ips, - preshared_key: device_network_config.preshared_key, - is_authorized: device_network_config.is_authorized, - }], + network_info: vec![network_info], })); } else { let msg = format!("Device {} does not exist", device_network_config.device_id); @@ -230,14 +223,12 @@ pub async fn process_device_access_changes( .assign_next_network_ip(&mut *transaction, location, &used_ips, reserved_ips, None) .await?; used_ips.extend(wireguard_network_device.wireguard_ips.iter().copied()); + let network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *transaction, location) + .await?; events.push(GatewayEvent::DeviceCreated(DeviceInfo { device, - network_info: vec![DeviceNetworkInfo { - network_id: location.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips, - preshared_key: wireguard_network_device.preshared_key, - is_authorized: wireguard_network_device.is_authorized, - }], + network_info: vec![network_info], })); } @@ -283,15 +274,13 @@ pub(crate) async fn handle_imported_devices( wireguard_network_device.insert(&mut *transaction).await?; // store ID of device with already generated config assigned_device_ids.push(existing_device.id); + let network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *transaction, location) + .await?; // send device to connected gateways events.push(GatewayEvent::DeviceModified(DeviceInfo { device: existing_device, - network_info: vec![DeviceNetworkInfo { - network_id: location.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips, - preshared_key: wireguard_network_device.preshared_key, - is_authorized: wireguard_network_device.is_authorized, - }], + network_info: vec![network_info], })); } None => { @@ -365,12 +354,11 @@ pub(crate) async fn handle_mapped_devices( mapped_device.wireguard_ips.clone(), ); wireguard_network_device.insert(&mut *conn).await?; - network_info.push(DeviceNetworkInfo { - network_id: location.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips, - preshared_key: wireguard_network_device.preshared_key, - is_authorized: wireguard_network_device.is_authorized, - }); + network_info.push( + wireguard_network_device + .to_device_network_info_runtime(&mut *conn, location) + .await?, + ); } // Assign IP addresses in other networks. diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index fb4ad257b5..9a66343f69 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -13,7 +13,10 @@ use defguard_common::{ VERSION, db::{ Id, - models::{Settings, WireguardNetwork, gateway::Gateway, wireguard::DEFAULT_WIREGUARD_MTU}, + models::{ + DeviceNetworkInfo, Settings, WireguardNetwork, gateway::Gateway, + wireguard::DEFAULT_WIREGUARD_MTU, + }, }, messages::peer_stats_update::PeerStatsUpdate, }; @@ -446,6 +449,74 @@ impl GatewayUpdatesHandler { } } + #[must_use] + fn runtime_peer_update( + &self, + peer_label: &str, + peer_pubkey: String, + allowed_ips: Vec, + is_authorized: bool, + preshared_key: Option, + ) -> Option { + if !self.network.mfa_enabled() { + return Some(Peer { + pubkey: peer_pubkey, + allowed_ips, + preshared_key: None, + keepalive_interval: Some(self.network.keepalive_interval.cast_unsigned()), + }); + } + + if !is_authorized { + debug!( + "Skipping gateway peer update for WireGuard device {peer_label} in MFA enabled location {} because there is no active MFA session", + self.network.name + ); + return None; + } + + let Some(preshared_key) = preshared_key else { + debug!( + "Skipping gateway peer update for WireGuard device {peer_label} in location {} because the runtime preshared key is missing", + self.network.name + ); + return None; + }; + + Some(Peer { + pubkey: peer_pubkey, + allowed_ips, + preshared_key: Some(preshared_key), + keepalive_interval: Some(self.network.keepalive_interval.cast_unsigned()), + }) + } + + fn send_runtime_device_update( + &self, + peer_label: &str, + peer_pubkey: String, + network_info: &DeviceNetworkInfo, + update_type: i32, + ) -> Result<(), Status> { + let allowed_ips = network_info + .device_wireguard_ips + .iter() + .map(IpAddr::to_string) + .collect(); + + let Some(peer) = self.runtime_peer_update( + peer_label, + peer_pubkey, + allowed_ips, + network_info.is_authorized, + network_info.preshared_key.clone(), + ) else { + return Ok(()); + }; + + self.send_peer_update(peer, update_type) + } + /// Process incoming Gateway events /// /// Main gRPC server uses a shared channel for broadcasting all gateway events @@ -495,33 +566,12 @@ impl GatewayUpdatesHandler { .iter() .find(|info| info.network_id == self.network_id) { - Some(network_info) => { - // FIXME: this shouldn't happen, since when the device is created - // it's impossible for MFA authorization to already be completed - if self.network.mfa_enabled() && !network_info.is_authorized { - debug!( - "Created WireGuard device {} is not authorized to connect to \ - MFA enabled location {}", - device.device.name, self.network.name - ); - continue; - } - self.send_peer_update( - Peer { - pubkey: device.device.wireguard_pubkey, - allowed_ips: network_info - .device_wireguard_ips - .iter() - .map(IpAddr::to_string) - .collect(), - preshared_key: network_info.preshared_key.clone(), - keepalive_interval: Some( - self.network.keepalive_interval.cast_unsigned(), - ), - }, - 0, - ) - } + Some(network_info) => self.send_runtime_device_update( + &device.device.name, + device.device.wireguard_pubkey, + network_info, + 0, + ), None => Ok(()), } } @@ -532,31 +582,12 @@ impl GatewayUpdatesHandler { .iter() .find(|info| info.network_id == self.network_id) { - Some(network_info) => { - if self.network.mfa_enabled() && !network_info.is_authorized { - debug!( - "Modified WireGuard device {} is not authorized to connect to \ - MFA enabled location {}", - device.device.name, self.network.name - ); - continue; - } - self.send_peer_update( - Peer { - pubkey: device.device.wireguard_pubkey, - allowed_ips: network_info - .device_wireguard_ips - .iter() - .map(IpAddr::to_string) - .collect(), - preshared_key: network_info.preshared_key.clone(), - keepalive_interval: Some( - self.network.keepalive_interval.cast_unsigned(), - ), - }, - 1, - ) - } + Some(network_info) => self.send_runtime_device_update( + &device.device.name, + device.device.wireguard_pubkey, + network_info, + 1, + ), None => Ok(()), } } @@ -592,39 +623,19 @@ impl GatewayUpdatesHandler { Ok(()) } } - GatewayEvent::MfaSessionAuthorized(location_id, device, network_device) => { + GatewayEvent::MfaSessionAuthorized(location_id, device, network_info) => { if location_id == self.network_id { - // validate that network info is for the correct location - if network_device.wireguard_network_id != location_id { + if network_info.network_id != location_id { error!( - "Received MFA authorization success event for location {location_id} with invalid device config: {network_device:?}" + "Received MFA authorization success event for location {location_id} with invalid runtime network info: {network_info:?}" ); continue; } - // FIXME: at this point the device authorization should already have been verified - if self.network.mfa_enabled() && !network_device.is_authorized { - debug!( - "Created WireGuard device {} is not authorized to connect to \ - MFA enabled location {}", - device.name, self.network.name - ); - continue; - } - - self.send_peer_update( - Peer { - pubkey: device.wireguard_pubkey, - allowed_ips: network_device - .wireguard_ips - .iter() - .map(IpAddr::to_string) - .collect(), - preshared_key: network_device.preshared_key.clone(), - keepalive_interval: Some( - self.network.keepalive_interval.cast_unsigned(), - ), - }, + self.send_runtime_device_update( + &device.name, + device.wireguard_pubkey, + &network_info, 0, ) } else { @@ -852,3 +863,219 @@ fn gen_config( fwmark: network.fwmark as u32, } } + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, net::IpAddr, str::FromStr, sync::Arc}; + + use chrono::Utc; + use defguard_common::db::{ + Id, + models::{ + Device, DeviceType, User, + device::WireguardNetworkDevice, + gateway::Gateway, + vpn_client_session::VpnClientSession, + wireguard::{LocationMfaMode, ServiceLocationMode, WireguardNetwork}, + }, + setup_pool, + }; + use defguard_core::grpc::GatewayEvent; + use defguard_proto::gateway::core_response; + use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + use tokio::sync::{broadcast, mpsc::unbounded_channel, watch}; + + use super::{GatewayHandler, GatewayUpdatesHandler}; + + fn test_network(location_mfa_mode: LocationMfaMode) -> WireguardNetwork { + WireguardNetwork::new( + "test-network".into(), + 51820, + "127.0.0.1".into(), + None, + Vec::new(), + true, + false, + false, + location_mfa_mode, + ServiceLocationMode::Disabled, + ) + .with_id(1) + } + + fn test_handler(location_mfa_mode: LocationMfaMode) -> GatewayUpdatesHandler { + let network = test_network(location_mfa_mode); + let (events_tx, events_rx) = broadcast::channel(1); + let (tx, _rx) = unbounded_channel(); + drop(events_tx); + + GatewayUpdatesHandler::new(network.id, network, "gateway".into(), events_rx, tx) + } + + #[test] + fn test_runtime_peer_update_strips_preshared_key_for_non_mfa_locations() { + let handler = test_handler(LocationMfaMode::Disabled); + + let peer = handler + .runtime_peer_update( + "device", + "device-pubkey".into(), + vec!["10.1.1.2".into()], + true, + Some("legacy-psk".into()), + ) + .unwrap(); + + assert_eq!(peer.pubkey, "device-pubkey"); + assert_eq!(peer.allowed_ips, ["10.1.1.2"]); + assert_eq!(peer.preshared_key, None); + assert_eq!(peer.keepalive_interval, Some(25)); + } + + #[test] + fn test_runtime_peer_update_skips_authorized_mfa_peer_without_session_preshared_key() { + let handler = test_handler(LocationMfaMode::Internal); + + let peer = handler.runtime_peer_update( + "device", + "device-pubkey".into(), + vec!["10.1.1.2".into()], + true, + None, + ); + + assert_eq!(peer, None); + } + + #[test] + fn test_runtime_peer_update_preserves_session_preshared_key_for_authorized_mfa_peer() { + let handler = test_handler(LocationMfaMode::Internal); + + let peer = handler + .runtime_peer_update( + "device", + "device-pubkey".into(), + vec!["10.1.1.2".into()], + true, + Some("session-psk".into()), + ) + .unwrap(); + + assert_eq!(peer.preshared_key, Some("session-psk".into())); + } + + #[sqlx::test] + async fn test_send_configuration_includes_mfa_peers_with_session_preshared_key( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password123"), + "Test", + "User", + "test@example.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let new_device = Device::new( + "device-new".into(), + "pubkey-new".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let connected_device = Device::new( + "device-connected".into(), + "pubkey-connected".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let mut network = WireguardNetwork::default() + .try_set_address("10.7.1.1/24") + .unwrap(); + network.name = "mfa-full-config-location".to_string(); + network.location_mfa_mode = LocationMfaMode::Internal; + network.service_location_mode = ServiceLocationMode::Disabled; + let network = network.save(&pool).await.unwrap(); + + WireguardNetworkDevice::new( + network.id, + new_device.id, + vec![IpAddr::from_str("10.7.1.2").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + WireguardNetworkDevice::new( + network.id, + connected_device.id, + vec![IpAddr::from_str("10.7.1.3").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + let mut new_session = VpnClientSession::new(network.id, user.id, new_device.id, None, None); + new_session.preshared_key = Some("new-session-psk".into()); + new_session.save(&pool).await.unwrap(); + + let mut connected_session = VpnClientSession::new( + network.id, + user.id, + connected_device.id, + Some(Utc::now().naive_utc()), + None, + ); + connected_session.preshared_key = Some("connected-session-psk".into()); + connected_session.save(&pool).await.unwrap(); + + let gateway = Gateway::new(network.id, "gateway", "127.0.0.1", 50051, "test") + .save(&pool) + .await + .unwrap(); + let (events_tx, _events_rx) = broadcast::channel::(1); + let (peer_stats_tx, _peer_stats_rx) = unbounded_channel(); + let (_certs_tx, certs_rx) = watch::channel(Arc::new(HashMap::::new())); + let handler = + GatewayHandler::new(gateway, pool.clone(), events_tx, peer_stats_tx, certs_rx).unwrap(); + let (tx, mut rx) = unbounded_channel(); + + handler.send_configuration(&tx).await.unwrap(); + + let response = rx.recv().await.unwrap(); + let Some(core_response::Payload::Config(configuration)) = response.payload else { + panic!("expected gateway config payload"); + }; + + assert_eq!(configuration.peers.len(), 2); + assert_eq!( + configuration + .peers + .iter() + .map(|peer| (peer.pubkey.as_str(), peer.preshared_key.as_deref())) + .collect::>(), + [ + ("pubkey-new", Some("new-session-psk")), + ("pubkey-connected", Some("connected-session-psk")), + ] + ); + } +} diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index 51af8e28a7..a8c516e7ef 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -4,7 +4,6 @@ use defguard_common::{ Id, models::{ Device, User, WireguardNetwork, - device::WireguardNetworkDevice, vpn_client_session::{VpnClientSession, VpnClientSessionState}, }, }, @@ -296,16 +295,6 @@ impl SessionManager { // remove peers from GW for MFA locations if location.mfa_enabled() { - // FIXME: remove once MFA-related data is no longer stored here - // update device network config - if let Some(mut device_network_info) = - WireguardNetworkDevice::find(&mut *transaction, device.id, location.id).await? - { - device_network_info.is_authorized = false; - device_network_info.preshared_key = None; - device_network_info.update(&mut *transaction).await?; - } - self.send_peer_disconnect_message(location, &device)?; } diff --git a/crates/defguard_session_manager/tests/common/mod.rs b/crates/defguard_session_manager/tests/common/mod.rs index 2c714c4ee7..792cd2b74d 100644 --- a/crates/defguard_session_manager/tests/common/mod.rs +++ b/crates/defguard_session_manager/tests/common/mod.rs @@ -11,7 +11,7 @@ use defguard_common::{ Device, DeviceType, User, WireguardNetwork, device::WireguardNetworkDevice, gateway::Gateway, - vpn_client_session::{VpnClientMfaMethod, VpnClientSession}, + vpn_client_session::{VpnClientMfaMethod, VpnClientSession, VpnClientSessionState}, vpn_session_stats::VpnSessionStats, wireguard::{LocationMfaMode, ServiceLocationMode}, }, @@ -216,20 +216,23 @@ pub(crate) async fn create_gateway_named( pub(crate) async fn authorize_device_in_location( pool: &sqlx::PgPool, location_id: Id, + user_id: Id, device_id: Id, preshared_key: &str, -) { - let mut network_device = WireguardNetworkDevice::find(pool, device_id, location_id) - .await - .expect("failed to load device network info") - .expect("expected device network info"); - network_device.is_authorized = true; - network_device.authorized_at = Some(chrono::Utc::now().naive_utc()); - network_device.preshared_key = Some(preshared_key.to_string()); - network_device - .update(pool) +) -> VpnClientSession { + let mut session = VpnClientSession::new( + location_id, + user_id, + device_id, + Some(truncate_timestamp(chrono::Utc::now().naive_utc())), + Some(VpnClientMfaMethod::Totp), + ); + session.preshared_key = Some(preshared_key.to_string()); + session.state = VpnClientSessionState::Connected; + session + .save(pool) .await - .expect("failed to authorize device in location"); + .expect("failed to create authorized session") } #[allow(clippy::too_many_arguments)] @@ -277,8 +280,12 @@ pub(crate) async fn create_session( device_id: Id, connected_at: Option, mfa_method: Option, + preshared_key: Option<&str>, ) -> VpnClientSession { - VpnClientSession::new(location_id, user_id, device_id, connected_at, mfa_method) + let mut session = + VpnClientSession::new(location_id, user_id, device_id, connected_at, mfa_method); + session.preshared_key = preshared_key.map(str::to_owned); + session .save(pool) .await .expect("failed to create vpn client session") diff --git a/crates/defguard_session_manager/tests/session_manager/db_invariants.rs b/crates/defguard_session_manager/tests/session_manager/db_invariants.rs new file mode 100644 index 0000000000..3b62ccf2fb --- /dev/null +++ b/crates/defguard_session_manager/tests/session_manager/db_invariants.rs @@ -0,0 +1,126 @@ +use chrono::Utc; +use defguard_common::db::{Id, setup_pool}; +use sqlx::{ + postgres::{PgConnectOptions, PgPoolOptions}, + query, query_scalar, +}; + +use crate::common::{attach_device_to_location, create_device, create_location, create_user}; + +const ACTIVE_SESSION_UNIQUE_INDEX: &str = "vpn_client_session_active_location_device_unique"; + +async fn insert_session( + pool: &sqlx::PgPool, + location_id: Id, + user_id: Id, + device_id: Id, + state: &str, +) -> Result { + let connected_at = (state == "connected").then(|| Utc::now().naive_utc()); + + query_scalar( + "INSERT INTO vpn_client_session (location_id, user_id, device_id, connected_at, mfa_method, state, preshared_key) \ + VALUES ($1, $2, $3, $4, NULL, $5::vpn_client_session_state, NULL) \ + RETURNING id", + ) + .bind(location_id) + .bind(user_id) + .bind(device_id) + .bind(connected_at) + .bind(state) + .fetch_one(pool) + .await +} + +async fn count_active_sessions(pool: &sqlx::PgPool, location_id: Id, device_id: Id) -> i64 { + query_scalar( + "SELECT COUNT(*) FROM vpn_client_session \ + WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected')", + ) + .bind(location_id) + .bind(device_id) + .fetch_one(pool) + .await + .expect("failed to count active sessions") +} + +#[sqlx::test] +async fn test_db_rejects_second_active_session_for_same_device_location( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let location = create_location(&pool).await; + let user = create_user(&pool).await; + let device = create_device(&pool, user.id).await; + attach_device_to_location(&pool, location.id, device.id).await; + + insert_session(&pool, location.id, user.id, device.id, "new") + .await + .expect("failed to create first active session"); + + let error = insert_session(&pool, location.id, user.id, device.id, "connected") + .await + .expect_err("expected unique index to reject duplicate active session"); + + match error { + sqlx::Error::Database(database_error) => { + assert_eq!(database_error.code().as_deref(), Some("23505")); + assert_eq!( + database_error.constraint(), + Some(ACTIVE_SESSION_UNIQUE_INDEX) + ); + } + other => panic!("expected database uniqueness error, got {other:?}"), + } +} + +#[sqlx::test] +async fn test_db_allows_new_active_session_after_previous_session_disconnects( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let location = create_location(&pool).await; + let user = create_user(&pool).await; + let device = create_device(&pool, user.id).await; + attach_device_to_location(&pool, location.id, device.id).await; + + let disconnected_session_id = + insert_session(&pool, location.id, user.id, device.id, "connected") + .await + .expect("failed to create initial active session"); + + query( + "UPDATE vpn_client_session \ + SET state = 'disconnected', disconnected_at = NOW() \ + WHERE id = $1", + ) + .bind(disconnected_session_id) + .execute(&pool) + .await + .expect("failed to disconnect initial session"); + + let new_session_id = insert_session(&pool, location.id, user.id, device.id, "new") + .await + .expect("disconnected session should not block new active session"); + + assert_eq!( + count_active_sessions(&pool, location.id, device.id).await, + 1 + ); + + let active_session_id: Id = query_scalar( + "SELECT id FROM vpn_client_session \ + WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') \ + ORDER BY created_at DESC, id DESC \ + LIMIT 1", + ) + .bind(location.id) + .bind(device.id) + .fetch_one(&pool) + .await + .expect("failed to fetch remaining active session"); + + assert_eq!(active_session_id, new_session_id); +} diff --git a/crates/defguard_session_manager/tests/session_manager/disconnects.rs b/crates/defguard_session_manager/tests/session_manager/disconnects.rs index 938c428761..d288bedeb4 100644 --- a/crates/defguard_session_manager/tests/session_manager/disconnects.rs +++ b/crates/defguard_session_manager/tests/session_manager/disconnects.rs @@ -33,6 +33,7 @@ async fn test_inactive_connected_sessions_are_disconnected_after_threshold( device.id, Some(stale_handshake), None, + None, ) .await; create_session_stats( @@ -80,6 +81,7 @@ async fn test_recent_connected_sessions_remain_active(_: PgPoolOptions, options: device.id, Some(recent_handshake), None, + None, ) .await; create_session_stats( diff --git a/crates/defguard_session_manager/tests/session_manager/event_flow.rs b/crates/defguard_session_manager/tests/session_manager/event_flow.rs index a73f1f74f2..68ddbf9fe5 100644 --- a/crates/defguard_session_manager/tests/session_manager/event_flow.rs +++ b/crates/defguard_session_manager/tests/session_manager/event_flow.rs @@ -79,6 +79,7 @@ async fn test_reusing_existing_connected_session_does_not_emit_duplicate_connect device.id, Some(connected_at), None, + None, ) .await; @@ -120,6 +121,7 @@ async fn test_session_manager_emits_disconnect_event_for_inactive_standard_sessi device.id, Some(stale_handshake), None, + None, ) .await; create_session_stats( diff --git a/crates/defguard_session_manager/tests/session_manager/mfa.rs b/crates/defguard_session_manager/tests/session_manager/mfa.rs index 086d15fd2a..a3a688ccdc 100644 --- a/crates/defguard_session_manager/tests/session_manager/mfa.rs +++ b/crates/defguard_session_manager/tests/session_manager/mfa.rs @@ -3,7 +3,6 @@ use std::net::SocketAddr; use chrono::{TimeDelta, Utc}; use defguard_common::db::{ models::{ - device::WireguardNetworkDevice, vpn_client_session::{VpnClientMfaMethod, VpnClientSession, VpnClientSessionState}, vpn_session_stats::VpnSessionStats, wireguard::LocationMfaMode, @@ -85,6 +84,7 @@ async fn test_mfa_new_session_upgrades_to_connected_on_stats( device.id, None, Some(VpnClientMfaMethod::Totp), + None, ) .await; @@ -196,6 +196,7 @@ async fn test_duplicate_first_stats_on_mfa_new_session_are_idempotent( device.id, None, Some(VpnClientMfaMethod::Totp), + None, ) .await; @@ -280,6 +281,7 @@ async fn test_repeated_later_stats_on_mfa_session_remain_idempotent( device.id, None, Some(VpnClientMfaMethod::Totp), + None, ) .await; @@ -384,6 +386,7 @@ async fn test_closed_event_channel_keeps_mfa_first_stats_upgrade_idempotent( device.id, None, Some(VpnClientMfaMethod::Totp), + None, ) .await; @@ -448,20 +451,21 @@ async fn test_inactive_mfa_connected_sessions_disconnect_and_clear_authorization let user = create_user(&pool).await; let device = create_device(&pool, user.id).await; attach_device_to_location(&pool, location.id, device.id).await; - authorize_device_in_location(&pool, location.id, device.id, "psk-before-disconnect").await; let gateway = create_gateway(&pool, location.id, user.fullname()).await; let mut harness = SessionManagerHarness::new(pool.clone()); let stale_handshake = stale_session_timestamp(&location); - let session = create_session( + let mut session = authorize_device_in_location( &pool, location.id, user.id, device.id, - Some(stale_handshake), - Some(VpnClientMfaMethod::Totp), + "psk-before-disconnect", ) .await; + session.connected_at = Some(stale_handshake); + session.created_at = stale_handshake; + session.save(&pool).await.expect("failed to age session"); create_session_stats( &pool, session.id, @@ -487,12 +491,12 @@ async fn test_inactive_mfa_connected_sessions_disconnect_and_clear_authorization VpnClientSessionState::Disconnected ); - let network_device = WireguardNetworkDevice::find(&pool, device.id, location.id) - .await - .expect("failed to query network device") - .expect("expected network device"); - assert!(!network_device.is_authorized); - assert_eq!(network_device.preshared_key, None); + assert!( + VpnClientSession::try_get_active_session(&pool, location.id, device.id) + .await + .expect("failed to query active session") + .is_none() + ); let gateway_event = timeout(RECEIVE_TIMEOUT, harness.gateway_rx.recv()) .await @@ -529,7 +533,6 @@ async fn test_never_connected_mfa_new_sessions_disconnect_after_threshold( let user = create_user(&pool).await; let device = create_device(&pool, user.id).await; attach_device_to_location(&pool, location.id, device.id).await; - authorize_device_in_location(&pool, location.id, device.id, "psk-before-timeout").await; let mut harness = SessionManagerHarness::new(pool.clone()); let session = create_session( @@ -539,6 +542,7 @@ async fn test_never_connected_mfa_new_sessions_disconnect_after_threshold( device.id, None, Some(VpnClientMfaMethod::Totp), + Some("psk-before-timeout"), ) .await; set_session_created_at(&pool, session.id, stale_session_timestamp(&location)).await; @@ -554,13 +558,12 @@ async fn test_never_connected_mfa_new_sessions_disconnect_after_threshold( VpnClientSessionState::Disconnected ); assert!(disconnected_session.disconnected_at.is_some()); - - let network_device = WireguardNetworkDevice::find(&pool, device.id, location.id) - .await - .expect("failed to query network device") - .expect("expected network device"); - assert!(!network_device.is_authorized); - assert_eq!(network_device.preshared_key, None); + assert!( + VpnClientSession::try_get_active_session(&pool, location.id, device.id) + .await + .expect("failed to query active session") + .is_none() + ); let gateway_event = timeout(RECEIVE_TIMEOUT, harness.gateway_rx.recv()) .await diff --git a/crates/defguard_session_manager/tests/session_manager/mod.rs b/crates/defguard_session_manager/tests/session_manager/mod.rs index 1cf8461d46..1ffdee11f2 100644 --- a/crates/defguard_session_manager/tests/session_manager/mod.rs +++ b/crates/defguard_session_manager/tests/session_manager/mod.rs @@ -1,3 +1,4 @@ +mod db_invariants; mod disconnects; mod event_flow; mod mfa; diff --git a/crates/defguard_session_manager/tests/session_manager/sessions.rs b/crates/defguard_session_manager/tests/session_manager/sessions.rs index 5dfc359b8e..e76a7abd22 100644 --- a/crates/defguard_session_manager/tests/session_manager/sessions.rs +++ b/crates/defguard_session_manager/tests/session_manager/sessions.rs @@ -270,7 +270,8 @@ async fn test_existing_new_session_becomes_connected_on_stats( let gateway = create_gateway(&pool, location.id, user.fullname()).await; let mut harness = SessionManagerHarness::new(pool.clone()); - let existing_session = create_session(&pool, location.id, user.id, device.id, None, None).await; + let existing_session = + create_session(&pool, location.id, user.id, device.id, None, None, None).await; assert_eq!(existing_session.state, VpnClientSessionState::New); let endpoint: SocketAddr = "203.0.113.10:51820".parse().unwrap(); @@ -495,6 +496,7 @@ async fn test_existing_session_in_db_is_reused_instead_of_creating_duplicate( device.id, Some(base_time - TimeDelta::seconds(5)), None, + None, ) .await; create_session_stats( diff --git a/crates/defguard_session_manager/tests/session_manager/stats.rs b/crates/defguard_session_manager/tests/session_manager/stats.rs index a8a6cf3293..f74bf82120 100644 --- a/crates/defguard_session_manager/tests/session_manager/stats.rs +++ b/crates/defguard_session_manager/tests/session_manager/stats.rs @@ -203,6 +203,7 @@ async fn test_out_of_order_updates_for_existing_db_session_are_discarded( device.id, Some(first_handshake), None, + None, ) .await; create_session_stats( diff --git a/flake.lock b/flake.lock index d909857580..1b6bbdcda0 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1773646010, - "narHash": "sha256-iYrs97hS7p5u4lQzuNWzuALGIOdkPXvjz7bviiBjUu8=", + "lastModified": 1773821835, + "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5b2c2d84341b2afb5647081c1386a80d7a8d8605", + "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1773630837, - "narHash": "sha256-zJhgAGnbVKeBMJOb9ctZm4BGS/Rnrz+5lfSXTVah4HQ=", + "lastModified": 1773889863, + "narHash": "sha256-tSsmZOHBgq4qfu5MNCAEsKZL1cI4avNLw2oUTXWeb74=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "f600ea449c7b5bb596fa1cf21c871cc5b9e31316", + "rev": "dbfd51be2692cb7022e301d14c139accb4ee63f0", "type": "github" }, "original": { diff --git a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql new file mode 100644 index 0000000000..5fd9557e6a --- /dev/null +++ b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql @@ -0,0 +1,27 @@ +ALTER TABLE wireguard_network_device + ADD COLUMN preshared_key text NULL, + ADD COLUMN is_authorized bool NOT NULL DEFAULT false, + ADD COLUMN authorized_at timestamp without time zone NULL; + +-- Rollback is lossy: only preshared_key is repopulated from an active +-- session with a non-null preshared_key per (device_id, location_id); +-- is_authorized and authorized_at are recreated with default/NULL values and +-- are not reconstructed. +UPDATE wireguard_network_device AS network_device +SET preshared_key = latest_active_session.preshared_key +FROM ( + SELECT DISTINCT ON (session.device_id, session.location_id) + session.device_id, + session.location_id, + session.preshared_key + FROM vpn_client_session AS session + WHERE session.state IN ('new', 'connected') + AND session.preshared_key IS NOT NULL + ORDER BY session.device_id, session.location_id, session.created_at DESC, session.id DESC +) AS latest_active_session +WHERE network_device.device_id = latest_active_session.device_id + AND network_device.wireguard_network_id = latest_active_session.location_id; + +DROP INDEX IF EXISTS vpn_client_session_active_location_device_unique; + +ALTER TABLE vpn_client_session DROP COLUMN preshared_key; diff --git a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql new file mode 100644 index 0000000000..ceac7eef68 --- /dev/null +++ b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql @@ -0,0 +1,13 @@ +-- Add session-level preshared_key, enforce the active-session invariant, +-- then drop device-level preshared_key/auth fields. +-- WARNING: rollback is lossy for dropped wireguard_network_device columns. +ALTER TABLE vpn_client_session ADD COLUMN preshared_key text NULL; + +CREATE UNIQUE INDEX vpn_client_session_active_location_device_unique + ON vpn_client_session(location_id, device_id) + WHERE state IN ('new', 'connected'); + +ALTER TABLE wireguard_network_device + DROP COLUMN preshared_key, + DROP COLUMN is_authorized, + DROP COLUMN authorized_at;