8d803563 Andy McCall 1 month ago ↶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use crate :: ini;
21
use crate :: StoreUrl;
22
use object_store:: aws:: { AmazonS3, AmazonS3Builder, S3ConditionalPut} ;
23
use oxy_core:: strata:: { Error, Result } ;
24
use std:: path:: PathBuf;
25
26
pub const HINT : & str = " set AWS_PROFILE (or add ?profile=NAME to the remote URL) to a profile in ~/.aws/credentials, run `aws sso login` or `aws login` for it, or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY" ;
27
28
struct Found {
29
key_id : String,
30
secret : String,
31
token : Option< String> ,
32
}
33
34
fn env ( name : & str ) -> Option< String> {
35
std:: env:: var( name) .ok ( ) .filter ( | v | ! v.is_empty ( ) )
36
}
37
38
fn home ( ) -> Option< PathBuf> {
39
env ( " HOME" )
40
.or_else ( | | env ( " USERPROFILE" ) )
41
.map ( PathBuf:: from)
42
}
43
44
fn read_ini ( env_override : & str , default_rel : & str ) -> ini:: Sections {
45
let path = env ( env_override)
46
.map ( PathBuf:: from)
47
.or_else ( | | home ( ) .map ( | h | h.join ( default_rel) ) ) ;
48
path.and_then ( | p | std:: fs:: read_to_string( p) .ok ( ) )
49
.map ( | t | ini:: parse( & t) )
50
.unwrap_or_default ( )
51
}
52
53
54
55
fn config_section < 'a > (
56
config : & 'a ini:: Sections,
57
profile : & str ,
58
) -> Option< & 'a std:: collections:: BTreeMap< String, String> > {
59
if profile = = " default" {
60
config.get ( " default" )
61
} else {
62
config
63
.get ( & format! ( " profile {profile} " ) )
64
.or_else ( | | config.get ( profile) )
65
}
66
}
67
68
69
70
fn from_cli ( profile : & str ) -> Option< Found> {
71
let out = std:: process:: Command:: new( " aws" )
72
.args ( [
73
" configure" ,
74
" export-credentials" ,
75
" --profile" ,
76
profile,
77
" --format" ,
78
" process" ,
79
] )
80
.stdin ( std:: process:: Stdio:: null( ) )
81
.stderr ( std:: process:: Stdio:: null( ) )
82
.output ( )
83
.ok ( ) ? ;
84
if ! out.status.success ( ) {
85
return None ;
86
}
87
let v: serde_json:: Value = serde_json:: from_slice( & out.stdout) .ok ( ) ? ;
88
Some ( Found {
89
key_id: v.get ( " AccessKeyId" ) ? .as_str ( ) ? .to_string ( ) ,
90
secret: v.get ( " SecretAccessKey" ) ? .as_str ( ) ? .to_string ( ) ,
91
token: v
92
.get ( " SessionToken" )
93
.and_then ( | t | t.as_str ( ) )
94
.map ( str :: to_string) ,
95
} )
96
}
97
7026bd7d Andy McCall 1 month ago ↶
98
99
100
pub fn open ( url : & StoreUrl, keys : Option< ( & str , & str , Option< & str > ) > ) -> Result< AmazonS3> {
101
if let Some ( ( id, secret, token) ) = keys {
102
let mut b = AmazonS3Builder:: new( )
103
.with_bucket_name ( & url.host)
104
.with_http_connector ( crate :: http:: UreqConnector)
105
.with_conditional_put ( S3ConditionalPut:: ETagMatch)
106
.with_access_key_id ( id)
107
.with_secret_access_key ( secret) ;
108
if let Some ( t) = token {
109
b = b.with_token ( t) ;
110
}
111
let endpoint = url.param ( " endpoint" ) .map ( str :: to_string) ;
112
return finish ( b, url, endpoint, url.param ( " region" ) .map ( str :: to_string) ) ;
113
}
8d803563 Andy McCall 1 month ago ↶
114
let bucket = url.host.clone ( ) ;
115
let explicit_profile = url.param ( " profile" ) .map ( str :: to_string) ;
116
let profile = explicit_profile
117
.clone ( )
118
.or_else ( | | env ( " AWS_PROFILE" ) )
119
.unwrap_or_else ( | | " default" .into ( ) ) ;
120
let config = read_ini ( " AWS_CONFIG_FILE" , " .aws/config" ) ;
121
let creds = read_ini ( " AWS_SHARED_CREDENTIALS_FILE" , " .aws/credentials" ) ;
122
let section = config_section ( & config, & profile) ;
123
let known_profile = section.is_some ( ) | | creds.contains_key ( & profile) ;
124
125
126
127
let mut b = AmazonS3Builder:: from_env( )
128
.with_bucket_name ( & bucket)
2cd40ea1 Andy McCall 1 month ago ↶
129
.with_http_connector ( crate :: http:: UreqConnector)
8d803563 Andy McCall 1 month ago ↶
130
.with_conditional_put ( S3ConditionalPut:: ETagMatch) ;
131
132
let env_keys = match ( env ( " AWS_ACCESS_KEY_ID" ) , env ( " AWS_SECRET_ACCESS_KEY" ) ) {
133
( Some ( k) , Some ( s) ) if explicit_profile.is_none ( ) => Some ( Found {
134
key_id: k,
135
secret: s,
136
token: env ( " AWS_SESSION_TOKEN" ) ,
137
} ) ,
138
_ => None ,
139
} ;
140
let found = env_keys
141
.or_else ( | | {
142
let c = creds.get ( & profile) ? ;
143
Some ( Found {
144
key_id: c.get ( " aws_access_key_id" ) ? .clone ( ) ,
145
secret: c.get ( " aws_secret_access_key" ) ? .clone ( ) ,
146
token: c.get ( " aws_session_token" ) .cloned ( ) ,
147
} )
148
} )
149
.or_else ( | | {
150
if known_profile {
151
from_cli ( & profile)
152
} else {
153
None
154
}
155
} ) ;
156
match found {
157
Some ( f) => {
158
b = b
159
.with_access_key_id ( f.key_id)
160
.with_secret_access_key ( f.secret) ;
161
if let Some ( t) = f.token {
162
b = b.with_token ( t) ;
163
}
164
}
165
None if explicit_profile.is_some ( ) & & ! known_profile => {
166
return Err ( Error:: Backend( format! (
167
" {} : profile {profile:?} is not in ~/.aws/config or ~/.aws/credentials: {HINT} " ,
168
url.plain ( )
169
) ) ) ;
170
}
171
None => { }
172
}
173
174
let endpoint = url
175
.param ( " endpoint" )
176
.map ( str :: to_string)
177
.or_else ( | | env ( " AWS_ENDPOINT_URL_S3" ) )
178
.or_else ( | | env ( " AWS_ENDPOINT_URL" ) )
179
.or_else ( | | env ( " AWS_ENDPOINT" ) )
180
.or_else ( | | section.and_then ( | s | s.get ( " endpoint_url" ) .cloned ( ) ) ) ;
7026bd7d Andy McCall 1 month ago ↶
181
let region = url
182
.param ( " region" )
183
.map ( str :: to_string)
184
.or_else ( | | env ( " AWS_REGION" ) )
185
.or_else ( | | env ( " AWS_DEFAULT_REGION" ) )
186
.or_else ( | | section.and_then ( | s | s.get ( " region" ) .cloned ( ) ) ) ;
187
finish ( b, url, endpoint, region)
188
}
189
190
191
fn finish (
192
mut b : AmazonS3Builder,
193
url : & StoreUrl,
194
endpoint : Option< String> ,
195
region : Option< String> ,
196
) -> Result< AmazonS3> {
197
let bucket = url.host.clone ( ) ;
8d803563 Andy McCall 1 month ago ↶
198
if let Some ( e) = & endpoint {
199
b = b
200
.with_endpoint ( e.clone ( ) )
201
.with_allow_http ( e.starts_with ( " http://" ) ) ;
202
203
b = b.with_virtual_hosted_style_request ( false ) ;
204
}
205
206
let region = match region {
207
Some ( r) => r,
208
None if endpoint.is_some ( ) => " us-east-1" .into ( ) ,
2cd40ea1 Andy McCall 1 month ago ↶
209
None => crate :: http:: bucket_region( & bucket) .ok_or_else ( | | {
210
Error:: Backend( format! (
211
" {} : no region was given and S3 did not say which region holds the bucket: add ?region=NAME to the remote URL, set AWS_REGION, or put region in the profile" ,
212
url.plain ( )
213
) )
214
} ) ? ,
8d803563 Andy McCall 1 month ago ↶
215
} ;
216
b = b.with_region ( region) ;
217
218
b.build ( )
219
.map_err ( | e | Error:: Backend( format! ( " {} : {e} : {HINT} " , url.plain ( ) ) ) )
220
}
221
222
# [ cfg ( test ) ]
223
mod tests {
224
use super :: * ;
225
226
# [ test ]
227
fn profile_sections_are_found_under_either_heading ( ) {
228
let config =
229
ini:: parse( " [default]\n region = a\n [profile work]\n region = b\n [plain]\n region = c\n " ) ;
230
assert_eq! ( config_section ( & config, " default" ) .unwrap ( ) [ " region" ] , " a" ) ;
231
assert_eq! ( config_section ( & config, " work" ) .unwrap ( ) [ " region" ] , " b" ) ;
232
assert_eq! ( config_section ( & config, " plain" ) .unwrap ( ) [ " region" ] , " c" ) ;
233
assert! ( config_section ( & config, " none" ) .is_none ( ) ) ;
234
}
235
}