1+ pub ( crate ) mod jwt;
2+ pub ( crate ) mod whitelist;
3+
14use eyre:: { eyre, Result } ;
2- use jsonwebtoken:: DecodingKey ;
3- use notify:: {
4- event:: ModifyKind , Error , Event , EventKind , RecommendedWatcher , RecursiveMode , Watcher ,
5- } ;
6- use serde:: { Deserialize , Serialize } ;
7- use serde_json:: Value ;
5+ use jwt:: load_jwt_key;
86use std:: {
97 collections:: HashMap ,
10- io:: Read ,
11- path:: Path ,
128 sync:: { Arc , Mutex } ,
139} ;
14- use tracing:: { debug, error, info} ;
10+ use tracing:: debug;
11+ use whitelist:: load_authorization_whitelist;
1512
16- use crate :: {
17- read_pem_file , util :: parse_csv_file , AuthorizationModeProperties , JwtClaim , JwtClaimValueType ,
18- NotaryServerProperties ,
13+ pub use jwt :: { Jwt , JwtError } ;
14+ pub use whitelist :: {
15+ watch_and_reload_authorization_whitelist , AuthorizationWhitelistRecord , X_API_KEY_HEADER ,
1916} ;
2017
18+ use crate :: { AuthorizationModeProperties , NotaryServerProperties } ;
19+
2120/// Supported authorization modes.
2221#[ derive( Clone ) ]
2322pub enum AuthorizationMode {
@@ -36,96 +35,6 @@ impl AuthorizationMode {
3635 }
3736}
3837
39- /// Custom error returned if JWT claims validation fails.
40- #[ derive( Debug , thiserror:: Error , PartialEq ) ]
41- #[ error( "JWT validation error: {0}" ) ]
42- pub struct JwtValidationError ( String ) ;
43-
44- /// JWT config which also encapsulates claims validation logic.
45- #[ derive( Clone ) ]
46- pub struct Jwt {
47- pub key : DecodingKey ,
48- pub claims : Vec < JwtClaim > ,
49- }
50-
51- impl Jwt {
52- pub fn validate ( & self , claims : Value ) -> Result < ( ) , JwtValidationError > {
53- Jwt :: validate_claims ( & self . claims , claims)
54- }
55-
56- fn validate_claims ( expected : & [ JwtClaim ] , claims : Value ) -> Result < ( ) , JwtValidationError > {
57- expected
58- . iter ( )
59- . try_for_each ( |expected| Self :: validate_claim ( expected, claims. clone ( ) ) )
60- }
61-
62- fn validate_claim ( expected : & JwtClaim , given : Value ) -> Result < ( ) , JwtValidationError > {
63- let field = Jwt :: get_field ( & expected. name , & given) . ok_or ( JwtValidationError ( format ! (
64- "missing claim '{}'" ,
65- expected. name
66- ) ) ) ?;
67-
68- match expected. value_type {
69- JwtClaimValueType :: String => {
70- let field_typed = field. as_str ( ) . ok_or ( JwtValidationError ( format ! (
71- "unexpected type for claim '{}': expected '{:?}'" ,
72- expected. name, expected. value_type
73- ) ) ) ?;
74- if !expected. values . is_empty ( ) {
75- expected. values . iter ( ) . any ( |exp| exp == field_typed) . then_some ( ( ) ) . ok_or_else ( || {
76- let expected_values = expected. values . iter ( ) . map ( |x| format ! ( "'{x}'" ) ) . collect :: < Vec < String > > ( ) . join ( ", " ) ;
77- JwtValidationError ( format ! (
78- "unexpected value for claim '{}': expected one of [ {expected_values} ], received '{field_typed}'" , expected. name
79- ) )
80- } ) ?;
81- }
82- }
83- }
84-
85- Ok ( ( ) )
86- }
87-
88- fn get_field < ' a > ( path : & ' a str , value : & ' a Value ) -> Option < & ' a Value > {
89- let ( field, path) = match path. split_once ( '.' ) {
90- Some ( ( field, path) ) => ( field, Some ( path) ) ,
91- None => ( path, None ) ,
92- } ;
93- if let Some ( value) = value. get ( field) {
94- match path {
95- Some ( path) => Jwt :: get_field ( path, value) ,
96- None => Some ( value) ,
97- }
98- } else {
99- None
100- }
101- }
102- }
103-
104- /// Custom HTTP header used for specifying a whitelisted API key
105- pub const X_API_KEY_HEADER : & str = "X-API-Key" ;
106-
107- /// Structure of each whitelisted record of the API key whitelist for
108- /// authorization purpose
109- #[ derive( Clone , Debug , Deserialize , Serialize ) ]
110- #[ serde( rename_all = "PascalCase" ) ]
111- pub struct AuthorizationWhitelistRecord {
112- pub name : String ,
113- pub api_key : String ,
114- pub created_at : String ,
115- }
116-
117- /// Convert whitelist data structure from vector to hashmap using api_key as the
118- /// key to speed up lookup
119- pub fn authorization_whitelist_vec_into_hashmap (
120- authorization_whitelist : Vec < AuthorizationWhitelistRecord > ,
121- ) -> HashMap < String , AuthorizationWhitelistRecord > {
122- let mut hashmap = HashMap :: new ( ) ;
123- authorization_whitelist. iter ( ) . for_each ( |record| {
124- hashmap. insert ( record. api_key . clone ( ) , record. to_owned ( ) ) ;
125- } ) ;
126- hashmap
127- }
128-
12938/// Load authorization mode if it is enabled
13039pub async fn load_authorization_mode (
13140 config : & NotaryServerProperties ,
@@ -141,9 +50,14 @@ pub async fn load_authorization_mode(
14150 )
14251 } ) ? {
14352 AuthorizationModeProperties :: Jwt ( jwt_opts) => {
144- let key = load_jwt_key ( & jwt_opts. public_key_path ) . await ? ;
53+ let algorithm = jwt_opts. algorithm ;
14554 let claims = jwt_opts. claims . clone ( ) ;
146- AuthorizationMode :: Jwt ( Jwt { key, claims } )
55+ let key = load_jwt_key ( & jwt_opts. public_key_path , algorithm) . await ?;
56+ AuthorizationMode :: Jwt ( Jwt {
57+ key,
58+ claims,
59+ algorithm,
60+ } )
14761 }
14862 AuthorizationModeProperties :: Whitelist ( whitelist_csv_path) => {
14963 let whitelist = load_authorization_whitelist ( whitelist_csv_path) ?;
@@ -153,212 +67,3 @@ pub async fn load_authorization_mode(
15367
15468 Ok ( Some ( auth_mode) )
15569}
156-
157- /// Load JWT public key
158- async fn load_jwt_key ( public_key_pem_path : & str ) -> Result < DecodingKey > {
159- let mut reader = read_pem_file ( public_key_pem_path) . await ?;
160- let mut key: Vec < u8 > = Vec :: new ( ) ;
161- reader. read_to_end ( & mut key) ?;
162- let key = DecodingKey :: from_rsa_pem ( & key) ?;
163- Ok ( key)
164- }
165-
166- /// Load authorization whitelist
167- fn load_authorization_whitelist (
168- whitelist_csv_path : & str ,
169- ) -> Result < HashMap < String , AuthorizationWhitelistRecord > > {
170- // Load the csv
171- let whitelist_csv = parse_csv_file :: < AuthorizationWhitelistRecord > ( whitelist_csv_path)
172- . map_err ( |err| eyre ! ( "Failed to parse authorization whitelist csv: {:?}" , err) ) ?;
173- // Convert the whitelist record into hashmap for faster lookup
174- let whitelist_hashmap = authorization_whitelist_vec_into_hashmap ( whitelist_csv) ;
175- Ok ( whitelist_hashmap)
176- }
177-
178- // Setup a watcher to detect any changes to authorization whitelist
179- // When the list file is modified, the watcher thread will reload the whitelist
180- // The watcher is setup in a separate thread by the notify library which is
181- // synchronous
182- pub fn watch_and_reload_authorization_whitelist (
183- authorization_whitelist : Arc < Mutex < HashMap < String , AuthorizationWhitelistRecord > > > ,
184- whitelist_csv_path : String ,
185- ) -> Result < RecommendedWatcher > {
186- let whitelist_csv_path_cloned = whitelist_csv_path. clone ( ) ;
187- // Setup watcher by giving it a function that will be triggered when an event is
188- // detected
189- let mut watcher = RecommendedWatcher :: new (
190- move |event : Result < Event , Error > | {
191- match event {
192- Ok ( event) => {
193- // Only reload whitelist if it's an event that modified the file data
194- if let EventKind :: Modify ( ModifyKind :: Data ( _) ) = event. kind {
195- debug ! ( "Authorization whitelist is modified" ) ;
196- match load_authorization_whitelist ( & whitelist_csv_path_cloned) {
197- Ok ( new_authorization_whitelist) => {
198- * authorization_whitelist. lock ( ) . unwrap ( ) =
199- new_authorization_whitelist;
200- info ! ( "Successfully reloaded authorization whitelist!" ) ;
201- }
202- // Ensure that error from reloading doesn't bring the server down
203- Err ( err) => error ! ( "{err}" ) ,
204- }
205- }
206- }
207- Err ( err) => {
208- error ! ( "Error occured when watcher detected an event: {err}" )
209- }
210- }
211- } ,
212- notify:: Config :: default ( ) ,
213- )
214- . map_err ( |err| eyre ! ( "Error occured when setting up watcher for hot reload: {err}" ) ) ?;
215-
216- // Start watcher to listen to any changes on the whitelist file
217- watcher
218- . watch ( Path :: new ( & whitelist_csv_path) , RecursiveMode :: Recursive )
219- . map_err ( |err| eyre ! ( "Error occured when starting up watcher for hot reload: {err}" ) ) ?;
220-
221- // Need to return the watcher to parent function, else it will be dropped and
222- // stop listening
223- Ok ( watcher)
224- }
225-
226- #[ cfg( test) ]
227- mod test {
228- use super :: * ;
229-
230- mod whitelist {
231- use std:: { fs:: OpenOptions , time:: Duration } ;
232-
233- use csv:: WriterBuilder ;
234-
235- use super :: * ;
236-
237- #[ tokio:: test]
238- async fn test_watch_and_reload_authorization_whitelist ( ) {
239- // Clone fixture auth whitelist for testing
240- let original_whitelist_csv_path = "./fixture/auth/whitelist.csv" ;
241- let whitelist_csv_path = "./fixture/auth/whitelist_copied.csv" . to_string ( ) ;
242- std:: fs:: copy ( original_whitelist_csv_path, & whitelist_csv_path) . unwrap ( ) ;
243-
244- // Setup watcher
245- let authorization_whitelist = load_authorization_whitelist ( & whitelist_csv_path) . expect (
246- "Authorization whitelist csv from fixture should be able
247- to be loaded" ,
248- ) ;
249- let authorization_whitelist = Arc :: new ( Mutex :: new ( authorization_whitelist) ) ;
250- let _watcher = watch_and_reload_authorization_whitelist (
251- authorization_whitelist. clone ( ) ,
252- whitelist_csv_path. clone ( ) ,
253- )
254- . expect ( "Watcher should be able to be setup successfully" ) ;
255-
256- // Sleep to buy a bit of time for hot reload task and watcher thread to run
257- tokio:: time:: sleep ( Duration :: from_millis ( 50 ) ) . await ;
258-
259- // Write a new record to the whitelist to trigger modify event
260- let new_record = AuthorizationWhitelistRecord {
261- name : "unit-test-name" . to_string ( ) ,
262- api_key : "unit-test-api-key" . to_string ( ) ,
263- created_at : "unit-test-created-at" . to_string ( ) ,
264- } ;
265- let file = OpenOptions :: new ( )
266- . append ( true )
267- . open ( & whitelist_csv_path)
268- . unwrap ( ) ;
269- let mut wtr = WriterBuilder :: new ( )
270- . has_headers ( false ) // Set to false to avoid writing header again
271- . from_writer ( file) ;
272- wtr. serialize ( new_record) . unwrap ( ) ;
273- wtr. flush ( ) . unwrap ( ) ;
274-
275- // Sleep to buy a bit of time for updated whitelist to be hot reloaded
276- tokio:: time:: sleep ( Duration :: from_millis ( 50 ) ) . await ;
277-
278- assert ! ( authorization_whitelist
279- . lock( )
280- . unwrap( )
281- . contains_key( "unit-test-api-key" ) ) ;
282-
283- // Delete the cloned whitelist
284- std:: fs:: remove_file ( & whitelist_csv_path) . unwrap ( ) ;
285- }
286- }
287-
288- mod jwt {
289- use serde_json:: json;
290-
291- use super :: * ;
292-
293- #[ test]
294- fn validates_presence ( ) {
295- let expected = JwtClaim {
296- name : "sub" . to_string ( ) ,
297- ..Default :: default ( )
298- } ;
299- let given = json ! ( {
300- "exp" : 12345 ,
301- "sub" : "test" ,
302- } ) ;
303- assert ! ( Jwt :: validate_claim( & expected, given) . is_ok( ) ) ;
304- }
305-
306- #[ test]
307- fn validates_expected_value ( ) {
308- let expected = JwtClaim {
309- name : "custom.host" . to_string ( ) ,
310- values : vec ! [ "tlsn.com" . to_string( ) , "api.tlsn.com" . to_string( ) ] ,
311- ..Default :: default ( )
312- } ;
313- let given = json ! ( {
314- "exp" : 12345 ,
315- "custom" : {
316- "host" : "api.tlsn.com" ,
317- } ,
318- } ) ;
319- assert ! ( Jwt :: validate_claim( & expected, given) . is_ok( ) )
320- }
321-
322- #[ test]
323- fn validates_with_unknown_claims ( ) {
324- let given = json ! ( {
325- "exp" : 12345 ,
326- "sub" : "test" ,
327- "what" : "is_this" ,
328- } ) ;
329- assert ! ( Jwt :: validate_claims( & [ ] , given) . is_ok( ) )
330- }
331-
332- #[ test]
333- fn fails_if_claim_missing ( ) {
334- let expected = JwtClaim {
335- name : "sub" . to_string ( ) ,
336- ..Default :: default ( )
337- } ;
338- let given = json ! ( {
339- "exp" : 12345 ,
340- "host" : "localhost" ,
341- } ) ;
342- assert_eq ! (
343- Jwt :: validate_claim( & expected, given) ,
344- Err ( JwtValidationError ( "missing claim 'sub'" . to_string( ) ) )
345- )
346- }
347-
348- #[ test]
349- fn fails_if_claim_has_unknown_value ( ) {
350- let expected = JwtClaim {
351- name : "sub" . to_string ( ) ,
352- values : vec ! [ "tlsn_prod" . to_string( ) , "tlsn_test" . to_string( ) ] ,
353- ..Default :: default ( )
354- } ;
355- let given = json ! ( {
356- "sub" : "tlsn" ,
357- } ) ;
358- assert_eq ! (
359- Jwt :: validate_claim( & expected, given) ,
360- Err ( JwtValidationError ( "unexpected value for claim 'sub': expected one of [ 'tlsn_prod', 'tlsn_test' ], received 'tlsn'" . to_string( ) ) )
361- )
362- }
363- }
364- }
0 commit comments