Display temporal paths when a policy has prefixes to allow navigation (#2011)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Alex
2022-05-19 00:40:52 -05:00
committed by GitHub
parent dc3e7f5888
commit a160b92529
11 changed files with 1218 additions and 7 deletions

2
go.mod
View File

@@ -22,7 +22,7 @@ require (
github.com/minio/cli v1.22.0
github.com/minio/highwayhash v1.0.2
github.com/minio/kes v0.19.2
github.com/minio/madmin-go v1.3.13
github.com/minio/madmin-go v1.3.14
github.com/minio/mc v0.0.0-20220512134321-aa60a8db1e4d
github.com/minio/minio-go/v7 v7.0.26
github.com/minio/operator v0.0.0-20220414212219-ba4c097324b2

856
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// PermissionResource permission resource
//
// swagger:model permissionResource
type PermissionResource struct {
// condition operator
ConditionOperator string `json:"conditionOperator,omitempty"`
// prefixes
Prefixes []string `json:"prefixes"`
// resource
Resource string `json:"resource,omitempty"`
}
// Validate validates this permission resource
func (m *PermissionResource) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this permission resource based on context it is used
func (m *PermissionResource) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *PermissionResource) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *PermissionResource) UnmarshalBinary(b []byte) error {
var res PermissionResource
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -25,6 +25,7 @@ package models
import (
"context"
"encoding/json"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
@@ -37,6 +38,9 @@ import (
// swagger:model sessionResponse
type SessionResponse struct {
// allow resources
AllowResources []*PermissionResource `json:"allowResources"`
// distributed mode
DistributedMode bool `json:"distributedMode,omitempty"`
@@ -58,6 +62,10 @@ type SessionResponse struct {
func (m *SessionResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateAllowResources(formats); err != nil {
res = append(res, err)
}
if err := m.validateStatus(formats); err != nil {
res = append(res, err)
}
@@ -68,6 +76,32 @@ func (m *SessionResponse) Validate(formats strfmt.Registry) error {
return nil
}
func (m *SessionResponse) validateAllowResources(formats strfmt.Registry) error {
if swag.IsZero(m.AllowResources) { // not required
return nil
}
for i := 0; i < len(m.AllowResources); i++ {
if swag.IsZero(m.AllowResources[i]) { // not required
continue
}
if m.AllowResources[i] != nil {
if err := m.AllowResources[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("allowResources" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("allowResources" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
var sessionResponseTypeStatusPropEnum []interface{}
func init() {
@@ -107,8 +141,37 @@ func (m *SessionResponse) validateStatus(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this session response based on context it is used
// ContextValidate validate this session response based on the context it is used
func (m *SessionResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateAllowResources(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *SessionResponse) contextValidateAllowResources(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.AllowResources); i++ {
if m.AllowResources[i] != nil {
if err := m.AllowResources[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("allowResources" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("allowResources" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}

View File

@@ -50,9 +50,12 @@ import {
} from "../../../../Common/FormComponents/common/styleLibrary";
import { Badge, Typography } from "@mui/material";
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
import { download, extensionPreview, sortListObjects } from "../utils";
import {
download,
extensionPreview,
permissionItems,
sortListObjects,
} from "../utils";
import {
BucketInfo,
BucketObjectLocking,
@@ -87,6 +90,7 @@ import ActionsListSection from "./ActionsListSection";
import { listModeColumns, rewindModeColumns } from "./ListObjectsHelpers";
import VersionsNavigator from "../ObjectDetails/VersionsNavigator";
import CheckboxWrapper from "../../../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import {
setErrorSnackMessage,
setSnackBarMessage,
@@ -305,6 +309,9 @@ const ListObjects = ({ match, history }: IListObjectsProps) => {
const bucketInfo = useSelector(
(state: AppState) => state.buckets.bucketDetails.bucketInfo
);
const allowResources = useSelector(
(state: AppState) => state.console.session.allowResources
);
const [records, setRecords] = useState<BucketObjectItem[]>([]);
const [deleteMultipleOpen, setDeleteMultipleOpen] = useState<boolean>(false);
@@ -673,8 +680,19 @@ const ListObjects = ({ match, history }: IListObjectsProps) => {
}
})
.catch((err: ErrorResponseHandler) => {
const permitItems = permissionItems(
bucketName,
pathPrefix,
allowResources || []
);
if (!permitItems || permitItems.length === 0) {
dispatch(setErrorSnackMessage(err));
} else {
setRecords(permitItems);
}
dispatch(setLoadingObjectsList(false));
dispatch(setErrorSnackMessage(err));
});
} else {
dispatch(setLoadingObjectsList(false));
@@ -692,6 +710,7 @@ const ListObjects = ({ match, history }: IListObjectsProps) => {
showDeleted,
displayListObjects,
bucketToRewind,
allowResources,
]);
// bucket info

View File

@@ -15,6 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { BucketObjectItem } from "./ListObjects/types";
import { IAllowResources } from "../../../types";
export const download = (
bucketName: string,
@@ -161,3 +162,100 @@ export const sortListObjects = (fieldSort: string) => {
(a.size || -1) - (b.size || -1);
}
};
export const permissionItems = (
bucketName: string,
currentPath: string,
permissionsArray: IAllowResources[]
): BucketObjectItem[] | null => {
if (permissionsArray.length === 0) {
return null;
}
// We get permissions applied to the current bucket
const filteredPermissionsForBucket = permissionsArray.filter(
(permissionItem) =>
permissionItem.resource.endsWith(`:${bucketName}`) ||
permissionItem.resource.includes(`:${bucketName}/`)
);
// No permissions for this bucket. we can throw the error message at this point
if (filteredPermissionsForBucket.length === 0) {
return null;
}
const returnElements: BucketObjectItem[] = [];
// We split current path
const splitCurrentPath = currentPath.split("/");
filteredPermissionsForBucket.forEach((permissionElement) => {
// We review paths in resource address
// We split ARN & get the last item to check the URL
const splitARN = permissionElement.resource.split(":");
const url = splitARN.pop() || "";
// We split the paths of the URL & compare against current location to see if there are more items to include. In case current level is a wildcard or is the last one, we omit this validation
const splitURL = url.split("/");
// splitURL has more items than bucket name, we can continue validating
if (splitURL.length > 1) {
splitURL.every((currentElementInPath, index) => {
// It is a wildcard element. We can stor the verification as value should be included (?)
if (currentElementInPath === "*") {
return false;
}
// Element is not included in the path. The user is trying to browse something else.
if (
splitCurrentPath[index] &&
splitCurrentPath[index] !== currentElementInPath
) {
return false;
}
// This element is not included by index in the current paths list. We add it so user can browse into it
if (!splitCurrentPath[index]) {
returnElements.push({
name: `${currentElementInPath}/`,
size: 0,
last_modified: new Date(),
version_id: "",
});
}
return true;
});
}
// We review prefixes in allow resources for StringEquals variant only.
if (permissionElement.conditionOperator === "StringEquals" || permissionElement.conditionOperator === "StringLike") {
permissionElement.prefixes.forEach((prefixItem) => {
// Prefix Item is not empty?
if (prefixItem !== "") {
const splitItems = prefixItem.split("/");
splitItems.every((splitElement, index) => {
if (!splitElement.includes("*")) {
if (splitElement !== splitURL[index]) {
returnElements.push({
name: `${splitElement}/`,
size: 0,
last_modified: new Date(),
version_id: "",
});
return false;
}
return true;
}
return false;
});
}
});
}
});
return returnElements;
};

View File

@@ -28,6 +28,7 @@ const initialState: ConsoleState = {
features: [],
distributedMode: false,
permissions: {},
allowResources: null,
},
};

View File

@@ -18,10 +18,17 @@ export interface ISessionPermissions {
[key: string]: string[];
}
export interface IAllowResources {
conditionOperator: string;
prefixes: string[];
resource: string;
}
export interface ISessionResponse {
status: string;
features: string[];
operator: boolean;
distributedMode: boolean;
permissions: ISessionPermissions;
allowResources: IAllowResources[] | null;
}

View File

@@ -5803,6 +5803,23 @@ func init() {
}
}
},
"permissionResource": {
"type": "object",
"properties": {
"conditionOperator": {
"type": "string"
},
"prefixes": {
"type": "array",
"items": {
"type": "string"
}
},
"resource": {
"type": "string"
}
}
},
"policy": {
"type": "object",
"properties": {
@@ -6173,6 +6190,12 @@ func init() {
"sessionResponse": {
"type": "object",
"properties": {
"allowResources": {
"type": "array",
"items": {
"$ref": "#/definitions/permissionResource"
}
},
"distributedMode": {
"type": "boolean"
},
@@ -12877,6 +12900,23 @@ func init() {
}
}
},
"permissionResource": {
"type": "object",
"properties": {
"conditionOperator": {
"type": "string"
},
"prefixes": {
"type": "array",
"items": {
"type": "string"
}
},
"resource": {
"type": "string"
}
}
},
"policy": {
"type": "object",
"properties": {
@@ -13247,6 +13287,12 @@ func init() {
"sessionResponse": {
"type": "object",
"properties": {
"allowResources": {
"type": "array",
"items": {
"$ref": "#/definitions/permissionResource"
}
},
"distributedMode": {
"type": "boolean"
},

View File

@@ -39,6 +39,10 @@ import (
authApi "github.com/minio/console/restapi/operations/auth"
)
type Conditions struct {
S3Prefix []string `json:"s3:prefix"`
}
func registerSessionHandlers(api *operations.ConsoleAPI) {
// session check
api.AuthSessionCheckHandler = authApi.SessionCheckHandlerFunc(func(params authApi.SessionCheckParams, session *models.Principal) middleware.Responder {
@@ -138,10 +142,15 @@ func getSessionResponse(ctx context.Context, session *models.Principal) (*models
ConsoleResourceName: defaultActions,
}
deniedActions := map[string]minioIAMPolicy.ActionSet{}
var allowResources []*models.PermissionResource
for _, statement := range policy.Statements {
for _, resource := range statement.Resources.ToSlice() {
resourceName := resource.String()
statementActions := statement.Actions.ToSlice()
var prefixes []string
if statement.Effect == "Allow" {
// check if val are denied before adding them to the map
var allowedActions []minioIAMPolicy.Action
@@ -164,6 +173,30 @@ func getSessionResponse(ctx context.Context, session *models.Principal) (*models
mergedActions := append(defaultActions.ToSlice(), allowedActions...)
permissions[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...)
}
// Allow Permissions request
conditions, err := statement.Conditions.MarshalJSON()
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
var wrapper map[string]Conditions
if err := json.Unmarshal(conditions, &wrapper); err != nil {
return nil, ErrorWithContext(ctx, err)
}
for condition, elements := range wrapper {
prefixes = elements.S3Prefix
resourceElement := models.PermissionResource{
Resource: resourceName,
Prefixes: prefixes,
ConditionOperator: condition,
}
allowResources = append(allowResources, &resourceElement)
}
} else {
// Add new banned actions to the map
if resourceActions, ok := deniedActions[resourceName]; ok {
@@ -210,6 +243,7 @@ func getSessionResponse(ctx context.Context, session *models.Principal) (*models
Operator: false,
DistributedMode: erasure,
Permissions: resourcePermissions,
AllowResources: allowResources,
}
return sessionResp, nil
}

View File

@@ -3686,6 +3686,10 @@ definitions:
type: array
items:
type: string
allowResources:
type: array
items:
$ref: "#/definitions/permissionResource"
widgetResult:
type: object
@@ -4778,3 +4782,15 @@ definitions:
type: string
latest_version:
type: string
permissionResource:
type: object
properties:
resource:
type: string
conditionOperator:
type: string
prefixes:
type: array
items:
type: string