Improved error handling in Object Browser (#3097)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
116
portal-ui/tests/permissions-7/errorsVisibleOB.ts
Normal file
116
portal-ui/tests/permissions-7/errorsVisibleOB.ts
Normal 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);
|
||||
});
|
||||
44
portal-ui/tests/policies/conditionsPolicy4.json
Normal file
44
portal-ui/tests/policies/conditionsPolicy4.json
Normal 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/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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__() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user