Improved error handling in Object Browser (#3097)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2023-10-23 10:17:44 -06:00
committed by GitHub
parent 1767a37162
commit 8dbad84a58
11 changed files with 244 additions and 16 deletions

View File

@@ -14,7 +14,7 @@
// 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 { BucketObject } from "api/consoleApi";
import { ApiError, BucketObject } from "api/consoleApi";
import { IFileInfo } from "../ObjectDetails/types";
export interface BucketObjectItem {
@@ -38,13 +38,18 @@ export interface WebsocketRequest {
export interface WebsocketResponse {
request_id: number;
error?: string;
error?: WebsocketErrorResponse;
request_end?: boolean;
data?: ObjectResponse[];
prefix?: string;
bucketName?: string;
}
export interface WebsocketErrorResponse {
Code: number;
APIError: ApiError;
}
export interface ObjectResponse {
name: string;
last_modified: string;

View File

@@ -85,6 +85,12 @@ export const objectBrowserWSMiddleware = (
};
objectsWS.onmessage = (message) => {
const basicErrorMessage = {
errorMessage: "An error occurred",
detailedMessage:
"An unknown error occurred. Please refer to Console logs to get more information.",
};
const response: WebsocketResponse = JSON.parse(
message.data.toString(),
);
@@ -94,13 +100,10 @@ export const objectBrowserWSMiddleware = (
return;
}
if (
response.error ===
"The Access Key Id you provided does not exist in our records."
) {
// Session expired.
if (response.error?.Code === 401) {
// Session expired. We reload this page
window.location.reload();
} else if (response.error === "Access Denied.") {
} else if (response.error?.Code === 403) {
const internalPathsPrefix = response.prefix;
let pathPrefix = "";
@@ -119,10 +122,15 @@ export const objectBrowserWSMiddleware = (
);
if (!permitItems || permitItems.length === 0) {
const errorMsg = response.error.APIError;
dispatch(
setErrorSnackMessage({
errorMessage: response.error,
detailedError: response.error,
errorMessage:
errorMsg.message || basicErrorMessage.errorMessage,
detailedError:
errorMsg.detailedMessage ||
basicErrorMessage.detailedMessage,
}),
);
} else {
@@ -131,6 +139,19 @@ export const objectBrowserWSMiddleware = (
}
return;
} else if (response.error) {
const errorMsg = response.error.APIError;
dispatch(setRequestInProgress(false));
dispatch(
setErrorSnackMessage({
errorMessage:
errorMsg.message || basicErrorMessage.errorMessage,
detailedError:
errorMsg.detailedMessage ||
basicErrorMessage.detailedMessage,
}),
);
}
// This indicates final messages is received.

View File

@@ -0,0 +1,116 @@
// 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/>.
import * as roles from "../utils/roles";
import { Selector } from "testcafe";
import * as functions from "../utils/functions";
import { namedTestBucketBrowseButtonFor } from "../utils/functions";
fixture("Test error visibility in Object Browser Navigation").page(
"http://localhost:9090/",
);
const bucketName = "my-company";
const bucketName2 = "my-company2";
const bucketBrowseButton = namedTestBucketBrowseButtonFor(bucketName);
const bucketBrowseButton2 = namedTestBucketBrowseButtonFor(bucketName2);
export const file = Selector(".ReactVirtualized__Table__rowColumn").withText(
"test.txt",
);
export const deniedError = Selector(".message-text").withText("Access Denied.");
test
.before(async (t) => {
await functions.setUpNamedBucket(t, bucketName);
await functions.uploadNamedObjectToBucket(
t,
bucketName,
"test.txt",
"portal-ui/tests/uploads/test.txt",
);
await functions.uploadNamedObjectToBucket(
t,
bucketName,
"home/UserY/test.txt",
"portal-ui/tests/uploads/test.txt",
);
await functions.uploadNamedObjectToBucket(
t,
bucketName,
"home/UserX/test.txt",
"portal-ui/tests/uploads/test.txt",
);
})(
"Error Notification is shown in Object Browser when no privileges are set",
async (t) => {
await t
.useRole(roles.conditions3)
.navigateTo(`http://localhost:9090/browser`)
.click(bucketBrowseButton)
.click(Selector(".ReactVirtualized__Table__rowColumn").withText("home"))
.click(
Selector(".ReactVirtualized__Table__rowColumn").withText("UserX"),
)
.expect(deniedError.exists)
.ok();
},
)
.after(async (t) => {
await functions.cleanUpNamedBucketAndUploads(t, bucketName);
});
test
.before(async (t) => {
await functions.setUpNamedBucket(t, bucketName2);
await functions.setVersionedBucket(t, bucketName2);
await functions.uploadNamedObjectToBucket(
t,
bucketName2,
"test.txt",
"portal-ui/tests/uploads/test.txt",
);
await functions.uploadNamedObjectToBucket(
t,
bucketName2,
"home/UserY/test.txt",
"portal-ui/tests/uploads/test.txt",
);
await functions.uploadNamedObjectToBucket(
t,
bucketName2,
"home/UserX/test.txt",
"portal-ui/tests/uploads/test.txt",
);
})(
"Error Notification is shown in Object Browser with Rewind request set",
async (t) => {
await t
.useRole(roles.conditions4)
.navigateTo(`http://localhost:9090/browser`)
.click(bucketBrowseButton2)
.click(Selector("label").withText("Show deleted objects"))
.wait(1500)
.click(Selector(".ReactVirtualized__Table__rowColumn").withText("home"))
.click(
Selector(".ReactVirtualized__Table__rowColumn").withText("UserX"),
)
.expect(deniedError.exists)
.ok();
},
)
.after(async (t) => {
await functions.cleanUpNamedBucketAndUploads(t, bucketName2);
});

View File

@@ -0,0 +1,44 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowUserToSeeBucketListInTheConsole",
"Action": [
"s3:ListAllMyBuckets",
"s3:GetBucketLocation",
"s3:GetBucketVersioning"
],
"Effect": "Allow",
"Resource": ["arn:aws:s3:::*"]
},
{
"Sid": "AllowRootAndHomeListingOfCompanyBucket",
"Action": ["s3:ListBucket", "s3:List*"],
"Effect": "Allow",
"Resource": ["arn:aws:s3:::my-company2"],
"Condition": {
"StringEquals": {
"s3:prefix": ["", "home/", "home/User"],
"s3:delimiter": ["/"]
}
}
},
{
"Sid": "AllowListingOfUserFolder",
"Action": ["s3:ListBucket", "s3:List*"],
"Effect": "Allow",
"Resource": ["arn:aws:s3:::my-company2"],
"Condition": {
"StringLike": {
"s3:prefix": ["home/User/*"]
}
}
},
{
"Sid": "AllowAllS3ActionsInUserFolder",
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": ["arn:aws:s3:::my-company2/home/User/*"]
}
]
}

View File

@@ -31,6 +31,7 @@ remove_users() {
mc admin user remove minio conditions-$TIMESTAMP
mc admin user remove minio conditions-2-$TIMESTAMP
mc admin user remove minio conditions-3-$TIMESTAMP
mc admin user remove minio conditions-4-$TIMESTAMP
}
remove_policies() {
@@ -56,6 +57,7 @@ remove_policies() {
mc admin policy remove minio conditions-policy-$TIMESTAMP
mc admin policy remove minio conditions-policy-2-$TIMESTAMP
mc admin policy remove minio conditions-policy-3-$TIMESTAMP
mc admin policy remove minio conditions-policy-4-$TIMESTAMP
}
__init__() {

View File

@@ -49,6 +49,7 @@ create_policies() {
mc admin policy create minio conditions-policy-$TIMESTAMP portal-ui/tests/policies/conditionsPolicy.json
mc admin policy create minio conditions-policy-2-$TIMESTAMP portal-ui/tests/policies/conditionsPolicy2.json
mc admin policy create minio conditions-policy-3-$TIMESTAMP portal-ui/tests/policies/conditionsPolicy3.json
mc admin policy create minio conditions-policy-4-$TIMESTAMP portal-ui/tests/policies/conditionsPolicy4.json
}
create_users() {
@@ -79,6 +80,7 @@ create_users() {
mc admin user add minio conditions-$TIMESTAMP conditions1234
mc admin user add minio conditions-2-$TIMESTAMP conditions1234
mc admin user add minio conditions-3-$TIMESTAMP conditions1234
mc admin user add minio conditions-4-$TIMESTAMP conditions1234
}
create_buckets() {
@@ -114,4 +116,5 @@ assign_policies() {
mc admin policy attach minio conditions-policy-$TIMESTAMP --user conditions-$TIMESTAMP
mc admin policy attach minio conditions-policy-2-$TIMESTAMP --user conditions-2-$TIMESTAMP
mc admin policy attach minio conditions-policy-3-$TIMESTAMP --user conditions-3-$TIMESTAMP
mc admin policy attach minio conditions-policy-4-$TIMESTAMP --user conditions-4-$TIMESTAMP
}

View File

@@ -39,6 +39,7 @@ remove_users() {
mc admin user remove minio conditions-"$TIMESTAMP"
mc admin user remove minio conditions-2-"$TIMESTAMP"
mc admin user remove minio conditions-3-"$TIMESTAMP"
mc admin user remove minio conditions-4-"$TIMESTAMP"
}
remove_policies() {
@@ -65,6 +66,7 @@ remove_policies() {
mc admin policy remove conditions-policy-"$TIMESTAMP"
mc admin policy remove conditions-policy-2-"$TIMESTAMP"
mc admin policy remove conditions-policy-3-"$TIMESTAMP"
mc admin policy remove conditions-policy-4-"$TIMESTAMP"
}
remove_buckets() {

View File

@@ -283,3 +283,14 @@ export const conditions3 = Role(
},
{ preserveUrl: true },
);
export const conditions4 = Role(
loginUrl,
async (t) => {
await t
.typeText("#accessKey", "conditions-4-" + unixTimestamp)
.typeText("#secretKey", "conditions1234")
.click(submitButton);
},
{ preserveUrl: true },
);

View File

@@ -41,7 +41,7 @@ type ObjectsRequest struct {
type WSResponse struct {
RequestID int64 `json:"request_id,omitempty"`
Error string `json:"error,omitempty"`
Error *CodedAPIError `json:"error,omitempty"`
RequestEnd bool `json:"request_end,omitempty"`
Prefix string `json:"prefix,omitempty"`
BucketName string `json:"bucketName,omitempty"`

View File

@@ -108,6 +108,10 @@ func ErrorWithContext(ctx context.Context, err ...interface{}) *CodedAPIError {
errorCode = 401
errorMessage = ErrInvalidLogin.Error()
}
if strings.Contains(strings.ToLower(err1.Error()), ErrAccessDenied.Error()) {
errorCode = 403
errorMessage = err1.Error()
}
// If the last error is ErrInvalidLogin, this is a login failure
if errors.Is(lastError, ErrInvalidLogin) {
errorCode = 401

View File

@@ -60,6 +60,7 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
err := json.Unmarshal(message, &messageRequest)
if err != nil {
LogInfo("Error on message request unmarshal")
close(done)
return
}
@@ -95,6 +96,14 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
if err != nil {
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, err),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
}
return
}
var buffer []ObjectResponse
@@ -105,7 +114,7 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
if lsObj.Err != nil {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: lsObj.Err.Error(),
Error: ErrorWithContext(ctx, lsObj.Err),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
}
@@ -159,7 +168,13 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
if err != nil {
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
cancel()
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, err),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
}
return
}
@@ -167,8 +182,13 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
s3Client, err := newS3BucketClient(session, objectRqConfigs.BucketName, objectRqConfigs.Prefix, clientIP)
if err != nil {
LogError("error creating S3Client:", err)
close(done)
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, err),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
}
cancel()
return
}
@@ -181,7 +201,7 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
if lsObj.Err != nil {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: lsObj.Err.String(),
Error: ErrorWithContext(ctx, lsObj.Err.ToGoError()),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
}