Changed Share Object logic to use Access Keys (#2827)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2023-05-24 11:52:40 -06:00
committed by GitHub
parent 17e791afb9
commit 7a9b775b09
16 changed files with 661 additions and 296 deletions

View File

@@ -461,12 +461,24 @@ func ListObjects(bucketName, prefix, withVersions string) (*http.Response, error
return response, err
}
func SharesAnObjectOnAUrl(bucketName, prefix, versionID, expires string) (*http.Response, error) {
// Helper function to share an object on a url
func SharesAnObjectOnAUrl(bucketName, prefix, versionID, expires, accessKey, secretKey string) (*http.Response, error) {
// Helper function to share an object on an url
requestDataAdd := map[string]interface{}{
"prefix": prefix,
"version_id": versionID,
"expires": expires,
"access_key": accessKey,
"secret_key": secretKey,
}
requestDataJSON, _ := json.Marshal(requestDataAdd)
requestDataBody := bytes.NewReader(requestDataJSON)
request, err := http.NewRequest(
"GET",
"http://localhost:9090/api/v1/buckets/"+bucketName+"/objects/share?prefix="+prefix+"&version_id="+versionID+"&expires="+expires,
nil,
"POST",
"http://localhost:9090/api/v1/buckets/"+bucketName+"/objects/share",
requestDataBody,
)
if err != nil {
log.Println(err)
@@ -743,6 +755,39 @@ func PutObjectsLegalholdStatus(bucketName, prefix, status, versionID string) (*h
return response, err
}
func PostServiceAccountCredentials(accessKey, secretKey, policy string) (*http.Response, error) {
/*
Helper function to create a service account
POST: {{baseUrl}}/service-account-credentials
{
"accessKey":"testsa",
"secretKey":"secretsa",
"policy":""
}
*/
requestDataAdd := map[string]interface{}{
"accessKey": accessKey,
"secretKey": secretKey,
"policy": policy,
}
requestDataJSON, _ := json.Marshal(requestDataAdd)
requestDataBody := bytes.NewReader(requestDataJSON)
request, err := http.NewRequest("POST",
"http://localhost:9090/api/v1/service-account-credentials",
requestDataBody)
if err != nil {
log.Println(err)
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
client := &http.Client{
Timeout: 2 * time.Second,
}
response, err := client.Do(request)
return response, err
}
func TestPutObjectsLegalholdStatus(t *testing.T) {
// Variables
assert := assert.New(t)
@@ -1514,6 +1559,8 @@ func TestShareObjectOnURL(t *testing.T) {
tags := make(map[string]string)
tags["tag"] = "testputobjecttagbucketonetagone"
versionID := "null"
accessKey := "testaccesskey"
secretKey := "secretAccessKey"
// 1. Create the bucket
if !setupBucket(bucketName, false, false, nil, nil, assert, 200) {
@@ -1534,6 +1581,21 @@ func TestShareObjectOnURL(t *testing.T) {
inspectHTTPResponse(uploadResponse),
)
}
// 2. Create Access Key
accKeyRsp, createError := PostServiceAccountCredentials(accessKey, secretKey, "")
if createError != nil {
log.Println(createError)
return
}
if accKeyRsp != nil {
assert.Equal(
201,
accKeyRsp.StatusCode,
inspectHTTPResponse(accKeyRsp),
)
}
type args struct {
prefix string
@@ -1561,7 +1623,7 @@ func TestShareObjectOnURL(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 3. Share the object on a URL
shareResponse, shareError := SharesAnObjectOnAUrl(bucketName, tt.args.prefix, versionID, "604800s")
shareResponse, shareError := SharesAnObjectOnAUrl(bucketName, tt.args.prefix, versionID, "604800s", accessKey, secretKey)
assert.Nil(shareError)
if shareError != nil {
log.Println(shareError)

142
models/share_request.go Normal file
View File

@@ -0,0 +1,142 @@
// 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 <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// ShareRequest share request
//
// swagger:model shareRequest
type ShareRequest struct {
// access key
// Required: true
AccessKey *string `json:"access_key"`
// expires
Expires string `json:"expires,omitempty"`
// prefix
// Required: true
Prefix *string `json:"prefix"`
// secret key
// Required: true
SecretKey *string `json:"secret_key"`
// version id
// Required: true
VersionID *string `json:"version_id"`
}
// Validate validates this share request
func (m *ShareRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateAccessKey(formats); err != nil {
res = append(res, err)
}
if err := m.validatePrefix(formats); err != nil {
res = append(res, err)
}
if err := m.validateSecretKey(formats); err != nil {
res = append(res, err)
}
if err := m.validateVersionID(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *ShareRequest) validateAccessKey(formats strfmt.Registry) error {
if err := validate.Required("access_key", "body", m.AccessKey); err != nil {
return err
}
return nil
}
func (m *ShareRequest) validatePrefix(formats strfmt.Registry) error {
if err := validate.Required("prefix", "body", m.Prefix); err != nil {
return err
}
return nil
}
func (m *ShareRequest) validateSecretKey(formats strfmt.Registry) error {
if err := validate.Required("secret_key", "body", m.SecretKey); err != nil {
return err
}
return nil
}
func (m *ShareRequest) validateVersionID(formats strfmt.Registry) error {
if err := validate.Required("version_id", "body", m.VersionID); err != nil {
return err
}
return nil
}
// ContextValidate validates this share request based on context it is used
func (m *ShareRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *ShareRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *ShareRequest) UnmarshalBinary(b []byte) error {
var res ShareRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -66,7 +66,7 @@
},
"proxy": "http://localhost:9090/",
"devDependencies": {
"@playwright/test": "^1.32.3",
"@playwright/test": "^1.34.0",
"@types/lodash": "^4.14.194",
"@types/luxon": "^3.3.0",
"@types/minio": "^7.0.18",
@@ -85,6 +85,7 @@
"@types/websocket": "^1.0.0",
"babel-plugin-istanbul": "^6.1.1",
"customize-cra": "^1.0.0",
"minio": "^7.0.33",
"nyc": "^15.1.0",
"playwright": "^1.31.3",
"prettier": "2.8.8",
@@ -92,8 +93,7 @@
"react-app-rewired": "^2.2.1",
"react-scripts": "5.0.1",
"testcafe": "^2.5.0",
"typescript": "^4.4.3",
"minio": "^7.0.33"
"typescript": "^4.4.3"
},
"resolutions": {
"nth-check": "^2.0.1",

View File

@@ -1494,6 +1494,14 @@ export interface LdapPolicyEntity {
groups?: string[];
}
export interface ShareRequest {
prefix: string;
version_id: string;
expires?: string;
access_key: string;
secret_key: string;
}
export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
@@ -2171,23 +2179,20 @@ export class Api<
* @tags Object
* @name ShareObject
* @summary Shares an Object on a url
* @request GET:/buckets/{bucket_name}/objects/share
* @request POST:/buckets/{bucket_name}/objects/share
* @secure
*/
shareObject: (
bucketName: string,
query: {
prefix: string;
version_id: string;
expires?: string;
},
body: ShareRequest,
params: RequestParams = {}
) =>
this.request<IamEntity, Error>({
path: `/buckets/${bucketName}/objects/share`,
method: "GET",
query: query,
method: "POST",
body: body,
secure: true,
type: ContentType.Json,
format: "json",
...params,
}),

View File

@@ -17,11 +17,20 @@
import React, { Fragment, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { Theme } from "@mui/material/styles";
import { Button, CopyIcon, ReadBox, ShareIcon } from "mds";
import {
Button,
CopyIcon,
FormLayout,
Grid,
InputBox,
RadioGroup,
ReadBox,
Select,
ShareIcon,
} from "mds";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import CopyToClipboard from "react-copy-to-clipboard";
import Grid from "@mui/material/Grid";
import LinearProgress from "@mui/material/LinearProgress";
import {
formFieldStyles,
@@ -36,10 +45,13 @@ import DaysSelector from "../../../../Common/FormComponents/DaysSelector/DaysSel
import { encodeURLString } from "../../../../../../common/utils";
import {
selDistSet,
setErrorSnackMessage,
setModalErrorSnackMessage,
setModalSnackMessage,
} from "../../../../../../systemSlice";
import { useAppDispatch } from "../../../../../../store";
import { DateTime } from "luxon";
import { stringSort } from "../../../../../../utils/sortFunctions";
const styles = (theme: Theme) =>
createStyles({
@@ -85,11 +97,17 @@ const ShareFile = ({
const dispatch = useAppDispatch();
const distributedSetup = useSelector(selDistSet);
const [shareURL, setShareURL] = useState<string>("");
const [isLoadingVersion, setIsLoadingVersion] = useState<boolean>(true);
const [isLoadingFile, setIsLoadingFile] = useState<boolean>(false);
const [isLoadingURL, setIsLoadingURL] = useState<boolean>(false);
const [isLoadingAccessKeys, setLoadingAccessKeys] = useState<boolean>(true);
const [selectedDate, setSelectedDate] = useState<string>("");
const [dateValid, setDateValid] = useState<boolean>(true);
const [versionID, setVersionID] = useState<string>("null");
const [displayURL, setDisplayURL] = useState<boolean>(false);
const [accessKeys, setAccessKeys] = useState<any[]>([]);
const [selectedAccessKey, setSelectedAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [authType, setAuthType] = useState<string>("acc-list");
const [otherAK, setOtherAK] = useState<string>("");
const initialDate = new Date();
@@ -134,20 +152,19 @@ const ShareFile = ({
dispatch(setModalErrorSnackMessage(error));
});
setIsLoadingVersion(false);
setIsLoadingURL(false);
return;
}
setVersionID("null");
setIsLoadingVersion(false);
setIsLoadingURL(false);
return;
}
setVersionID(dataObject.version_id || "null");
setIsLoadingVersion(false);
setIsLoadingURL(false);
}, [bucketName, dataObject, distributedSetup, dispatch]);
useEffect(() => {
if (dateValid && !isLoadingVersion) {
setIsLoadingFile(true);
if (dateValid && isLoadingURL) {
setShareURL("");
const slDate = new Date(`${selectedDate}`);
@@ -157,28 +174,33 @@ const ShareFile = ({
(slDate.getTime() - currDate.getTime()) / 1000
);
const accKey = authType === "acc-list" ? selectedAccessKey : otherAK;
if (diffDate > 0) {
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects/share?prefix=${encodeURLString(
dataObject.name
)}&version_id=${versionID}${
selectedDate !== "" ? `&expires=${diffDate}s` : ""
}`
)
.invoke("POST", `/api/v1/buckets/${bucketName}/objects/share`, {
prefix: encodeURLString(dataObject.name),
version_id: versionID,
expires: selectedDate !== "" ? `${diffDate}s` : "",
access_key: accKey,
secret_key: secretKey,
})
.then((res: string) => {
setShareURL(res);
setIsLoadingFile(false);
setIsLoadingURL(false);
setDisplayURL(true);
})
.catch((error: ErrorResponseHandler) => {
dispatch(setModalErrorSnackMessage(error));
setShareURL("");
setIsLoadingFile(false);
setIsLoadingURL(false);
setDisplayURL(false);
});
}
}
}, [
secretKey,
selectedAccessKey,
dataObject,
selectedDate,
bucketName,
@@ -186,80 +208,205 @@ const ShareFile = ({
setShareURL,
dispatch,
distributedSetup,
isLoadingVersion,
versionID,
isLoadingURL,
authType,
otherAK,
]);
useEffect(() => {
if (isLoadingAccessKeys) {
const userLoggedIn = localStorage.getItem("userLoggedIn");
api
.invoke(
"GET",
`/api/v1/user/${encodeURLString(userLoggedIn)}/service-accounts`
)
.then((res: string[]) => {
if (res.length === 0) {
setAuthType("acc-other");
}
const serviceAccounts = res
.sort(stringSort)
.map((element) => ({ value: element, label: element }));
setLoadingAccessKeys(false);
setAccessKeys(serviceAccounts);
setSelectedAccessKey(serviceAccounts[0].value);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoadingAccessKeys(false);
});
}
}, [isLoadingAccessKeys, dispatch]);
const generateLink = () => {
setIsLoadingURL(true);
};
const generateAnotherLink = () => {
setIsLoadingURL(false);
setDisplayURL(false);
setSelectedDate("");
setShareURL("");
};
return (
<React.Fragment>
<ModalWrapper
title="Share File"
titleIcon={<ShareIcon style={{ fill: "#4CCB92" }} />}
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
>
{isLoadingVersion && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
{!isLoadingVersion && (
<Fragment>
<Grid item xs={12} className={classes.shareLinkInfo}>
This is a temporary URL with integrated access credentials for
sharing objects valid for up to 7 days.
<br />
<br />
The temporary URL expires after the configured time limit.
</Grid>
<ModalWrapper
title="Share File"
titleIcon={<ShareIcon style={{ fill: "#4CCB92" }} />}
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
>
{displayURL ? (
<Fragment>
<Grid item xs={12} className={classes.shareLinkInfo}>
This is a temporary URL with integrated access credentials for
sharing <strong>{dataObject.name}</strong> until{" "}
<strong>
{DateTime.fromISO(selectedDate).toFormat(
"ccc, LLL dd yyyy HH:mm (ZZZZ)"
)}
</strong>
<br />
<Grid item xs={12} className={classes.dateContainer}>
<DaysSelector
initialDate={initialDate}
id="date"
label="Active for"
maxDays={7}
onChange={dateChanged}
entity="Link"
/>
</Grid>
<Grid
item
xs={12}
className={`${classes.copyShareLink} ${classes.formFieldRow} `}
<br />
This temporary URL will expiry after this time.
</Grid>
<Grid
item
xs={12}
className={`${classes.copyShareLink} ${classes.formFieldRow} `}
sx={{ marginTop: 12 }}
>
<ReadBox
actionButton={
<CopyToClipboard text={shareURL}>
<Button
id={"copy-path"}
variant="regular"
onClick={() => {
dispatch(
setModalSnackMessage("Share URL Copied to clipboard")
);
}}
disabled={shareURL === "" || isLoadingURL}
style={{
marginRight: "5px",
width: "28px",
height: "28px",
padding: "0px",
}}
icon={<CopyIcon />}
/>
</CopyToClipboard>
}
>
<ReadBox
actionButton={
<CopyToClipboard text={shareURL}>
<Button
id={"copy-path"}
variant="regular"
onClick={() => {
dispatch(
setModalSnackMessage("Share URL Copied to clipboard")
);
}}
disabled={shareURL === "" || isLoadingFile}
style={{
marginRight: "5px",
width: "28px",
height: "28px",
padding: "0px",
}}
icon={<CopyIcon />}
/>
</CopyToClipboard>
}
>
{shareURL}
</ReadBox>
{shareURL}
</ReadBox>
</Grid>
<Grid sx={{ display: "flex", justifyContent: "flex-end" }}>
<Button
id={"generate-link"}
variant={"callAction"}
type={"button"}
onClick={generateAnotherLink}
>
Generate Another Link
</Button>
</Grid>
</Fragment>
) : (
<Fragment>
<Grid item xs={12} className={classes.shareLinkInfo}>
To generate a temporary URL, please provide a set of credentials,
this link can ve valid up to 7 days.
<br />
<br />
</Grid>
<FormLayout sx={{ border: 0, padding: 0 }}>
{accessKeys.length > 0 && (
<RadioGroup
id={"sign-selector-kind"}
selectorOptions={[
{ label: "Account's Access Key", value: "acc-list" },
{
label: "Custom Access Key",
value: "acc-other",
},
]}
label={""}
name={"sign-selector-kind"}
onChange={(e) => {
setAuthType(e.target.value);
}}
currentValue={authType}
/>
)}
{authType === "acc-other" ? (
<InputBox
id={"other-ak"}
value={otherAK}
onChange={(e) => {
setOtherAK(e.target.value);
}}
label={"Access Key"}
/>
) : (
<Select
options={accessKeys}
value={selectedAccessKey}
id={"select-ak"}
onChange={(item) => {
setSelectedAccessKey(item);
}}
label={"Access Key"}
/>
)}
<InputBox
id={"secret-key"}
type={"password"}
value={secretKey}
onChange={(e) => {
setSecretKey(e.target.value);
}}
label={"Secret Key"}
/>
</FormLayout>
<Grid item xs={12} className={classes.dateContainer}>
<DaysSelector
initialDate={initialDate}
id="date"
label="Link Duration"
maxDays={7}
onChange={dateChanged}
entity="Link"
/>
</Grid>
{isLoadingURL && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
</Fragment>
)}
</ModalWrapper>
</React.Fragment>
)}
<Grid sx={{ display: "flex", justifyContent: "flex-end" }}>
<Button
id={"generate-link"}
variant={"callAction"}
type={"button"}
onClick={generateLink}
disabled={secretKey === "" || !dateValid}
>
Generate Link
</Button>
</Grid>
</Fragment>
)}
</ModalWrapper>
);
};

View File

@@ -20,7 +20,7 @@ import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import { LinkIcon, InputLabel, InputBox, Grid } from "mds";
import { Grid, InputBox, InputLabel, LinkIcon } from "mds";
interface IDaysSelector {
classes: any;
@@ -177,6 +177,10 @@ const DaysSelector = ({
valid = false;
}
if (selectedDays <= 0 && selectedHours <= 0 && selectedMinutes <= 0) {
valid = false;
}
setValidDate(valid);
}, [
dateSelected,
@@ -203,9 +207,7 @@ const DaysSelector = ({
<Fragment>
<Grid container className={classes.fieldContainer}>
<Grid item xs={12} className={classes.labelContainer}>
<InputLabel htmlFor={id} sx={{ marginLeft: "10px" }}>
{label}
</InputLabel>
<InputLabel htmlFor={id}>{label}</InputLabel>
</Grid>
<Grid item xs={12} className={classes.durationInputs}>
<Grid item xs className={classes.dateInputContainer}>

View File

@@ -21,7 +21,10 @@ import { AppState, useAppDispatch } from "../../../../store";
import { Box } from "@mui/material";
import { AlertCloseIcon } from "mds";
import { Portal } from "@mui/base";
import { setErrorSnackMessage } from "../../../../systemSlice";
import {
setErrorSnackMessage,
setModalSnackMessage,
} from "../../../../systemSlice";
interface IMainErrorProps {
isModal?: boolean;
@@ -51,6 +54,7 @@ const MainError = ({ isModal = false }: IMainErrorProps) => {
useEffect(() => {
if (!displayErrorMsg) {
dispatch(setErrorSnackMessage({ detailedError: "", errorMessage: "" }));
dispatch(setModalSnackMessage(""));
clearInterval(timerI);
}
}, [dispatch, displayErrorMsg]);

View File

@@ -1879,13 +1879,13 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@playwright/test@^1.32.3":
version "1.32.3"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.32.3.tgz#75be8346d4ef289896835e1d2a86fdbe3d9be92a"
integrity sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==
"@playwright/test@^1.34.0":
version "1.34.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.34.0.tgz#1f3359523c3fd7460c83fe83c8152751a9e21f49"
integrity sha512-GIALJVODOIrMflLV54H3Cow635OfrTwOu24ZTDyKC66uchtFX2NcCRq83cLdakMjZKYK78lODNLQSYBj2OgaTw==
dependencies:
"@types/node" "*"
playwright-core "1.32.3"
playwright-core "1.34.0"
optionalDependencies:
fsevents "2.3.2"
@@ -9159,6 +9159,11 @@ playwright-core@1.32.3:
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.32.3.tgz#e6dc7db0b49e9b6c0b8073c4a2d789a96f519c48"
integrity sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg==
playwright-core@1.34.0:
version "1.34.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.34.0.tgz#6a8f05c657400677591ed82b6749ef7e120a152d"
integrity sha512-fMUY1+iR6kYbJF/EsOOqzBA99ZHXbw9sYPNjwA4X/oV0hVF/1aGlWYBGPVUEqxBkGANDKMziYoOdKGU5DIP5Gg==
playwright@^1.31.3:
version "1.32.3"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.32.3.tgz#88583167880e42ca04fa8c4cc303680f4ff457d0"

View File

@@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"io"
"net/url"
"path"
"strings"
"time"
@@ -221,6 +222,10 @@ func (c minioClient) copyObject(ctx context.Context, dst minio.CopyDestOptions,
return c.client.CopyObject(ctx, dst, src)
}
func (c minioClient) presignedGetObject(ctx context.Context, bucketName, objectName string, expiry time.Duration, reqParams url.Values) (*url.URL, error) {
return c.client.PresignedGetObject(ctx, bucketName, objectName, expiry, reqParams)
}
// MCClient interface with all functions to be implemented
// by mock when testing, it should include all mc/S3Client respective api calls
// that are used within this project.

View File

@@ -1858,7 +1858,7 @@ func init() {
}
},
"/buckets/{bucket_name}/objects/share": {
"get": {
"post": {
"tags": [
"Object"
],
@@ -1872,21 +1872,12 @@ func init() {
"required": true
},
{
"type": "string",
"name": "prefix",
"in": "query",
"required": true
},
{
"type": "string",
"name": "version_id",
"in": "query",
"required": true
},
{
"type": "string",
"name": "expires",
"in": "query"
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/shareRequest"
}
}
],
"responses": {
@@ -8319,6 +8310,32 @@ func init() {
}
}
},
"shareRequest": {
"type": "object",
"required": [
"prefix",
"version_id",
"access_key",
"secret_key"
],
"properties": {
"access_key": {
"type": "string"
},
"expires": {
"type": "string"
},
"prefix": {
"type": "string"
},
"secret_key": {
"type": "string"
},
"version_id": {
"type": "string"
}
}
},
"siteReplicationAddRequest": {
"type": "array",
"items": {
@@ -10860,7 +10877,7 @@ func init() {
}
},
"/buckets/{bucket_name}/objects/share": {
"get": {
"post": {
"tags": [
"Object"
],
@@ -10874,21 +10891,12 @@ func init() {
"required": true
},
{
"type": "string",
"name": "prefix",
"in": "query",
"required": true
},
{
"type": "string",
"name": "version_id",
"in": "query",
"required": true
},
{
"type": "string",
"name": "expires",
"in": "query"
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/shareRequest"
}
}
],
"responses": {
@@ -17450,6 +17458,32 @@ func init() {
}
}
},
"shareRequest": {
"type": "object",
"required": [
"prefix",
"version_id",
"access_key",
"secret_key"
],
"properties": {
"access_key": {
"type": "string"
},
"expires": {
"type": "string"
},
"prefix": {
"type": "string"
},
"secret_key": {
"type": "string"
},
"version_id": {
"type": "string"
}
}
},
"siteReplicationAddRequest": {
"type": "array",
"items": {

View File

@@ -2149,10 +2149,10 @@ func (o *ConsoleAPI) initHandlerCache() {
o.handlers["PUT"] = make(map[string]http.Handler)
}
o.handlers["PUT"]["/service-accounts/{access_key}/policy"] = service_account.NewSetServiceAccountPolicy(o.context, o.ServiceAccountSetServiceAccountPolicyHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/buckets/{bucket_name}/objects/share"] = object.NewShareObject(o.context, o.ObjectShareObjectHandler)
o.handlers["POST"]["/buckets/{bucket_name}/objects/share"] = object.NewShareObject(o.context, o.ObjectShareObjectHandler)
if o.handlers["PUT"] == nil {
o.handlers["PUT"] = make(map[string]http.Handler)
}

View File

@@ -49,7 +49,7 @@ func NewShareObject(ctx *middleware.Context, handler ShareObjectHandler) *ShareO
}
/*
ShareObject swagger:route GET /buckets/{bucket_name}/objects/share Object shareObject
ShareObject swagger:route POST /buckets/{bucket_name}/objects/share Object shareObject
Shares an Object on a url
*/

View File

@@ -23,6 +23,7 @@ package object
// Editing this file might prove futile when you re-run the swagger generate command
import (
"io"
"net/http"
"github.com/go-openapi/errors"
@@ -30,6 +31,8 @@ import (
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"github.com/minio/console/models"
)
// NewShareObjectParams creates a new ShareObjectParams object
@@ -49,25 +52,16 @@ type ShareObjectParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: body
*/
Body *models.ShareRequest
/*
Required: true
In: path
*/
BucketName string
/*
In: query
*/
Expires *string
/*
Required: true
In: query
*/
Prefix string
/*
Required: true
In: query
*/
VersionID string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
@@ -79,27 +73,38 @@ func (o *ShareObjectParams) BindRequest(r *http.Request, route *middleware.Match
o.HTTPRequest = r
qs := runtime.Values(r.URL.Query())
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.ShareRequest
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("body", "body", ""))
} else {
res = append(res, errors.NewParseError("body", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
ctx := validate.WithOperationRequest(r.Context())
if err := body.ContextValidate(ctx, route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Body = &body
}
}
} else {
res = append(res, errors.Required("body", "body", ""))
}
rBucketName, rhkBucketName, _ := route.Params.GetOK("bucket_name")
if err := o.bindBucketName(rBucketName, rhkBucketName, route.Formats); err != nil {
res = append(res, err)
}
qExpires, qhkExpires, _ := qs.GetOK("expires")
if err := o.bindExpires(qExpires, qhkExpires, route.Formats); err != nil {
res = append(res, err)
}
qPrefix, qhkPrefix, _ := qs.GetOK("prefix")
if err := o.bindPrefix(qPrefix, qhkPrefix, route.Formats); err != nil {
res = append(res, err)
}
qVersionID, qhkVersionID, _ := qs.GetOK("version_id")
if err := o.bindVersionID(qVersionID, qhkVersionID, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
@@ -119,63 +124,3 @@ func (o *ShareObjectParams) bindBucketName(rawData []string, hasKey bool, format
return nil
}
// bindExpires binds and validates parameter Expires from query.
func (o *ShareObjectParams) bindExpires(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
o.Expires = &raw
return nil
}
// bindPrefix binds and validates parameter Prefix from query.
func (o *ShareObjectParams) bindPrefix(rawData []string, hasKey bool, formats strfmt.Registry) error {
if !hasKey {
return errors.Required("prefix", "query", rawData)
}
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// AllowEmptyValue: false
if err := validate.RequiredString("prefix", "query", raw); err != nil {
return err
}
o.Prefix = raw
return nil
}
// bindVersionID binds and validates parameter VersionID from query.
func (o *ShareObjectParams) bindVersionID(rawData []string, hasKey bool, formats strfmt.Registry) error {
if !hasKey {
return errors.Required("version_id", "query", rawData)
}
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// AllowEmptyValue: false
if err := validate.RequiredString("version_id", "query", raw); err != nil {
return err
}
o.VersionID = raw
return nil
}

View File

@@ -33,10 +33,6 @@ import (
type ShareObjectURL struct {
BucketName string
Expires *string
Prefix string
VersionID string
_basePath string
// avoid unkeyed usage
_ struct{}
@@ -76,28 +72,6 @@ func (o *ShareObjectURL) Build() (*url.URL, error) {
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
qs := make(url.Values)
var expiresQ string
if o.Expires != nil {
expiresQ = *o.Expires
}
if expiresQ != "" {
qs.Set("expires", expiresQ)
}
prefixQ := o.Prefix
if prefixQ != "" {
qs.Set("prefix", prefixQ)
}
versionIDQ := o.VersionID
if versionIDQ != "" {
qs.Set("version_id", versionIDQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil
}

View File

@@ -31,6 +31,8 @@ import (
"strings"
"time"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/klauspost/compress/zip"
@@ -117,7 +119,7 @@ func registerObjectsHandlers(api *operations.ConsoleAPI) {
})
// get share object url
api.ObjectShareObjectHandler = objectApi.ShareObjectHandlerFunc(func(params objectApi.ShareObjectParams, session *models.Principal) middleware.Responder {
resp, err := getShareObjectResponse(session, params)
resp, err := getShareObjectResponse(params)
if err != nil {
return objectApi.NewShareObjectDefault(int(err.Code)).WithPayload(err)
}
@@ -892,34 +894,60 @@ func uploadFiles(ctx context.Context, client MinioClient, params objectApi.PostB
return nil
}
// getShareObjectResponse returns a share object url
func getShareObjectResponse(session *models.Principal, params objectApi.ShareObjectParams) (*string, *models.Error) {
// getShareObjectResponse returns a share object url, Session is omitted as we will sign the URl with a new static token
func getShareObjectResponse(params objectApi.ShareObjectParams) (*string, *models.Error) {
ctx := params.HTTPRequest.Context()
bodyPrefix := *params.Body.Prefix
accessKey := *params.Body.AccessKey
secretKey := *params.Body.SecretKey
creds := credentials.NewStaticV4(accessKey, secretKey, "")
mClient, err := minio.New(getMinIOEndpoint(), &minio.Options{
Creds: creds,
Secure: getMinIOEndpointIsSecure(),
Transport: GetConsoleHTTPClient(getMinIOServer()).Transport,
})
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
minioClient := minioClient{client: mClient}
var prefix string
if params.Prefix != "" {
encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
if bodyPrefix != "" {
encodedPrefix := SanitizeEncodedPrefix(bodyPrefix)
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
prefix = string(decodedPrefix)
}
s3Client, err := newS3BucketClient(session, params.BucketName, prefix)
expireDuration := time.Duration(604800) * time.Second
if params.Body.Expires != "" {
expireDuration, err = time.ParseDuration(params.Body.Expires)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
}
reqParams := make(url.Values)
if *params.Body.VersionID != "" {
reqParams.Set("versionId", *params.Body.VersionID)
}
urlParams, err := minioClient.presignedGetObject(ctx, params.BucketName, prefix, expireDuration, reqParams)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
// create a mc S3Client interface implementation
// defining the client to be used
mcClient := mcClient{client: s3Client}
var expireDuration string
if params.Expires != nil {
expireDuration = *params.Expires
}
url, err := getShareObjectURL(ctx, mcClient, params.VersionID, expireDuration)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return url, nil
stringURL := urlParams.String()
return &stringURL, nil
}
func getShareObjectURL(ctx context.Context, client MCClient, versionID string, duration string) (url *string, err error) {

View File

@@ -484,7 +484,7 @@ paths:
- Object
/buckets/{bucket_name}/objects/share:
get:
post:
summary: Shares an Object on a url
operationId: ShareObject
parameters:
@@ -492,18 +492,11 @@ paths:
in: path
required: true
type: string
- name: prefix
in: query
- name: body
in: body
required: true
type: string
- name: version_id
in: query
required: true
type: string
- name: expires
in: query
required: false
type: string
schema:
$ref: "#/definitions/shareRequest"
responses:
200:
description: A successful response.
@@ -6152,4 +6145,23 @@ definitions:
groups:
type: array
items:
type: string
type: string
shareRequest:
type: object
required:
- prefix
- version_id
- access_key
- secret_key
properties:
prefix:
type: string
version_id:
type: string
expires:
type: string
access_key:
type: string
secret_key:
type: string