@@ -2,6 +2,8 @@ use anyhow::{bail, Context as _, Result};
2
2
use axum:: http:: HeaderValue ;
3
3
use tonic:: Status ;
4
4
5
+ use crate :: { namespace:: NamespaceName , rpc:: NAMESPACE_METADATA_KEY } ;
6
+
5
7
static GRPC_AUTH_HEADER : & str = "x-authorization" ;
6
8
static GRPC_PROXY_AUTH_HEADER : & str = "x-proxy-authorization" ;
7
9
@@ -42,16 +44,22 @@ pub enum AuthError {
42
44
Other ,
43
45
}
44
46
47
+ #[ derive( Clone , Debug , PartialEq , Eq ) ]
48
+ pub struct Authorized {
49
+ pub namespace : Option < NamespaceName > ,
50
+ pub permission : Permission ,
51
+ }
52
+
45
53
#[ non_exhaustive]
46
54
#[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
47
- pub enum Authorized {
55
+ pub enum Permission {
48
56
FullAccess ,
49
57
ReadOnly ,
50
58
}
51
59
52
60
/// A witness that the user has been authenticated.
53
61
#[ non_exhaustive]
54
- #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
62
+ #[ derive( Clone , Debug , PartialEq , Eq ) ]
55
63
pub enum Authenticated {
56
64
Anonymous ,
57
65
Authorized ( Authorized ) ,
@@ -61,9 +69,13 @@ impl Auth {
61
69
pub fn authenticate_http (
62
70
& self ,
63
71
auth_header : Option < & hyper:: header:: HeaderValue > ,
72
+ disable_namespaces : bool ,
64
73
) -> Result < Authenticated , AuthError > {
65
74
if self . disabled {
66
- return Ok ( Authenticated :: Authorized ( Authorized :: FullAccess ) ) ;
75
+ return Ok ( Authenticated :: Authorized ( Authorized {
76
+ namespace : None ,
77
+ permission : Permission :: FullAccess ,
78
+ } ) ) ;
67
79
}
68
80
69
81
let Some ( auth_header) = auth_header else {
@@ -80,57 +92,98 @@ impl Auth {
80
92
let actual_value = actual_value. trim_end_matches ( '=' ) ;
81
93
let expected_value = expected_value. trim_end_matches ( '=' ) ;
82
94
if actual_value == expected_value {
83
- Ok ( Authenticated :: Authorized ( Authorized :: FullAccess ) )
95
+ Ok ( Authenticated :: Authorized ( Authorized {
96
+ namespace : None ,
97
+ permission : Permission :: FullAccess ,
98
+ } ) )
84
99
} else {
85
100
Err ( AuthError :: BasicRejected )
86
101
}
87
102
}
88
- HttpAuthHeader :: Bearer ( token) => self . validate_jwt ( & token) ,
103
+ HttpAuthHeader :: Bearer ( token) => self . validate_jwt ( & token, disable_namespaces ) ,
89
104
}
90
105
}
91
106
92
- pub fn authenticate_grpc < T > ( & self , req : & tonic:: Request < T > ) -> Result < Authenticated , Status > {
107
+ pub fn authenticate_grpc < T > (
108
+ & self ,
109
+ req : & tonic:: Request < T > ,
110
+ disable_namespaces : bool ,
111
+ ) -> Result < Authenticated , Status > {
93
112
let metadata = req. metadata ( ) ;
94
113
95
114
let auth = metadata
96
115
. get ( GRPC_AUTH_HEADER )
97
116
. map ( |v| v. to_bytes ( ) . expect ( "Auth should always be ASCII" ) )
98
117
. map ( |v| HeaderValue :: from_maybe_shared ( v) . expect ( "Should already be valid header" ) ) ;
99
118
100
- self . authenticate_http ( auth. as_ref ( ) ) . map_err ( Into :: into)
119
+ self . authenticate_http ( auth. as_ref ( ) , disable_namespaces)
120
+ . map_err ( Into :: into)
101
121
}
102
122
103
- pub fn authenticate_jwt ( & self , jwt : Option < & str > ) -> Result < Authenticated , AuthError > {
123
+ pub fn authenticate_jwt (
124
+ & self ,
125
+ jwt : Option < & str > ,
126
+ disable_namespaces : bool ,
127
+ ) -> Result < Authenticated , AuthError > {
104
128
if self . disabled {
105
- return Ok ( Authenticated :: Authorized ( Authorized :: FullAccess ) ) ;
129
+ return Ok ( Authenticated :: Authorized ( Authorized {
130
+ namespace : None ,
131
+ permission : Permission :: FullAccess ,
132
+ } ) ) ;
106
133
}
107
134
108
135
let Some ( jwt) = jwt else {
109
136
return Err ( AuthError :: JwtMissing )
110
137
} ;
111
138
112
- self . validate_jwt ( jwt)
139
+ self . validate_jwt ( jwt, disable_namespaces )
113
140
}
114
141
115
- fn validate_jwt ( & self , jwt : & str ) -> Result < Authenticated , AuthError > {
142
+ fn validate_jwt (
143
+ & self ,
144
+ jwt : & str ,
145
+ disable_namespaces : bool ,
146
+ ) -> Result < Authenticated , AuthError > {
116
147
let Some ( jwt_key) = self . jwt_key . as_ref ( ) else {
117
148
return Err ( AuthError :: JwtNotAllowed )
118
149
} ;
119
- validate_jwt ( jwt_key, jwt)
150
+ validate_jwt ( jwt_key, jwt, disable_namespaces )
120
151
}
121
152
}
122
153
123
154
impl Authenticated {
124
- pub fn from_proxy_grpc_request < T > ( req : & tonic:: Request < T > ) -> Result < Self , Status > {
155
+ pub fn from_proxy_grpc_request < T > (
156
+ req : & tonic:: Request < T > ,
157
+ disable_namespace : bool ,
158
+ ) -> Result < Self , Status > {
159
+ let namespace = if disable_namespace {
160
+ None
161
+ } else {
162
+ req. metadata ( )
163
+ . get_bin ( NAMESPACE_METADATA_KEY )
164
+ . map ( |c| c. to_bytes ( ) )
165
+ . transpose ( )
166
+ . map_err ( |_| Status :: invalid_argument ( "failed to parse namespace header" ) ) ?
167
+ . map ( NamespaceName :: from_bytes)
168
+ . transpose ( )
169
+ . map_err ( |_| Status :: invalid_argument ( "invalid namespace name" ) ) ?
170
+ } ;
171
+
125
172
let auth = match req
126
173
. metadata ( )
127
174
. get ( GRPC_PROXY_AUTH_HEADER )
128
175
. map ( |v| v. to_str ( ) )
129
176
. transpose ( )
130
177
. map_err ( |_| Status :: invalid_argument ( "missing authorization header" ) ) ?
131
178
{
132
- Some ( "full_access" ) => Authenticated :: Authorized ( Authorized :: FullAccess ) ,
133
- Some ( "read_only" ) => Authenticated :: Authorized ( Authorized :: ReadOnly ) ,
179
+ Some ( "full_access" ) => Authenticated :: Authorized ( Authorized {
180
+ namespace,
181
+ permission : Permission :: FullAccess ,
182
+ } ) ,
183
+ Some ( "read_only" ) => Authenticated :: Authorized ( Authorized {
184
+ namespace,
185
+ permission : Permission :: ReadOnly ,
186
+ } ) ,
134
187
Some ( "anonymous" ) => Authenticated :: Anonymous ,
135
188
Some ( level) => {
136
189
return Err ( Status :: permission_denied ( format ! (
@@ -149,14 +202,34 @@ impl Authenticated {
149
202
150
203
let auth = match self {
151
204
Authenticated :: Anonymous => "anonymous" ,
152
- Authenticated :: Authorized ( Authorized :: FullAccess ) => "full_access" ,
153
- Authenticated :: Authorized ( Authorized :: ReadOnly ) => "read_only" ,
205
+ Authenticated :: Authorized ( Authorized {
206
+ permission : Permission :: FullAccess ,
207
+ ..
208
+ } ) => "full_access" ,
209
+ Authenticated :: Authorized ( Authorized {
210
+ permission : Permission :: ReadOnly ,
211
+ ..
212
+ } ) => "read_only" ,
154
213
} ;
155
214
156
215
let value = tonic:: metadata:: AsciiMetadataValue :: try_from ( auth) . unwrap ( ) ;
157
216
158
217
req. metadata_mut ( ) . insert ( key, value) ;
159
218
}
219
+
220
+ pub fn is_namespace_authorized ( & self , namespace : & NamespaceName ) -> bool {
221
+ match self {
222
+ Authenticated :: Anonymous => true ,
223
+ Authenticated :: Authorized ( Authorized {
224
+ namespace : Some ( ns) ,
225
+ ..
226
+ } ) => ns == namespace,
227
+ // we threat the absence of a specific namespace has a permission to any namespace
228
+ Authenticated :: Authorized ( Authorized {
229
+ namespace : None , ..
230
+ } ) => true ,
231
+ }
232
+ }
160
233
}
161
234
162
235
#[ derive( Debug ) ]
@@ -188,6 +261,7 @@ fn parse_http_auth_header(
188
261
fn validate_jwt (
189
262
jwt_key : & jsonwebtoken:: DecodingKey ,
190
263
jwt : & str ,
264
+ disable_namespace : bool ,
191
265
) -> Result < Authenticated , AuthError > {
192
266
use jsonwebtoken:: errors:: ErrorKind ;
193
267
@@ -197,13 +271,26 @@ fn validate_jwt(
197
271
match jsonwebtoken:: decode :: < serde_json:: Value > ( jwt, jwt_key, & validation) . map ( |t| t. claims ) {
198
272
Ok ( serde_json:: Value :: Object ( claims) ) => {
199
273
tracing:: trace!( "Claims: {claims:#?}" ) ;
200
- Ok ( match claims. get ( "a" ) . and_then ( |s| s. as_str ( ) ) {
201
- Some ( "ro" ) => Authenticated :: Authorized ( Authorized :: ReadOnly ) ,
202
- Some ( "rw" ) => Authenticated :: Authorized ( Authorized :: FullAccess ) ,
203
- Some ( _) => Authenticated :: Anonymous ,
274
+ let namespace = if disable_namespace {
275
+ None
276
+ } else {
277
+ claims
278
+ . get ( "id" )
279
+ . and_then ( |ns| NamespaceName :: from_string ( ns. as_str ( ) ?. into ( ) ) . ok ( ) )
280
+ } ;
281
+
282
+ let permission = match claims. get ( "a" ) . and_then ( |s| s. as_str ( ) ) {
283
+ Some ( "ro" ) => Permission :: ReadOnly ,
284
+ Some ( "rw" ) => Permission :: FullAccess ,
285
+ Some ( _) => return Ok ( Authenticated :: Anonymous ) ,
204
286
// Backward compatibility - no access claim means full access
205
- None => Authenticated :: Authorized ( Authorized :: FullAccess ) ,
206
- } )
287
+ None => Permission :: FullAccess ,
288
+ } ;
289
+
290
+ Ok ( Authenticated :: Authorized ( Authorized {
291
+ namespace,
292
+ permission,
293
+ } ) )
207
294
}
208
295
Ok ( _) => Err ( AuthError :: JwtInvalid ) ,
209
296
Err ( error) => Err ( match error. kind ( ) {
@@ -280,7 +367,7 @@ mod tests {
280
367
use hyper:: header:: HeaderValue ;
281
368
282
369
fn authenticate_http ( auth : & Auth , header : & str ) -> Result < Authenticated , AuthError > {
283
- auth. authenticate_http ( Some ( & HeaderValue :: from_str ( header) . unwrap ( ) ) )
370
+ auth. authenticate_http ( Some ( & HeaderValue :: from_str ( header) . unwrap ( ) ) , false )
284
371
}
285
372
286
373
const VALID_JWT_KEY : & str = "zaMv-aFGmB7PXkjM4IrMdF6B5zCYEiEGXW3RgMjNAtc" ;
@@ -312,9 +399,9 @@ mod tests {
312
399
#[ test]
313
400
fn test_default ( ) {
314
401
let auth = Auth :: default ( ) ;
315
- assert_err ! ( auth. authenticate_http( None ) ) ;
402
+ assert_err ! ( auth. authenticate_http( None , false ) ) ;
316
403
assert_err ! ( authenticate_http( & auth, "Basic d29qdGVrOnRoZWJlYXI=" ) ) ;
317
- assert_err ! ( auth. authenticate_jwt( Some ( VALID_JWT ) ) ) ;
404
+ assert_err ! ( auth. authenticate_jwt( Some ( VALID_JWT ) , false ) ) ;
318
405
}
319
406
320
407
#[ test]
@@ -332,7 +419,7 @@ mod tests {
332
419
assert_err ! ( authenticate_http( & auth, "Basic d29qdgvronrozwjlyxi=" ) ) ;
333
420
assert_err ! ( authenticate_http( & auth, "Basic d29qdGVrOnRoZWZveA==" ) ) ;
334
421
335
- assert_err ! ( auth. authenticate_http( None ) ) ;
422
+ assert_err ! ( auth. authenticate_http( None , false ) ) ;
336
423
assert_err ! ( authenticate_http( & auth, "" ) ) ;
337
424
assert_err ! ( authenticate_http( & auth, "foobar" ) ) ;
338
425
assert_err ! ( authenticate_http( & auth, "foo bar" ) ) ;
@@ -356,7 +443,10 @@ mod tests {
356
443
357
444
assert_eq ! (
358
445
authenticate_http( & auth, & format!( "Bearer {VALID_READONLY_JWT}" ) ) . unwrap( ) ,
359
- Authenticated :: Authorized ( Authorized :: ReadOnly )
446
+ Authenticated :: Authorized ( Authorized {
447
+ namespace: None ,
448
+ permission: Permission :: ReadOnly
449
+ } )
360
450
) ;
361
451
}
362
452
@@ -366,7 +456,7 @@ mod tests {
366
456
jwt_key : Some ( parse_jwt_key ( VALID_JWT_KEY ) . unwrap ( ) ) ,
367
457
..Auth :: default ( )
368
458
} ;
369
- assert_ok ! ( auth. authenticate_jwt( Some ( VALID_JWT ) ) ) ;
370
- assert_err ! ( auth. authenticate_jwt( Some ( & VALID_JWT [ ..80 ] ) ) ) ;
459
+ assert_ok ! ( auth. authenticate_jwt( Some ( VALID_JWT ) , false ) ) ;
460
+ assert_err ! ( auth. authenticate_jwt( Some ( & VALID_JWT [ ..80 ] ) , false ) ) ;
371
461
}
372
462
}
0 commit comments