1- use std:: { error:: Error , io, path:: PathBuf } ;
1+ use std:: { error:: Error , io, path:: PathBuf , sync :: Arc } ;
22
33use clap:: {
44 Arg , Args , Command as ClapCommand , CommandFactory , Error as ClapError , FromArgMatches , Parser ,
55 ValueEnum , error:: ErrorKind ,
66} ;
7+ use influxdb3_authz:: TokenInfo ;
8+ use influxdb3_catalog:: catalog:: { compute_token_hash, create_token_and_hash} ;
79use influxdb3_client:: Client ;
810use influxdb3_types:: http:: CreateTokenWithPermissionsResponse ;
911use owo_colors:: OwoColorize ;
1012use secrecy:: Secret ;
13+ use serde:: { Deserialize , Serialize } ;
1114use url:: Url ;
1215
16+ #[ derive( Debug , Serialize , Deserialize ) ]
17+ pub ( crate ) struct AdminTokenFile {
18+ /// The raw token string
19+ pub token : String ,
20+ /// The token name
21+ pub name : String ,
22+ /// Optional expiry timestamp in milliseconds
23+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
24+ pub expiry_millis : Option < i64 > ,
25+ }
1326pub ( crate ) async fn handle_token_creation_with_config (
1427 client : Client ,
1528 config : CreateTokenConfig ,
@@ -32,39 +45,129 @@ pub(crate) async fn handle_admin_token_creation(
3245 client : Client ,
3346 config : CreateAdminTokenConfig ,
3447) -> Result < CreateTokenWithPermissionsResponse , Box < dyn Error > > {
35- let json_body = if config. regenerate {
36- println ! ( "Are you sure you want to regenerate admin token? Enter 'yes' to confirm" , ) ;
37- let mut confirmation = String :: new ( ) ;
38- let _ = io:: stdin ( ) . read_line ( & mut confirmation) ;
39- if confirmation. trim ( ) == "yes" {
48+ if config. offline {
49+ // Generate token without server
50+ let token = generate_offline_token ( ) ;
51+
52+ let output_file = config
53+ . output_file
54+ . ok_or ( "--output-file is required with --offline" ) ?;
55+
56+ // Create admin token file with metadata
57+ let token_file = AdminTokenFile {
58+ token : token. clone ( ) ,
59+ name : "_admin" . to_string ( ) ,
60+ expiry_millis : None ,
61+ } ;
62+
63+ let json = serde_json:: to_string_pretty ( & token_file) ?;
64+
65+ // Write token atomically with correct permissions
66+ write_file_atomically ( & output_file, & json) ?;
67+
68+ println ! ( "Token saved to: {}" , output_file. display( ) ) ;
69+
70+ // For offline mode, we return a mock success response
71+ let hash = compute_token_hash ( & token) ;
72+ let token_info = Arc :: new ( TokenInfo {
73+ id : 0 . into ( ) ,
74+ name : Arc :: from ( "_admin" ) ,
75+ hash,
76+ description : None ,
77+ created_by : None ,
78+ created_at : chrono:: Utc :: now ( ) . timestamp_millis ( ) ,
79+ updated_at : None ,
80+ updated_by : None ,
81+ expiry_millis : i64:: MAX ,
82+ permissions : vec ! [ ] , // Admin tokens don't need explicit permissions
83+ } ) ;
84+
85+ CreateTokenWithPermissionsResponse :: from_token_info ( token_info, token)
86+ . ok_or_else ( || "Failed to create token response" . into ( ) )
87+ } else {
88+ let json_body = if config. regenerate {
89+ println ! ( "Are you sure you want to regenerate admin token? Enter 'yes' to confirm" , ) ;
90+ let mut confirmation = String :: new ( ) ;
91+ io:: stdin ( ) . read_line ( & mut confirmation) ?;
92+ if confirmation. trim ( ) == "yes" {
93+ client
94+ . api_v3_configure_regenerate_admin_token ( )
95+ . await ?
96+ . expect ( "token creation to return full token info" )
97+ } else {
98+ return Err ( "Cannot regenerate token without confirmation" . into ( ) ) ;
99+ }
100+ } else {
40101 client
41- . api_v3_configure_regenerate_admin_token ( )
102+ . api_v3_configure_create_admin_token ( )
42103 . await ?
43104 . expect ( "token creation to return full token info" )
44- } else {
45- return Err ( "Cannot regenerate token without confirmation" . into ( ) ) ;
46- }
47- } else {
48- client
49- . api_v3_configure_create_admin_token ( )
50- . await ?
51- . expect ( "token creation to return full token info" )
52- } ;
53- Ok ( json_body)
105+ } ;
106+ Ok ( json_body)
107+ }
108+ }
109+
110+ fn generate_offline_token ( ) -> String {
111+ create_token_and_hash ( ) . 0
54112}
55113
56114pub ( crate ) async fn handle_named_admin_token_creation (
57115 client : Client ,
58116 config : CreateAdminTokenConfig ,
59117) -> Result < CreateTokenWithPermissionsResponse , Box < dyn Error > > {
60- let json_body = client
61- . api_v3_configure_create_named_admin_token (
62- config. name . expect ( "token name to be present" ) ,
63- config. expiry . map ( |expiry| expiry. as_secs ( ) ) ,
64- )
65- . await ?
66- . expect ( "token creation to return full token info" ) ;
67- Ok ( json_body)
118+ if config. offline {
119+ // Generate token without server
120+ let token = generate_offline_token ( ) ;
121+
122+ let output_file = config
123+ . output_file
124+ . ok_or ( "--output-file is required with --offline" ) ?;
125+
126+ let token_name = config. name . expect ( "token name to be present" ) ;
127+
128+ let expiry_millis = config
129+ . expiry
130+ . map ( |expiry| chrono:: Utc :: now ( ) . timestamp_millis ( ) + ( expiry. as_secs ( ) as i64 * 1000 ) ) ;
131+ let token_file = AdminTokenFile {
132+ token : token. clone ( ) ,
133+ name : token_name. clone ( ) ,
134+ expiry_millis,
135+ } ;
136+
137+ let json = serde_json:: to_string_pretty ( & token_file) ?;
138+
139+ // Write token atomically with correct permissions
140+ write_file_atomically ( & output_file, & json) ?;
141+
142+ println ! ( "Token saved to: {}" , output_file. display( ) ) ;
143+
144+ // For offline mode, we return a mock success response
145+ let hash = compute_token_hash ( & token) ;
146+ let token_info = Arc :: new ( TokenInfo {
147+ id : 0 . into ( ) ,
148+ name : Arc :: from ( token_name. as_str ( ) ) ,
149+ hash,
150+ description : None ,
151+ created_by : None ,
152+ created_at : chrono:: Utc :: now ( ) . timestamp_millis ( ) ,
153+ updated_at : None ,
154+ updated_by : None ,
155+ expiry_millis : expiry_millis. unwrap_or ( i64:: MAX ) ,
156+ permissions : vec ! [ ] , // Admin tokens don't need explicit permissions
157+ } ) ;
158+
159+ CreateTokenWithPermissionsResponse :: from_token_info ( token_info, token)
160+ . ok_or_else ( || "Failed to create token response" . into ( ) )
161+ } else {
162+ let json_body = client
163+ . api_v3_configure_create_named_admin_token (
164+ config. name . expect ( "token name to be present" ) ,
165+ config. expiry . map ( |expiry| expiry. as_secs ( ) ) ,
166+ )
167+ . await ?
168+ . expect ( "token creation to return full token info" ) ;
169+ Ok ( json_body)
170+ }
68171}
69172
70173#[ derive( Debug , ValueEnum , Clone , Copy ) ]
@@ -131,6 +234,14 @@ pub struct CreateAdminTokenConfig {
131234 /// Output format for token, supports just json or text
132235 #[ clap( long) ]
133236 pub format : Option < TokenOutputFormat > ,
237+
238+ /// Generate token without connecting to server (enterprise feature)
239+ #[ clap( long, requires = "output_file" ) ]
240+ pub offline : bool ,
241+
242+ /// File path to save the token (required with --offline)
243+ #[ clap( long, value_name = "FILE" ) ]
244+ pub output_file : Option < PathBuf > ,
134245}
135246
136247impl CreateAdminTokenConfig {
@@ -245,3 +356,60 @@ impl CommandFactory for CreateTokenConfig {
245356 Self :: command ( )
246357 }
247358}
359+
360+ /// Write a file atomically by writing to a temporary file and moving it into place
361+ /// This ensures the file either exists with correct permissions or doesn't exist at all
362+ pub ( crate ) fn write_file_atomically ( path : & PathBuf , content : & str ) -> Result < ( ) , Box < dyn Error > > {
363+ use std:: io:: Write ;
364+
365+ // Ensure content ends with a newline
366+ let content_with_newline = if content. ends_with ( '\n' ) {
367+ content. to_string ( )
368+ } else {
369+ format ! ( "{content}\n " )
370+ } ;
371+
372+ // Create a temporary file in the same directory as the target
373+ let parent = path. parent ( ) . ok_or ( "Invalid file path" ) ?;
374+ let file_name = path. file_name ( ) . ok_or ( "Invalid file name" ) ?;
375+ let temp_path = parent. join ( format ! ( ".{}.tmp" , file_name. to_string_lossy( ) ) ) ;
376+
377+ // Write to temporary file with proper error handling
378+ if let Err ( e) = || -> Result < ( ) , Box < dyn Error > > {
379+ #[ cfg( unix) ]
380+ {
381+ use std:: os:: unix:: fs:: OpenOptionsExt ;
382+ let mut file = std:: fs:: OpenOptions :: new ( )
383+ . write ( true )
384+ . create ( true )
385+ . truncate ( true )
386+ . mode ( 0o600 ) // Set permissions atomically during creation
387+ . open ( & temp_path) ?;
388+
389+ file. write_all ( content_with_newline. as_bytes ( ) ) ?;
390+ file. sync_all ( ) ?; // Ensure data is written to disk
391+ }
392+
393+ #[ cfg( not( unix) ) ]
394+ {
395+ use std:: fs:: File ;
396+ let mut file = File :: create ( & temp_path) ?;
397+ file. write_all ( content_with_newline. as_bytes ( ) ) ?;
398+ file. sync_all ( ) ?;
399+ }
400+
401+ Ok ( ( ) )
402+ } ( ) {
403+ // Clean up temp file on error
404+ let _ = std:: fs:: remove_file ( & temp_path) ;
405+ return Err ( e) ;
406+ }
407+
408+ std:: fs:: rename ( & temp_path, path) . map_err ( |e| -> Box < dyn Error > {
409+ // Clean up temp file on rename error
410+ let _ = std:: fs:: remove_file ( & temp_path) ;
411+ format ! ( "Failed to atomically rename temporary file: {e}" ) . into ( )
412+ } ) ?;
413+
414+ Ok ( ( ) )
415+ }
0 commit comments