@@ -2,138 +2,174 @@ import { data } from 'react-router';
22import { isDataWithApiError } from '~/server/headscale/api/error-client' ;
33import { Capabilities } from '~/server/web/roles' ;
44import type { Route } from './+types/overview' ;
5+ import {
6+ getApiErrorMessage ,
7+ parseSyntaxError ,
8+ parseTestResultsFromError ,
9+ } from './utils/parsing' ;
10+ import {
11+ saveError ,
12+ saveSuccess ,
13+ testError ,
14+ testSuccess ,
15+ } from './utils/responses' ;
516
6- // We only check capabilities here and assume it is writable
7- // If it isn't, it'll gracefully error anyways, since this means some
8- // fishy client manipulation is happening.
9- export async function aclAction ( { request, context } : Route . ActionArgs ) {
10- const session = await context . sessions . auth ( request ) ;
11- const check = await context . sessions . check (
17+ async function handleTestPolicy (
18+ request : Request ,
19+ context : Route . ActionArgs [ 'context' ] ,
20+ policyData : string ,
21+ apiKey : string ,
22+ ) {
23+ const hasPermission = await context . sessions . check (
1224 request ,
13- Capabilities . write_policy ,
25+ Capabilities . read_policy ,
1426 ) ;
15- if ( ! check ) {
16- throw data ( 'You do not have permission to write to the ACL policy' , {
27+ if ( ! hasPermission ) {
28+ throw data ( 'You do not have permission to access the ACL policy' , {
1729 status : 403 ,
1830 } ) ;
1931 }
2032
21- // Try to write to the ACL policy via the API or via config file (TODO).
22- const formData = await request . formData ( ) ;
23- const policyData = formData . get ( 'policy' ) ?. toString ( ) ;
24- if ( ! policyData ) {
25- throw data ( 'Missing `policy` in the form data.' , {
26- status : 400 ,
33+ const api = context . hsApi . getRuntimeClient ( apiKey ) ;
34+
35+ try {
36+ return testSuccess ( await api . testPolicy ( policyData ) ) ;
37+ } catch ( error ) {
38+ // Handle client-side errors (syntax errors, no tests found, etc.)
39+ if ( error instanceof Error ) {
40+ if (
41+ error . message . includes ( 'No tests found' ) ||
42+ error . message . includes ( 'Syntax Error' )
43+ ) {
44+ return testError ( error . message ) ;
45+ }
46+ }
47+
48+ if ( ! isDataWithApiError ( error ) ) {
49+ // Unknown error - return generic message
50+ if ( error instanceof Error ) {
51+ return testError ( `Error: ${ error . message } ` ) ;
52+ }
53+ return testError ( 'An unknown error occurred while testing the policy.' ) ;
54+ }
55+
56+ const { statusCode } = error . data ;
57+ if ( statusCode === 404 || statusCode === 501 ) {
58+ return testError (
59+ 'ACL testing is not supported by your Headscale version. Please upgrade to a version that includes ACL testing support.' ,
60+ ) ;
61+ }
62+
63+ const message = getApiErrorMessage ( error . data . data ) ;
64+ if ( message ) return testError ( message ) ;
65+
66+ return testError ( `Server Error: Failed to test policy (${ statusCode } ).` ) ;
67+ }
68+ }
69+
70+ async function handleSavePolicy (
71+ request : Request ,
72+ context : Route . ActionArgs [ 'context' ] ,
73+ policyData : string ,
74+ apiKey : string ,
75+ ) {
76+ const hasPermission = await context . sessions . check (
77+ request ,
78+ Capabilities . write_policy ,
79+ ) ;
80+ if ( ! hasPermission ) {
81+ throw data ( 'You do not have permission to write to the ACL policy' , {
82+ status : 403 ,
2783 } ) ;
2884 }
2985
30- const api = context . hsApi . getRuntimeClient ( session . api_key ) ;
86+ const api = context . hsApi . getRuntimeClient ( apiKey ) ;
87+
3188 try {
3289 const { policy, updatedAt } = await api . setPolicy ( policyData ) ;
33- return data ( {
34- success : true ,
35- error : undefined ,
36- policy,
37- updatedAt,
38- } ) ;
90+ return saveSuccess ( policy , updatedAt ) ;
3991 } catch ( error ) {
40- if ( isDataWithApiError ( error ) ) {
41- const rawData = error . data . rawData ;
42- // https://github.com/juanfont/headscale/blob/c4600346f9c29b514dc9725ac103efb9d0381f23/hscontrol/types/policy.go#L11
43- if ( rawData . includes ( 'update is disabled' ) ) {
44- throw data ( 'Policy is not writable' , { status : 403 } ) ;
45- }
92+ return handleSaveError ( error , context , policyData ) ;
93+ }
94+ }
4695
47- const message =
48- error . data . data != null &&
49- 'message' in error . data . data &&
50- typeof error . data . data . message === 'string'
51- ? error . data . data . message
52- : undefined ;
96+ function handleSaveError (
97+ error : unknown ,
98+ context : Route . ActionArgs [ 'context' ] ,
99+ policyData : string ,
100+ ) {
101+ if ( ! isDataWithApiError ( error ) ) {
102+ if ( error instanceof Error ) {
103+ return saveError ( `Error: ${ error . message } ` , undefined , 500 ) ;
104+ }
105+ return saveError (
106+ 'Unknown Error: An unexpected error occurred.' ,
107+ undefined ,
108+ 500 ,
109+ ) ;
110+ }
53111
54- if ( message == null ) {
55- throw error ;
56- }
112+ const { rawData, statusCode, data : errorData } = error . data ;
57113
58- // Starting in Headscale 0.27.0 the ACLs parsing was changed meaning
59- // we need to reference other error messages based on API version.
60- if ( context . hsApi . clientHelpers . isAtleast ( '0.27.0' ) ) {
61- if ( message . includes ( 'parsing HuJSON:' ) ) {
62- const cutIndex = message . indexOf ( 'parsing HuJSON:' ) ;
63- const trimmed =
64- cutIndex > - 1
65- ? `Syntax error: ${ message . slice ( cutIndex + 16 ) . trim ( ) } `
66- : message ;
67-
68- return data (
69- {
70- success : false ,
71- error : trimmed ,
72- policy : undefined ,
73- updatedAt : undefined ,
74- } ,
75- 400 ,
76- ) ;
77- }
78-
79- if ( message . includes ( 'parsing policy from bytes:' ) ) {
80- const cutIndex = message . indexOf ( 'parsing policy from bytes:' ) ;
81- const trimmed =
82- cutIndex > - 1
83- ? `Syntax error: ${ message . slice ( cutIndex + 26 ) . trim ( ) } `
84- : message ;
85-
86- return data (
87- {
88- success : false ,
89- error : trimmed ,
90- policy : undefined ,
91- updatedAt : undefined ,
92- } ,
93- 400 ,
94- ) ;
95- }
96- } else {
97- // Pre-0.27.0 error messages
98- if ( message . includes ( 'parsing hujson' ) ) {
99- const cutIndex = message . indexOf ( 'err: hujson:' ) ;
100- const trimmed =
101- cutIndex > - 1
102- ? `Syntax error: ${ message . slice ( cutIndex + 12 ) } `
103- : message ;
104-
105- return data (
106- {
107- success : false ,
108- error : trimmed ,
109- policy : undefined ,
110- updatedAt : undefined ,
111- } ,
112- 400 ,
113- ) ;
114- }
115-
116- if ( message . includes ( 'unmarshalling policy' ) ) {
117- const cutIndex = message . indexOf ( 'err:' ) ;
118- const trimmed =
119- cutIndex > - 1
120- ? `Syntax error: ${ message . slice ( cutIndex + 5 ) } `
121- : message ;
122-
123- return data (
124- {
125- success : false ,
126- error : trimmed ,
127- policy : undefined ,
128- updatedAt : undefined ,
129- } ,
130- 400 ,
131- ) ;
132- }
133- }
134- }
114+ // Gateway errors - Headscale unreachable
115+ if ( statusCode >= 502 && statusCode <= 504 ) {
116+ return saveError (
117+ `Gateway Error: Headscale server is unavailable (${ statusCode } ).` ,
118+ undefined ,
119+ statusCode ,
120+ ) ;
121+ }
122+
123+ // Policy updates disabled in config
124+ if ( rawData . includes ( 'update is disabled' ) ) {
125+ return saveError (
126+ 'Policy Error: Policy updates are disabled in Headscale configuration.' ,
127+ undefined ,
128+ 403 ,
129+ ) ;
130+ }
131+
132+ // Check for test failure results in error response
133+ const testResults = parseTestResultsFromError ( errorData , policyData ) ;
134+ if ( testResults ) {
135+ const failedCount = testResults . results . filter ( ( r ) => ! r . passed ) . length ;
136+ return saveError (
137+ `Test Failure: ${ failedCount } test${ failedCount !== 1 ? 's' : '' } failed` ,
138+ testResults ,
139+ statusCode ,
140+ ) ;
141+ }
142+
143+ // Try to extract meaningful error message
144+ const message = getApiErrorMessage ( errorData ) ;
145+ if ( message ) {
146+ const isModernVersion = context . hsApi . clientHelpers . isAtleast ( '0.27.0' ) ;
147+ const syntaxError = parseSyntaxError ( message , isModernVersion ) ;
148+ if ( syntaxError ) return saveError ( syntaxError , undefined , statusCode ) ;
149+ return saveError ( `Policy Error: ${ message } ` , undefined , statusCode ) ;
150+ }
151+
152+ return saveError (
153+ `Server Error: Failed to save policy (${ statusCode } ).` ,
154+ undefined ,
155+ statusCode ,
156+ ) ;
157+ }
158+
159+ export async function aclAction ( { request, context } : Route . ActionArgs ) {
160+ const session = await context . sessions . auth ( request ) ;
161+ const formData = await request . formData ( ) ;
162+
163+ const actionType = formData . get ( 'action' ) ?. toString ( ) ;
164+ const policyData = formData . get ( 'policy' ) ?. toString ( ) ;
135165
136- // Otherwise, this is a Headscale error that we can just propagate.
137- throw error ;
166+ if ( ! policyData ) {
167+ throw data ( 'Missing `policy` in the form data.' , { status : 400 } ) ;
138168 }
169+
170+ if ( actionType === 'test_policy' ) {
171+ return handleTestPolicy ( request , context , policyData , session . api_key ) ;
172+ }
173+
174+ return handleSavePolicy ( request , context , policyData , session . api_key ) ;
139175}
0 commit comments