Skip to content

Commit 864cf7a

Browse files
authored
Apply policy resource restrictions for file extensions (#2842)
1 parent b76f460 commit 864cf7a

File tree

4 files changed

+222
-17
lines changed

4 files changed

+222
-17
lines changed

portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ import { isVersionedMode } from "../../../../../../utils/validationFunctions";
139139
import { api } from "api";
140140
import { errorToHandler } from "api/errors";
141141
import { BucketQuota } from "api/consoleApi";
142+
import {
143+
extractFileExtn,
144+
getPolicyAllowedFileExtensions,
145+
getSessionGrantsWildCard,
146+
} from "../../UploadPermissionUtils";
142147

143148
const DeleteMultipleObjects = withSuspense(
144149
React.lazy(() => import("./DeleteMultipleObjects"))
@@ -313,17 +318,37 @@ const ListObjects = () => {
313318
const fileUpload = useRef<HTMLInputElement>(null);
314319
const folderUpload = useRef<HTMLInputElement>(null);
315320

321+
const sessionGrants = useSelector((state: AppState) =>
322+
state.console.session ? state.console.session.permissions || {} : {}
323+
);
324+
325+
const putObjectPermScopes = [
326+
IAM_SCOPES.S3_PUT_OBJECT,
327+
IAM_SCOPES.S3_PUT_ACTIONS,
328+
];
329+
330+
const pathAsResourceInPolicy = uploadPath.join("/");
331+
const allowedFileExtensions = getPolicyAllowedFileExtensions(
332+
sessionGrants,
333+
pathAsResourceInPolicy,
334+
putObjectPermScopes
335+
);
336+
337+
const sessionGrantWildCards = getSessionGrantsWildCard(
338+
sessionGrants,
339+
pathAsResourceInPolicy,
340+
putObjectPermScopes
341+
);
342+
316343
const canDownload = hasPermission(bucketName, [
317344
IAM_SCOPES.S3_GET_OBJECT,
318345
IAM_SCOPES.S3_GET_ACTIONS,
319346
]);
320347
const canDelete = hasPermission(bucketName, [IAM_SCOPES.S3_DELETE_OBJECT]);
321348
const canUpload =
322349
hasPermission(
323-
uploadPath,
324-
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
325-
true,
326-
true
350+
[pathAsResourceInPolicy, ...sessionGrantWildCards],
351+
putObjectPermScopes
327352
) || anonymousMode;
328353

329354
const displayDeleteObject = hasPermission(bucketName, [
@@ -710,7 +735,53 @@ const ListObjects = () => {
710735
(acceptedFiles: any[]) => {
711736
if (acceptedFiles && acceptedFiles.length > 0 && canUpload) {
712737
let newFolderPath: string = acceptedFiles[0].path;
713-
uploadObject(acceptedFiles, newFolderPath);
738+
//Should we filter by allowed file extensions if any?.
739+
let allowedFiles = [];
740+
if (allowedFileExtensions.length > 0) {
741+
allowedFiles = acceptedFiles.filter((file) => {
742+
const fileExtn = extractFileExtn(file.name);
743+
return allowedFileExtensions.includes(fileExtn);
744+
});
745+
} else {
746+
allowedFiles = acceptedFiles;
747+
}
748+
749+
if (allowedFiles.length) {
750+
uploadObject(allowedFiles, newFolderPath);
751+
console.log(
752+
`${allowedFiles.length} Allowed Files Processed out of ${acceptedFiles.length}.`,
753+
pathAsResourceInPolicy,
754+
...sessionGrantWildCards
755+
);
756+
757+
if (allowedFiles.length !== acceptedFiles.length) {
758+
dispatch(
759+
setErrorSnackMessage({
760+
errorMessage: "Upload is restricted.",
761+
detailedError: permissionTooltipHelper(
762+
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
763+
"upload objects to this location"
764+
),
765+
})
766+
);
767+
}
768+
} else {
769+
dispatch(
770+
setErrorSnackMessage({
771+
errorMessage: "Could not process drag and drop.",
772+
detailedError: permissionTooltipHelper(
773+
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
774+
"upload objects to this location"
775+
),
776+
})
777+
);
778+
779+
console.error(
780+
"Could not process drag and drop . upload may be restricted.",
781+
pathAsResourceInPolicy,
782+
...sessionGrantWildCards
783+
);
784+
}
714785
}
715786
if (!canUpload) {
716787
dispatch(
@@ -1060,6 +1131,9 @@ const ListObjects = () => {
10601131
<input
10611132
type="file"
10621133
multiple
1134+
accept={
1135+
allowedFileExtensions ? allowedFileExtensions : undefined
1136+
}
10631137
onChange={handleUploadButton}
10641138
style={{ display: "none" }}
10651139
ref={fileUpload}
@@ -1073,7 +1147,7 @@ const ListObjects = () => {
10731147
/>
10741148
<UploadFilesButton
10751149
bucketName={bucketName}
1076-
uploadPath={uploadPath.join("/")}
1150+
uploadPath={pathAsResourceInPolicy}
10771151
uploadFileFunction={(closeMenu) => {
10781152
if (fileUpload && fileUpload.current) {
10791153
fileUpload.current.click();

portal-ui/src/screens/Console/Buckets/ListBuckets/UploadFilesButton.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { hasPermission } from "../../../../common/SecureComponent";
3131
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
3232
import { useSelector } from "react-redux";
3333
import { AppState } from "../../../../store";
34+
import { getSessionGrantsWildCard } from "./UploadPermissionUtils";
3435

3536
interface IUploadFilesButton {
3637
uploadPath: string;
@@ -65,6 +66,22 @@ const UploadFilesButton = ({
6566
const anonymousMode = useSelector(
6667
(state: AppState) => state.system.anonymousMode
6768
);
69+
70+
const sessionGrants = useSelector((state: AppState) =>
71+
state.console.session ? state.console.session.permissions || {} : {}
72+
);
73+
74+
const putObjectPermScopes = [
75+
IAM_SCOPES.S3_PUT_OBJECT,
76+
IAM_SCOPES.S3_PUT_ACTIONS,
77+
];
78+
79+
const sessionGrantWildCards = getSessionGrantsWildCard(
80+
sessionGrants,
81+
uploadPath,
82+
putObjectPermScopes
83+
);
84+
6885
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
6986
const openUploadMenu = Boolean(anchorEl);
7087
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
@@ -75,13 +92,14 @@ const UploadFilesButton = ({
7592
};
7693

7794
const uploadObjectAllowed =
78-
hasPermission(uploadPath, [
79-
IAM_SCOPES.S3_PUT_OBJECT,
80-
IAM_SCOPES.S3_PUT_ACTIONS,
81-
]) || anonymousMode;
95+
hasPermission(
96+
[uploadPath, ...sessionGrantWildCards],
97+
putObjectPermScopes
98+
) || anonymousMode;
99+
82100
const uploadFolderAllowed = hasPermission(
83-
bucketName,
84-
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
101+
[bucketName, ...sessionGrantWildCards],
102+
putObjectPermScopes,
85103
false,
86104
true
87105
);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2023 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
export const extractFileExtn = (resourceStr: string) => {
18+
//file extensions may contain query string. so exclude query strings !
19+
return (resourceStr.match(/\.([^.]*?)(?=\?|#|$)/) || [])[1];
20+
};
21+
export const getPolicyAllowedFileExtensions = (
22+
sessionGrants: Record<string, string[]>,
23+
uploadPath: string,
24+
scopes: string[] = []
25+
) => {
26+
const sessionGrantWildCards = getSessionGrantsWildCard(
27+
sessionGrants,
28+
uploadPath,
29+
scopes
30+
);
31+
32+
//get acceptable files if any in the policy.
33+
const allowedFileExtensions = sessionGrantWildCards.reduce(
34+
(acc: string[], cv: string) => {
35+
const extension: string = extractFileExtn(cv);
36+
if (extension) {
37+
acc.push(`.${extension}`); //strict extension matching.
38+
}
39+
return acc;
40+
},
41+
[]
42+
);
43+
44+
const uniqueExtensions = [...new Set(allowedFileExtensions)];
45+
return uniqueExtensions.join(",");
46+
};
47+
48+
// The resource should not have the extensions (*.ext) for the hasPermission to work.
49+
// so sanitize this and also use to extract the allowed extensions outside of permission check.
50+
export const getSessionGrantsWildCard = (
51+
sessionGrants: Record<string, string[]>,
52+
uploadPath: string,
53+
scopes: string[] = []
54+
) => {
55+
//get only the path matching grants to reduce processing.
56+
const grantsWithExtension = Object.keys(sessionGrants).reduce(
57+
(acc: Record<string, string[]>, grantKey: string) => {
58+
if (extractFileExtn(grantKey) && grantKey.includes(uploadPath)) {
59+
acc[grantKey] = sessionGrants[grantKey];
60+
}
61+
return acc;
62+
},
63+
{}
64+
);
65+
66+
const checkPathsForPermission = (sessionGrantKey: string) => {
67+
const grantActions = grantsWithExtension[sessionGrantKey];
68+
const hasScope = grantActions.some((actionKey) =>
69+
scopes.find((scopeKey) => {
70+
let wildCardMatch = false;
71+
const hasWildCard = scopeKey.indexOf("*") !== -1;
72+
if (hasWildCard) {
73+
const scopeActionKey = scopeKey.substring(0, scopeKey.length - 1);
74+
75+
wildCardMatch = actionKey.includes(scopeActionKey);
76+
}
77+
78+
return wildCardMatch || actionKey === scopeKey;
79+
})
80+
);
81+
82+
const sessionGrantKeyPath = sessionGrantKey.substring(
83+
0,
84+
sessionGrantKey.indexOf("/*.") //start of extension part.
85+
);
86+
const isUploadPathMatching =
87+
sessionGrantKeyPath === `arn:aws:s3:::${uploadPath}`;
88+
89+
const hasGrant =
90+
isUploadPathMatching && sessionGrantKey !== "arn:aws:s3:::*";
91+
92+
return hasScope && hasGrant;
93+
};
94+
95+
return Object.keys(grantsWithExtension).filter(checkPathsForPermission);
96+
};

portal-ui/src/screens/Console/ObjectBrowser/BrowserBreadcrumbs.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import withSuspense from "../Common/Components/withSuspense";
3333
import { setSnackBarMessage } from "../../../systemSlice";
3434
import { AppState, useAppDispatch } from "../../../store";
3535
import { setVersionsModeEnabled } from "./objectBrowserSlice";
36+
import { getSessionGrantsWildCard } from "../Buckets/ListBuckets/UploadPermissionUtils";
3637

3738
const CreatePathModal = withSuspense(
3839
React.lazy(
@@ -81,11 +82,14 @@ const BrowserBreadcrumbs = ({
8182

8283
const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
8384

84-
const canCreatePath =
85-
hasPermission(bucketName, [
86-
IAM_SCOPES.S3_PUT_OBJECT,
87-
IAM_SCOPES.S3_PUT_ACTIONS,
88-
]) || anonymousMode;
85+
const putObjectPermScopes = [
86+
IAM_SCOPES.S3_PUT_OBJECT,
87+
IAM_SCOPES.S3_PUT_ACTIONS,
88+
];
89+
90+
const sessionGrants = useSelector((state: AppState) =>
91+
state.console.session ? state.console.session.permissions || {} : {}
92+
);
8993

9094
let paths = internalPaths;
9195

@@ -96,6 +100,19 @@ const BrowserBreadcrumbs = ({
96100
const splitPaths = paths.split("/").filter((path) => path !== "");
97101
const lastBreadcrumbsIndex = splitPaths.length - 1;
98102

103+
const pathToCheckPerms = paths || bucketName;
104+
const sessionGrantWildCards = getSessionGrantsWildCard(
105+
sessionGrants,
106+
pathToCheckPerms,
107+
putObjectPermScopes
108+
);
109+
110+
const canCreatePath =
111+
hasPermission(
112+
[pathToCheckPerms, ...sessionGrantWildCards],
113+
putObjectPermScopes
114+
) || anonymousMode;
115+
99116
let breadcrumbsMap = splitPaths.map((objectItem: string, index: number) => {
100117
const subSplit = `${splitPaths.slice(0, index + 1).join("/")}/`;
101118
const route = `/browser/${bucketName}/${

0 commit comments

Comments
 (0)