Anonymous Access (#2600)

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2023-01-27 12:23:30 -08:00
committed by GitHub
parent c141b6d65e
commit b218cbf503
39 changed files with 1596 additions and 891 deletions

View File

@@ -45,8 +45,8 @@ import (
// Session token errors // Session token errors
var ( var (
ErrNoAuthToken = errors.New("session token missing") ErrNoAuthToken = errors.New("session token missing")
errTokenExpired = errors.New("session token has expired") ErrTokenExpired = errors.New("session token has expired")
errReadingToken = errors.New("session token internal data is malformed") ErrReadingToken = errors.New("session token internal data is malformed")
) )
// derivedKey is the key used to encrypt the session token claims, its derived using pbkdf on CONSOLE_PBKDF_PASSPHRASE with CONSOLE_PBKDF_SALT // derivedKey is the key used to encrypt the session token claims, its derived using pbkdf on CONSOLE_PBKDF_PASSPHRASE with CONSOLE_PBKDF_SALT
@@ -101,12 +101,12 @@ func SessionTokenAuthenticate(token string) (*TokenClaims, error) {
decryptedToken, err := DecryptToken(token) decryptedToken, err := DecryptToken(token)
if err != nil { if err != nil {
// fail decrypting token // fail decrypting token
return nil, errReadingToken return nil, ErrReadingToken
} }
claimTokens, err := ParseClaimsFromToken(string(decryptedToken)) claimTokens, err := ParseClaimsFromToken(string(decryptedToken))
if err != nil { if err != nil {
// fail unmarshalling token into data structure // fail unmarshalling token into data structure
return nil, errReadingToken return nil, ErrReadingToken
} }
// claimsTokens contains the decrypted JWT for Console // claimsTokens contains the decrypted JWT for Console
return claimTokens, nil return claimTokens, nil
@@ -321,7 +321,7 @@ func GetTokenFromRequest(r *http.Request) (string, error) {
} }
currentTime := time.Now() currentTime := time.Now()
if tokenCookie.Expires.After(currentTime) { if tokenCookie.Expires.After(currentTime) {
return "", errTokenExpired return "", ErrTokenExpired
} }
return strings.TrimSpace(tokenCookie.Value), nil return strings.TrimSpace(tokenCookie.Value), nil
} }

View File

@@ -27,6 +27,7 @@ import {
globalSetDistributedSetup, globalSetDistributedSetup,
operatorMode, operatorMode,
selOpMode, selOpMode,
setAnonymousMode,
setOverrideStyles, setOverrideStyles,
setSiteReplicationInfo, setSiteReplicationInfo,
userLogged, userLogged,
@@ -48,7 +49,9 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
const [sessionLoading, setSessionLoading] = useState<boolean>(true); const [sessionLoading, setSessionLoading] = useState<boolean>(true);
const userLoggedIn = useSelector((state: AppState) => state.system.loggedIn); const userLoggedIn = useSelector((state: AppState) => state.system.loggedIn);
const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode
);
const { pathname = "" } = useLocation(); const { pathname = "" } = useLocation();
const StorePathAndRedirect = () => { const StorePathAndRedirect = () => {
@@ -56,6 +59,9 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
return <Navigate to={{ pathname: `login` }} />; return <Navigate to={{ pathname: `login` }} />;
}; };
const pathnameParts = pathname.split("/");
const screen = pathnameParts.length > 2 ? pathnameParts[1] : "";
useEffect(() => { useEffect(() => {
api api
.invoke("GET", `/api/v1/session`) .invoke("GET", `/api/v1/session`)
@@ -81,8 +87,37 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
} }
} }
}) })
.catch(() => setSessionLoading(false)); .catch(() => {
}, [dispatch]); // if we are trying to browse, probe access to the requested prefix
if (screen === "browser") {
const bucket = pathnameParts.length >= 3 ? pathnameParts[2] : "";
// no bucket, no business
if (bucket === "") {
setSessionLoading(false);
return;
}
// before marking the session as done, let's check if the bucket is publicly accessible
api
.invoke(
"GET",
`/api/v1/buckets/${bucket}/objects?limit=1`,
undefined,
{
"X-Anonymous": "1",
}
)
.then((value) => {
dispatch(setAnonymousMode());
setSessionLoading(false);
})
.catch(() => {
setSessionLoading(false);
});
} else {
setSessionLoading(false);
}
});
}, [dispatch, screen, pathnameParts]);
const [, invokeSRInfoApi] = useApi( const [, invokeSRInfoApi] = useApi(
(res: any) => { (res: any) => {
@@ -112,7 +147,7 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
); );
useEffect(() => { useEffect(() => {
if (userLoggedIn && !sessionLoading && !isOperatorMode) { if (userLoggedIn && !sessionLoading && !isOperatorMode && !anonymousMode) {
invokeSRInfoApi("GET", `api/v1/admin/site-replication`); invokeSRInfoApi("GET", `api/v1/admin/site-replication`);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -28,7 +28,9 @@ const hasPermission = (
return false; return false;
} }
const state = store.getState(); const state = store.getState();
const sessionGrants = state.console.session.permissions || {}; const sessionGrants = state.console.session
? state.console.session.permissions || {}
: {};
const globalGrants = sessionGrants["arn:aws:s3:::*"] || []; const globalGrants = sessionGrants["arn:aws:s3:::*"] || [];
let resources: string[] = []; let resources: string[] = [];

View File

@@ -20,13 +20,23 @@ import { clearSession } from "../utils";
import { ErrorResponseHandler } from "../types"; import { ErrorResponseHandler } from "../types";
import { baseUrl } from "../../history"; import { baseUrl } from "../../history";
type RequestHeaders = { [name: string]: string };
export class API { export class API {
invoke(method: string, url: string, data?: object) { invoke(method: string, url: string, data?: object, headers?: RequestHeaders) {
let targetURL = url; let targetURL = url;
if (targetURL[0] === "/") { if (targetURL[0] === "/") {
targetURL = targetURL.slice(1); targetURL = targetURL.slice(1);
} }
return request(method, targetURL) let req = request(method, targetURL);
if (headers) {
for (let k in headers) {
req.set(k, headers[k]);
}
}
return req
.send(data) .send(data)
.then((res) => res.body) .then((res) => res.body)
.catch((err) => { .catch((err) => {

View File

@@ -0,0 +1,88 @@
// 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/>.
import React, { Fragment, Suspense } from "react";
import ObjectBrowser from "../Console/ObjectBrowser/ObjectBrowser";
import LoadingComponent from "../../common/LoadingComponent";
import ObjectManager from "../Console/Common/ObjectManager/ObjectManager";
import { ApplicationLogo } from "mds";
import { Route, Routes } from "react-router-dom";
import { IAM_PAGES } from "../../common/SecureComponent/permissions";
import { resetSession } from "../Console/consoleSlice";
import { useAppDispatch } from "../../store";
import { resetSystem } from "../../systemSlice";
import { getLogoVar } from "../../config";
import ObjectManagerButton from "../Console/Common/ObjectManager/ObjectManagerButton";
import { Button } from "@mui/material";
const AnonymousAccess = () => {
const dispatch = useAppDispatch();
return (
<Fragment>
<div
style={{
background:
"linear-gradient(90deg, rgba(16,47,81,1) 0%, rgba(13,28,64,1) 100%)",
height: 100,
width: "100%",
alignItems: "center",
display: "flex",
paddingLeft: 16,
paddingRight: 16,
}}
>
<div style={{ width: 200, flexShrink: 1 }}>
<ApplicationLogo
applicationName={"console"}
subVariant={getLogoVar()}
inverse={true}
/>
</div>
<div style={{ flexGrow: 1 }}></div>
<div style={{ flexShrink: 1, display: "flex", flexDirection: "row" }}>
<Button
id={"go-to-login"}
variant={"text"}
onClick={() => {
dispatch(resetSession());
dispatch(resetSystem());
}}
style={{ color: "white" }}
>
Login
</Button>
<ObjectManagerButton />
</div>
</div>
<Suspense fallback={<LoadingComponent />}>
<ObjectManager />
</Suspense>
<Routes>
<Route
path={`${IAM_PAGES.OBJECT_BROWSER_VIEW}/*`}
element={
<Suspense fallback={<LoadingComponent />}>
<ObjectBrowser />
</Suspense>
}
/>
</Routes>
</Fragment>
);
};
export default AnonymousAccess;

View File

@@ -16,26 +16,15 @@
import React, { Fragment, useCallback, useEffect } from "react"; import React, { Fragment, useCallback, useEffect } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useLocation, useNavigate, useParams } from "react-router-dom"; import { useLocation, useParams } from "react-router-dom";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles"; import withStyles from "@mui/styles/withStyles";
import { Grid } from "@mui/material";
import { AppState, useAppDispatch } from "../../../../store"; import { AppState, useAppDispatch } from "../../../../store";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary"; import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import ListObjects from "../ListBuckets/Objects/ListObjects/ListObjects"; import ListObjects from "../ListBuckets/Objects/ListObjects/ListObjects";
import PageHeader from "../../Common/PageHeader/PageHeader"; import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
import { SettingsIcon } from "mds";
import { SecureComponent } from "../../../../common/SecureComponent";
import {
IAM_PAGES,
IAM_PERMISSIONS,
IAM_ROLES,
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import BackLink from "../../../../common/BackLink";
import { import {
newMessage, newMessage,
resetMessages, resetMessages,
@@ -50,17 +39,10 @@ import {
setLockingEnabled, setLockingEnabled,
setObjectDetailsView, setObjectDetailsView,
setRecords, setRecords,
setSearchObjects,
setSearchVersions,
setSelectedObjectView, setSelectedObjectView,
setSimplePathHandler, setSimplePathHandler,
setVersionsModeEnabled, setVersionsModeEnabled,
} from "../../ObjectBrowser/objectBrowserSlice"; } from "../../ObjectBrowser/objectBrowserSlice";
import SearchBox from "../../Common/SearchBox";
import { selFeatures } from "../../consoleSlice";
import AutoColorIcon from "../../Common/Components/AutoColorIcon";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import { Button } from "mds";
import hasPermission from "../../../../common/SecureComponent/accessControl"; import hasPermission from "../../../../common/SecureComponent/accessControl";
import { IMessageEvent } from "websocket"; import { IMessageEvent } from "websocket";
import { wsProtocol } from "../../../../utils/wsUtils"; import { wsProtocol } from "../../../../utils/wsUtils";
@@ -74,6 +56,7 @@ import { setErrorSnackMessage } from "../../../../systemSlice";
import api from "../../../../common/api"; import api from "../../../../common/api";
import { BucketObjectLocking, BucketVersioning } from "../types"; import { BucketObjectLocking, BucketVersioning } from "../types";
import { ErrorResponseHandler } from "../../../../common/types"; import { ErrorResponseHandler } from "../../../../common/types";
import OBHeader from "../../ObjectBrowser/OBHeader";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -145,7 +128,6 @@ const initWSConnection = (
const BrowserHandler = () => { const BrowserHandler = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate();
const params = useParams(); const params = useParams();
const location = useLocation(); const location = useLocation();
@@ -153,18 +135,6 @@ const BrowserHandler = () => {
(state: AppState) => state.objectBrowser.loadingVersioning (state: AppState) => state.objectBrowser.loadingVersioning
); );
const versionsMode = useSelector(
(state: AppState) => state.objectBrowser.versionsMode
);
const searchObjects = useSelector(
(state: AppState) => state.objectBrowser.searchObjects
);
const versionedFile = useSelector(
(state: AppState) => state.objectBrowser.versionedFile
);
const searchVersions = useSelector(
(state: AppState) => state.objectBrowser.searchVersions
);
const rewindEnabled = useSelector( const rewindEnabled = useSelector(
(state: AppState) => state.objectBrowser.rewind.rewindEnabled (state: AppState) => state.objectBrowser.rewind.rewindEnabled
); );
@@ -195,15 +165,14 @@ const BrowserHandler = () => {
const isOpeningOD = useSelector( const isOpeningOD = useSelector(
(state: AppState) => state.objectBrowser.isOpeningObjectDetail (state: AppState) => state.objectBrowser.isOpeningObjectDetail
); );
const anonymousMode = useSelector(
const features = useSelector(selFeatures); (state: AppState) => state.system.anonymousMode
);
const bucketName = params.bucketName || ""; const bucketName = params.bucketName || "";
const pathSegment = location.pathname.split(`/browser/${bucketName}/`); const pathSegment = location.pathname.split(`/browser/${bucketName}/`);
const internalPaths = pathSegment.length === 2 ? pathSegment[1] : ""; const internalPaths = pathSegment.length === 2 ? pathSegment[1] : "";
const obOnly = !!features?.includes("object-browser-only");
/*WS Request Handlers*/ /*WS Request Handlers*/
const onMessageCallBack = useCallback( const onMessageCallBack = useCallback(
(message: IMessageEvent) => { (message: IMessageEvent) => {
@@ -297,7 +266,6 @@ const BrowserHandler = () => {
const dupRequest = () => { const dupRequest = () => {
initWSRequest(path, date); initWSRequest(path, date);
}; };
initWSConnection(dupRequest, onMessageCallBack); initWSConnection(dupRequest, onMessageCallBack);
} }
}, },
@@ -377,10 +345,11 @@ const BrowserHandler = () => {
simplePath, simplePath,
]); ]);
const displayListObjects = hasPermission(bucketName, [ const displayListObjects =
IAM_SCOPES.S3_LIST_BUCKET, hasPermission(bucketName, [
IAM_SCOPES.S3_ALL_LIST_BUCKET, IAM_SCOPES.S3_LIST_BUCKET,
]); IAM_SCOPES.S3_ALL_LIST_BUCKET,
]) || anonymousMode;
// Common objects list // Common objects list
useEffect(() => { useEffect(() => {
@@ -408,7 +377,6 @@ const BrowserHandler = () => {
if (rewindEnabled && rewindDate) { if (rewindEnabled && rewindDate) {
requestDate = rewindDate; requestDate = rewindDate;
} }
initWSRequest(pathPrefix, requestDate); initWSRequest(pathPrefix, requestDate);
} else { } else {
dispatch(setLoadingObjects(false)); dispatch(setLoadingObjects(false));
@@ -429,7 +397,7 @@ const BrowserHandler = () => {
}, [internalPaths, dispatch]); }, [internalPaths, dispatch]);
useEffect(() => { useEffect(() => {
if (loadingVersioning) { if (loadingVersioning && !anonymousMode) {
if (displayListObjects) { if (displayListObjects) {
api api
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`) .invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
@@ -449,7 +417,13 @@ const BrowserHandler = () => {
dispatch(resetMessages()); dispatch(resetMessages());
} }
} }
}, [bucketName, loadingVersioning, dispatch, displayListObjects]); }, [
bucketName,
loadingVersioning,
dispatch,
displayListObjects,
anonymousMode,
]);
useEffect(() => { useEffect(() => {
if (loadingLocking) { if (loadingLocking) {
@@ -497,127 +471,10 @@ const BrowserHandler = () => {
} }
}, [bucketName, loadingLocking, dispatch, displayListObjects]); }, [bucketName, loadingLocking, dispatch, displayListObjects]);
const openBucketConfiguration = () => {
navigate(`/buckets/${bucketName}/admin`);
};
const configureBucketAllowed = hasPermission(bucketName, [
IAM_SCOPES.S3_GET_BUCKET_POLICY,
IAM_SCOPES.S3_PUT_BUCKET_POLICY,
IAM_SCOPES.S3_GET_BUCKET_VERSIONING,
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION,
IAM_SCOPES.S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION,
IAM_SCOPES.S3_DELETE_BUCKET,
IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS,
IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS,
IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_GET_LIFECYCLE_CONFIGURATION,
IAM_SCOPES.S3_PUT_LIFECYCLE_CONFIGURATION,
IAM_SCOPES.ADMIN_GET_BUCKET_QUOTA,
IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA,
IAM_SCOPES.S3_PUT_BUCKET_TAGGING,
IAM_SCOPES.S3_GET_BUCKET_TAGGING,
IAM_SCOPES.S3_LIST_BUCKET_VERSIONS,
IAM_SCOPES.S3_GET_BUCKET_POLICY_STATUS,
IAM_SCOPES.S3_DELETE_BUCKET_POLICY,
IAM_SCOPES.S3_GET_ACTIONS,
IAM_SCOPES.S3_PUT_ACTIONS,
]);
const searchBar = (
<Fragment>
{!versionsMode ? (
<SecureComponent
scopes={[IAM_SCOPES.S3_LIST_BUCKET, IAM_SCOPES.S3_ALL_LIST_BUCKET]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<SearchBox
placeholder={"Start typing to filter objects in the bucket"}
onChange={(value) => {
dispatch(setSearchObjects(value));
}}
value={searchObjects}
/>
</SecureComponent>
) : (
<Fragment>
<SearchBox
placeholder={`Start typing to filter versions of ${versionedFile}`}
onChange={(value) => {
dispatch(setSearchVersions(value));
}}
value={searchVersions}
/>
</Fragment>
)}
</Fragment>
);
return ( return (
<Fragment> <Fragment>
{!obOnly ? ( {!anonymousMode && <OBHeader bucketName={bucketName} />}
<PageHeader <ListObjects />
label={
<BackLink
label={"Object Browser"}
to={IAM_PAGES.OBJECT_BROWSER_VIEW}
/>
}
actions={
<SecureComponent
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<TooltipWrapper
tooltip={
configureBucketAllowed
? "Configure Bucket"
: "You do not have the required permissions to configure this bucket. Please contact your MinIO administrator to request " +
IAM_ROLES.BUCKET_ADMIN +
" permisions."
}
>
<Button
id={"configure-bucket-main"}
color="primary"
aria-label="Configure Bucket"
onClick={openBucketConfiguration}
icon={
<SettingsIcon
style={{ width: 20, height: 20, marginTop: -3 }}
/>
}
style={{
padding: "0 10px",
}}
/>
</TooltipWrapper>
</SecureComponent>
}
middleComponent={searchBar}
/>
) : (
<Grid
container
sx={{
padding: "20px 32px 0",
}}
>
<Grid>
<AutoColorIcon marginRight={30} marginTop={10} />
</Grid>
<Grid item xs>
{searchBar}
</Grid>
</Grid>
)}
<Grid>
<ListObjects />
</Grid>
</Fragment> </Fragment>
); );
}; };

View File

@@ -241,9 +241,6 @@ const BucketDetails = ({ classes }: IBucketDetailsProps) => {
<PageLayout className={classes.pageContainer}> <PageLayout className={classes.pageContainer}>
<Grid item xs={12}> <Grid item xs={12}>
<ScreenTitle <ScreenTitle
classes={{
screenTitle: classes.screenTitle,
}}
icon={ icon={
<Fragment> <Fragment>
<BucketsIcon width={40} /> <BucketsIcon width={40} />

View File

@@ -26,7 +26,16 @@ import { useSelector } from "react-redux";
import { useLocation, useNavigate, useParams } from "react-router-dom"; import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import { Button } from "mds"; import {
BucketsIcon,
Button,
DeleteIcon,
DownloadIcon,
HistoryIcon,
PreviewIcon,
RefreshIcon,
ShareIcon,
} from "mds";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
@@ -68,15 +77,6 @@ import {
SecureComponent, SecureComponent,
} from "../../../../../../common/SecureComponent"; } from "../../../../../../common/SecureComponent";
import withSuspense from "../../../../Common/Components/withSuspense"; import withSuspense from "../../../../Common/Components/withSuspense";
import {
BucketsIcon,
DownloadIcon,
PreviewIcon,
ShareIcon,
HistoryIcon,
RefreshIcon,
DeleteIcon,
} from "mds";
import UploadFilesButton from "../../UploadFilesButton"; import UploadFilesButton from "../../UploadFilesButton";
import DetailsListPanel from "./DetailsListPanel"; import DetailsListPanel from "./DetailsListPanel";
import ObjectDetailPanel from "./ObjectDetailPanel"; import ObjectDetailPanel from "./ObjectDetailPanel";
@@ -136,6 +136,8 @@ import {
openShare, openShare,
} from "../../../../ObjectBrowser/objectBrowserThunks"; } from "../../../../ObjectBrowser/objectBrowserThunks";
import FilterObjectsSB from "../../../../ObjectBrowser/FilterObjectsSB";
const DeleteMultipleObjects = withSuspense( const DeleteMultipleObjects = withSuspense(
React.lazy(() => import("./DeleteMultipleObjects")) React.lazy(() => import("./DeleteMultipleObjects"))
); );
@@ -158,12 +160,6 @@ const useStyles = makeStyles((theme: Theme) =>
minWidth: 5, minWidth: 5,
}, },
}, },
screenTitle: {
borderBottom: 0,
paddingTop: 0,
paddingLeft: 0,
paddingRight: 0,
},
...tableStyles, ...tableStyles,
...actionsTray, ...actionsTray,
...searchField, ...searchField,
@@ -174,7 +170,6 @@ const useStyles = makeStyles((theme: Theme) =>
}, },
screenTitleContainer: { screenTitleContainer: {
border: "#EAEDEE 1px solid", border: "#EAEDEE 1px solid",
padding: "0.8rem 15px 0",
}, },
labelStyle: { labelStyle: {
color: "#969FA8", color: "#969FA8",
@@ -194,6 +189,11 @@ const useStyles = makeStyles((theme: Theme) =>
display: "none", display: "none",
}, },
}, },
actionsSection: {
display: "flex",
justifyContent: "space-between",
width: "100%",
},
...objectBrowserExtras, ...objectBrowserExtras,
...objectBrowserCommon, ...objectBrowserCommon,
...containerForHeader(theme.spacing(4)), ...containerForHeader(theme.spacing(4)),
@@ -273,6 +273,9 @@ const ListObjects = () => {
const selectedBucket = useSelector( const selectedBucket = useSelector(
(state: AppState) => state.objectBrowser.selectedBucket (state: AppState) => state.objectBrowser.selectedBucket
); );
const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode
);
const loadingBucket = useSelector(selBucketDetailsLoading); const loadingBucket = useSelector(selBucketDetailsLoading);
const bucketInfo = useSelector(selBucketDetailsInfo); const bucketInfo = useSelector(selBucketDetailsInfo);
@@ -305,12 +308,13 @@ const ListObjects = () => {
IAM_SCOPES.S3_GET_ACTIONS, IAM_SCOPES.S3_GET_ACTIONS,
]); ]);
const canDelete = hasPermission(bucketName, [IAM_SCOPES.S3_DELETE_OBJECT]); const canDelete = hasPermission(bucketName, [IAM_SCOPES.S3_DELETE_OBJECT]);
const canUpload = hasPermission( const canUpload =
uploadPath, hasPermission(
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS], uploadPath,
true, [IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
true true,
); true
) || anonymousMode;
const displayDeleteObject = hasPermission(bucketName, [ const displayDeleteObject = hasPermission(bucketName, [
IAM_SCOPES.S3_DELETE_OBJECT, IAM_SCOPES.S3_DELETE_OBJECT,
@@ -365,7 +369,7 @@ const ListObjects = () => {
}, [selectedObjects]); }, [selectedObjects]);
useEffect(() => { useEffect(() => {
if (!quota) { if (!quota && !anonymousMode) {
api api
.invoke("GET", `/api/v1/buckets/${bucketName}/quota`) .invoke("GET", `/api/v1/buckets/${bucketName}/quota`)
.then((res: BucketQuota) => { .then((res: BucketQuota) => {
@@ -382,7 +386,7 @@ const ListObjects = () => {
setQuota(null); setQuota(null);
}); });
} }
}, [quota, bucketName]); }, [quota, bucketName, anonymousMode]);
useEffect(() => { useEffect(() => {
if (selectedObjects.length > 0) { if (selectedObjects.length > 0) {
@@ -408,7 +412,7 @@ const ListObjects = () => {
// bucket info // bucket info
useEffect(() => { useEffect(() => {
if (loadingBucket) { if (loadingBucket && !anonymousMode) {
api api
.invoke("GET", `/api/v1/buckets/${bucketName}`) .invoke("GET", `/api/v1/buckets/${bucketName}`)
.then((res: BucketInfo) => { .then((res: BucketInfo) => {
@@ -421,7 +425,7 @@ const ListObjects = () => {
dispatch(setErrorSnackMessage(err)); dispatch(setErrorSnackMessage(err));
}); });
} }
}, [bucketName, loadingBucket, dispatch]); }, [bucketName, loadingBucket, dispatch, anonymousMode]);
// Load retention Config // Load retention Config
@@ -538,6 +542,10 @@ const ListObjects = () => {
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
xhr.open("POST", uploadUrl, true); xhr.open("POST", uploadUrl, true);
if (anonymousMode) {
xhr.setRequestHeader("X-Anonymous", "1");
}
// xhr.setRequestHeader("X-Anonymous", "1");
const areMultipleFiles = files.length > 1; const areMultipleFiles = files.length > 1;
let errorMessage = `An error occurred while uploading the file${ let errorMessage = `An error occurred while uploading the file${
@@ -677,7 +685,7 @@ const ListObjects = () => {
upload(files, bucketName, pathPrefix, folderPath); upload(files, bucketName, pathPrefix, folderPath);
}, },
[bucketName, dispatch, simplePath] [bucketName, dispatch, simplePath, anonymousMode]
); );
const onDrop = useCallback( const onDrop = useCallback(
@@ -879,60 +887,67 @@ const ListObjects = () => {
}} }}
/> />
)} )}
<PageLayout variant={"full"}> <PageLayout variant={"full"}>
{anonymousMode && (
<div style={{ paddingBottom: 16 }}>
<FilterObjectsSB />
</div>
)}
<Grid item xs={12} className={classes.screenTitleContainer}> <Grid item xs={12} className={classes.screenTitleContainer}>
<ScreenTitle <ScreenTitle
className={classes.screenTitle}
icon={ icon={
<span className={classes.listIcon}> <span>
<BucketsIcon /> <BucketsIcon style={{ width: 30 }} />
</span> </span>
} }
title={<span className={classes.titleSpacer}>{bucketName}</span>} title={<span className={classes.titleSpacer}>{bucketName}</span>}
subTitle={ subTitle={
<Fragment> !anonymousMode ? (
<Grid item xs={12} className={classes.bucketDetails}> <Fragment>
<span className={classes.detailsSpacer}> <Grid item xs={12} className={classes.bucketDetails}>
Created on:&nbsp;&nbsp; <span className={classes.detailsSpacer}>
<strong> Created on:&nbsp;&nbsp;
{bucketInfo?.creation_date <strong>
? createdTime.toFormat( {bucketInfo?.creation_date
"ccc, LLL dd yyyy HH:mm:ss (ZZZZ)" ? createdTime.toFormat(
) "ccc, LLL dd yyyy HH:mm:ss (ZZZZ)"
: ""} )
</strong> : ""}
</span> </strong>
<span className={classes.detailsSpacer}> </span>
Access:&nbsp;&nbsp;&nbsp; <span className={classes.detailsSpacer}>
<strong>{bucketInfo?.access || ""}</strong> Access:&nbsp;&nbsp;&nbsp;
</span> <strong>{bucketInfo?.access || ""}</strong>
{bucketInfo && ( </span>
<Fragment> {bucketInfo && (
<span className={classes.detailsSpacer}> <Fragment>
{bucketInfo.size && ( <span className={classes.detailsSpacer}>
<Fragment>{niceBytesInt(bucketInfo.size)}</Fragment> {bucketInfo.size && (
)} <Fragment>{niceBytesInt(bucketInfo.size)}</Fragment>
{bucketInfo.size && quota && ( )}
<Fragment> / {niceBytesInt(quota.quota)}</Fragment> {bucketInfo.size && quota && (
)} <Fragment> / {niceBytesInt(quota.quota)}</Fragment>
{bucketInfo.size && bucketInfo.objects ? " - " : ""} )}
{bucketInfo.objects && ( {bucketInfo.size && bucketInfo.objects ? " - " : ""}
<Fragment> {bucketInfo.objects && (
{bucketInfo.objects}&nbsp;Object <Fragment>
{bucketInfo.objects && bucketInfo.objects !== 1 {bucketInfo.objects}&nbsp;Object
? "s" {bucketInfo.objects && bucketInfo.objects !== 1
: ""} ? "s"
</Fragment> : ""}
)} </Fragment>
</span> )}
</Fragment> </span>
)} </Fragment>
</Grid> )}
</Fragment> </Grid>
</Fragment>
) : null
} }
actions={ actions={
<Fragment> <div className={classes.actionsSection}>
<div className={classes.actionsSection}> {!anonymousMode && (
<TooltipWrapper tooltip={"Rewind Bucket"}> <TooltipWrapper tooltip={"Rewind Bucket"}>
<Button <Button
id={"rewind-objects-list"} id={"rewind-objects-list"}
@@ -970,61 +985,63 @@ const ListObjects = () => {
} }
/> />
</TooltipWrapper> </TooltipWrapper>
<TooltipWrapper tooltip={"Reload List"}> )}
<Button <TooltipWrapper tooltip={"Reload List"}>
id={"refresh-objects-list"} <Button
label={"Refresh"} id={"refresh-objects-list"}
icon={<RefreshIcon />} label={"Refresh"}
variant={"regular"} icon={<RefreshIcon />}
onClick={() => { variant={"regular"}
if (versionsMode) { onClick={() => {
dispatch(setLoadingVersions(true)); if (versionsMode) {
} else { dispatch(setLoadingVersions(true));
dispatch(resetMessages()); } else {
dispatch(setLoadingRecords(true)); dispatch(resetMessages());
dispatch(setLoadingObjects(true)); dispatch(setLoadingRecords(true));
} dispatch(setLoadingObjects(true));
}}
disabled={
!hasPermission(bucketName, [
IAM_SCOPES.S3_LIST_BUCKET,
IAM_SCOPES.S3_ALL_LIST_BUCKET,
]) || rewindEnabled
} }
/>
</TooltipWrapper>
<input
type="file"
multiple
onChange={handleUploadButton}
style={{ display: "none" }}
ref={fileUpload}
/>
<input
type="file"
multiple
onChange={handleUploadButton}
style={{ display: "none" }}
ref={folderUpload}
/>
<UploadFilesButton
bucketName={bucketName}
uploadPath={uploadPath.join("/")}
uploadFileFunction={(closeMenu) => {
if (fileUpload && fileUpload.current) {
fileUpload.current.click();
}
closeMenu();
}}
uploadFolderFunction={(closeMenu) => {
if (folderUpload && folderUpload.current) {
folderUpload.current.click();
}
closeMenu();
}} }}
disabled={
anonymousMode
? false
: !hasPermission(bucketName, [
IAM_SCOPES.S3_LIST_BUCKET,
IAM_SCOPES.S3_ALL_LIST_BUCKET,
]) || rewindEnabled
}
/> />
</div> </TooltipWrapper>
</Fragment> <input
type="file"
multiple
onChange={handleUploadButton}
style={{ display: "none" }}
ref={fileUpload}
/>
<input
type="file"
multiple
onChange={handleUploadButton}
style={{ display: "none" }}
ref={folderUpload}
/>
<UploadFilesButton
bucketName={bucketName}
uploadPath={uploadPath.join("/")}
uploadFileFunction={(closeMenu) => {
if (fileUpload && fileUpload.current) {
fileUpload.current.click();
}
closeMenu();
}}
uploadFolderFunction={(closeMenu) => {
if (folderUpload && folderUpload.current) {
folderUpload.current.click();
}
closeMenu();
}}
/>
</div>
} }
/> />
</Grid> </Grid>
@@ -1058,66 +1075,70 @@ const ListObjects = () => {
errorProps={{ disabled: true }} errorProps={{ disabled: true }}
> >
<Grid item xs={12} className={classes.fullContainer}> <Grid item xs={12} className={classes.fullContainer}>
<Grid item xs={12} className={classes.breadcrumbsContainer}> {!anonymousMode && (
<BrowserBreadcrumbs <Grid item xs={12} className={classes.breadcrumbsContainer}>
bucketName={bucketName} <BrowserBreadcrumbs
internalPaths={pageTitle} bucketName={bucketName}
additionalOptions={ internalPaths={pageTitle}
!isVersioned || rewindEnabled ? null : ( additionalOptions={
<div> !isVersioned || rewindEnabled ? null : (
<CheckboxWrapper <div>
name={"deleted_objects"} <CheckboxWrapper
id={"showDeletedObjects"} name={"deleted_objects"}
value={"deleted_on"} id={"showDeletedObjects"}
label={"Show deleted objects"} value={"deleted_on"}
onChange={setDeletedAction} label={"Show deleted objects"}
checked={showDeleted} onChange={setDeletedAction}
overrideLabelClasses={classes.labelStyle} checked={showDeleted}
className={classes.overrideShowDeleted} overrideLabelClasses={classes.labelStyle}
noTopMargin className={classes.overrideShowDeleted}
/> noTopMargin
</div> />
) </div>
} )
hidePathButton={false} }
/> hidePathButton={false}
</Grid> />
<ListObjectsTable /> </Grid>
)}
<ListObjectsTable internalPaths={selectedInternalPaths} />
</Grid> </Grid>
</SecureComponent> </SecureComponent>
)} )}
<SecureComponent {!anonymousMode && (
scopes={[ <SecureComponent
IAM_SCOPES.S3_LIST_BUCKET, scopes={[
IAM_SCOPES.S3_ALL_LIST_BUCKET, IAM_SCOPES.S3_LIST_BUCKET,
]} IAM_SCOPES.S3_ALL_LIST_BUCKET,
resource={bucketName} ]}
errorProps={{ disabled: true }} resource={bucketName}
> errorProps={{ disabled: true }}
<DetailsListPanel
open={detailsOpen}
closePanel={() => {
onClosePanel(false);
}}
className={`${versionsMode ? classes.hideListOnSmall : ""}`}
> >
{selectedObjects.length > 0 && ( <DetailsListPanel
<ActionsListSection open={detailsOpen}
items={multiActionButtons} closePanel={() => {
title={"Selected Objects:"} onClosePanel(false);
/> }}
)} className={`${versionsMode ? classes.hideListOnSmall : ""}`}
{selectedInternalPaths !== null && ( >
<ObjectDetailPanel {selectedObjects.length > 0 && (
internalPaths={selectedInternalPaths} <ActionsListSection
bucketName={bucketName} items={multiActionButtons}
onClosePanel={onClosePanel} title={"Selected Objects:"}
versioning={isVersioned} />
locking={lockingEnabled} )}
/> {selectedInternalPaths !== null && (
)} <ObjectDetailPanel
</DetailsListPanel> internalPaths={selectedInternalPaths}
</SecureComponent> bucketName={bucketName}
onClosePanel={onClosePanel}
versioning={isVersioned}
locking={lockingEnabled}
/>
)}
</DetailsListPanel>
</SecureComponent>
)}
</Grid> </Grid>
</div> </div>
</PageLayout> </PageLayout>

View File

@@ -43,6 +43,8 @@ import {
permissionTooltipHelper, permissionTooltipHelper,
} from "../../../../../../common/SecureComponent/permissions"; } from "../../../../../../common/SecureComponent/permissions";
import { hasPermission } from "../../../../../../common/SecureComponent"; import { hasPermission } from "../../../../../../common/SecureComponent";
import { downloadObject } from "../../../../ObjectBrowser/utils";
import { IFileInfo } from "../ObjectDetails/types";
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -77,7 +79,11 @@ const useStyles = makeStyles((theme: Theme) =>
}) })
); );
const ListObjectsTable = () => { interface IListObjectTable {
internalPaths: string | null;
}
const ListObjectsTable = ({ internalPaths }: IListObjectTable) => {
const classes = useStyles(); const classes = useStyles();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const params = useParams(); const params = useParams();
@@ -111,7 +117,9 @@ const ListObjectsTable = () => {
const selectedObjects = useSelector( const selectedObjects = useSelector(
(state: AppState) => state.objectBrowser.selectedObjects (state: AppState) => state.objectBrowser.selectedObjects
); );
const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode
);
const displayListObjects = hasPermission(bucketName, [ const displayListObjects = hasPermission(bucketName, [
IAM_SCOPES.S3_LIST_BUCKET, IAM_SCOPES.S3_LIST_BUCKET,
IAM_SCOPES.S3_ALL_LIST_BUCKET, IAM_SCOPES.S3_ALL_LIST_BUCKET,
@@ -141,29 +149,43 @@ const ListObjectsTable = () => {
payload = sortASC.reverse(); payload = sortASC.reverse();
} }
const openPath = (idElement: string) => { const openPath = (object: IFileInfo) => {
dispatch(setSelectedObjects([])); const idElement = object.name;
const newPath = `/browser/${bucketName}${ const newPath = `/browser/${bucketName}${
idElement ? `/${encodeURLString(idElement)}` : `` idElement ? `/${encodeURLString(idElement)}` : ``
}`; }`;
// for anonymous start download
if (anonymousMode && internalPaths !== null && !object.name.endsWith("/")) {
downloadObject(
dispatch,
bucketName,
`${encodeURLString(idElement)}`,
object
);
return;
}
dispatch(setSelectedObjects([]));
navigate(newPath); navigate(newPath);
dispatch(setObjectDetailsView(true)); if (!anonymousMode) {
dispatch(setLoadingVersions(true)); dispatch(setObjectDetailsView(true));
dispatch(setLoadingVersions(true));
dispatch(setIsOpeningOD(true));
}
dispatch( dispatch(
setSelectedObjectView( setSelectedObjectView(
`${idElement ? `${encodeURLString(idElement)}` : ``}` `${idElement ? `${encodeURLString(idElement)}` : ``}`
) )
); );
dispatch(setIsOpeningOD(true));
}; };
const tableActions: ItemActions[] = [ const tableActions: ItemActions[] = [
{ {
type: "view", type: "view",
label: "View", label: "View",
onClick: openPath, onClick: openPath,
sendOnlyId: true, sendOnlyId: false,
}, },
]; ];
@@ -218,9 +240,9 @@ const ListObjectsTable = () => {
obOnly ? "isEmbedded" : "" obOnly ? "isEmbedded" : ""
} ${detailsOpen ? "actionsPanelOpen" : ""}`} } ${detailsOpen ? "actionsPanelOpen" : ""}`}
selectedItems={selectedObjects} selectedItems={selectedObjects}
onSelect={selectListObjects} onSelect={!anonymousMode ? selectListObjects : undefined}
customEmptyMessage={ customEmptyMessage={
!displayListObjects !displayListObjects && !anonymousMode
? permissionTooltipHelper( ? permissionTooltipHelper(
[IAM_SCOPES.S3_LIST_BUCKET, IAM_SCOPES.S3_ALL_LIST_BUCKET], [IAM_SCOPES.S3_LIST_BUCKET, IAM_SCOPES.S3_ALL_LIST_BUCKET],
"view Objects in this bucket" "view Objects in this bucket"

View File

@@ -18,7 +18,21 @@ import React, { Fragment, useEffect, useState } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { withStyles } from "@mui/styles"; import { withStyles } from "@mui/styles";
import { Button } from "mds"; import {
Button,
DeleteIcon,
DownloadIcon,
InspectMenuIcon,
LegalHoldIcon,
Loader,
MetadataIcon,
ObjectInfoIcon,
PreviewIcon,
RetentionIcon,
ShareIcon,
TagsIcon,
VersionsIcon,
} from "mds";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import get from "lodash/get"; import get from "lodash/get";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
@@ -30,13 +44,11 @@ import {
textStyleUtils, textStyleUtils,
} from "../../../../Common/FormComponents/common/styleLibrary"; } from "../../../../Common/FormComponents/common/styleLibrary";
import { IFileInfo, MetadataResponse } from "../ObjectDetails/types"; import { IFileInfo, MetadataResponse } from "../ObjectDetails/types";
import { download, extensionPreview } from "../utils"; import { extensionPreview } from "../utils";
import { ErrorResponseHandler } from "../../../../../../common/types"; import { ErrorResponseHandler } from "../../../../../../common/types";
import { import {
decodeURLString, decodeURLString,
encodeURLString,
getClientOS,
niceBytes, niceBytes,
niceBytesInt, niceBytesInt,
niceDaysInt, niceDaysInt,
@@ -46,19 +58,6 @@ import {
permissionTooltipHelper, permissionTooltipHelper,
} from "../../../../../../common/SecureComponent/permissions"; } from "../../../../../../common/SecureComponent/permissions";
import { AppState, useAppDispatch } from "../../../../../../store"; import { AppState, useAppDispatch } from "../../../../../../store";
import {
DeleteIcon,
DownloadIcon,
LegalHoldIcon,
MetadataIcon,
ObjectInfoIcon,
PreviewIcon,
RetentionIcon,
ShareIcon,
TagsIcon,
VersionsIcon,
} from "mds";
import { InspectMenuIcon } from "mds";
import api from "../../../../../../common/api"; import api from "../../../../../../common/api";
import ShareFile from "../ObjectDetails/ShareFile"; import ShareFile from "../ObjectDetails/ShareFile";
import SetRetention from "../ObjectDetails/SetRetention"; import SetRetention from "../ObjectDetails/SetRetention";
@@ -74,25 +73,16 @@ import ActionsListSection from "./ActionsListSection";
import { displayFileIconName } from "./utils"; import { displayFileIconName } from "./utils";
import TagsModal from "../ObjectDetails/TagsModal"; import TagsModal from "../ObjectDetails/TagsModal";
import InspectObject from "./InspectObject"; import InspectObject from "./InspectObject";
import { Loader } from "mds";
import { selDistSet } from "../../../../../../systemSlice"; import { selDistSet } from "../../../../../../systemSlice";
import { import {
makeid,
storeCallForObjectWithID,
} from "../../../../ObjectBrowser/transferManager";
import {
cancelObjectInList,
completeObject,
failObject,
setLoadingObjectInfo, setLoadingObjectInfo,
setLoadingVersions, setLoadingVersions,
setNewObject,
setSelectedVersion, setSelectedVersion,
setVersionsModeEnabled, setVersionsModeEnabled,
updateProgress,
} from "../../../../ObjectBrowser/objectBrowserSlice"; } from "../../../../ObjectBrowser/objectBrowserSlice";
import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename"; import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper"; import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
import { downloadObject } from "../../../../ObjectBrowser/utils";
const styles = () => const styles = () =>
createStyles({ createStyles({
@@ -325,65 +315,6 @@ const ObjectDetailPanel = ({
setLongFileOpen(false); setLongFileOpen(false);
}; };
const downloadObject = (object: IFileInfo) => {
const identityDownload = encodeURLString(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
if (
object.name.length > 200 &&
getClientOS().toLowerCase().includes("win")
) {
setLongFileOpen(true);
return;
}
const ID = makeid(8);
const downloadCall = download(
bucketName,
internalPaths,
object.version_id,
parseInt(object.size || "0"),
null,
ID,
(progress) => {
dispatch(
updateProgress({
instanceID: identityDownload,
progress: progress,
})
);
},
() => {
dispatch(completeObject(identityDownload));
},
(msg: string) => {
dispatch(failObject({ instanceID: identityDownload, msg }));
},
() => {
dispatch(cancelObjectInList(identityDownload));
}
);
storeCallForObjectWithID(ID, downloadCall);
dispatch(
setNewObject({
ID,
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
failed: false,
cancelled: false,
errorMessage: "",
})
);
};
const closeDeleteModal = (closeAndReload: boolean) => { const closeDeleteModal = (closeAndReload: boolean) => {
setDeleteOpen(false); setDeleteOpen(false);
@@ -482,7 +413,7 @@ const ObjectDetailPanel = ({
const multiActionButtons = [ const multiActionButtons = [
{ {
action: () => { action: () => {
downloadObject(actualInfo); downloadObject(dispatch, bucketName, internalPaths, actualInfo);
}, },
label: "Download", label: "Download",
disabled: !!actualInfo.is_delete_marker || !canGetObject, disabled: !!actualInfo.is_delete_marker || !canGetObject,

View File

@@ -18,6 +18,7 @@ import { BucketObjectItem } from "./ListObjects/types";
import { IAllowResources } from "../../../types"; import { IAllowResources } from "../../../types";
import { encodeURLString } from "../../../../../common/utils"; import { encodeURLString } from "../../../../../common/utils";
import { removeTrace } from "../../../ObjectBrowser/transferManager"; import { removeTrace } from "../../../ObjectBrowser/transferManager";
import store from "../../../../../store";
export const download = ( export const download = (
bucketName: string, bucketName: string,
@@ -34,6 +35,8 @@ export const download = (
const anchor = document.createElement("a"); const anchor = document.createElement("a");
document.body.appendChild(anchor); document.body.appendChild(anchor);
let basename = document.baseURI.replace(window.location.origin, ""); let basename = document.baseURI.replace(window.location.origin, "");
const state = store.getState();
const anonymousMode = state.system.anonymousMode;
let path = `${ let path = `${
window.location.origin window.location.origin
@@ -48,6 +51,9 @@ export const download = (
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.open("GET", path, true); req.open("GET", path, true);
if (anonymousMode) {
req.setRequestHeader("X-Anonymous", "1");
}
req.addEventListener( req.addEventListener(
"progress", "progress",
function (evt) { function (evt) {

View File

@@ -14,21 +14,22 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment } from "react"; import React, { Fragment, useState } from "react";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import { Menu, MenuItem } from "@mui/material"; import { Menu, MenuItem } from "@mui/material";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles"; import withStyles from "@mui/styles/withStyles";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import { UploadFolderIcon, UploadIcon } from "mds"; import { Button, UploadFolderIcon, UploadIcon } from "mds";
import { import {
IAM_SCOPES, IAM_SCOPES,
permissionTooltipHelper, permissionTooltipHelper,
} from "../../../../common/SecureComponent/permissions"; } from "../../../../common/SecureComponent/permissions";
import { hasPermission } from "../../../../common/SecureComponent"; import { hasPermission } from "../../../../common/SecureComponent";
import { Button } from "mds";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper"; import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import { useSelector } from "react-redux";
import { AppState } from "../../../../store";
interface IUploadFilesButton { interface IUploadFilesButton {
uploadPath: string; uploadPath: string;
@@ -58,7 +59,10 @@ const UploadFilesButton = ({
uploadFolderFunction, uploadFolderFunction,
classes, classes,
}: IUploadFilesButton) => { }: IUploadFilesButton) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode
);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const openUploadMenu = Boolean(anchorEl); const openUploadMenu = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => { const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@@ -67,10 +71,11 @@ const UploadFilesButton = ({
setAnchorEl(null); setAnchorEl(null);
}; };
const uploadObjectAllowed = hasPermission(uploadPath, [ const uploadObjectAllowed =
IAM_SCOPES.S3_PUT_OBJECT, hasPermission(uploadPath, [
IAM_SCOPES.S3_PUT_ACTIONS, IAM_SCOPES.S3_PUT_OBJECT,
]); IAM_SCOPES.S3_PUT_ACTIONS,
]) || anonymousMode;
const uploadFolderAllowed = hasPermission( const uploadFolderAllowed = hasPermission(
bucketName, bucketName,
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS], [IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],

View File

@@ -456,12 +456,6 @@ export const objectBrowserCommon = {
flexDirection: "row" as const, flexDirection: "row" as const,
}, },
}, },
actionsSection: {
display: "flex",
justifyContent: "space-between",
width: "100%",
marginTop: 15,
},
}; };
// ** According to W3 spec, default minimum values for flex width flex-grow is "auto" (https://drafts.csswg.org/css-flexbox/#min-size-auto). So in this case we need to enforce the use of an absolute width. // ** According to W3 spec, default minimum values for flex width flex-grow is "auto" (https://drafts.csswg.org/css-flexbox/#min-size-auto). So in this case we need to enforce the use of an absolute width.

View File

@@ -27,6 +27,7 @@ import {
cleanList, cleanList,
deleteFromList, deleteFromList,
} from "../../ObjectBrowser/objectBrowserSlice"; } from "../../ObjectBrowser/objectBrowserSlice";
import clsx from "clsx";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -36,7 +37,7 @@ const styles = (theme: Theme) =>
backgroundColor: "#fff", backgroundColor: "#fff",
position: "absolute", position: "absolute",
right: 20, right: 20,
top: 60, top: 62,
width: 400, width: 400,
overflowY: "hidden", overflowY: "hidden",
overflowX: "hidden", overflowX: "hidden",
@@ -51,6 +52,9 @@ const styles = (theme: Theme) =>
minHeight: 400, minHeight: 400,
}, },
}, },
downloadContainerAnonymous: {
top: 70,
},
title: { title: {
fontSize: 16, fontSize: 16,
fontWeight: "bold", fontWeight: "bold",
@@ -94,13 +98,17 @@ const ObjectManager = ({ classes }: IObjectManager) => {
const managerOpen = useSelector( const managerOpen = useSelector(
(state: AppState) => state.objectBrowser.objectManager.managerOpen (state: AppState) => state.objectBrowser.objectManager.managerOpen
); );
const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode
);
return ( return (
<Fragment> <Fragment>
{managerOpen && ( {managerOpen && (
<div <div
className={`${classes.downloadContainer} ${ className={clsx(classes.downloadContainer, {
managerOpen ? "open" : "" [classes.downloadContainerAnonymous]: anonymousMode,
}`} open: managerOpen,
})}
> >
<div className={classes.cleanIcon}> <div className={classes.cleanIcon}>
<Tooltip title={"Clean Completed Objects"} placement="bottom-start"> <Tooltip title={"Clean Completed Objects"} placement="bottom-start">

View File

@@ -0,0 +1,107 @@
// 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/>.
import React, { Fragment, useEffect, useState } from "react";
import { Button, CircleIcon, ObjectManagerIcon } from "mds";
import { toggleList } from "../../ObjectBrowser/objectBrowserSlice";
import { AppState, useAppDispatch } from "../../../../store";
import { useSelector } from "react-redux";
import makeStyles from "@mui/styles/makeStyles";
const useStyles = makeStyles((theme) => ({
indicator: {
position: "absolute",
display: "block",
width: 15,
height: 15,
top: 0,
right: 4,
marginTop: 4,
transitionDuration: "0.2s",
color: "#32C787",
"& svg": {
width: 10,
height: 10,
top: "50%",
left: "50%",
transitionDuration: "0.2s",
},
"&.newItem": {
color: "#2781B0",
"& svg": {
width: 15,
height: 15,
},
},
},
}));
const ObjectManagerButton = () => {
const dispatch = useAppDispatch();
const classes = useStyles();
const managerObjects = useSelector(
(state: AppState) => state.objectBrowser.objectManager.objectsToManage
);
const newItems = useSelector(
(state: AppState) => state.objectBrowser.objectManager.newItems
);
const managerOpen = useSelector(
(state: AppState) => state.objectBrowser.objectManager.managerOpen
);
const [newObject, setNewObject] = useState<boolean>(false);
useEffect(() => {
if (managerObjects.length > 0 && !managerOpen) {
setNewObject(true);
setTimeout(() => {
setNewObject(false);
}, 300);
}
}, [managerObjects.length, managerOpen]);
return (
<Fragment>
{managerObjects && managerObjects.length > 0 && (
<Button
aria-label="Refresh List"
onClick={() => {
dispatch(toggleList());
}}
icon={
<Fragment>
<div
className={`${classes.indicator} ${newObject ? "newItem" : ""}`}
style={{
opacity: managerObjects.length > 0 && newItems ? 1 : 0,
}}
>
<CircleIcon />
</div>
<ObjectManagerIcon
style={{ width: 20, height: 20, marginTop: -2 }}
/>
</Fragment>
}
id="object-manager-toggle"
style={{ position: "relative", padding: "0 10px" }}
/>
)}
</Fragment>
);
};
export default ObjectManagerButton;

View File

@@ -33,8 +33,8 @@ const TrafficMonitor = () => {
(state: AppState) => state.objectBrowser.objectManager.objectsToManage (state: AppState) => state.objectBrowser.objectManager.objectsToManage
); );
const limitVars = useSelector( const limitVars = useSelector((state: AppState) =>
(state: AppState) => state.console.session.envConstants state.console.session ? state.console.session.envConstants : null
); );
const currentDIP = useSelector( const currentDIP = useSelector(

View File

@@ -14,20 +14,20 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react"; import React, { Fragment } from "react";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles"; import withStyles from "@mui/styles/withStyles";
import { AppState, useAppDispatch } from "../../../../store"; import { AppState } from "../../../../store";
import { CircleIcon, ObjectManagerIcon } from "mds"; import { ApplicationLogo } from "mds";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { toggleList } from "../../ObjectBrowser/objectBrowserSlice";
import { selFeatures } from "../../consoleSlice"; import { selFeatures } from "../../consoleSlice";
import { selDirectPVMode, selOpMode } from "../../../../systemSlice"; import { selDirectPVMode, selOpMode } from "../../../../systemSlice";
import { ApplicationLogo, Button } from "mds"; import ObjectManagerButton from "../ObjectManager/ObjectManagerButton";
import { getLogoVar } from "../../../../config";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -63,31 +63,6 @@ const styles = (theme: Theme) =>
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}, },
indicator: {
position: "absolute",
display: "block",
width: 15,
height: 15,
top: 0,
right: 4,
marginTop: 4,
transitionDuration: "0.2s",
color: "#32C787",
"& svg": {
width: 10,
height: 10,
top: "50%",
left: "50%",
transitionDuration: "0.2s",
},
"&.newItem": {
color: "#2781B0",
"& svg": {
width: 15,
height: 15,
},
},
},
}); });
interface IPageHeader { interface IPageHeader {
@@ -103,45 +78,12 @@ const PageHeader = ({
actions, actions,
middleComponent, middleComponent,
}: IPageHeader) => { }: IPageHeader) => {
const dispatch = useAppDispatch();
const sidebarOpen = useSelector( const sidebarOpen = useSelector(
(state: AppState) => state.system.sidebarOpen (state: AppState) => state.system.sidebarOpen
); );
const operatorMode = useSelector(selOpMode); const operatorMode = useSelector(selOpMode);
const directPVMode = useSelector(selDirectPVMode); const directPVMode = useSelector(selDirectPVMode);
const managerObjects = useSelector(
(state: AppState) => state.objectBrowser.objectManager.objectsToManage
);
const features = useSelector(selFeatures); const features = useSelector(selFeatures);
const managerOpen = useSelector(
(state: AppState) => state.objectBrowser.objectManager.managerOpen
);
const newItems = useSelector(
(state: AppState) => state.objectBrowser.objectManager.newItems
);
const licenseInfo = useSelector(
(state: AppState) => state?.system?.licenseInfo
);
const [newObject, setNewObject] = useState<boolean>(false);
const { plan = "" } = licenseInfo || {};
let logoPlan = "AGPL";
if (plan === "STANDARD" || plan === "ENTERPRISE") {
logoPlan = plan.toLowerCase();
}
useEffect(() => {
if (managerObjects.length > 0 && !managerOpen) {
setNewObject(true);
setTimeout(() => {
setNewObject(false);
}, 300);
}
}, [managerObjects.length, managerOpen]);
if (features.includes("hide-menu")) { if (features.includes("hide-menu")) {
return <Fragment />; return <Fragment />;
@@ -168,14 +110,7 @@ const PageHeader = ({
{!operatorMode && !directPVMode ? ( {!operatorMode && !directPVMode ? (
<ApplicationLogo <ApplicationLogo
applicationName={"console"} applicationName={"console"}
subVariant={ subVariant={getLogoVar()}
logoPlan as
| "AGPL"
| "simple"
| "standard"
| "enterprise"
| undefined
}
/> />
) : ( ) : (
<Fragment> <Fragment>
@@ -220,33 +155,7 @@ const PageHeader = ({
className={classes.rightMenu} className={classes.rightMenu}
> >
{actions && actions} {actions && actions}
{managerObjects && managerObjects.length > 0 && ( <ObjectManagerButton />
<Button
aria-label="Refresh List"
onClick={() => {
dispatch(toggleList());
}}
icon={
<Fragment>
<div
className={`${classes.indicator} ${
newObject ? "newItem" : ""
}`}
style={{
opacity: managerObjects.length > 0 && newItems ? 1 : 0,
}}
>
<CircleIcon />
</div>
<ObjectManagerIcon
style={{ width: 20, height: 20, marginTop: -2 }}
/>
</Fragment>
}
id="object-manager-toggle"
style={{ position: "relative", padding: "0 10px" }}
/>
)}
</Grid> </Grid>
</Grid> </Grid>
); );

View File

@@ -17,12 +17,9 @@
import React from "react"; import React from "react";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
interface IScreenTitle { interface IScreenTitle {
classes: any;
icon?: any; icon?: any;
title?: any; title?: any;
subTitle?: any; subTitle?: any;
@@ -30,80 +27,79 @@ interface IScreenTitle {
className?: any; className?: any;
} }
const styles = (theme: Theme) => const useStyles = makeStyles((theme: Theme) => ({
createStyles({ headerBarIcon: {
headerBarIcon: { marginRight: ".7rem",
marginRight: ".7rem", color: theme.palette.primary.main,
color: theme.palette.primary.main, "& .min-icon": {
"& .min-icon": { width: 44,
width: 44, height: 44,
height: 44,
},
"@media (max-width: 600px)": {
display: "none",
},
}, },
headerBarSubheader: { "@media (max-width: 600px)": {
color: "grey", display: "none",
"@media (max-width: 900px)": {
maxWidth: 200,
},
}, },
screenTitle: { },
display: "flex", headerBarSubheader: {
alignItems: "center", color: "grey",
justifyContent: "space-between", "@media (max-width: 900px)": {
padding: "1rem", maxWidth: 200,
},
},
stContainer: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: 8,
borderBottom: "1px solid #EAEAEA", borderBottom: "1px solid #EAEAEA",
"@media (max-width: 600px)": { "@media (max-width: 600px)": {
flexFlow: "column",
},
},
titleColumn: {
height: "auto",
justifyContent: "center",
display: "flex",
flexFlow: "column", flexFlow: "column",
alignItems: "flex-start",
"& h1": {
fontSize: 19,
},
}, },
leftItems: { },
display: "flex", titleColumn: {
alignItems: "center", height: "auto",
"@media (max-width: 600px)": { justifyContent: "center",
flexFlow: "column", display: "flex",
width: "100%", flexFlow: "column",
}, alignItems: "flex-start",
"& h1": {
fontSize: 19,
}, },
rightItems: { },
display: "flex", leftItems: {
alignItems: "center", display: "flex",
"& button": { alignItems: "center",
marginLeft: 8, "@media (max-width: 600px)": {
}, flexFlow: "column",
"@media (max-width: 600px)": { width: "100%",
width: "100%",
},
}, },
}); },
rightItems: {
display: "flex",
alignItems: "center",
"& button": {
marginLeft: 8,
},
"@media (max-width: 600px)": {
width: "100%",
},
},
}));
const ScreenTitle = ({ const ScreenTitle = ({
classes,
icon, icon,
title, title,
subTitle, subTitle,
actions, actions,
className, className,
}: IScreenTitle) => { }: IScreenTitle) => {
const classes = useStyles();
return ( return (
<Grid container> <Grid container>
<Grid <Grid
item item
xs={12} xs={12}
className={`${classes.screenTitle} ${className ? className : ""}`} className={`${classes.stContainer} ${className ? className : ""}`}
> >
<div className={classes.leftItems}> <div className={classes.leftItems}>
{icon ? <div className={classes.headerBarIcon}>{icon}</div> : null} {icon ? <div className={classes.headerBarIcon}>{icon}</div> : null}
@@ -119,4 +115,4 @@ const ScreenTitle = ({
); );
}; };
export default withStyles(styles)(ScreenTitle); export default ScreenTitle;

View File

@@ -20,13 +20,29 @@ import { useSelector } from "react-redux";
import CommandBar from "./CommandBar"; import CommandBar from "./CommandBar";
import { selFeatures } from "./consoleSlice"; import { selFeatures } from "./consoleSlice";
import TrafficMonitor from "./Common/ObjectManager/TrafficMonitor"; import TrafficMonitor from "./Common/ObjectManager/TrafficMonitor";
import { AppState } from "../../store";
import AnonymousAccess from "../AnonymousAccess/AnonymousAccess";
import { Fragment } from "react";
const ConsoleKBar = () => { const ConsoleKBar = () => {
const features = useSelector(selFeatures); const features = useSelector(selFeatures);
const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode
);
// if we are hiding the menu also disable the k-bar so just return console // if we are hiding the menu also disable the k-bar so just return console
if (features?.includes("hide-menu")) { if (features?.includes("hide-menu")) {
return <Console />; return <Console />;
} }
// for anonymous mode, we don't load Console, only AnonymousAccess
if (anonymousMode) {
return (
<Fragment>
<TrafficMonitor />
<AnonymousAccess />
</Fragment>
);
}
return ( return (
<KBarProvider <KBarProvider

View File

@@ -48,7 +48,6 @@ import ScreenTitle from "../Common/ScreenTitle/ScreenTitle";
import DeleteIDPConfigurationModal from "./DeleteIDPConfigurationModal"; import DeleteIDPConfigurationModal from "./DeleteIDPConfigurationModal";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import LabelValuePair from "../Common/UsageBarWrapper/LabelValuePair"; import LabelValuePair from "../Common/UsageBarWrapper/LabelValuePair";
type IDPConfigurationDetailsProps = { type IDPConfigurationDetailsProps = {
classes?: any; classes?: any;
formFields: object; formFields: object;
@@ -384,9 +383,6 @@ const IDPConfigurationDetails = ({
<PageLayout className={classes.pageContainer}> <PageLayout className={classes.pageContainer}>
<Box> <Box>
<ScreenTitle <ScreenTitle
classes={{
screenTitle: classes.screenTitle,
}}
icon={icon} icon={icon}
title={configurationName === "_" ? "Default" : configurationName} title={configurationName === "_" ? "Default" : configurationName}
actions={ actions={

View File

@@ -79,13 +79,17 @@ const BrowserBreadcrumbs = ({
const versionedFile = useSelector( const versionedFile = useSelector(
(state: AppState) => state.objectBrowser.versionedFile (state: AppState) => state.objectBrowser.versionedFile
); );
const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode
);
const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false); const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
const canCreatePath = hasPermission(bucketName, [ const canCreatePath =
IAM_SCOPES.S3_PUT_OBJECT, hasPermission(bucketName, [
IAM_SCOPES.S3_PUT_ACTIONS, IAM_SCOPES.S3_PUT_OBJECT,
]); IAM_SCOPES.S3_PUT_ACTIONS,
]) || anonymousMode;
let paths = internalPaths; let paths = internalPaths;
@@ -240,7 +244,7 @@ const BrowserBreadcrumbs = ({
onClick={() => { onClick={() => {
setCreateFolderOpen(true); setCreateFolderOpen(true);
}} }}
disabled={rewindEnabled || !canCreatePath} disabled={anonymousMode ? false : rewindEnabled || !canCreatePath}
icon={<NewPathIcon style={{ fill: "#969FA8" }} />} icon={<NewPathIcon style={{ fill: "#969FA8" }} />}
style={{ style={{
whiteSpace: "nowrap", whiteSpace: "nowrap",

View File

@@ -0,0 +1,39 @@
// 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/>.
import React from "react";
import { setSearchObjects } from "./objectBrowserSlice";
import SearchBox from "../Common/SearchBox";
import { AppState, useAppDispatch } from "../../../store";
import { useSelector } from "react-redux";
const FilterObjectsSB = () => {
const dispatch = useAppDispatch();
const searchObjects = useSelector(
(state: AppState) => state.objectBrowser.searchObjects
);
return (
<SearchBox
placeholder={"Start typing to filter objects in the bucket"}
onChange={(value) => {
dispatch(setSearchObjects(value));
}}
value={searchObjects}
/>
);
};
export default FilterObjectsSB;

View File

@@ -0,0 +1,178 @@
// 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/>.
import React, { Fragment } from "react";
import PageHeader from "../Common/PageHeader/PageHeader";
import BackLink from "../../../common/BackLink";
import {
IAM_PAGES,
IAM_PERMISSIONS,
IAM_ROLES,
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
import { SecureComponent } from "../../../common/SecureComponent";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
import { Button, SettingsIcon } from "mds";
import { Grid } from "@mui/material";
import AutoColorIcon from "../Common/Components/AutoColorIcon";
import { useSelector } from "react-redux";
import { selFeatures } from "../consoleSlice";
import hasPermission from "../../../common/SecureComponent/accessControl";
import { useNavigate } from "react-router-dom";
import SearchBox from "../Common/SearchBox";
import { setSearchVersions } from "./objectBrowserSlice";
import { AppState, useAppDispatch } from "../../../store";
import FilterObjectsSB from "./FilterObjectsSB";
interface IOBHeader {
bucketName: string;
}
const OBHeader = ({ bucketName }: IOBHeader) => {
const dispatch = useAppDispatch();
const features = useSelector(selFeatures);
const versionsMode = useSelector(
(state: AppState) => state.objectBrowser.versionsMode
);
const versionedFile = useSelector(
(state: AppState) => state.objectBrowser.versionedFile
);
const searchVersions = useSelector(
(state: AppState) => state.objectBrowser.searchVersions
);
const obOnly = !!features?.includes("object-browser-only");
const navigate = useNavigate();
const configureBucketAllowed = hasPermission(bucketName, [
IAM_SCOPES.S3_GET_BUCKET_POLICY,
IAM_SCOPES.S3_PUT_BUCKET_POLICY,
IAM_SCOPES.S3_GET_BUCKET_VERSIONING,
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION,
IAM_SCOPES.S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION,
IAM_SCOPES.S3_DELETE_BUCKET,
IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS,
IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS,
IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_GET_LIFECYCLE_CONFIGURATION,
IAM_SCOPES.S3_PUT_LIFECYCLE_CONFIGURATION,
IAM_SCOPES.ADMIN_GET_BUCKET_QUOTA,
IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA,
IAM_SCOPES.S3_PUT_BUCKET_TAGGING,
IAM_SCOPES.S3_GET_BUCKET_TAGGING,
IAM_SCOPES.S3_LIST_BUCKET_VERSIONS,
IAM_SCOPES.S3_GET_BUCKET_POLICY_STATUS,
IAM_SCOPES.S3_DELETE_BUCKET_POLICY,
IAM_SCOPES.S3_GET_ACTIONS,
IAM_SCOPES.S3_PUT_ACTIONS,
]);
const openBucketConfiguration = () => {
navigate(`/buckets/${bucketName}/admin`);
};
const searchBar = (
<Fragment>
{!versionsMode ? (
<SecureComponent
scopes={[IAM_SCOPES.S3_LIST_BUCKET, IAM_SCOPES.S3_ALL_LIST_BUCKET]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<FilterObjectsSB />
</SecureComponent>
) : (
<Fragment>
<SearchBox
placeholder={`Start typing to filter versions of ${versionedFile}`}
onChange={(value) => {
dispatch(setSearchVersions(value));
}}
value={searchVersions}
/>
</Fragment>
)}
</Fragment>
);
return (
<Fragment>
{!obOnly ? (
<PageHeader
label={
<BackLink
label={"Object Browser"}
to={IAM_PAGES.OBJECT_BROWSER_VIEW}
/>
}
actions={
<SecureComponent
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<TooltipWrapper
tooltip={
configureBucketAllowed
? "Configure Bucket"
: "You do not have the required permissions to configure this bucket. Please contact your MinIO administrator to request " +
IAM_ROLES.BUCKET_ADMIN +
" permisions."
}
>
<Button
id={"configure-bucket-main"}
color="primary"
aria-label="Configure Bucket"
onClick={openBucketConfiguration}
icon={
<SettingsIcon
style={{ width: 20, height: 20, marginTop: -3 }}
/>
}
style={{
padding: "0 10px",
}}
/>
</TooltipWrapper>
</SecureComponent>
}
middleComponent={searchBar}
/>
) : (
<Grid
container
sx={{
padding: "20px 32px 0",
}}
>
<Grid>
<AutoColorIcon marginRight={30} marginTop={10} />
</Grid>
<Grid item xs>
{searchBar}
</Grid>
</Grid>
)}
</Fragment>
);
};
export default OBHeader;

View File

@@ -71,6 +71,7 @@ const initialState: ObjectBrowserState = {
unit: "", unit: "",
validity: 0, validity: 0,
}, },
longFileOpen: false,
}; };
export const objectBrowserSlice = createSlice({ export const objectBrowserSlice = createSlice({
@@ -356,6 +357,9 @@ export const objectBrowserSlice = createSlice({
setSelectedBucket: (state, action: PayloadAction<string>) => { setSelectedBucket: (state, action: PayloadAction<string>) => {
state.selectedBucket = action.payload; state.selectedBucket = action.payload;
}, },
setLongFileOpen: (state, action: PayloadAction<boolean>) => {
state.longFileOpen = action.payload;
},
}, },
}); });
export const { export const {
@@ -401,6 +405,7 @@ export const {
setIsOpeningOD, setIsOpeningOD,
setRetentionConfig, setRetentionConfig,
setSelectedBucket, setSelectedBucket,
setLongFileOpen,
} = objectBrowserSlice.actions; } = objectBrowserSlice.actions;
export default objectBrowserSlice.reducer; export default objectBrowserSlice.reducer;

View File

@@ -93,6 +93,7 @@ export interface ObjectBrowserState {
shareFileModalOpen: boolean; shareFileModalOpen: boolean;
isOpeningObjectDetail: boolean; isOpeningObjectDetail: boolean;
retentionConfig: IRetentionConfig | null; retentionConfig: IRetentionConfig | null;
longFileOpen: boolean;
} }
export interface ObjectManager { export interface ObjectManager {

View File

@@ -0,0 +1,90 @@
// 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/>.
import { IFileInfo } from "../Buckets/ListBuckets/Objects/ObjectDetails/types";
import { encodeURLString, getClientOS } from "../../../common/utils";
import { makeid, storeCallForObjectWithID } from "./transferManager";
import { download } from "../Buckets/ListBuckets/Objects/utils";
import {
cancelObjectInList,
completeObject,
failObject,
setLongFileOpen,
setNewObject,
updateProgress,
} from "./objectBrowserSlice";
import { AppDispatch } from "../../../store";
export const downloadObject = (
dispatch: AppDispatch,
bucketName: string,
internalPaths: string,
object: IFileInfo
) => {
const identityDownload = encodeURLString(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
if (object.name.length > 200 && getClientOS().toLowerCase().includes("win")) {
dispatch(setLongFileOpen(true));
return;
}
const ID = makeid(8);
const downloadCall = download(
bucketName,
internalPaths,
object.version_id,
parseInt(object.size || "0"),
null,
ID,
(progress) => {
dispatch(
updateProgress({
instanceID: identityDownload,
progress: progress,
})
);
},
() => {
dispatch(completeObject(identityDownload));
},
(msg: string) => {
dispatch(failObject({ instanceID: identityDownload, msg }));
},
() => {
dispatch(cancelObjectInList(identityDownload));
}
);
storeCallForObjectWithID(ID, downloadCall);
dispatch(
setNewObject({
ID,
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
failed: false,
cancelled: false,
errorMessage: "",
})
);
};

View File

@@ -51,6 +51,7 @@ export const consoleSlice = createSlice({
export const { saveSessionResponse, resetSession } = consoleSlice.actions; export const { saveSessionResponse, resetSession } = consoleSlice.actions;
export const selSession = (state: AppState) => state.console.session; export const selSession = (state: AppState) => state.console.session;
export const selFeatures = (state: AppState) => state.console.session.features; export const selFeatures = (state: AppState) =>
state.console.session ? state.console.session.features : [];
export default consoleSlice.reducer; export default consoleSlice.reducer;

View File

@@ -43,6 +43,7 @@ export interface SystemState {
siteReplicationInfo: SRInfoStateType; siteReplicationInfo: SRInfoStateType;
licenseInfo: null | SubnetInfo; licenseInfo: null | SubnetInfo;
overrideStyles: null | IEmbeddedCustomStyles; overrideStyles: null | IEmbeddedCustomStyles;
anonymousMode: boolean;
} }
const initialState: SystemState = { const initialState: SystemState = {
@@ -72,6 +73,7 @@ const initialState: SystemState = {
distributedSetup: false, distributedSetup: false,
licenseInfo: null, licenseInfo: null,
overrideStyles: null, overrideStyles: null,
anonymousMode: false,
}; };
export const systemSlice = createSlice({ export const systemSlice = createSlice({
@@ -159,6 +161,13 @@ export const systemSlice = createSlice({
) => { ) => {
state.overrideStyles = action.payload; state.overrideStyles = action.payload;
}, },
setAnonymousMode: (state) => {
state.anonymousMode = true;
state.loggedIn = true;
},
resetSystem: () => {
return initialState;
},
}, },
}); });
@@ -181,6 +190,8 @@ export const {
setSiteReplicationInfo, setSiteReplicationInfo,
setLicenseInfo, setLicenseInfo,
setOverrideStyles, setOverrideStyles,
setAnonymousMode,
resetSystem,
} = systemSlice.actions; } = systemSlice.actions;
export const selDistSet = (state: AppState) => state.system.distributedSetup; export const selDistSet = (state: AppState) => state.system.distributedSetup;

View File

@@ -369,6 +369,9 @@ func NewConsoleCredentials(accessKey, secretKey, location string) (*credentials.
// getConsoleCredentialsFromSession returns the *consoleCredentials.Login associated to the // getConsoleCredentialsFromSession returns the *consoleCredentials.Login associated to the
// provided session token, this is useful for running the Expire() or IsExpired() operations // provided session token, this is useful for running the Expire() or IsExpired() operations
func getConsoleCredentialsFromSession(claims *models.Principal) *credentials.Credentials { func getConsoleCredentialsFromSession(claims *models.Principal) *credentials.Credentials {
if claims == nil {
return credentials.NewStaticV4("", "", "")
}
return credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken) return credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken)
} }

View File

@@ -83,6 +83,9 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
api.KeyAuth = func(token string, scopes []string) (*models.Principal, error) { api.KeyAuth = func(token string, scopes []string) (*models.Principal, error) {
// we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt // we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt
// was generated and signed by us in the first place // was generated and signed by us in the first place
if token == "Anonymous" {
return &models.Principal{}, nil
}
claims, err := auth.ParseClaimsFromToken(token) claims, err := auth.ParseClaimsFromToken(token)
if err != nil { if err != nil {
api.Logger("Unable to validate the session token %s: %v", token, err) api.Logger("Unable to validate the session token %s: %v", token, err)
@@ -98,6 +101,9 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
CustomStyleOb: claims.CustomStyleOB, CustomStyleOb: claims.CustomStyleOB,
}, nil }, nil
} }
api.AnonymousAuth = func(s string) (*models.Principal, error) {
return &models.Principal{}, nil
}
// Register login handlers // Register login handlers
registerLoginHandlers(api) registerLoginHandlers(api)
@@ -291,6 +297,8 @@ func AuthenticationMiddleware(next http.Handler) http.Handler {
// handle it appropriately. // handle it appropriately.
if len(sessionToken) > 0 { if len(sessionToken) > 0 {
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", string(sessionToken))) r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", string(sessionToken)))
} else {
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "Anonymous"))
} }
ctx := r.Context() ctx := r.Context()
claims, _ := auth.ParseClaimsFromToken(string(sessionToken)) claims, _ := auth.ParseClaimsFromToken(string(sessionToken))

View File

@@ -1458,6 +1458,14 @@ func init() {
}, },
"/buckets/{bucket_name}/objects": { "/buckets/{bucket_name}/objects": {
"get": { "get": {
"security": [
{
"key": []
},
{
"anonymous": []
}
],
"tags": [ "tags": [
"Object" "Object"
], ],
@@ -1489,6 +1497,12 @@ func init() {
"type": "boolean", "type": "boolean",
"name": "with_metadata", "name": "with_metadata",
"in": "query" "in": "query"
},
{
"type": "integer",
"format": "int32",
"name": "limit",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -1566,6 +1580,14 @@ func init() {
}, },
"/buckets/{bucket_name}/objects/download": { "/buckets/{bucket_name}/objects/download": {
"get": { "get": {
"security": [
{
"key": []
},
{
"anonymous": []
}
],
"produces": [ "produces": [
"application/octet-stream" "application/octet-stream"
], ],
@@ -1930,6 +1952,14 @@ func init() {
}, },
"/buckets/{bucket_name}/objects/upload": { "/buckets/{bucket_name}/objects/upload": {
"post": { "post": {
"security": [
{
"key": []
},
{
"anonymous": []
}
],
"consumes": [ "consumes": [
"multipart/form-data" "multipart/form-data"
], ],
@@ -8469,6 +8499,11 @@ func init() {
} }
}, },
"securityDefinitions": { "securityDefinitions": {
"anonymous": {
"type": "apiKey",
"name": "X-Anonymous",
"in": "header"
},
"key": { "key": {
"type": "oauth2", "type": "oauth2",
"flow": "accessCode", "flow": "accessCode",
@@ -9906,6 +9941,14 @@ func init() {
}, },
"/buckets/{bucket_name}/objects": { "/buckets/{bucket_name}/objects": {
"get": { "get": {
"security": [
{
"key": []
},
{
"anonymous": []
}
],
"tags": [ "tags": [
"Object" "Object"
], ],
@@ -9937,6 +9980,12 @@ func init() {
"type": "boolean", "type": "boolean",
"name": "with_metadata", "name": "with_metadata",
"in": "query" "in": "query"
},
{
"type": "integer",
"format": "int32",
"name": "limit",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -10014,6 +10063,14 @@ func init() {
}, },
"/buckets/{bucket_name}/objects/download": { "/buckets/{bucket_name}/objects/download": {
"get": { "get": {
"security": [
{
"key": []
},
{
"anonymous": []
}
],
"produces": [ "produces": [
"application/octet-stream" "application/octet-stream"
], ],
@@ -10378,6 +10435,14 @@ func init() {
}, },
"/buckets/{bucket_name}/objects/upload": { "/buckets/{bucket_name}/objects/upload": {
"post": { "post": {
"security": [
{
"key": []
},
{
"anonymous": []
}
],
"consumes": [ "consumes": [
"multipart/form-data" "multipart/form-data"
], ],
@@ -17043,6 +17108,11 @@ func init() {
} }
}, },
"securityDefinitions": { "securityDefinitions": {
"anonymous": {
"type": "apiKey",
"name": "X-Anonymous",
"in": "header"
},
"key": { "key": {
"type": "oauth2", "type": "oauth2",
"flow": "accessCode", "flow": "accessCode",

View File

@@ -534,6 +534,10 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
return middleware.NotImplemented("operation user.UpdateUserInfo has not yet been implemented") return middleware.NotImplemented("operation user.UpdateUserInfo has not yet been implemented")
}), }),
// Applies when the "X-Anonymous" header is set
AnonymousAuth: func(token string) (*models.Principal, error) {
return nil, errors.NotImplemented("api key auth (anonymous) X-Anonymous from header param [X-Anonymous] has not yet been implemented")
},
KeyAuth: func(token string, scopes []string) (*models.Principal, error) { KeyAuth: func(token string, scopes []string) (*models.Principal, error) {
return nil, errors.NotImplemented("oauth2 bearer auth (key) has not yet been implemented") return nil, errors.NotImplemented("oauth2 bearer auth (key) has not yet been implemented")
}, },
@@ -584,6 +588,10 @@ type ConsoleAPI struct {
// - application/json // - application/json
JSONProducer runtime.Producer JSONProducer runtime.Producer
// AnonymousAuth registers a function that takes a token and returns a principal
// it performs authentication based on an api key X-Anonymous provided in the header
AnonymousAuth func(string) (*models.Principal, error)
// KeyAuth registers a function that takes an access token and a collection of required scopes and returns a principal // KeyAuth registers a function that takes an access token and a collection of required scopes and returns a principal
// it performs authentication based on an oauth2 bearer token provided in the request // it performs authentication based on an oauth2 bearer token provided in the request
KeyAuth func(string, []string) (*models.Principal, error) KeyAuth func(string, []string) (*models.Principal, error)
@@ -975,6 +983,9 @@ func (o *ConsoleAPI) Validate() error {
unregistered = append(unregistered, "JSONProducer") unregistered = append(unregistered, "JSONProducer")
} }
if o.AnonymousAuth == nil {
unregistered = append(unregistered, "XAnonymousAuth")
}
if o.KeyAuth == nil { if o.KeyAuth == nil {
unregistered = append(unregistered, "KeyAuth") unregistered = append(unregistered, "KeyAuth")
} }
@@ -1444,6 +1455,12 @@ func (o *ConsoleAPI) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) m
result := make(map[string]runtime.Authenticator) result := make(map[string]runtime.Authenticator)
for name := range schemes { for name := range schemes {
switch name { switch name {
case "anonymous":
scheme := schemes[name]
result[name] = o.APIKeyAuthenticator(scheme.Name, scheme.In, func(token string) (interface{}, error) {
return o.AnonymousAuth(token)
})
case "key": case "key":
result[name] = o.BearerAuthenticator(name, func(token string, scopes []string) (interface{}, error) { result[name] = o.BearerAuthenticator(name, func(token string, scopes []string) (interface{}, error) {
return o.KeyAuth(token, scopes) return o.KeyAuth(token, scopes)

View File

@@ -57,6 +57,10 @@ type ListObjectsParams struct {
/* /*
In: query In: query
*/ */
Limit *int32
/*
In: query
*/
Prefix *string Prefix *string
/* /*
In: query In: query
@@ -88,6 +92,11 @@ func (o *ListObjectsParams) BindRequest(r *http.Request, route *middleware.Match
res = append(res, err) res = append(res, err)
} }
qLimit, qhkLimit, _ := qs.GetOK("limit")
if err := o.bindLimit(qLimit, qhkLimit, route.Formats); err != nil {
res = append(res, err)
}
qPrefix, qhkPrefix, _ := qs.GetOK("prefix") qPrefix, qhkPrefix, _ := qs.GetOK("prefix")
if err := o.bindPrefix(qPrefix, qhkPrefix, route.Formats); err != nil { if err := o.bindPrefix(qPrefix, qhkPrefix, route.Formats); err != nil {
res = append(res, err) res = append(res, err)
@@ -127,6 +136,29 @@ func (o *ListObjectsParams) bindBucketName(rawData []string, hasKey bool, format
return nil return nil
} }
// bindLimit binds and validates parameter Limit from query.
func (o *ListObjectsParams) bindLimit(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
}
value, err := swag.ConvertInt32(raw)
if err != nil {
return errors.InvalidType("limit", "query", "int32", raw)
}
o.Limit = &value
return nil
}
// bindPrefix binds and validates parameter Prefix from query. // bindPrefix binds and validates parameter Prefix from query.
func (o *ListObjectsParams) bindPrefix(rawData []string, hasKey bool, formats strfmt.Registry) error { func (o *ListObjectsParams) bindPrefix(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string var raw string

View File

@@ -35,6 +35,7 @@ import (
type ListObjectsURL struct { type ListObjectsURL struct {
BucketName string BucketName string
Limit *int32
Prefix *string Prefix *string
Recursive *bool Recursive *bool
WithMetadata *bool WithMetadata *bool
@@ -81,6 +82,14 @@ func (o *ListObjectsURL) Build() (*url.URL, error) {
qs := make(url.Values) qs := make(url.Values)
var limitQ string
if o.Limit != nil {
limitQ = swag.FormatInt32(*o.Limit)
}
if limitQ != "" {
qs.Set("limit", limitQ)
}
var prefixQ string var prefixQ string
if o.Prefix != nil { if o.Prefix != nil {
prefixQ = *o.Prefix prefixQ = *o.Prefix

View File

@@ -205,7 +205,16 @@ func getListObjectsResponse(session *models.Principal, params objectApi.ListObje
// defining the client to be used // defining the client to be used
minioClient := minioClient{client: mClient} minioClient := minioClient{client: mClient}
objs, err := listBucketObjects(ctx, minioClient, params.BucketName, prefix, recursive, withVersions, withMetadata) objs, err := listBucketObjects(ListObjectsOpts{
ctx: ctx,
client: minioClient,
bucketName: params.BucketName,
prefix: prefix,
recursive: recursive,
withVersions: withVersions,
withMetadata: withMetadata,
limit: params.Limit,
})
if err != nil { if err != nil {
return nil, ErrorWithContext(ctx, err) return nil, ErrorWithContext(ctx, err)
} }
@@ -217,20 +226,35 @@ func getListObjectsResponse(session *models.Principal, params objectApi.ListObje
return resp, nil return resp, nil
} }
type ListObjectsOpts struct {
ctx context.Context
client MinioClient
bucketName string
prefix string
recursive bool
withVersions bool
withMetadata bool
limit *int32
}
// listBucketObjects gets an array of objects in a bucket // listBucketObjects gets an array of objects in a bucket
func listBucketObjects(ctx context.Context, client MinioClient, bucketName string, prefix string, recursive, withVersions bool, withMetadata bool) ([]*models.BucketObject, error) { func listBucketObjects(listOpts ListObjectsOpts) ([]*models.BucketObject, error) {
var objects []*models.BucketObject var objects []*models.BucketObject
opts := minio.ListObjectsOptions{ opts := minio.ListObjectsOptions{
Prefix: prefix, Prefix: listOpts.prefix,
Recursive: recursive, Recursive: listOpts.recursive,
WithVersions: withVersions, WithVersions: listOpts.withVersions,
WithMetadata: withMetadata, WithMetadata: listOpts.withMetadata,
MaxKeys: 100, MaxKeys: 100,
} }
if withMetadata { if listOpts.withMetadata {
opts.MaxKeys = 1 opts.MaxKeys = 1
} }
for lsObj := range client.listObjects(ctx, bucketName, opts) { if listOpts.limit != nil {
opts.MaxKeys = int(*listOpts.limit)
}
var totalObjs int32
for lsObj := range listOpts.client.listObjects(listOpts.ctx, listOpts.bucketName, opts) {
if lsObj.Err != nil { if lsObj.Err != nil {
return nil, lsObj.Err return nil, lsObj.Err
} }
@@ -248,37 +272,44 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin
Etag: lsObj.ETag, Etag: lsObj.ETag,
} }
// only if single object with or without versions; get legalhold, retention and tags // only if single object with or without versions; get legalhold, retention and tags
if !lsObj.IsDeleteMarker && prefix != "" && !strings.HasSuffix(prefix, "/") { if !lsObj.IsDeleteMarker && listOpts.prefix != "" && !strings.HasSuffix(listOpts.prefix, "/") {
// Add Legal Hold Status if available // Add Legal Hold Status if available
legalHoldStatus, err := client.getObjectLegalHold(ctx, bucketName, lsObj.Key, minio.GetObjectLegalHoldOptions{VersionID: lsObj.VersionID}) legalHoldStatus, err := listOpts.client.getObjectLegalHold(listOpts.ctx, listOpts.bucketName, lsObj.Key, minio.GetObjectLegalHoldOptions{VersionID: lsObj.VersionID})
if err != nil { if err != nil {
errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError()) errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" { if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" {
ErrorWithContext(ctx, fmt.Errorf("error getting legal hold status for %s : %v", lsObj.VersionID, err)) ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting legal hold status for %s : %v", lsObj.VersionID, err))
} }
} else if legalHoldStatus != nil { } else if legalHoldStatus != nil {
obj.LegalHoldStatus = string(*legalHoldStatus) obj.LegalHoldStatus = string(*legalHoldStatus)
} }
// Add Retention Status if available // Add Retention Status if available
retention, retUntilDate, err := client.getObjectRetention(ctx, bucketName, lsObj.Key, lsObj.VersionID) retention, retUntilDate, err := listOpts.client.getObjectRetention(listOpts.ctx, listOpts.bucketName, lsObj.Key, lsObj.VersionID)
if err != nil { if err != nil {
errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError()) errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" { if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" {
ErrorWithContext(ctx, fmt.Errorf("error getting retention status for %s : %v", lsObj.VersionID, err)) ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting retention status for %s : %v", lsObj.VersionID, err))
} }
} else if retention != nil && retUntilDate != nil { } else if retention != nil && retUntilDate != nil {
date := *retUntilDate date := *retUntilDate
obj.RetentionMode = string(*retention) obj.RetentionMode = string(*retention)
obj.RetentionUntilDate = date.Format(time.RFC3339) obj.RetentionUntilDate = date.Format(time.RFC3339)
} }
tags, err := client.getObjectTagging(ctx, bucketName, lsObj.Key, minio.GetObjectTaggingOptions{VersionID: lsObj.VersionID}) objTags, err := listOpts.client.getObjectTagging(listOpts.ctx, listOpts.bucketName, lsObj.Key, minio.GetObjectTaggingOptions{VersionID: lsObj.VersionID})
if err != nil { if err != nil {
ErrorWithContext(ctx, fmt.Errorf("error getting object tags for %s : %v", lsObj.VersionID, err)) ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting object tags for %s : %v", lsObj.VersionID, err))
} else { } else {
obj.Tags = tags.ToMap() obj.Tags = objTags.ToMap()
} }
} }
objects = append(objects, obj) objects = append(objects, obj)
totalObjs++
if listOpts.limit != nil {
if totalObjs >= *listOpts.limit {
break
}
}
} }
return objects, nil return objects, nil
} }
@@ -499,7 +530,15 @@ func getDownloadFolderResponse(session *models.Principal, params objectApi.Downl
return nil, ErrorWithContext(ctx, err) return nil, ErrorWithContext(ctx, err)
} }
minioClient := minioClient{client: mClient} minioClient := minioClient{client: mClient}
objects, err := listBucketObjects(ctx, minioClient, params.BucketName, prefix, true, false, false) objects, err := listBucketObjects(ListObjectsOpts{
ctx: ctx,
client: minioClient,
bucketName: params.BucketName,
prefix: prefix,
recursive: true,
withVersions: false,
withMetadata: false,
})
if err != nil { if err != nil {
return nil, ErrorWithContext(ctx, err) return nil, ErrorWithContext(ctx, err)
} }

View File

@@ -121,6 +121,7 @@ func Test_listObjects(t *testing.T) {
recursive bool recursive bool
withVersions bool withVersions bool
withMetadata bool withMetadata bool
limit *int32
listFunc func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo listFunc func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
objectLegalHoldFunc func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) objectLegalHoldFunc func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error)
objectRetentionFunc func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) objectRetentionFunc func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error)
@@ -553,6 +554,150 @@ func Test_listObjects(t *testing.T) {
}, },
wantError: nil, wantError: nil,
}, },
{
test: "Return objects",
args: args{
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
withVersions: false,
withMetadata: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
defer close(objectStatCh)
for _, bucket := range []minio.ObjectInfo{
{
Key: "obj1",
LastModified: t1,
Size: int64(1024),
ContentType: "content",
},
{
Key: "obj2",
LastModified: t1,
Size: int64(512),
ContentType: "content",
},
} {
objectStatCh <- bucket
}
}(objectStatCh)
return objectStatCh
},
objectLegalHoldFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
s := minio.LegalHoldEnabled
return &s, nil
},
objectRetentionFunc: func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
m := minio.Governance
return &m, &tretention, nil
},
objectGetTaggingFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectTaggingOptions) (*tags.Tags, error) {
tagMap := map[string]string{
"tag1": "value1",
}
otags, err := tags.MapToObjectTags(tagMap)
if err != nil {
return nil, err
}
return otags, nil
},
},
expectedResp: []*models.BucketObject{
{
Name: "obj1",
LastModified: t1.Format(time.RFC3339),
Size: int64(1024),
ContentType: "content",
LegalHoldStatus: string(minio.LegalHoldEnabled),
RetentionMode: string(minio.Governance),
RetentionUntilDate: tretention.Format(time.RFC3339),
Tags: map[string]string{
"tag1": "value1",
},
}, {
Name: "obj2",
LastModified: t1.Format(time.RFC3339),
Size: int64(512),
ContentType: "content",
LegalHoldStatus: string(minio.LegalHoldEnabled),
RetentionMode: string(minio.Governance),
RetentionUntilDate: tretention.Format(time.RFC3339),
Tags: map[string]string{
"tag1": "value1",
},
},
},
wantError: nil,
},
{
test: "Limit 1",
args: args{
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
withVersions: false,
withMetadata: false,
limit: swag.Int32(1),
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
defer close(objectStatCh)
for _, bucket := range []minio.ObjectInfo{
{
Key: "obj1",
LastModified: t1,
Size: int64(1024),
ContentType: "content",
},
{
Key: "obj2",
LastModified: t1,
Size: int64(512),
ContentType: "content",
},
} {
objectStatCh <- bucket
}
}(objectStatCh)
return objectStatCh
},
objectLegalHoldFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
s := minio.LegalHoldEnabled
return &s, nil
},
objectRetentionFunc: func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
m := minio.Governance
return &m, &tretention, nil
},
objectGetTaggingFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectTaggingOptions) (*tags.Tags, error) {
tagMap := map[string]string{
"tag1": "value1",
}
otags, err := tags.MapToObjectTags(tagMap)
if err != nil {
return nil, err
}
return otags, nil
},
},
expectedResp: []*models.BucketObject{
{
Name: "obj1",
LastModified: t1.Format(time.RFC3339),
Size: int64(1024),
ContentType: "content",
LegalHoldStatus: string(minio.LegalHoldEnabled),
RetentionMode: string(minio.Governance),
RetentionUntilDate: tretention.Format(time.RFC3339),
Tags: map[string]string{
"tag1": "value1",
},
},
},
wantError: nil,
},
} }
t.Parallel() t.Parallel()
@@ -563,7 +708,16 @@ func Test_listObjects(t *testing.T) {
minioGetObjectLegalHoldMock = tt.args.objectLegalHoldFunc minioGetObjectLegalHoldMock = tt.args.objectLegalHoldFunc
minioGetObjectRetentionMock = tt.args.objectRetentionFunc minioGetObjectRetentionMock = tt.args.objectRetentionFunc
minioGetObjectTaggingMock = tt.args.objectGetTaggingFunc minioGetObjectTaggingMock = tt.args.objectGetTaggingFunc
resp, err := listBucketObjects(ctx, minClient, tt.args.bucketName, tt.args.prefix, tt.args.recursive, tt.args.withVersions, tt.args.withMetadata) resp, err := listBucketObjects(ListObjectsOpts{
ctx: ctx,
client: minClient,
bucketName: tt.args.bucketName,
prefix: tt.args.prefix,
recursive: tt.args.recursive,
withVersions: tt.args.withVersions,
withMetadata: tt.args.withMetadata,
limit: tt.args.limit,
})
switch { switch {
case err == nil && tt.wantError != nil: case err == nil && tt.wantError != nil:
t.Errorf("listBucketObjects() error: %v, wantErr: %v", err, tt.wantError) t.Errorf("listBucketObjects() error: %v, wantErr: %v", err, tt.wantError)

View File

@@ -18,7 +18,7 @@ package restapi
import ( import (
"context" "context"
"encoding/json" "errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@@ -30,7 +30,7 @@ import (
"github.com/minio/console/pkg/utils" "github.com/minio/console/pkg/utils"
"github.com/go-openapi/errors" errorsApi "github.com/go-openapi/errors"
"github.com/minio/console/models" "github.com/minio/console/models"
"github.com/minio/console/pkg/auth" "github.com/minio/console/pkg/auth"
"github.com/minio/websocket" "github.com/minio/websocket"
@@ -139,15 +139,15 @@ func (c wsConn) readMessage() (messageType int, p []byte, err error) {
// Request should come like ws://<host>:<port>/ws/<api> // Request should come like ws://<host>:<port>/ws/<api>
func serveWS(w http.ResponseWriter, req *http.Request) { func serveWS(w http.ResponseWriter, req *http.Request) {
ctx := req.Context() ctx := req.Context()
wsPath := strings.TrimPrefix(req.URL.Path, wsBasePath)
// Perform authentication before upgrading to a Websocket Connection // Perform authentication before upgrading to a Websocket Connection
// authenticate WS connection with Console // authenticate WS connection with Console
session, err := auth.GetClaimsFromTokenInRequest(req) session, err := auth.GetClaimsFromTokenInRequest(req)
if err != nil { if err != nil && (errors.Is(err, auth.ErrReadingToken) && !strings.HasPrefix(wsPath, `/objectManager`)) {
ErrorWithContext(ctx, err) ErrorWithContext(ctx, err)
errors.ServeError(w, req, errors.New(http.StatusUnauthorized, err.Error())) errorsApi.ServeError(w, req, errorsApi.New(http.StatusUnauthorized, err.Error()))
return return
} }
// Development mode validation // Development mode validation
if getConsoleDevMode() { if getConsoleDevMode() {
upgrader.CheckOrigin = func(r *http.Request) bool { upgrader.CheckOrigin = func(r *http.Request) bool {
@@ -159,11 +159,10 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
conn, err := upgrader.Upgrade(w, req, nil) conn, err := upgrader.Upgrade(w, req, nil)
if err != nil { if err != nil {
ErrorWithContext(ctx, err) ErrorWithContext(ctx, err)
errors.ServeError(w, req, err) errorsApi.ServeError(w, req, err)
return return
} }
wsPath := strings.TrimPrefix(req.URL.Path, wsBasePath)
switch { switch {
case strings.HasPrefix(wsPath, `/trace`): case strings.HasPrefix(wsPath, `/trace`):
wsAdminClient, err := newWebSocketAdminClient(conn, session) wsAdminClient, err := newWebSocketAdminClient(conn, session)
@@ -539,227 +538,6 @@ func (wsc *wsAdminClient) profile(ctx context.Context, opts *profileOptions) {
sendWsCloseMessage(wsc.conn, err) sendWsCloseMessage(wsc.conn, err)
} }
func (wsc *wsMinioClient) objectManager(session *models.Principal) {
// Storage of Cancel Contexts for this connection
cancelContexts := make(map[int64]context.CancelFunc)
// Initial goroutine
defer func() {
// We close socket at the end of requests
wsc.conn.close()
for _, c := range cancelContexts {
// invoke cancel
c()
}
}()
writeChannel := make(chan WSResponse)
done := make(chan interface{})
// Read goroutine
go func() {
for {
mType, message, err := wsc.conn.readMessage()
if err != nil {
LogInfo("Error while reading objectManager message", err)
close(done)
return
}
if mType == websocket.TextMessage {
// We get request data & review information
var messageRequest ObjectsRequest
err := json.Unmarshal(message, &messageRequest)
if err != nil {
LogInfo("Error on message request unmarshal")
close(done)
return
}
// new message, new context
ctx, cancel := context.WithCancel(context.Background())
// We store the cancel func associated with this request
cancelContexts[messageRequest.RequestID] = cancel
const itemsPerBatch = 1000
switch messageRequest.Mode {
case "close":
close(done)
return
case "cancel":
// if we have that request id, cancel it
if cancelFunc, ok := cancelContexts[messageRequest.RequestID]; ok {
cancelFunc()
delete(cancelContexts, messageRequest.RequestID)
}
case "objects":
// cancel all previous open objects requests for listing
for rid, c := range cancelContexts {
if rid < messageRequest.RequestID {
// invoke cancel
c()
}
}
// start listing and writing to web socket
go func() {
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
if err != nil {
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
return
}
var buffer []ObjectResponse
for lsObj := range startObjectsListing(ctx, wsc.client, objectRqConfigs) {
if cancelContexts[messageRequest.RequestID] == nil {
return
}
if lsObj.Err != nil {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: lsObj.Err.Error(),
Prefix: messageRequest.Prefix,
}
continue
}
objItem := ObjectResponse{
Name: lsObj.Key,
Size: lsObj.Size,
LastModified: lsObj.LastModified.Format(time.RFC3339),
VersionID: lsObj.VersionID,
IsLatest: lsObj.IsLatest,
DeleteMarker: lsObj.IsDeleteMarker,
}
buffer = append(buffer, objItem)
if len(buffer) >= itemsPerBatch {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
}
buffer = nil
}
}
if len(buffer) > 0 {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
}
}
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
RequestEnd: true,
}
// remove the cancellation context
delete(cancelContexts, messageRequest.RequestID)
}()
case "rewind":
// cancel all previous open objects requests for listing
for rid, c := range cancelContexts {
if rid < messageRequest.RequestID {
// invoke cancel
c()
}
}
// start listing and writing to web socket
go func() {
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
if err != nil {
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
cancel()
return
}
s3Client, err := newS3BucketClient(session, objectRqConfigs.BucketName, objectRqConfigs.Prefix)
if err != nil {
LogError("error creating S3Client:", err)
close(done)
cancel()
return
}
mcS3C := mcClient{client: s3Client}
var buffer []ObjectResponse
for lsObj := range startRewindListing(ctx, mcS3C, objectRqConfigs) {
if lsObj.Err != nil {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: lsObj.Err.String(),
Prefix: messageRequest.Prefix,
}
continue
}
name := strings.ReplaceAll(lsObj.URL.Path, fmt.Sprintf("/%s/", objectRqConfigs.BucketName), "")
objItem := ObjectResponse{
Name: name,
Size: lsObj.Size,
LastModified: lsObj.Time.Format(time.RFC3339),
VersionID: lsObj.VersionID,
IsLatest: lsObj.IsLatest,
DeleteMarker: lsObj.IsDeleteMarker,
}
buffer = append(buffer, objItem)
if len(buffer) >= itemsPerBatch {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
}
buffer = nil
}
}
if len(buffer) > 0 {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
}
}
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
RequestEnd: true,
}
// remove the cancellation context
delete(cancelContexts, messageRequest.RequestID)
}()
}
}
}
}()
// Write goroutine
go func() {
for writeM := range writeChannel {
jsonData, err := json.Marshal(writeM)
if err != nil {
LogInfo("Error while parsing the response", err)
return
}
err = wsc.conn.writeMessage(websocket.TextMessage, jsonData)
if err != nil {
LogInfo("Error while writing the message", err)
return
}
}
}()
<-done
}
// sendWsCloseMessage sends Websocket Connection Close Message indicating the Status Code // sendWsCloseMessage sends Websocket Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45 // see https://tools.ietf.org/html/rfc6455#page-45
func sendWsCloseMessage(conn WSConn, err error) { func sendWsCloseMessage(conn WSConn, err error) {

248
restapi/ws_objects.go Normal file
View File

@@ -0,0 +1,248 @@
// 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 restapi
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/minio/console/models"
"github.com/minio/websocket"
)
func (wsc *wsMinioClient) objectManager(session *models.Principal) {
// Storage of Cancel Contexts for this connection
cancelContexts := make(map[int64]context.CancelFunc)
// Initial goroutine
defer func() {
// We close socket at the end of requests
wsc.conn.close()
for _, c := range cancelContexts {
// invoke cancel
c()
}
}()
writeChannel := make(chan WSResponse)
done := make(chan interface{})
// Read goroutine
go func() {
for {
mType, message, err := wsc.conn.readMessage()
if err != nil {
LogInfo("Error while reading objectManager message", err)
close(done)
return
}
if mType == websocket.TextMessage {
// We get request data & review information
var messageRequest ObjectsRequest
err := json.Unmarshal(message, &messageRequest)
if err != nil {
LogInfo("Error on message request unmarshal")
close(done)
return
}
// new message, new context
ctx, cancel := context.WithCancel(context.Background())
// We store the cancel func associated with this request
cancelContexts[messageRequest.RequestID] = cancel
const itemsPerBatch = 1000
switch messageRequest.Mode {
case "close":
close(done)
return
case "cancel":
// if we have that request id, cancel it
if cancelFunc, ok := cancelContexts[messageRequest.RequestID]; ok {
cancelFunc()
delete(cancelContexts, messageRequest.RequestID)
}
case "objects":
// cancel all previous open objects requests for listing
for rid, c := range cancelContexts {
if rid < messageRequest.RequestID {
// invoke cancel
c()
}
}
// start listing and writing to web socket
go func() {
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
if err != nil {
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
return
}
var buffer []ObjectResponse
for lsObj := range startObjectsListing(ctx, wsc.client, objectRqConfigs) {
if cancelContexts[messageRequest.RequestID] == nil {
return
}
if lsObj.Err != nil {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: lsObj.Err.Error(),
Prefix: messageRequest.Prefix,
}
continue
}
objItem := ObjectResponse{
Name: lsObj.Key,
Size: lsObj.Size,
LastModified: lsObj.LastModified.Format(time.RFC3339),
VersionID: lsObj.VersionID,
IsLatest: lsObj.IsLatest,
DeleteMarker: lsObj.IsDeleteMarker,
}
buffer = append(buffer, objItem)
if len(buffer) >= itemsPerBatch {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
}
buffer = nil
}
}
if len(buffer) > 0 {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
}
}
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
RequestEnd: true,
}
// remove the cancellation context
delete(cancelContexts, messageRequest.RequestID)
}()
case "rewind":
// cancel all previous open objects requests for listing
for rid, c := range cancelContexts {
if rid < messageRequest.RequestID {
// invoke cancel
c()
}
}
// start listing and writing to web socket
go func() {
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
if err != nil {
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
cancel()
return
}
s3Client, err := newS3BucketClient(session, objectRqConfigs.BucketName, objectRqConfigs.Prefix)
if err != nil {
LogError("error creating S3Client:", err)
close(done)
cancel()
return
}
mcS3C := mcClient{client: s3Client}
var buffer []ObjectResponse
for lsObj := range startRewindListing(ctx, mcS3C, objectRqConfigs) {
if lsObj.Err != nil {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: lsObj.Err.String(),
Prefix: messageRequest.Prefix,
}
continue
}
name := strings.ReplaceAll(lsObj.URL.Path, fmt.Sprintf("/%s/", objectRqConfigs.BucketName), "")
objItem := ObjectResponse{
Name: name,
Size: lsObj.Size,
LastModified: lsObj.Time.Format(time.RFC3339),
VersionID: lsObj.VersionID,
IsLatest: lsObj.IsLatest,
DeleteMarker: lsObj.IsDeleteMarker,
}
buffer = append(buffer, objItem)
if len(buffer) >= itemsPerBatch {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
}
buffer = nil
}
}
if len(buffer) > 0 {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
}
}
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
RequestEnd: true,
}
// remove the cancellation context
delete(cancelContexts, messageRequest.RequestID)
}()
}
}
}
}()
// Write goroutine
go func() {
for writeM := range writeChannel {
jsonData, err := json.Marshal(writeM)
if err != nil {
LogInfo("Error while parsing the response", err)
return
}
err = wsc.conn.writeMessage(websocket.TextMessage, jsonData)
if err != nil {
LogInfo("Error while writing the message", err)
return
}
}
}()
<-done
}

View File

@@ -17,6 +17,10 @@ securityDefinitions:
flow: accessCode flow: accessCode
authorizationUrl: http://min.io authorizationUrl: http://min.io
tokenUrl: http://min.io tokenUrl: http://min.io
anonymous:
name: X-Anonymous
in: header
type: apiKey
# Apply the key security definition to all APIs # Apply the key security definition to all APIs
security: security:
- key: [ ] - key: [ ]
@@ -290,6 +294,9 @@ paths:
/buckets/{bucket_name}/objects: /buckets/{bucket_name}/objects:
get: get:
summary: List Objects summary: List Objects
security:
- key: [ ]
- anonymous: [ ]
operationId: ListObjects operationId: ListObjects
parameters: parameters:
- name: bucket_name - name: bucket_name
@@ -312,6 +319,11 @@ paths:
in: query in: query
required: false required: false
type: boolean type: boolean
- name: limit
in: query
required: false
type: integer
format: int32
responses: responses:
200: 200:
description: A successful response. description: A successful response.
@@ -402,6 +414,9 @@ paths:
/buckets/{bucket_name}/objects/upload: /buckets/{bucket_name}/objects/upload:
post: post:
summary: Uploads an Object. summary: Uploads an Object.
security:
- key: [ ]
- anonymous: [ ]
consumes: consumes:
- multipart/form-data - multipart/form-data
parameters: parameters:
@@ -426,6 +441,9 @@ paths:
get: get:
summary: Download Object summary: Download Object
operationId: Download Object operationId: Download Object
security:
- key: [ ]
- anonymous: [ ]
produces: produces:
- application/octet-stream - application/octet-stream
parameters: parameters:
@@ -5679,7 +5697,7 @@ definitions:
latencyHistogram: latencyHistogram:
type: array type: array
items: items:
$ref: "#/definitions/kmsLatencyHistogram" $ref: "#/definitions/kmsLatencyHistogram"
uptime: uptime:
type: integer type: integer
cpus: cpus: