From ceee83f03acaaa9f0d71f3492ea6753eaec706ec Mon Sep 17 00:00:00 2001 From: Cesar N <11819101+cesnietor@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:16:17 -0700 Subject: [PATCH] Use Console as proxy for share object logic (#3284) --- api/configure_console.go | 2 + api/embedded_spec.go | 70 +++++++++ api/operations/console_api.go | 13 ++ .../public/download_shared_object.go | 73 ++++++++++ .../download_shared_object_parameters.go | 88 ++++++++++++ .../download_shared_object_responses.go | 134 ++++++++++++++++++ .../download_shared_object_urlbuilder.go | 116 +++++++++++++++ api/public_objects.go | 125 ++++++++++++++++ api/public_objects_test.go | 103 ++++++++++++++ api/user_objects.go | 22 ++- api/user_objects_test.go | 42 +++++- swagger.yml | 61 +++++--- web-app/src/api/consoleApi.ts | 16 +++ 13 files changed, 839 insertions(+), 26 deletions(-) create mode 100644 api/operations/public/download_shared_object.go create mode 100644 api/operations/public/download_shared_object_parameters.go create mode 100644 api/operations/public/download_shared_object_responses.go create mode 100644 api/operations/public/download_shared_object_urlbuilder.go create mode 100644 api/public_objects.go create mode 100644 api/public_objects_test.go diff --git a/api/configure_console.go b/api/configure_console.go index f44dfaa01e..852685faaf 100644 --- a/api/configure_console.go +++ b/api/configure_console.go @@ -170,6 +170,8 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler { registerReleasesHandlers(api) + registerPublicObjectsHandlers(api) + api.PreServerShutdown = func() {} api.ServerShutdown = func() {} diff --git a/api/embedded_spec.go b/api/embedded_spec.go index bb43237f50..a10c80d419 100644 --- a/api/embedded_spec.go +++ b/api/embedded_spec.go @@ -2743,6 +2743,41 @@ func init() { } } }, + "/download-shared-object/{url}": { + "get": { + "security": [], + "produces": [ + "application/octet-stream" + ], + "tags": [ + "Public" + ], + "summary": "Downloads an object from a presigned url", + "operationId": "DownloadSharedObject", + "parameters": [ + { + "type": "string", + "name": "url", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "file" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, "/group/{name}": { "get": { "tags": [ @@ -11928,6 +11963,41 @@ func init() { } } }, + "/download-shared-object/{url}": { + "get": { + "security": [], + "produces": [ + "application/octet-stream" + ], + "tags": [ + "Public" + ], + "summary": "Downloads an object from a presigned url", + "operationId": "DownloadSharedObject", + "parameters": [ + { + "type": "string", + "name": "url", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "file" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, "/group/{name}": { "get": { "tags": [ diff --git a/api/operations/console_api.go b/api/operations/console_api.go index 7d01d4cd73..62ec5eba1d 100644 --- a/api/operations/console_api.go +++ b/api/operations/console_api.go @@ -49,6 +49,7 @@ import ( "github.com/minio/console/api/operations/object" "github.com/minio/console/api/operations/policy" "github.com/minio/console/api/operations/profile" + "github.com/minio/console/api/operations/public" "github.com/minio/console/api/operations/release" "github.com/minio/console/api/operations/service" "github.com/minio/console/api/operations/service_account" @@ -211,6 +212,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI { ObjectDownloadMultipleObjectsHandler: object.DownloadMultipleObjectsHandlerFunc(func(params object.DownloadMultipleObjectsParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation object.DownloadMultipleObjects has not yet been implemented") }), + PublicDownloadSharedObjectHandler: public.DownloadSharedObjectHandlerFunc(func(params public.DownloadSharedObjectParams) middleware.Responder { + return middleware.NotImplemented("operation public.DownloadSharedObject has not yet been implemented") + }), TieringEditTierCredentialsHandler: tiering.EditTierCredentialsHandlerFunc(func(params tiering.EditTierCredentialsParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation tiering.EditTierCredentials has not yet been implemented") }), @@ -704,6 +708,8 @@ type ConsoleAPI struct { ObjectDownloadObjectHandler object.DownloadObjectHandler // ObjectDownloadMultipleObjectsHandler sets the operation handler for the download multiple objects operation ObjectDownloadMultipleObjectsHandler object.DownloadMultipleObjectsHandler + // PublicDownloadSharedObjectHandler sets the operation handler for the download shared object operation + PublicDownloadSharedObjectHandler public.DownloadSharedObjectHandler // TieringEditTierCredentialsHandler sets the operation handler for the edit tier credentials operation TieringEditTierCredentialsHandler tiering.EditTierCredentialsHandler // BucketEnableBucketEncryptionHandler sets the operation handler for the enable bucket encryption operation @@ -1150,6 +1156,9 @@ func (o *ConsoleAPI) Validate() error { if o.ObjectDownloadMultipleObjectsHandler == nil { unregistered = append(unregistered, "object.DownloadMultipleObjectsHandler") } + if o.PublicDownloadSharedObjectHandler == nil { + unregistered = append(unregistered, "public.DownloadSharedObjectHandler") + } if o.TieringEditTierCredentialsHandler == nil { unregistered = append(unregistered, "tiering.EditTierCredentialsHandler") } @@ -1769,6 +1778,10 @@ func (o *ConsoleAPI) initHandlerCache() { o.handlers["POST"] = make(map[string]http.Handler) } o.handlers["POST"]["/buckets/{bucket_name}/objects/download-multiple"] = object.NewDownloadMultipleObjects(o.context, o.ObjectDownloadMultipleObjectsHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } + o.handlers["GET"]["/download-shared-object/{url}"] = public.NewDownloadSharedObject(o.context, o.PublicDownloadSharedObjectHandler) if o.handlers["PUT"] == nil { o.handlers["PUT"] = make(map[string]http.Handler) } diff --git a/api/operations/public/download_shared_object.go b/api/operations/public/download_shared_object.go new file mode 100644 index 0000000000..3d67abba5b --- /dev/null +++ b/api/operations/public/download_shared_object.go @@ -0,0 +1,73 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package public + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// DownloadSharedObjectHandlerFunc turns a function with the right signature into a download shared object handler +type DownloadSharedObjectHandlerFunc func(DownloadSharedObjectParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn DownloadSharedObjectHandlerFunc) Handle(params DownloadSharedObjectParams) middleware.Responder { + return fn(params) +} + +// DownloadSharedObjectHandler interface for that can handle valid download shared object params +type DownloadSharedObjectHandler interface { + Handle(DownloadSharedObjectParams) middleware.Responder +} + +// NewDownloadSharedObject creates a new http.Handler for the download shared object operation +func NewDownloadSharedObject(ctx *middleware.Context, handler DownloadSharedObjectHandler) *DownloadSharedObject { + return &DownloadSharedObject{Context: ctx, Handler: handler} +} + +/* + DownloadSharedObject swagger:route GET /download-shared-object/{url} Public downloadSharedObject + +Downloads an object from a presigned url +*/ +type DownloadSharedObject struct { + Context *middleware.Context + Handler DownloadSharedObjectHandler +} + +func (o *DownloadSharedObject) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewDownloadSharedObjectParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/api/operations/public/download_shared_object_parameters.go b/api/operations/public/download_shared_object_parameters.go new file mode 100644 index 0000000000..9284605201 --- /dev/null +++ b/api/operations/public/download_shared_object_parameters.go @@ -0,0 +1,88 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package public + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" +) + +// NewDownloadSharedObjectParams creates a new DownloadSharedObjectParams object +// +// There are no default values defined in the spec. +func NewDownloadSharedObjectParams() DownloadSharedObjectParams { + + return DownloadSharedObjectParams{} +} + +// DownloadSharedObjectParams contains all the bound params for the download shared object operation +// typically these are obtained from a http.Request +// +// swagger:parameters DownloadSharedObject +type DownloadSharedObjectParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: path + */ + URL string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewDownloadSharedObjectParams() beforehand. +func (o *DownloadSharedObjectParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + rURL, rhkURL, _ := route.Params.GetOK("url") + if err := o.bindURL(rURL, rhkURL, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindURL binds and validates parameter URL from path. +func (o *DownloadSharedObjectParams) bindURL(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.URL = raw + + return nil +} diff --git a/api/operations/public/download_shared_object_responses.go b/api/operations/public/download_shared_object_responses.go new file mode 100644 index 0000000000..75c072ff7d --- /dev/null +++ b/api/operations/public/download_shared_object_responses.go @@ -0,0 +1,134 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package public + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "io" + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/minio/console/models" +) + +// DownloadSharedObjectOKCode is the HTTP code returned for type DownloadSharedObjectOK +const DownloadSharedObjectOKCode int = 200 + +/* +DownloadSharedObjectOK A successful response. + +swagger:response downloadSharedObjectOK +*/ +type DownloadSharedObjectOK struct { + + /* + In: Body + */ + Payload io.ReadCloser `json:"body,omitempty"` +} + +// NewDownloadSharedObjectOK creates DownloadSharedObjectOK with default headers values +func NewDownloadSharedObjectOK() *DownloadSharedObjectOK { + + return &DownloadSharedObjectOK{} +} + +// WithPayload adds the payload to the download shared object o k response +func (o *DownloadSharedObjectOK) WithPayload(payload io.ReadCloser) *DownloadSharedObjectOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the download shared object o k response +func (o *DownloadSharedObjectOK) SetPayload(payload io.ReadCloser) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DownloadSharedObjectOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +/* +DownloadSharedObjectDefault Generic error response. + +swagger:response downloadSharedObjectDefault +*/ +type DownloadSharedObjectDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.APIError `json:"body,omitempty"` +} + +// NewDownloadSharedObjectDefault creates DownloadSharedObjectDefault with default headers values +func NewDownloadSharedObjectDefault(code int) *DownloadSharedObjectDefault { + if code <= 0 { + code = 500 + } + + return &DownloadSharedObjectDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the download shared object default response +func (o *DownloadSharedObjectDefault) WithStatusCode(code int) *DownloadSharedObjectDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the download shared object default response +func (o *DownloadSharedObjectDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the download shared object default response +func (o *DownloadSharedObjectDefault) WithPayload(payload *models.APIError) *DownloadSharedObjectDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the download shared object default response +func (o *DownloadSharedObjectDefault) SetPayload(payload *models.APIError) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DownloadSharedObjectDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(o._statusCode) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/api/operations/public/download_shared_object_urlbuilder.go b/api/operations/public/download_shared_object_urlbuilder.go new file mode 100644 index 0000000000..138ad4038d --- /dev/null +++ b/api/operations/public/download_shared_object_urlbuilder.go @@ -0,0 +1,116 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package public + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// DownloadSharedObjectURL generates an URL for the download shared object operation +type DownloadSharedObjectURL struct { + URL string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DownloadSharedObjectURL) WithBasePath(bp string) *DownloadSharedObjectURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DownloadSharedObjectURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *DownloadSharedObjectURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/download-shared-object/{url}" + + url := o.URL + if url != "" { + _path = strings.Replace(_path, "{url}", url, -1) + } else { + return nil, errors.New("url is required on DownloadSharedObjectURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *DownloadSharedObjectURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *DownloadSharedObjectURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *DownloadSharedObjectURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on DownloadSharedObjectURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on DownloadSharedObjectURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *DownloadSharedObjectURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/api/public_objects.go b/api/public_objects.go new file mode 100644 index 0000000000..d718584658 --- /dev/null +++ b/api/public_objects.go @@ -0,0 +1,125 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package api + +import ( + b64 "encoding/base64" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/swag" + "github.com/minio/console/api/operations" + "github.com/minio/console/api/operations/public" + xnet "github.com/minio/pkg/v2/net" +) + +func registerPublicObjectsHandlers(api *operations.ConsoleAPI) { + api.PublicDownloadSharedObjectHandler = public.DownloadSharedObjectHandlerFunc(func(params public.DownloadSharedObjectParams) middleware.Responder { + resp, err := getDownloadPublicObjectResponse(params) + if err != nil { + return public.NewDownloadSharedObjectDefault(err.Code).WithPayload(err.APIError) + } + return resp + }) +} + +func getDownloadPublicObjectResponse(params public.DownloadSharedObjectParams) (middleware.Responder, *CodedAPIError) { + ctx := params.HTTPRequest.Context() + + inputURLDecoded, err := b64toMinIOStringURL(params.URL) + if err != nil { + return nil, ErrorWithContext(ctx, err) + } + if inputURLDecoded == nil { + return nil, ErrorWithContext(ctx, ErrDefault, fmt.Errorf("decoded url is null")) + } + + req, err := http.NewRequest(http.MethodGet, *inputURLDecoded, nil) + if err != nil { + return nil, ErrorWithContext(ctx, err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, ErrorWithContext(ctx, err) + } + + return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) { + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + http.Error(rw, resp.Status, resp.StatusCode) + return + } + + urlObj, err := url.Parse(*inputURLDecoded) + if err != nil { + http.Error(rw, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Add the filename + _, objectName := url2BucketAndObject(urlObj) + escapedName := url.PathEscape(objectName) + rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", escapedName)) + + _, err = io.Copy(rw, resp.Body) + if err != nil { + http.Error(rw, "Internal Server Error", http.StatusInternalServerError) + return + } + }), nil +} + +// b64toMinIOStringURL decodes url and validates is a MinIO url endpoint +func b64toMinIOStringURL(inputEncodedURL string) (*string, error) { + inputURLDecoded, err := b64.StdEncoding.DecodeString(inputEncodedURL) + if err != nil { + return nil, err + } + // Validate input URL + inputURL, err := xnet.ParseHTTPURL(string(inputURLDecoded)) + if err != nil { + return nil, err + } + // Ensure incoming url points to MinIO Server + minIOHost := getMinIOEndpoint() + if inputURL.Host != minIOHost { + return nil, ErrForbidden + } + return swag.String(string(inputURLDecoded)), nil +} + +func url2BucketAndObject(u *url.URL) (bucketName, objectName string) { + tokens := splitStr(u.Path, "/", 3) + return tokens[1], tokens[2] +} + +// splitStr splits a string into n parts, empty strings are added +// if we are not able to reach n elements +func splitStr(path, sep string, n int) []string { + splits := strings.SplitN(path, sep, n) + // Add empty strings if we found elements less than nr + for i := n - len(splits); i > 0; i-- { + splits = append(splits, "") + } + return splits +} diff --git a/api/public_objects_test.go b/api/public_objects_test.go new file mode 100644 index 0000000000..8ec1a23572 --- /dev/null +++ b/api/public_objects_test.go @@ -0,0 +1,103 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2024 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package api + +import ( + "testing" + + "github.com/go-openapi/swag" + "github.com/stretchr/testify/assert" +) + +func Test_b64toMinIOStringURL(t *testing.T) { + tAssert := assert.New(t) + type args struct { + encodedURL string + } + tests := []struct { + test string + args args + wantError *string + expected *string + }{ + { + test: "valid encoded minIO URL returns decoded URL string", // http://localhost:9000/... + args: args{ + encodedURL: "aHR0cDovL2xvY2FsaG9zdDo5MDAwL2J1Y2tldDEyMy9BdWRpbyUyMGljb24lMjgxJTI5LnN2Zz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPVVCTzFMMUM3VTg3UDFCUDI1MVRTJTJGMjAyNDA0MDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNDA1VDIxMDEzM1omWC1BbXotRXhwaXJlcz00MzIwMCZYLUFtei1TZWN1cml0eS1Ub2tlbj1leUpoYkdjaU9pSklVelV4TWlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKaFkyTmxjM05MWlhraU9pSlZRazh4VERGRE4xVTROMUF4UWxBeU5URlVVeUlzSW1WNGNDSTZNVGN4TWpNNU5EQTRPU3dpY0dGeVpXNTBJam9pYldsdWFXOWhaRzFwYmlKOS5WLUtEZ3JMTVVCbG5KSEtYNlZ4SGw5LUFfLVBGRVdvazJkcFRxLTQ2YmxMbUxzdWVUeHNoVmFZNERad0dmb200VFQ1azhwaFVmZ2pjUWFuc25icmtlQSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmdmVyc2lvbklkPW51bGwmWC1BbXotU2lnbmF0dXJlPTA3Y2FkM2ViMmE2NzIyYjViYWVkMDljNmYxZmU0YTcwMWJmMTJmNDhlMTYyOGI5ZDQ1YzAxMWQ1OTU1Njc4NDU=", + }, + wantError: nil, + expected: swag.String("http://localhost:9000/bucket123/Audio%20icon%281%29.svg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=UBO1L1C7U87P1BP251TS%2F20240405%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240405T210133Z&X-Amz-Expires=43200&X-Amz-Security-Token=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJVQk8xTDFDN1U4N1AxQlAyNTFUUyIsImV4cCI6MTcxMjM5NDA4OSwicGFyZW50IjoibWluaW9hZG1pbiJ9.V-KDgrLMUBlnJHKX6VxHl9-A_-PFEWok2dpTq-46blLmLsueTxshVaY4DZwGfom4TT5k8phUfgjcQansnbrkeA&X-Amz-SignedHeaders=host&versionId=null&X-Amz-Signature=07cad3eb2a6722b5baed09c6f1fe4a701bf12f48e1628b9d45c011d595567845"), + }, + { + test: "valid encoded url but not coming from MinIO server returns forbidden error", // http://non-minio-host:9000/... + args: args{ + encodedURL: "aHR0cDovL25vbi1taW5pby1ob3N0OjkwMDAvYnVja2V0MTIzL0F1ZGlvJTIwaWNvbiUyODElMjkuc3ZnP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9VUJPMUwxQzdVODdQMUJQMjUxVFMlMkYyMDI0MDQwNSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA0MDVUMjEwMTMzWiZYLUFtei1FeHBpcmVzPTQzMjAwJlgtQW16LVNlY3VyaXR5LVRva2VuPWV5SmhiR2NpT2lKSVV6VXhNaUlzSW5SNWNDSTZJa3BYVkNKOS5leUpoWTJObGMzTkxaWGtpT2lKVlFrOHhUREZETjFVNE4xQXhRbEF5TlRGVVV5SXNJbVY0Y0NJNk1UY3hNak01TkRBNE9Td2ljR0Z5Wlc1MElqb2liV2x1YVc5aFpHMXBiaUo5LlYtS0RnckxNVUJsbkpIS1g2VnhIbDktQV8tUEZFV29rMmRwVHEtNDZibExtTHN1ZVR4c2hWYVk0RFp3R2ZvbTRUVDVrOHBoVWZnamNRYW5zbmJya2VBJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZ2ZXJzaW9uSWQ9bnVsbCZYLUFtei1TaWduYXR1cmU9MDdjYWQzZWIyYTY3MjJiNWJhZWQwOWM2ZjFmZTRhNzAxYmYxMmY0OGUxNjI4YjlkNDVjMDExZDU5NTU2Nzg0NQ==", + }, + wantError: swag.String("403 Forbidden"), + expected: nil, + }, + { + test: "valid encoded url but not coming from MinIO server port returns forbidden error", // other port http://localhost:8902/... + args: args{ + encodedURL: "aHR0cDovL2xvY2FsaG9zdDo4OTAyL2J1Y2tldDEyMy9BdWRpbyUyMGljb24lMjgxJTI5LnN2Zz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPVVCTzFMMUM3VTg3UDFCUDI1MVRTJTJGMjAyNDA0MDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNDA1VDIxMDEzM1omWC1BbXotRXhwaXJlcz00MzIwMCZYLUFtei1TZWN1cml0eS1Ub2tlbj1leUpoYkdjaU9pSklVelV4TWlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKaFkyTmxjM05MWlhraU9pSlZRazh4VERGRE4xVTROMUF4UWxBeU5URlVVeUlzSW1WNGNDSTZNVGN4TWpNNU5EQTRPU3dpY0dGeVpXNTBJam9pYldsdWFXOWhaRzFwYmlKOS5WLUtEZ3JMTVVCbG5KSEtYNlZ4SGw5LUFfLVBGRVdvazJkcFRxLTQ2YmxMbUxzdWVUeHNoVmFZNERad0dmb200VFQ1azhwaFVmZ2pjUWFuc25icmtlQSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmdmVyc2lvbklkPW51bGwmWC1BbXotU2lnbmF0dXJlPTA3Y2FkM2ViMmE2NzIyYjViYWVkMDljNmYxZmU0YTcwMWJmMTJmNDhlMTYyOGI5ZDQ1YzAxMWQ1OTU1Njc4NDU=", + }, + wantError: swag.String("403 Forbidden"), + expected: nil, + }, + { + test: "valid url but with invalid schema returns error", + args: args{ + encodedURL: "cG9zdGdyZXM6Ly9wb3N0Z3JlczoxMjM0NTZAMTI3LjAuMC4xOjU0MzIvZHVtbXk=", // postgres://postgres:123456@127.0.0.1:5432/dummy + + }, + wantError: swag.String("unexpected scheme found postgres"), + expected: nil, + }, + { + test: "invalid url returns error", + args: args{ + encodedURL: "YXNkc2Fkc2Rh", // asdsadsda + + }, + wantError: swag.String("unexpected scheme found "), + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.test, func(_ *testing.T) { + url, err := b64toMinIOStringURL(tt.args.encodedURL) + if tt.wantError != nil { + if err != nil { + if err.Error() != *tt.wantError { + t.Errorf("b64toMinIOStringURL() error: `%v`, wantErr: `%s`", err, *tt.wantError) + return + } + } else { + t.Errorf("b64toMinIOStringURL() error: `%v`, wantErr: `%s`", err, *tt.wantError) + return + } + } else { + if err != nil { + t.Errorf("b64toMinIOStringURL() error: `%s`, wantErr: `%v`", err, tt.wantError) + return + } + tAssert.Equal(*tt.expected, *url) + } + }) + } +} diff --git a/api/user_objects.go b/api/user_objects.go index 0977a8729e..5701239e17 100644 --- a/api/user_objects.go +++ b/api/user_objects.go @@ -19,6 +19,7 @@ package api import ( "context" "encoding/base64" + b64 "encoding/base64" "errors" "fmt" "io" @@ -1077,30 +1078,43 @@ func getShareObjectResponse(session *models.Principal, params objectApi.ShareObj if params.Expires != nil { expireDuration = *params.Expires } - url, err := getShareObjectURL(ctx, mcClient, params.VersionID, expireDuration) + url, err := getShareObjectURL(ctx, mcClient, params.HTTPRequest, params.VersionID, expireDuration) if err != nil { return nil, ErrorWithContext(ctx, err) } + return url, nil } -func getShareObjectURL(ctx context.Context, client MCClient, versionID string, duration string) (url *string, err error) { +func getShareObjectURL(ctx context.Context, client MCClient, r *http.Request, versionID string, duration string) (url *string, err error) { // default duration 7d if not defined if strings.TrimSpace(duration) == "" { duration = "168h" } - expiresDuration, err := time.ParseDuration(duration) if err != nil { return nil, err } - objURL, pErr := client.shareDownload(ctx, versionID, expiresDuration) + minioURL, pErr := client.shareDownload(ctx, versionID, expiresDuration) if pErr != nil { return nil, pErr.Cause } + + encodedMinIOURL := b64.StdEncoding.EncodeToString([]byte(minioURL)) + requestURL := getRequestURLWithScheme(r) + objURL := fmt.Sprintf("%s/api/v1/download-shared-object/%s", requestURL, encodedMinIOURL) return &objURL, nil } +func getRequestURLWithScheme(r *http.Request) string { + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + + return fmt.Sprintf("%s://%s", scheme, r.Host) +} + func getSetObjectLegalHoldResponse(session *models.Principal, params objectApi.PutObjectLegalHoldParams) *CodedAPIError { ctx := params.HTTPRequest.Context() mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) diff --git a/api/user_objects_test.go b/api/user_objects_test.go index d10867b6da..8bf59cd62f 100644 --- a/api/user_objects_test.go +++ b/api/user_objects_test.go @@ -18,6 +18,7 @@ package api import ( "context" + "crypto/tls" "encoding/json" "errors" "fmt" @@ -914,6 +915,7 @@ func Test_shareObject(t *testing.T) { defer cancel() client := s3ClientMock{} type args struct { + r *http.Request versionID string expires string shareFunc func(ctx context.Context, versionID string, expires time.Duration) (string, *probe.Error) @@ -927,18 +929,44 @@ func Test_shareObject(t *testing.T) { { test: "Get share object url", args: args{ + r: &http.Request{ + TLS: nil, + Host: "localhost:9090", + }, versionID: "2121434", expires: "30s", shareFunc: func(_ context.Context, _ string, _ time.Duration) (string, *probe.Error) { return "http://someurl", nil }, }, + wantError: nil, - expected: "http://someurl", + expected: "http://localhost:9090/api/v1/download-shared-object/aHR0cDovL3NvbWV1cmw=", + }, + { + test: "URL with TLS uses https scheme", + args: args{ + r: &http.Request{ + TLS: &tls.ConnectionState{}, + Host: "localhost:9090", + }, + versionID: "2121434", + expires: "30s", + shareFunc: func(_ context.Context, _ string, _ time.Duration) (string, *probe.Error) { + return "http://someurl", nil + }, + }, + + wantError: nil, + expected: "https://localhost:9090/api/v1/download-shared-object/aHR0cDovL3NvbWV1cmw=", }, { test: "handle invalid expire duration", args: args{ + r: &http.Request{ + TLS: nil, + Host: "localhost:9090", + }, versionID: "2121434", expires: "invalid", shareFunc: func(_ context.Context, _ string, _ time.Duration) (string, *probe.Error) { @@ -950,6 +978,10 @@ func Test_shareObject(t *testing.T) { { test: "handle empty expire duration", args: args{ + r: &http.Request{ + TLS: nil, + Host: "localhost:9090", + }, versionID: "2121434", expires: "", shareFunc: func(_ context.Context, _ string, _ time.Duration) (string, *probe.Error) { @@ -957,11 +989,15 @@ func Test_shareObject(t *testing.T) { }, }, wantError: nil, - expected: "http://someurl", + expected: "http://localhost:9090/api/v1/download-shared-object/aHR0cDovL3NvbWV1cmw=", }, { test: "handle error on share func", args: args{ + r: &http.Request{ + TLS: nil, + Host: "localhost:9090", + }, versionID: "2121434", expires: "3h", shareFunc: func(_ context.Context, _ string, _ time.Duration) (string, *probe.Error) { @@ -975,7 +1011,7 @@ func Test_shareObject(t *testing.T) { for _, tt := range tests { t.Run(tt.test, func(_ *testing.T) { mcShareDownloadMock = tt.args.shareFunc - url, err := getShareObjectURL(ctx, client, tt.args.versionID, tt.args.expires) + url, err := getShareObjectURL(ctx, client, tt.args.r, tt.args.versionID, tt.args.expires) if tt.wantError != nil { if !reflect.DeepEqual(err, tt.wantError) { t.Errorf("getShareObjectURL() error: `%s`, wantErr: `%s`", err, tt.wantError) diff --git a/swagger.yml b/swagger.yml index 495d6a7725..e9c0e13556 100644 --- a/swagger.yml +++ b/swagger.yml @@ -25,7 +25,7 @@ securityDefinitions: type: apiKey # Apply the key security definition to all APIs security: - - key: [ ] + - key: [] parameters: limit: name: limit @@ -54,7 +54,7 @@ paths: schema: $ref: "#/definitions/ApiError" # Exclude this API from the authentication requirement - security: [ ] + security: [] tags: - Auth post: @@ -74,7 +74,7 @@ paths: schema: $ref: "#/definitions/ApiError" # Exclude this API from the authentication requirement - security: [ ] + security: [] tags: - Auth /login/oauth2/auth: @@ -94,7 +94,7 @@ paths: description: Generic error response. schema: $ref: "#/definitions/ApiError" - security: [ ] + security: [] tags: - Auth @@ -295,8 +295,8 @@ paths: get: summary: List Objects security: - - key: [ ] - - anonymous: [ ] + - key: [] + - anonymous: [] operationId: ListObjects parameters: - name: bucket_name @@ -411,8 +411,8 @@ paths: post: summary: Uploads an Object. security: - - key: [ ] - - anonymous: [ ] + - key: [] + - anonymous: [] consumes: - multipart/form-data parameters: @@ -438,8 +438,8 @@ paths: summary: Download Multiple Objects operationId: DownloadMultipleObjects security: - - key: [ ] - - anonymous: [ ] + - key: [] + - anonymous: [] produces: - application/octet-stream parameters: @@ -471,8 +471,8 @@ paths: summary: Download Object operationId: Download Object security: - - key: [ ] - - anonymous: [ ] + - key: [] + - anonymous: [] produces: - application/octet-stream parameters: @@ -542,7 +542,6 @@ paths: $ref: "#/definitions/ApiError" tags: - Object - /buckets/{bucket_name}/objects/legalhold: put: summary: Put Object's legalhold status @@ -2885,7 +2884,7 @@ paths: - name: order in: query type: string - enum: [ timeDesc, timeAsc ] + enum: [timeDesc, timeAsc] default: timeDesc - name: timeStart in: query @@ -3518,6 +3517,30 @@ paths: tags: - Support + /download-shared-object/{url}: + get: + summary: Downloads an object from a presigned url + operationId: DownloadSharedObject + security: [] + produces: + - application/octet-stream + parameters: + - name: url + in: path + required: true + type: string + responses: + 200: + description: A successful response. + schema: + type: file + default: + description: Generic error response. + schema: + $ref: "#/definitions/ApiError" + tags: + - Public + definitions: accountChangePasswordRequest: type: object @@ -4329,7 +4352,7 @@ definitions: properties: loginStrategy: type: string - enum: [ form, redirect, service-account, redirect-service-account ] + enum: [form, redirect, service-account, redirect-service-account] redirectRules: type: array items: @@ -4428,7 +4451,7 @@ definitions: type: string status: type: string - enum: [ ok ] + enum: [ok] operator: type: boolean distributedMode: @@ -4459,7 +4482,7 @@ definitions: type: string values: type: array - items: { } + items: {} resultTarget: type: object properties: @@ -4915,7 +4938,7 @@ definitions: type: string service: type: string - enum: [ replication ] + enum: [replication] syncMode: type: string bandwidth: @@ -6209,7 +6232,7 @@ definitions: format: int64 required: - exp - + selectedSAs: type: array items: diff --git a/web-app/src/api/consoleApi.ts b/web-app/src/api/consoleApi.ts index 8946cf0645..162d468370 100644 --- a/web-app/src/api/consoleApi.ts +++ b/web-app/src/api/consoleApi.ts @@ -5277,4 +5277,20 @@ export class Api< ...params, }), }; + downloadSharedObject = { + /** + * No description + * + * @tags Public + * @name DownloadSharedObject + * @summary Downloads an object from a presigned url + * @request GET:/download-shared-object/{url} + */ + downloadSharedObject: (url: string, params: RequestParams = {}) => + this.request({ + path: `/download-shared-object/${url}`, + method: "GET", + ...params, + }), + }; }