Apply policy resource restrictions for file extensions (#2842)

This commit is contained in:
Prakash Senthil Vel
2023-06-16 00:03:52 +05:30
committed by GitHub
parent b76f460979
commit 864cf7af99
4 changed files with 222 additions and 17 deletions

View File

@@ -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();

View File

@@ -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
);

View File

@@ -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);
};

View File

@@ -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}/${