Skip to content

Commit 25ff498

Browse files
authored
Fix Upload Button Logic (#1580)
* Fix Upload Button Logic Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> * Re-org function hasAccessToResource Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> * Fix Warnings Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
1 parent 0ac6cec commit 25ff498

38 files changed

+312
-212
lines changed

portal-ui/src/common/SecureComponent/SecureComponent.tsx

Lines changed: 1 addition & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -15,95 +15,7 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
import React, { cloneElement } from "react";
18-
import get from "lodash/get";
19-
import { store } from "../../store";
20-
import { hasAccessToResource } from "./permissions";
21-
22-
export const hasPermission = (
23-
resource: string | string[] | undefined,
24-
scopes: string[],
25-
matchAll?: boolean,
26-
containsResource?: boolean
27-
) => {
28-
if (!resource) {
29-
return false;
30-
}
31-
const state = store.getState();
32-
const sessionGrants = state.console.session.permissions || {};
33-
34-
const globalGrants = sessionGrants["arn:aws:s3:::*"] || [];
35-
let resources: string[] = [];
36-
let resourceGrants: string[] = [];
37-
let containsResourceGrants: string[] = [];
38-
39-
if (resource) {
40-
if (Array.isArray(resource)) {
41-
resources = [...resources, ...resource];
42-
} else {
43-
resources.push(resource);
44-
}
45-
46-
// Filter wildcard items
47-
const wildcards = Object.keys(sessionGrants).filter(
48-
(item) => item.includes("*") && item !== "arn:aws:s3:::*"
49-
);
50-
51-
const getMatchingWildcards = (path: string) => {
52-
const items = wildcards.map((element) => {
53-
const wildcardItemSection = element.split(":").slice(-1)[0];
54-
55-
const replaceWildcard = wildcardItemSection
56-
.replace("/", "\\/")
57-
.replace("\\/*", "($|(\\/.*?))");
58-
59-
const inRegExp = new RegExp(`${replaceWildcard}$`, "gm");
60-
61-
if(inRegExp.exec(path)) {
62-
return element;
63-
}
64-
65-
return null;
66-
});
67-
68-
return items.filter(itm => itm !== null);
69-
};
70-
71-
resources.forEach((rsItem) => {
72-
// Validation against inner paths & wildcards
73-
let wildcardRules =getMatchingWildcards(rsItem);
74-
75-
let wildcardGrants: string[] = [];
76-
77-
wildcardRules.forEach((rule) => {
78-
if(rule) {
79-
const wcResources = get(sessionGrants, rule, []);
80-
wildcardGrants = [...wildcardGrants, ...wcResources];
81-
}
82-
});
83-
84-
const simpleResources = get(sessionGrants, rsItem, []);
85-
const s3Resources = get(sessionGrants, `arn:aws:s3:::${rsItem}/*`, []);
86-
87-
resourceGrants = [...simpleResources, ...s3Resources, ...wildcardGrants];
88-
89-
if (containsResource) {
90-
const matchResource = `arn:aws:s3:::${rsItem}`;
91-
92-
Object.entries(sessionGrants).forEach(([key, value]) => {
93-
if (key.includes(matchResource)) {
94-
containsResourceGrants = [...containsResourceGrants, ...value];
95-
}
96-
});
97-
}
98-
});
99-
}
100-
101-
return hasAccessToResource(
102-
[...resourceGrants, ...globalGrants, ...containsResourceGrants],
103-
scopes,
104-
matchAll
105-
);
106-
};
18+
import hasPermission from "./accessControl";
10719

10820
interface ISecureComponentProps {
10921
errorProps?: any;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2022 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+
import hasPermission from "../accessControl";
18+
import { store } from "../../../store";
19+
import { SESSION_RESPONSE } from "../../../screens/Console/actions";
20+
21+
const setPolicy1 = () => {
22+
store.dispatch({
23+
type: SESSION_RESPONSE,
24+
message: {
25+
distributedMode: true,
26+
features: ["log-search"],
27+
permissions: {
28+
"arn:aws:s3:::testcafe": [
29+
"admin:CreateUser",
30+
"s3:GetBucketLocation",
31+
"s3:ListBucket",
32+
"admin:CreateServiceAccount",
33+
],
34+
"arn:aws:s3:::testcafe/*": [
35+
"admin:CreateServiceAccount",
36+
"admin:CreateUser",
37+
"s3:GetObject",
38+
"s3:ListBucket",
39+
],
40+
"arn:aws:s3:::testcafe/write/*": [
41+
"admin:CreateServiceAccount",
42+
"admin:CreateUser",
43+
"s3:PutObject",
44+
"s3:DeleteObject",
45+
"s3:GetObject",
46+
"s3:ListBucket",
47+
],
48+
"console-ui": ["admin:CreateServiceAccount", "admin:CreateUser"],
49+
},
50+
operator: false,
51+
status: "ok",
52+
},
53+
});
54+
};
55+
56+
test("Upload button disabled", () => {
57+
setPolicy1();
58+
expect(hasPermission("testcafe", ["s3:PutObject"])).toBe(false);
59+
});
60+
61+
test("Upload button enabled valid prefix", () => {
62+
setPolicy1();
63+
expect(hasPermission("testcafe/write", ["s3:PutObject"], false, true)).toBe(
64+
true
65+
);
66+
});
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2022 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+
import { store } from "../../store";
18+
import get from "lodash/get";
19+
import { IAM_SCOPES } from "./permissions";
20+
21+
const hasPermission = (
22+
resource: string | string[] | undefined,
23+
scopes: string[],
24+
matchAll?: boolean,
25+
containsResource?: boolean
26+
) => {
27+
if (!resource) {
28+
return false;
29+
}
30+
const state = store.getState();
31+
const sessionGrants = state.console.session.permissions || {};
32+
33+
const globalGrants = sessionGrants["arn:aws:s3:::*"] || [];
34+
let resources: string[] = [];
35+
let resourceGrants: string[] = [];
36+
let containsResourceGrants: string[] = [];
37+
38+
if (resource) {
39+
if (Array.isArray(resource)) {
40+
resources = [...resources, ...resource];
41+
} else {
42+
resources.push(resource);
43+
}
44+
45+
// Filter wildcard items
46+
const wildcards = Object.keys(sessionGrants).filter(
47+
(item) => item.includes("*") && item !== "arn:aws:s3:::*"
48+
);
49+
50+
const getMatchingWildcards = (path: string) => {
51+
const items = wildcards.map((element) => {
52+
const wildcardItemSection = element.split(":").slice(-1)[0];
53+
54+
const replaceWildcard = wildcardItemSection
55+
.replace("/", "\\/")
56+
.replace("\\/*", "($|(\\/.*?))");
57+
58+
const inRegExp = new RegExp(`${replaceWildcard}$`, "gm");
59+
60+
if (inRegExp.exec(path)) {
61+
return element;
62+
}
63+
64+
return null;
65+
});
66+
67+
return items.filter((itm) => itm !== null);
68+
};
69+
70+
resources.forEach((rsItem) => {
71+
// Validation against inner paths & wildcards
72+
let wildcardRules = getMatchingWildcards(rsItem);
73+
74+
let wildcardGrants: string[] = [];
75+
76+
wildcardRules.forEach((rule) => {
77+
if (rule) {
78+
const wcResources = get(sessionGrants, rule, []);
79+
wildcardGrants = [...wildcardGrants, ...wcResources];
80+
}
81+
});
82+
83+
const simpleResources = get(sessionGrants, rsItem, []);
84+
const s3Resources = get(sessionGrants, `arn:aws:s3:::${rsItem}/*`, []);
85+
86+
resourceGrants = [...simpleResources, ...s3Resources, ...wildcardGrants];
87+
88+
if (containsResource) {
89+
const matchResource = `arn:aws:s3:::${rsItem}`;
90+
91+
Object.entries(sessionGrants).forEach(([key, value]) => {
92+
if (key.includes(matchResource)) {
93+
containsResourceGrants = [...containsResourceGrants, ...value];
94+
}
95+
});
96+
}
97+
});
98+
}
99+
100+
return hasAccessToResource(
101+
[...resourceGrants, ...globalGrants, ...containsResourceGrants],
102+
scopes,
103+
matchAll
104+
);
105+
};
106+
107+
// hasAccessToResource receives a list of user permissions to perform on a specific resource, then compares those permissions against
108+
// a list of required permissions and return true or false depending of the level of required access (match all permissions,
109+
// match some of the permissions)
110+
const hasAccessToResource = (
111+
userPermissionsOnBucket: string[] | null | undefined,
112+
requiredPermissions: string[] = [],
113+
matchAll?: boolean
114+
) => {
115+
if (!userPermissionsOnBucket) {
116+
return false;
117+
}
118+
119+
const s3All = userPermissionsOnBucket.includes(IAM_SCOPES.S3_ALL_ACTIONS);
120+
const AdminAll = userPermissionsOnBucket.includes(
121+
IAM_SCOPES.ADMIN_ALL_ACTIONS
122+
);
123+
124+
const permissions = requiredPermissions.filter(function (n) {
125+
return (
126+
userPermissionsOnBucket.indexOf(n) !== -1 ||
127+
(n.indexOf("s3:") !== -1 && s3All) ||
128+
(n.indexOf("admin:") !== -1 && AdminAll)
129+
);
130+
});
131+
return matchAll
132+
? permissions.length === requiredPermissions.length
133+
: permissions.length > 0;
134+
};
135+
136+
export default hasPermission;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2022 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 { default as hasPermission } from "./accessControl";
18+
export { default as SecureComponent } from "./SecureComponent";

portal-ui/src/common/SecureComponent/permissions.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,6 @@
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17-
// hasAccessToResource receives a list of user permissions to perform on a specific resource, then compares those permissions against
18-
// a list of required permissions and return true or false depending of the level of required access (match all permissions,
19-
// match some of the permissions)
20-
export const hasAccessToResource = (
21-
userPermissionsOnBucket: string[] | null | undefined,
22-
requiredPermissions: string[] = [],
23-
matchAll?: boolean
24-
) => {
25-
if (!userPermissionsOnBucket) {
26-
return false;
27-
}
28-
29-
const s3All = userPermissionsOnBucket.includes(IAM_SCOPES.S3_ALL_ACTIONS);
30-
const AdminAll = userPermissionsOnBucket.includes(
31-
IAM_SCOPES.ADMIN_ALL_ACTIONS
32-
);
33-
34-
const permissions = requiredPermissions.filter(function (n) {
35-
return (
36-
userPermissionsOnBucket.indexOf(n) !== -1 ||
37-
(n.indexOf("s3:") !== -1 && s3All) ||
38-
(n.indexOf("admin:") !== -1 && AdminAll)
39-
);
40-
});
41-
return matchAll
42-
? permissions.length === requiredPermissions.length
43-
: permissions.length > 0;
44-
};
45-
4617
export const IAM_ROLES = {
4718
BUCKET_OWNER: "BUCKET_OWNER", // upload/delete objects from the bucket
4819
BUCKET_VIEWER: "BUCKET_VIEWER", // only view objects on the bucket

portal-ui/src/screens/Console/Account/Account.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
CONSOLE_UI_RESOURCE,
5050
IAM_SCOPES,
5151
} from "../../../common/SecureComponent/permissions";
52-
import SecureComponent from "../../../common/SecureComponent/SecureComponent";
52+
import { SecureComponent } from "../../../common/SecureComponent";
5353
import RBIconButton from "../Buckets/BucketDetails/SummaryItems/RBIconButton";
5454
import { selectSAs } from "../Configurations/utils";
5555
import DeleteMultipleServiceAccounts from "../Users/DeleteMultipleServiceAccounts";

portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ import {
3636
IAM_SCOPES,
3737
} from "../../../../common/SecureComponent/permissions";
3838
import PanelTitle from "../../Common/PanelTitle/PanelTitle";
39-
import SecureComponent, {
39+
import {
40+
SecureComponent,
4041
hasPermission,
41-
} from "../../../../common/SecureComponent/SecureComponent";
42+
} from "../../../../common/SecureComponent";
4243
import { Theme } from "@mui/material/styles";
4344
import createStyles from "@mui/styles/createStyles";
4445
import { tableStyles } from "../../Common/FormComponents/common/styleLibrary";

portal-ui/src/screens/Console/Buckets/BucketDetails/AccessRulePanel.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ import {
3939
import { BucketInfo } from "../types";
4040
import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
4141
import PanelTitle from "../../Common/PanelTitle/PanelTitle";
42-
import SecureComponent, {
42+
import {
43+
SecureComponent,
4344
hasPermission,
44-
} from "../../../../common/SecureComponent/SecureComponent";
45+
} from "../../../../common/SecureComponent";
4546

4647
import withSuspense from "../../Common/Components/withSuspense";
4748
import RBIconButton from "./SummaryItems/RBIconButton";

portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import PageHeader from "../../Common/PageHeader/PageHeader";
3434
import SettingsIcon from "../../../../icons/SettingsIcon";
3535
import { BucketInfo } from "../types";
3636
import { setErrorSnackMessage } from "../../../../actions";
37-
import SecureComponent from "../../../../common/SecureComponent/SecureComponent";
37+
import { SecureComponent } from "../../../../common/SecureComponent";
3838
import {
3939
IAM_PERMISSIONS,
4040
IAM_ROLES,

0 commit comments

Comments
 (0)