-
Notifications
You must be signed in to change notification settings - Fork 353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for signed upload urls. #1661
base: main
Are you sure you want to change the base?
Conversation
All committers have signed the CLA. |
3d7696f
to
6bf10f5
Compare
export interface UploadUrlOptions { | ||
/** The expiration time of the url, in seconds from signing. The maximum | ||
* value is seven days */ | ||
ttl: number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Options should typically be optional:
ttl: number; | |
ttl?: number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
runtimes/js/src/objects.rs
Outdated
#[napi] | ||
pub async fn get_upload_url( | ||
&self, | ||
options: Option<UploadUrlOptions>, // TODO: can/should this be made non-optional, since ttl is required? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ttl shouldn't be required, right? The user-facing docs state the ttl defaults to 7 days if not provided
runtimes/js/src/objects.rs
Outdated
impl From<UploadUrlOptions> for core::UploadUrlOptions { | ||
fn from(value: UploadUrlOptions) -> Self { | ||
Self { | ||
ttl: value.ttl as u64, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Convert this to the default of 7 days here
runtimes/js/src/objects.rs
Outdated
#[napi(object)] | ||
#[derive(Debug, Default)] | ||
pub struct UploadUrlOptions { | ||
pub ttl: i64, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub ttl: i64, | |
pub ttl: Option<i64>, |
|
||
// WithTtl is used for signed URLs, to specify the lifetime of the generated | ||
// URL, in seconds. The max value is seven days. | ||
func WithTtl(ttl uint64) withTtlOption { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Go convention is TTL
not Ttl
. Also make it take a time.Duration
instead of an uint64
func WithTtl(ttl uint64) withTtlOption { | |
func WithTTL(ttl time.Duration) withTTLOption { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
* Anyone with possession of the URL can write to the given object name | ||
* without any additional auth. | ||
*/ | ||
async getUploadUrl(name: string, options?: UploadUrlOptions): Promise<string> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make this signedUploadUrl
, and have it return a SignedURL
object with just a single field {url: string}
async getUploadUrl(name: string, options?: UploadUrlOptions): Promise<string> { | |
async signedUploadUrl(name: string, options?: SignedUploadUrlOptions): Promise<SignedURL> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Although used SignedUploadUrl
instead of SignedUrl
. I think we ended up agreeing on making upload URLs more flexible, but not so far as folding in signed GET and PUT URLs in one type. But let me know if I got that wrong.
@@ -120,6 +120,7 @@ pub fn resolve_bucket_usage(data: &ResolveUsageData, bucket: Lrc<Bucket>) -> Opt | |||
"list" => Operation::ListObjects, | |||
"exists" | "attrs" => Operation::GetObjectMetadata, | |||
"upload" => Operation::WriteObject, | |||
"getUploadUrl" => Operation::WriteObject, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to signedUploadUrl
as per above. Also, let's introduce a new SignedUploadUrl
operation (needs to be done in legacymeta.rs as well)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
client: client, | ||
cfg: runtimeCfg, | ||
client: client, | ||
presign_client: s3.NewPresignClient(client), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instantiate in clientForProvider
instead so we can cache it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thanks
client *s3.Client | ||
cfg *config.Bucket | ||
client *s3.Client | ||
presign_client *s3.PresignClient |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
presign_client *s3.PresignClient | |
presignClient *s3.PresignClient |
Bucket: &b.cfg.CloudName, | ||
Key: &object, | ||
} | ||
sign_opts := func(opts *s3.PresignOptions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sign_opts := func(opts *s3.PresignOptions) { | |
signOpts := func(opts *s3.PresignOptions) { |
sign_opts := func(opts *s3.PresignOptions) { | ||
opts.Expires = time.Duration(data.Ttl) * time.Second | ||
} | ||
req, err := b.presign_client.PresignPutObject(data.Ctx, ¶ms, sign_opts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do the error check here instead
req, err := b.presign_client.PresignPutObject(data.Ctx, ¶ms, sign_opts) | |
req, err := b.presign_client.PresignPutObject(data.Ctx, ¶ms, sign_opts) | |
if err != nil { | |
return "", mapErr(err) | |
} | |
return req.URL, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
3b6d7b6
to
f165bfd
Compare
cli/daemon/run/runtime_config2.go
Outdated
@@ -833,3 +838,32 @@ func gzipBytes(data []byte) []byte { | |||
_ = w.Close() | |||
return buf.Bytes() | |||
} | |||
|
|||
const dummyPrivateKey = `-----BEGIN PRIVATE KEY----- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should base64-encode this and decode it at runtime; there are a bunch of tools that automatically flag private keys in source code and they get upset otherwise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, better to avoid those. I reversed instead of base64 just because I think it's easier to inspect. Let me know if you can think of a problem with that.
proto/encore/runtime/v1/infra.proto
Outdated
message LocalSignOptions { | ||
// Base prefix to use for presigned URLs. | ||
string base_url = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indentation could use some work :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agh, vscode tabs/spaces, fixed.
private_key: String, | ||
} | ||
|
||
fn local_sign_config_from_client(client: Arc<LazyGCSClient>) -> Option<LocalSignOptions> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can take &LazyGCSClient
instead to avoid the clone below (this code path isn't performance sensitive but more idiomatic)
Self { | ||
client, | ||
client: client.clone(), | ||
encore_name: cfg.encore_name.clone().into(), | ||
cloud_name: cfg.cloud_name.clone().into(), | ||
public_base_url: cfg.public_base_url.clone(), | ||
key_prefix: cfg.key_prefix.clone(), | ||
local_sign: local_sign_config_from_client(client.clone()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can instead do:
let local_sign = local_sign_config_from_client(&client);
Self {
client,
encore_name: cfg.encore_name.clone().into(),
cloud_name: cfg.cloud_name.clone().into(),
public_base_url: cfg.public_base_url.clone(),
key_prefix: cfg.key_prefix.clone(),
local_sign,
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
fn replace_url_prefix(orig_url: String, base: String) -> String { | ||
match Url::parse(&orig_url) { | ||
Ok(url) => { | ||
let mut out = format!( | ||
"{}/{}", | ||
base.trim_end_matches('/'), | ||
url.path().trim_start_matches("/") | ||
); | ||
if let Some(query) = url.query() { | ||
out.push('?'); | ||
out.push_str(query); | ||
} | ||
out | ||
} | ||
Err(_) => { | ||
// If the input URL fails parsing, just don't do the replace | ||
orig_url | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid unnecessary cloning of the input arguments we can do:
fn replace_url_prefix(orig_url: String, base: String) -> String { | |
match Url::parse(&orig_url) { | |
Ok(url) => { | |
let mut out = format!( | |
"{}/{}", | |
base.trim_end_matches('/'), | |
url.path().trim_start_matches("/") | |
); | |
if let Some(query) = url.query() { | |
out.push('?'); | |
out.push_str(query); | |
} | |
out | |
} | |
Err(_) => { | |
// If the input URL fails parsing, just don't do the replace | |
orig_url | |
} | |
} | |
} | |
fn replace_url_prefix<'a>(orig_url: &'a str, base: &str) -> Cow<'a, str> { | |
match url::Url::parse(orig_url) { | |
Ok(url) => { | |
let mut out = format!( | |
"{}/{}", | |
base.trim_end_matches('/'), | |
url.path().trim_start_matches("/") | |
); | |
if let Some(query) = url.query() { | |
out.push('?'); | |
out.push_str(query); | |
} | |
Cow::Owned(out) | |
} | |
Err(_) => { | |
// If the input URL fails parsing, just don't do the replace | |
Cow::Borrowed(orig_url) | |
} | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, done
opts := &storage.SignedURLOptions{ | ||
Scheme: storage.SigningSchemeV4, | ||
Method: "PUT", | ||
Expires: time.Now().Add(time.Duration(data.Ttl)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
data.Ttl
is already a time.Duration
right? Can skip the extra conversion then
Expires: time.Now().Add(time.Duration(data.Ttl)), | |
Expires: time.Now().Add(data.Ttl), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
return url, nil | ||
} | ||
|
||
func replaceURLPrefix(orig_url string, base string) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func replaceURLPrefix(orig_url string, base string) string { | |
func replaceURLPrefix(origURL string, base string) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
if err != nil { | ||
return orig_url // If the input URL is not valid, just return it as-is | ||
} | ||
out := strings.TrimRight(base, "/") + "/" + strings.TrimLeft(u.Path, "/") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds an extra trailing slash when u.Path
is empty, right?
out := strings.TrimRight(base, "/") + "/" + strings.TrimLeft(u.Path, "/") | |
out := strings.TrimRight(base, "/") | |
if u.Path != "" { | |
out += "/" + strings.TrimLeft(u.Path, "/") | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, but IMO we don't need to handle that case. 1) we currently use path style URLs which means the bucket name is in the path. And 2) we only support object scoped signed URLs atm, which means we have a non-empty object key for all valid requests. Both would need to change and if we change (1) we would anyway need to make some other changes to make local URLs work. But let me know if I'm missing something or if you prefer not to have this potential bug lurking :).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather fix it now, since it's a trivial fix and the code is more general that way
clients := clientSet{ | ||
client: client, | ||
presignClient: s3.NewPresignClient(client), | ||
} | ||
|
||
mgr.clients[prov] = &clients | ||
return &clients |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clients := clientSet{ | |
client: client, | |
presignClient: s3.NewPresignClient(client), | |
} | |
mgr.clients[prov] = &clients | |
return &clients | |
clients := &clientSet{ | |
client: client, | |
presignClient: s3.NewPresignClient(client), | |
} | |
mgr.clients[prov] = clients | |
return clients |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Ctx context.Context | ||
Object CloudObject | ||
|
||
Ttl time.Duration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ttl time.Duration | |
TTL time.Duration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
7e02e6c
to
82bc1a0
Compare
// protoc-gen-go v1.35.2 | ||
// protoc v5.29.1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would rather keep the protobuf versions the same so we don't end up with lots of spurious regenerations between different developers. Can you get those existing versions installed instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, done
proto/encore/runtime/v1/infra.pb.go
Outdated
@@ -1,7 +1,7 @@ | |||
// Code generated by protoc-gen-go. DO NOT EDIT. | |||
// versions: | |||
// protoc-gen-go v1.31.0 | |||
// protoc v4.23.4 | |||
// protoc-gen-go v1.35.2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
82bc1a0
to
cabdb99
Compare
cabdb99
to
b3c7614
Compare
No description provided.