From b218cbf503c0dc077da7ed91a213e46b41a97cf5 Mon Sep 17 00:00:00 2001 From: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:23:30 -0800 Subject: [PATCH] Anonymous Access (#2600) Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> --- pkg/auth/token.go | 10 +- portal-ui/src/ProtectedRoutes.tsx | 43 +- .../common/SecureComponent/accessControl.ts | 4 +- portal-ui/src/common/api/index.ts | 14 +- .../AnonymousAccess/AnonymousAccess.tsx | 88 ++++ .../Buckets/BucketDetails/BrowserHandler.tsx | 185 +-------- .../Buckets/BucketDetails/BucketDetails.tsx | 3 - .../Objects/ListObjects/ListObjects.tsx | 381 +++++++++--------- .../Objects/ListObjects/ListObjectsTable.tsx | 44 +- .../Objects/ListObjects/ObjectDetailPanel.tsx | 105 +---- .../Buckets/ListBuckets/Objects/utils.ts | 6 + .../Buckets/ListBuckets/UploadFilesButton.tsx | 21 +- .../FormComponents/common/styleLibrary.ts | 6 - .../Common/ObjectManager/ObjectManager.tsx | 16 +- .../ObjectManager/ObjectManagerButton.tsx | 107 +++++ .../Common/ObjectManager/TrafficMonitor.tsx | 4 +- .../Console/Common/PageHeader/PageHeader.tsx | 105 +---- .../Common/ScreenTitle/ScreenTitle.tsx | 114 +++--- portal-ui/src/screens/Console/ConsoleKBar.tsx | 16 + .../Console/IDP/IDPConfigurationDetails.tsx | 4 - .../ObjectBrowser/BrowserBreadcrumbs.tsx | 14 +- .../Console/ObjectBrowser/FilterObjectsSB.tsx | 39 ++ .../Console/ObjectBrowser/OBHeader.tsx | 178 ++++++++ .../ObjectBrowser/objectBrowserSlice.ts | 5 + .../screens/Console/ObjectBrowser/types.ts | 1 + .../screens/Console/ObjectBrowser/utils.ts | 90 +++++ portal-ui/src/screens/Console/consoleSlice.ts | 3 +- portal-ui/src/systemSlice.ts | 11 + restapi/client.go | 3 + restapi/configure_console.go | 8 + restapi/embedded_spec.go | 70 ++++ restapi/operations/console_api.go | 17 + .../object/list_objects_parameters.go | 32 ++ .../object/list_objects_urlbuilder.go | 9 + restapi/user_objects.go | 73 +++- restapi/user_objects_test.go | 156 ++++++- restapi/ws_handle.go | 234 +---------- restapi/ws_objects.go | 248 ++++++++++++ swagger-console.yml | 20 +- 39 files changed, 1596 insertions(+), 891 deletions(-) create mode 100644 portal-ui/src/screens/AnonymousAccess/AnonymousAccess.tsx create mode 100644 portal-ui/src/screens/Console/Common/ObjectManager/ObjectManagerButton.tsx create mode 100644 portal-ui/src/screens/Console/ObjectBrowser/FilterObjectsSB.tsx create mode 100644 portal-ui/src/screens/Console/ObjectBrowser/OBHeader.tsx create mode 100644 portal-ui/src/screens/Console/ObjectBrowser/utils.ts create mode 100644 restapi/ws_objects.go diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 8512031c4..bbc55224a 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -45,8 +45,8 @@ import ( // Session token errors var ( ErrNoAuthToken = errors.New("session token missing") - errTokenExpired = errors.New("session token has expired") - errReadingToken = errors.New("session token internal data is malformed") + ErrTokenExpired = errors.New("session token has expired") + 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 @@ -101,12 +101,12 @@ func SessionTokenAuthenticate(token string) (*TokenClaims, error) { decryptedToken, err := DecryptToken(token) if err != nil { // fail decrypting token - return nil, errReadingToken + return nil, ErrReadingToken } claimTokens, err := ParseClaimsFromToken(string(decryptedToken)) if err != nil { // fail unmarshalling token into data structure - return nil, errReadingToken + return nil, ErrReadingToken } // claimsTokens contains the decrypted JWT for Console return claimTokens, nil @@ -321,7 +321,7 @@ func GetTokenFromRequest(r *http.Request) (string, error) { } currentTime := time.Now() if tokenCookie.Expires.After(currentTime) { - return "", errTokenExpired + return "", ErrTokenExpired } return strings.TrimSpace(tokenCookie.Value), nil } diff --git a/portal-ui/src/ProtectedRoutes.tsx b/portal-ui/src/ProtectedRoutes.tsx index 3cf39c85e..d75c40fd8 100644 --- a/portal-ui/src/ProtectedRoutes.tsx +++ b/portal-ui/src/ProtectedRoutes.tsx @@ -27,6 +27,7 @@ import { globalSetDistributedSetup, operatorMode, selOpMode, + setAnonymousMode, setOverrideStyles, setSiteReplicationInfo, userLogged, @@ -48,7 +49,9 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => { const [sessionLoading, setSessionLoading] = useState(true); const userLoggedIn = useSelector((state: AppState) => state.system.loggedIn); - + const anonymousMode = useSelector( + (state: AppState) => state.system.anonymousMode + ); const { pathname = "" } = useLocation(); const StorePathAndRedirect = () => { @@ -56,6 +59,9 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => { return ; }; + const pathnameParts = pathname.split("/"); + const screen = pathnameParts.length > 2 ? pathnameParts[1] : ""; + useEffect(() => { api .invoke("GET", `/api/v1/session`) @@ -81,8 +87,37 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => { } } }) - .catch(() => setSessionLoading(false)); - }, [dispatch]); + .catch(() => { + // 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( (res: any) => { @@ -112,7 +147,7 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => { ); useEffect(() => { - if (userLoggedIn && !sessionLoading && !isOperatorMode) { + if (userLoggedIn && !sessionLoading && !isOperatorMode && !anonymousMode) { invokeSRInfoApi("GET", `api/v1/admin/site-replication`); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/portal-ui/src/common/SecureComponent/accessControl.ts b/portal-ui/src/common/SecureComponent/accessControl.ts index b265b0733..0d1f9c755 100644 --- a/portal-ui/src/common/SecureComponent/accessControl.ts +++ b/portal-ui/src/common/SecureComponent/accessControl.ts @@ -28,7 +28,9 @@ const hasPermission = ( return false; } 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:::*"] || []; let resources: string[] = []; diff --git a/portal-ui/src/common/api/index.ts b/portal-ui/src/common/api/index.ts index b80348dff..268347d4c 100644 --- a/portal-ui/src/common/api/index.ts +++ b/portal-ui/src/common/api/index.ts @@ -20,13 +20,23 @@ import { clearSession } from "../utils"; import { ErrorResponseHandler } from "../types"; import { baseUrl } from "../../history"; +type RequestHeaders = { [name: string]: string }; + export class API { - invoke(method: string, url: string, data?: object) { + invoke(method: string, url: string, data?: object, headers?: RequestHeaders) { let targetURL = url; if (targetURL[0] === "/") { 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) .then((res) => res.body) .catch((err) => { diff --git a/portal-ui/src/screens/AnonymousAccess/AnonymousAccess.tsx b/portal-ui/src/screens/AnonymousAccess/AnonymousAccess.tsx new file mode 100644 index 000000000..a264b351a --- /dev/null +++ b/portal-ui/src/screens/AnonymousAccess/AnonymousAccess.tsx @@ -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 . + +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 ( + +
+
+ +
+
+
+ + +
+
+ + }> + + + + }> + + + } + /> + +
+ ); +}; +export default AnonymousAccess; diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx index e6ec86715..45543c840 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx @@ -16,26 +16,15 @@ import React, { Fragment, useCallback, useEffect } from "react"; 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 createStyles from "@mui/styles/createStyles"; import withStyles from "@mui/styles/withStyles"; -import { Grid } from "@mui/material"; import { AppState, useAppDispatch } from "../../../../store"; import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary"; import ListObjects from "../ListBuckets/Objects/ListObjects/ListObjects"; -import PageHeader from "../../Common/PageHeader/PageHeader"; -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 { IAM_SCOPES } from "../../../../common/SecureComponent/permissions"; import { newMessage, resetMessages, @@ -50,17 +39,10 @@ import { setLockingEnabled, setObjectDetailsView, setRecords, - setSearchObjects, - setSearchVersions, setSelectedObjectView, setSimplePathHandler, setVersionsModeEnabled, } 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 { IMessageEvent } from "websocket"; import { wsProtocol } from "../../../../utils/wsUtils"; @@ -74,6 +56,7 @@ import { setErrorSnackMessage } from "../../../../systemSlice"; import api from "../../../../common/api"; import { BucketObjectLocking, BucketVersioning } from "../types"; import { ErrorResponseHandler } from "../../../../common/types"; +import OBHeader from "../../ObjectBrowser/OBHeader"; const styles = (theme: Theme) => createStyles({ @@ -145,7 +128,6 @@ const initWSConnection = ( const BrowserHandler = () => { const dispatch = useAppDispatch(); - const navigate = useNavigate(); const params = useParams(); const location = useLocation(); @@ -153,18 +135,6 @@ const BrowserHandler = () => { (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( (state: AppState) => state.objectBrowser.rewind.rewindEnabled ); @@ -195,15 +165,14 @@ const BrowserHandler = () => { const isOpeningOD = useSelector( (state: AppState) => state.objectBrowser.isOpeningObjectDetail ); - - const features = useSelector(selFeatures); + const anonymousMode = useSelector( + (state: AppState) => state.system.anonymousMode + ); const bucketName = params.bucketName || ""; const pathSegment = location.pathname.split(`/browser/${bucketName}/`); const internalPaths = pathSegment.length === 2 ? pathSegment[1] : ""; - const obOnly = !!features?.includes("object-browser-only"); - /*WS Request Handlers*/ const onMessageCallBack = useCallback( (message: IMessageEvent) => { @@ -297,7 +266,6 @@ const BrowserHandler = () => { const dupRequest = () => { initWSRequest(path, date); }; - initWSConnection(dupRequest, onMessageCallBack); } }, @@ -377,10 +345,11 @@ const BrowserHandler = () => { simplePath, ]); - const displayListObjects = hasPermission(bucketName, [ - IAM_SCOPES.S3_LIST_BUCKET, - IAM_SCOPES.S3_ALL_LIST_BUCKET, - ]); + const displayListObjects = + hasPermission(bucketName, [ + IAM_SCOPES.S3_LIST_BUCKET, + IAM_SCOPES.S3_ALL_LIST_BUCKET, + ]) || anonymousMode; // Common objects list useEffect(() => { @@ -408,7 +377,6 @@ const BrowserHandler = () => { if (rewindEnabled && rewindDate) { requestDate = rewindDate; } - initWSRequest(pathPrefix, requestDate); } else { dispatch(setLoadingObjects(false)); @@ -429,7 +397,7 @@ const BrowserHandler = () => { }, [internalPaths, dispatch]); useEffect(() => { - if (loadingVersioning) { + if (loadingVersioning && !anonymousMode) { if (displayListObjects) { api .invoke("GET", `/api/v1/buckets/${bucketName}/versioning`) @@ -449,7 +417,13 @@ const BrowserHandler = () => { dispatch(resetMessages()); } } - }, [bucketName, loadingVersioning, dispatch, displayListObjects]); + }, [ + bucketName, + loadingVersioning, + dispatch, + displayListObjects, + anonymousMode, + ]); useEffect(() => { if (loadingLocking) { @@ -497,127 +471,10 @@ const BrowserHandler = () => { } }, [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 = ( - - {!versionsMode ? ( - - { - dispatch(setSearchObjects(value)); - }} - value={searchObjects} - /> - - ) : ( - - { - dispatch(setSearchVersions(value)); - }} - value={searchVersions} - /> - - )} - - ); - return ( - {!obOnly ? ( - - } - actions={ - - -