@@ -136,19 +136,80 @@ fn load_or_create_secret(paths: &Paths) -> Result<[u8; ACCESS_TOKEN_BYTES]> {
136136 std:: fs:: create_dir_all ( & paths. root )
137137 . with_context ( || format ! ( "failed to create {}" , paths. root. display( ) ) ) ?;
138138 let path = token_path ( paths) ;
139- match open_secret_for_create ( & path) {
140- Ok ( mut file) => {
141- let mut secret = [ 0_u8 ; ACCESS_TOKEN_BYTES ] ;
142- fill_random ( & mut secret) ?;
143- let encoded = URL_SAFE_NO_PAD . encode ( secret) ;
139+ match publish_new_secret ( & path) ? {
140+ Some ( secret) => Ok ( secret) ,
141+ None => read_secret ( & path) ,
142+ }
143+ }
144+
145+ fn publish_new_secret ( path : & Path ) -> Result < Option < [ u8 ; ACCESS_TOKEN_BYTES ] > > {
146+ let parent = path
147+ . parent ( )
148+ . ok_or_else ( || anyhow ! ( "web auth token path has no parent: {}" , path. display( ) ) ) ?;
149+ let mut secret = [ 0_u8 ; ACCESS_TOKEN_BYTES ] ;
150+ fill_random ( & mut secret) ?;
151+ let encoded = URL_SAFE_NO_PAD . encode ( secret) ;
152+
153+ for _ in 0 ..16 {
154+ let mut suffix = [ 0_u8 ; 16 ] ;
155+ fill_random ( & mut suffix) ?;
156+ let temp_path = parent. join ( format ! (
157+ ".{TOKEN_FILE}.{}.tmp" ,
158+ URL_SAFE_NO_PAD . encode( suffix)
159+ ) ) ;
160+ let mut file = match open_secret_for_create ( & temp_path) {
161+ Ok ( file) => file,
162+ Err ( err) if err. kind ( ) == ErrorKind :: AlreadyExists => continue ,
163+ Err ( err) => {
164+ return Err ( err)
165+ . with_context ( || format ! ( "failed to create {}" , temp_path. display( ) ) ) ;
166+ }
167+ } ;
168+
169+ let write_result = ( || -> std:: io:: Result < ( ) > {
144170 file. write_all ( encoded. as_bytes ( ) ) ?;
145171 file. write_all ( b"\n " ) ?;
146- file. sync_all ( ) ?;
147- Ok ( secret)
172+ file. sync_all ( )
173+ } ) ( ) ;
174+ if let Err ( err) = write_result {
175+ drop ( file) ;
176+ let _ = std:: fs:: remove_file ( & temp_path) ;
177+ return Err ( err) . with_context ( || format ! ( "failed to write {}" , temp_path. display( ) ) ) ;
178+ }
179+ drop ( file) ;
180+
181+ match std:: fs:: hard_link ( & temp_path, path) {
182+ Ok ( ( ) ) => {
183+ std:: fs:: remove_file ( & temp_path) . with_context ( || {
184+ format ! ( "failed to remove temporary token {}" , temp_path. display( ) )
185+ } ) ?;
186+ sync_directory ( parent) ?;
187+ return Ok ( Some ( secret) ) ;
188+ }
189+ Err ( err) if err. kind ( ) == ErrorKind :: AlreadyExists => {
190+ let _ = std:: fs:: remove_file ( & temp_path) ;
191+ return Ok ( None ) ;
192+ }
193+ Err ( err) => {
194+ let _ = std:: fs:: remove_file ( & temp_path) ;
195+ return Err ( err) . with_context ( || format ! ( "failed to publish {}" , path. display( ) ) ) ;
196+ }
148197 }
149- Err ( err) if err. kind ( ) == ErrorKind :: AlreadyExists => read_secret ( & path) ,
150- Err ( err) => Err ( err) . with_context ( || format ! ( "failed to create {}" , path. display( ) ) ) ,
151198 }
199+
200+ bail ! ( "failed to allocate a temporary web auth token file" )
201+ }
202+
203+ #[ cfg( unix) ]
204+ fn sync_directory ( path : & Path ) -> Result < ( ) > {
205+ File :: open ( path)
206+ . and_then ( |directory| directory. sync_all ( ) )
207+ . with_context ( || format ! ( "failed to sync {}" , path. display( ) ) )
208+ }
209+
210+ #[ cfg( not( unix) ) ]
211+ fn sync_directory ( _path : & Path ) -> Result < ( ) > {
212+ Ok ( ( ) )
152213}
153214
154215fn read_secret ( path : & Path ) -> Result < [ u8 ; ACCESS_TOKEN_BYTES ] > {
@@ -254,6 +315,39 @@ mod tests {
254315 ) ;
255316 }
256317
318+ #[ test]
319+ fn concurrent_first_use_publishes_one_complete_access_token ( ) {
320+ let temp = TempDir :: new ( ) . unwrap ( ) ;
321+ let root = temp. path ( ) . to_path_buf ( ) ;
322+ let barrier = std:: sync:: Arc :: new ( std:: sync:: Barrier :: new ( 16 ) ) ;
323+ let threads: Vec < _ > = ( 0 ..16 )
324+ . map ( |_| {
325+ let root = root. clone ( ) ;
326+ let barrier = std:: sync:: Arc :: clone ( & barrier) ;
327+ std:: thread:: spawn ( move || {
328+ let paths = Paths :: new ( Some ( root) ) . unwrap ( ) ;
329+ barrier. wait ( ) ;
330+ WebAuth :: load_or_create ( & paths) . unwrap ( )
331+ } )
332+ } )
333+ . collect ( ) ;
334+ let auths: Vec < _ > = threads
335+ . into_iter ( )
336+ . map ( |thread| thread. join ( ) . unwrap ( ) )
337+ . collect ( ) ;
338+ let paths = Paths :: new ( Some ( root) ) . unwrap ( ) ;
339+ let token = std:: fs:: read_to_string ( token_path ( & paths) ) . unwrap ( ) ;
340+
341+ assert ! ( auths. iter( ) . all( |auth| auth. authorize_bearer( token. trim( ) ) ) ) ;
342+ assert_eq ! (
343+ std:: fs:: read_dir( & paths. root)
344+ . unwrap( )
345+ . filter_map( Result :: ok)
346+ . count( ) ,
347+ 1
348+ ) ;
349+ }
350+
257351 #[ test]
258352 fn bootstrap_tokens_are_short_lived_and_single_use ( ) {
259353 let temp = TempDir :: new ( ) . unwrap ( ) ;
0 commit comments