Apply policy resource restrictions for file extensions (#2842)
This commit is contained in:
committed by
GitHub
parent
b76f460979
commit
864cf7af99
@@ -139,6 +139,11 @@ import { isVersionedMode } from "../../../../../../utils/validationFunctions";
|
||||
import { api } from "api";
|
||||
import { errorToHandler } from "api/errors";
|
||||
import { BucketQuota } from "api/consoleApi";
|
||||
import {
|
||||
extractFileExtn,
|
||||
getPolicyAllowedFileExtensions,
|
||||
getSessionGrantsWildCard,
|
||||
} from "../../UploadPermissionUtils";
|
||||
|
||||
const DeleteMultipleObjects = withSuspense(
|
||||
React.lazy(() => import("./DeleteMultipleObjects"))
|
||||
@@ -313,6 +318,28 @@ const ListObjects = () => {
|
||||
const fileUpload = useRef<HTMLInputElement>(null);
|
||||
const folderUpload = useRef<HTMLInputElement>(null);
|
||||
|
||||
const sessionGrants = useSelector((state: AppState) =>
|
||||
state.console.session ? state.console.session.permissions || {} : {}
|
||||
);
|
||||
|
||||
const putObjectPermScopes = [
|
||||
IAM_SCOPES.S3_PUT_OBJECT,
|
||||
IAM_SCOPES.S3_PUT_ACTIONS,
|
||||
];
|
||||
|
||||
const pathAsResourceInPolicy = uploadPath.join("/");
|
||||
const allowedFileExtensions = getPolicyAllowedFileExtensions(
|
||||
sessionGrants,
|
||||
pathAsResourceInPolicy,
|
||||
putObjectPermScopes
|
||||
);
|
||||
|
||||
const sessionGrantWildCards = getSessionGrantsWildCard(
|
||||
sessionGrants,
|
||||
pathAsResourceInPolicy,
|
||||
putObjectPermScopes
|
||||
);
|
||||
|
||||
const canDownload = hasPermission(bucketName, [
|
||||
IAM_SCOPES.S3_GET_OBJECT,
|
||||
IAM_SCOPES.S3_GET_ACTIONS,
|
||||
@@ -320,10 +347,8 @@ const ListObjects = () => {
|
||||
const canDelete = hasPermission(bucketName, [IAM_SCOPES.S3_DELETE_OBJECT]);
|
||||
const canUpload =
|
||||
hasPermission(
|
||||
uploadPath,
|
||||
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
|
||||
true,
|
||||
true
|
||||
[pathAsResourceInPolicy, ...sessionGrantWildCards],
|
||||
putObjectPermScopes
|
||||
) || anonymousMode;
|
||||
|
||||
const displayDeleteObject = hasPermission(bucketName, [
|
||||
@@ -710,7 +735,53 @@ const ListObjects = () => {
|
||||
(acceptedFiles: any[]) => {
|
||||
if (acceptedFiles && acceptedFiles.length > 0 && canUpload) {
|
||||
let newFolderPath: string = acceptedFiles[0].path;
|
||||
uploadObject(acceptedFiles, newFolderPath);
|
||||
//Should we filter by allowed file extensions if any?.
|
||||
let allowedFiles = [];
|
||||
if (allowedFileExtensions.length > 0) {
|
||||
allowedFiles = acceptedFiles.filter((file) => {
|
||||
const fileExtn = extractFileExtn(file.name);
|
||||
return allowedFileExtensions.includes(fileExtn);
|
||||
});
|
||||
} else {
|
||||
allowedFiles = acceptedFiles;
|
||||
}
|
||||
|
||||
if (allowedFiles.length) {
|
||||
uploadObject(allowedFiles, newFolderPath);
|
||||
console.log(
|
||||
`${allowedFiles.length} Allowed Files Processed out of ${acceptedFiles.length}.`,
|
||||
pathAsResourceInPolicy,
|
||||
...sessionGrantWildCards
|
||||
);
|
||||
|
||||
if (allowedFiles.length !== acceptedFiles.length) {
|
||||
dispatch(
|
||||
setErrorSnackMessage({
|
||||
errorMessage: "Upload is restricted.",
|
||||
detailedError: permissionTooltipHelper(
|
||||
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
|
||||
"upload objects to this location"
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
dispatch(
|
||||
setErrorSnackMessage({
|
||||
errorMessage: "Could not process drag and drop.",
|
||||
detailedError: permissionTooltipHelper(
|
||||
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
|
||||
"upload objects to this location"
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
console.error(
|
||||
"Could not process drag and drop . upload may be restricted.",
|
||||
pathAsResourceInPolicy,
|
||||
...sessionGrantWildCards
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!canUpload) {
|
||||
dispatch(
|
||||
@@ -1060,6 +1131,9 @@ const ListObjects = () => {
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept={
|
||||
allowedFileExtensions ? allowedFileExtensions : undefined
|
||||
}
|
||||
onChange={handleUploadButton}
|
||||
style={{ display: "none" }}
|
||||
ref={fileUpload}
|
||||
@@ -1073,7 +1147,7 @@ const ListObjects = () => {
|
||||
/>
|
||||
<UploadFilesButton
|
||||
bucketName={bucketName}
|
||||
uploadPath={uploadPath.join("/")}
|
||||
uploadPath={pathAsResourceInPolicy}
|
||||
uploadFileFunction={(closeMenu) => {
|
||||
if (fileUpload && fileUpload.current) {
|
||||
fileUpload.current.click();
|
||||
|
||||
@@ -31,6 +31,7 @@ import { hasPermission } from "../../../../common/SecureComponent";
|
||||
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "../../../../store";
|
||||
import { getSessionGrantsWildCard } from "./UploadPermissionUtils";
|
||||
|
||||
interface IUploadFilesButton {
|
||||
uploadPath: string;
|
||||
@@ -65,6 +66,22 @@ const UploadFilesButton = ({
|
||||
const anonymousMode = useSelector(
|
||||
(state: AppState) => state.system.anonymousMode
|
||||
);
|
||||
|
||||
const sessionGrants = useSelector((state: AppState) =>
|
||||
state.console.session ? state.console.session.permissions || {} : {}
|
||||
);
|
||||
|
||||
const putObjectPermScopes = [
|
||||
IAM_SCOPES.S3_PUT_OBJECT,
|
||||
IAM_SCOPES.S3_PUT_ACTIONS,
|
||||
];
|
||||
|
||||
const sessionGrantWildCards = getSessionGrantsWildCard(
|
||||
sessionGrants,
|
||||
uploadPath,
|
||||
putObjectPermScopes
|
||||
);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const openUploadMenu = Boolean(anchorEl);
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
@@ -75,13 +92,14 @@ const UploadFilesButton = ({
|
||||
};
|
||||
|
||||
const uploadObjectAllowed =
|
||||
hasPermission(uploadPath, [
|
||||
IAM_SCOPES.S3_PUT_OBJECT,
|
||||
IAM_SCOPES.S3_PUT_ACTIONS,
|
||||
]) || anonymousMode;
|
||||
hasPermission(
|
||||
[uploadPath, ...sessionGrantWildCards],
|
||||
putObjectPermScopes
|
||||
) || anonymousMode;
|
||||
|
||||
const uploadFolderAllowed = hasPermission(
|
||||
bucketName,
|
||||
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
|
||||
[bucketName, ...sessionGrantWildCards],
|
||||
putObjectPermScopes,
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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/>.
|
||||
|
||||
export const extractFileExtn = (resourceStr: string) => {
|
||||
//file extensions may contain query string. so exclude query strings !
|
||||
return (resourceStr.match(/\.([^.]*?)(?=\?|#|$)/) || [])[1];
|
||||
};
|
||||
export const getPolicyAllowedFileExtensions = (
|
||||
sessionGrants: Record<string, string[]>,
|
||||
uploadPath: string,
|
||||
scopes: string[] = []
|
||||
) => {
|
||||
const sessionGrantWildCards = getSessionGrantsWildCard(
|
||||
sessionGrants,
|
||||
uploadPath,
|
||||
scopes
|
||||
);
|
||||
|
||||
//get acceptable files if any in the policy.
|
||||
const allowedFileExtensions = sessionGrantWildCards.reduce(
|
||||
(acc: string[], cv: string) => {
|
||||
const extension: string = extractFileExtn(cv);
|
||||
if (extension) {
|
||||
acc.push(`.${extension}`); //strict extension matching.
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const uniqueExtensions = [...new Set(allowedFileExtensions)];
|
||||
return uniqueExtensions.join(",");
|
||||
};
|
||||
|
||||
// The resource should not have the extensions (*.ext) for the hasPermission to work.
|
||||
// so sanitize this and also use to extract the allowed extensions outside of permission check.
|
||||
export const getSessionGrantsWildCard = (
|
||||
sessionGrants: Record<string, string[]>,
|
||||
uploadPath: string,
|
||||
scopes: string[] = []
|
||||
) => {
|
||||
//get only the path matching grants to reduce processing.
|
||||
const grantsWithExtension = Object.keys(sessionGrants).reduce(
|
||||
(acc: Record<string, string[]>, grantKey: string) => {
|
||||
if (extractFileExtn(grantKey) && grantKey.includes(uploadPath)) {
|
||||
acc[grantKey] = sessionGrants[grantKey];
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const checkPathsForPermission = (sessionGrantKey: string) => {
|
||||
const grantActions = grantsWithExtension[sessionGrantKey];
|
||||
const hasScope = grantActions.some((actionKey) =>
|
||||
scopes.find((scopeKey) => {
|
||||
let wildCardMatch = false;
|
||||
const hasWildCard = scopeKey.indexOf("*") !== -1;
|
||||
if (hasWildCard) {
|
||||
const scopeActionKey = scopeKey.substring(0, scopeKey.length - 1);
|
||||
|
||||
wildCardMatch = actionKey.includes(scopeActionKey);
|
||||
}
|
||||
|
||||
return wildCardMatch || actionKey === scopeKey;
|
||||
})
|
||||
);
|
||||
|
||||
const sessionGrantKeyPath = sessionGrantKey.substring(
|
||||
0,
|
||||
sessionGrantKey.indexOf("/*.") //start of extension part.
|
||||
);
|
||||
const isUploadPathMatching =
|
||||
sessionGrantKeyPath === `arn:aws:s3:::${uploadPath}`;
|
||||
|
||||
const hasGrant =
|
||||
isUploadPathMatching && sessionGrantKey !== "arn:aws:s3:::*";
|
||||
|
||||
return hasScope && hasGrant;
|
||||
};
|
||||
|
||||
return Object.keys(grantsWithExtension).filter(checkPathsForPermission);
|
||||
};
|
||||
@@ -33,6 +33,7 @@ import withSuspense from "../Common/Components/withSuspense";
|
||||
import { setSnackBarMessage } from "../../../systemSlice";
|
||||
import { AppState, useAppDispatch } from "../../../store";
|
||||
import { setVersionsModeEnabled } from "./objectBrowserSlice";
|
||||
import { getSessionGrantsWildCard } from "../Buckets/ListBuckets/UploadPermissionUtils";
|
||||
|
||||
const CreatePathModal = withSuspense(
|
||||
React.lazy(
|
||||
@@ -81,11 +82,14 @@ const BrowserBreadcrumbs = ({
|
||||
|
||||
const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
|
||||
|
||||
const canCreatePath =
|
||||
hasPermission(bucketName, [
|
||||
IAM_SCOPES.S3_PUT_OBJECT,
|
||||
IAM_SCOPES.S3_PUT_ACTIONS,
|
||||
]) || anonymousMode;
|
||||
const putObjectPermScopes = [
|
||||
IAM_SCOPES.S3_PUT_OBJECT,
|
||||
IAM_SCOPES.S3_PUT_ACTIONS,
|
||||
];
|
||||
|
||||
const sessionGrants = useSelector((state: AppState) =>
|
||||
state.console.session ? state.console.session.permissions || {} : {}
|
||||
);
|
||||
|
||||
let paths = internalPaths;
|
||||
|
||||
@@ -96,6 +100,19 @@ const BrowserBreadcrumbs = ({
|
||||
const splitPaths = paths.split("/").filter((path) => path !== "");
|
||||
const lastBreadcrumbsIndex = splitPaths.length - 1;
|
||||
|
||||
const pathToCheckPerms = paths || bucketName;
|
||||
const sessionGrantWildCards = getSessionGrantsWildCard(
|
||||
sessionGrants,
|
||||
pathToCheckPerms,
|
||||
putObjectPermScopes
|
||||
);
|
||||
|
||||
const canCreatePath =
|
||||
hasPermission(
|
||||
[pathToCheckPerms, ...sessionGrantWildCards],
|
||||
putObjectPermScopes
|
||||
) || anonymousMode;
|
||||
|
||||
let breadcrumbsMap = splitPaths.map((objectItem: string, index: number) => {
|
||||
const subSplit = `${splitPaths.slice(0, index + 1).join("/")}/`;
|
||||
const route = `/browser/${bucketName}/${
|
||||
|
||||
Reference in New Issue
Block a user