Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 1 addition & 89 deletions portal-ui/src/common/SecureComponent/SecureComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,95 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import React, { cloneElement } from "react";
import get from "lodash/get";
import { store } from "../../store";
import { hasAccessToResource } from "./permissions";

export const hasPermission = (
resource: string | string[] | undefined,
scopes: string[],
matchAll?: boolean,
containsResource?: boolean
) => {
if (!resource) {
return false;
}
const state = store.getState();
const sessionGrants = state.console.session.permissions || {};

const globalGrants = sessionGrants["arn:aws:s3:::*"] || [];
let resources: string[] = [];
let resourceGrants: string[] = [];
let containsResourceGrants: string[] = [];

if (resource) {
if (Array.isArray(resource)) {
resources = [...resources, ...resource];
} else {
resources.push(resource);
}

// Filter wildcard items
const wildcards = Object.keys(sessionGrants).filter(
(item) => item.includes("*") && item !== "arn:aws:s3:::*"
);

const getMatchingWildcards = (path: string) => {
const items = wildcards.map((element) => {
const wildcardItemSection = element.split(":").slice(-1)[0];

const replaceWildcard = wildcardItemSection
.replace("/", "\\/")
.replace("\\/*", "($|(\\/.*?))");

const inRegExp = new RegExp(`${replaceWildcard}$`, "gm");

if(inRegExp.exec(path)) {
return element;
}

return null;
});

return items.filter(itm => itm !== null);
};

resources.forEach((rsItem) => {
// Validation against inner paths & wildcards
let wildcardRules =getMatchingWildcards(rsItem);

let wildcardGrants: string[] = [];

wildcardRules.forEach((rule) => {
if(rule) {
const wcResources = get(sessionGrants, rule, []);
wildcardGrants = [...wildcardGrants, ...wcResources];
}
});

const simpleResources = get(sessionGrants, rsItem, []);
const s3Resources = get(sessionGrants, `arn:aws:s3:::${rsItem}/*`, []);

resourceGrants = [...simpleResources, ...s3Resources, ...wildcardGrants];

if (containsResource) {
const matchResource = `arn:aws:s3:::${rsItem}`;

Object.entries(sessionGrants).forEach(([key, value]) => {
if (key.includes(matchResource)) {
containsResourceGrants = [...containsResourceGrants, ...value];
}
});
}
});
}

return hasAccessToResource(
[...resourceGrants, ...globalGrants, ...containsResourceGrants],
scopes,
matchAll
);
};
import hasPermission from "./accessControl";

interface ISecureComponentProps {
errorProps?: any;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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/>.

import hasPermission from "../accessControl";
import { store } from "../../../store";
import { SESSION_RESPONSE } from "../../../screens/Console/actions";

const setPolicy1 = () => {
store.dispatch({
type: SESSION_RESPONSE,
message: {
distributedMode: true,
features: ["log-search"],
permissions: {
"arn:aws:s3:::testcafe": [
"admin:CreateUser",
"s3:GetBucketLocation",
"s3:ListBucket",
"admin:CreateServiceAccount",
],
"arn:aws:s3:::testcafe/*": [
"admin:CreateServiceAccount",
"admin:CreateUser",
"s3:GetObject",
"s3:ListBucket",
],
"arn:aws:s3:::testcafe/write/*": [
"admin:CreateServiceAccount",
"admin:CreateUser",
"s3:PutObject",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListBucket",
],
"console-ui": ["admin:CreateServiceAccount", "admin:CreateUser"],
},
operator: false,
status: "ok",
},
});
};

test("Upload button disabled", () => {
setPolicy1();
expect(hasPermission("testcafe", ["s3:PutObject"])).toBe(false);
});

test("Upload button enabled valid prefix", () => {
setPolicy1();
expect(hasPermission("testcafe/write", ["s3:PutObject"], false, true)).toBe(
true
);
});
136 changes: 136 additions & 0 deletions portal-ui/src/common/SecureComponent/accessControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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/>.

import { store } from "../../store";
import get from "lodash/get";
import { IAM_SCOPES } from "./permissions";

const hasPermission = (
resource: string | string[] | undefined,
scopes: string[],
matchAll?: boolean,
containsResource?: boolean
) => {
if (!resource) {
return false;
}
const state = store.getState();
const sessionGrants = state.console.session.permissions || {};

const globalGrants = sessionGrants["arn:aws:s3:::*"] || [];
let resources: string[] = [];
let resourceGrants: string[] = [];
let containsResourceGrants: string[] = [];

if (resource) {
if (Array.isArray(resource)) {
resources = [...resources, ...resource];
} else {
resources.push(resource);
}

// Filter wildcard items
const wildcards = Object.keys(sessionGrants).filter(
(item) => item.includes("*") && item !== "arn:aws:s3:::*"
);

const getMatchingWildcards = (path: string) => {
const items = wildcards.map((element) => {
const wildcardItemSection = element.split(":").slice(-1)[0];

const replaceWildcard = wildcardItemSection
.replace("/", "\\/")
.replace("\\/*", "($|(\\/.*?))");

const inRegExp = new RegExp(`${replaceWildcard}$`, "gm");

if (inRegExp.exec(path)) {
return element;
}

return null;
});

return items.filter((itm) => itm !== null);
};

resources.forEach((rsItem) => {
// Validation against inner paths & wildcards
let wildcardRules = getMatchingWildcards(rsItem);

let wildcardGrants: string[] = [];

wildcardRules.forEach((rule) => {
if (rule) {
const wcResources = get(sessionGrants, rule, []);
wildcardGrants = [...wildcardGrants, ...wcResources];
}
});

const simpleResources = get(sessionGrants, rsItem, []);
const s3Resources = get(sessionGrants, `arn:aws:s3:::${rsItem}/*`, []);

resourceGrants = [...simpleResources, ...s3Resources, ...wildcardGrants];

if (containsResource) {
const matchResource = `arn:aws:s3:::${rsItem}`;

Object.entries(sessionGrants).forEach(([key, value]) => {
if (key.includes(matchResource)) {
containsResourceGrants = [...containsResourceGrants, ...value];
}
});
}
});
}

return hasAccessToResource(
[...resourceGrants, ...globalGrants, ...containsResourceGrants],
scopes,
matchAll
);
};

// hasAccessToResource receives a list of user permissions to perform on a specific resource, then compares those permissions against
// a list of required permissions and return true or false depending of the level of required access (match all permissions,
// match some of the permissions)
const hasAccessToResource = (
userPermissionsOnBucket: string[] | null | undefined,
requiredPermissions: string[] = [],
matchAll?: boolean
) => {
if (!userPermissionsOnBucket) {
return false;
}

const s3All = userPermissionsOnBucket.includes(IAM_SCOPES.S3_ALL_ACTIONS);
const AdminAll = userPermissionsOnBucket.includes(
IAM_SCOPES.ADMIN_ALL_ACTIONS
);

const permissions = requiredPermissions.filter(function (n) {
return (
userPermissionsOnBucket.indexOf(n) !== -1 ||
(n.indexOf("s3:") !== -1 && s3All) ||
(n.indexOf("admin:") !== -1 && AdminAll)
);
});
return matchAll
? permissions.length === requiredPermissions.length
: permissions.length > 0;
};

export default hasPermission;
18 changes: 18 additions & 0 deletions portal-ui/src/common/SecureComponent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { default as hasPermission } from "./accessControl";
export { default as SecureComponent } from "./SecureComponent";
29 changes: 0 additions & 29 deletions portal-ui/src/common/SecureComponent/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,6 @@
// 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/>.

// hasAccessToResource receives a list of user permissions to perform on a specific resource, then compares those permissions against
// a list of required permissions and return true or false depending of the level of required access (match all permissions,
// match some of the permissions)
export const hasAccessToResource = (
userPermissionsOnBucket: string[] | null | undefined,
requiredPermissions: string[] = [],
matchAll?: boolean
) => {
if (!userPermissionsOnBucket) {
return false;
}

const s3All = userPermissionsOnBucket.includes(IAM_SCOPES.S3_ALL_ACTIONS);
const AdminAll = userPermissionsOnBucket.includes(
IAM_SCOPES.ADMIN_ALL_ACTIONS
);

const permissions = requiredPermissions.filter(function (n) {
return (
userPermissionsOnBucket.indexOf(n) !== -1 ||
(n.indexOf("s3:") !== -1 && s3All) ||
(n.indexOf("admin:") !== -1 && AdminAll)
);
});
return matchAll
? permissions.length === requiredPermissions.length
: permissions.length > 0;
};

export const IAM_ROLES = {
BUCKET_OWNER: "BUCKET_OWNER", // upload/delete objects from the bucket
BUCKET_VIEWER: "BUCKET_VIEWER", // only view objects on the bucket
Expand Down
2 changes: 1 addition & 1 deletion portal-ui/src/screens/Console/Account/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
import SecureComponent from "../../../common/SecureComponent/SecureComponent";
import { SecureComponent } from "../../../common/SecureComponent";
import RBIconButton from "../Buckets/BucketDetails/SummaryItems/RBIconButton";
import { selectSAs } from "../Configurations/utils";
import DeleteMultipleServiceAccounts from "../Users/DeleteMultipleServiceAccounts";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ import {
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import PanelTitle from "../../Common/PanelTitle/PanelTitle";
import SecureComponent, {
import {
SecureComponent,
hasPermission,
} from "../../../../common/SecureComponent/SecureComponent";
} from "../../../../common/SecureComponent";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { tableStyles } from "../../Common/FormComponents/common/styleLibrary";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ import {
import { BucketInfo } from "../types";
import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
import PanelTitle from "../../Common/PanelTitle/PanelTitle";
import SecureComponent, {
import {
SecureComponent,
hasPermission,
} from "../../../../common/SecureComponent/SecureComponent";
} from "../../../../common/SecureComponent";

import withSuspense from "../../Common/Components/withSuspense";
import RBIconButton from "./SummaryItems/RBIconButton";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import PageHeader from "../../Common/PageHeader/PageHeader";
import SettingsIcon from "../../../../icons/SettingsIcon";
import { BucketInfo } from "../types";
import { setErrorSnackMessage } from "../../../../actions";
import SecureComponent from "../../../../common/SecureComponent/SecureComponent";
import { SecureComponent } from "../../../../common/SecureComponent";
import {
IAM_PERMISSIONS,
IAM_ROLES,
Expand Down
Loading