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:
2
go.mod
2
go.mod
@@ -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
|
||||
|
||||
73
models/permission_resource.go
Normal file
73
models/permission_resource.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ const initialState: ConsoleState = {
|
||||
features: [],
|
||||
distributedMode: false,
|
||||
permissions: {},
|
||||
allowResources: null,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user