Files
rustfs-console/utils/console-policy-parser.ts
T

178 lines
6.1 KiB
TypeScript

import { resourceMatch } from './bucket-policy'
import { CONSOLE_SCOPES } from './console-permissions'
export interface ConsoleStatement {
Effect: 'Allow' | 'Deny'
Action: string[]
Resource?: string[]
}
export interface ConsolePolicy {
Version: string
Statement: ConsoleStatement[]
}
/**
* Helper to match action against patterns (supports wildcards)
*/
function matchAction(policyActions: string[], requestAction: string): boolean {
return policyActions.some(pattern => resourceMatch(pattern, requestAction))
}
// Define implied scopes based on MinIO permissions
const IMPLIED_SCOPES: Record<string, string[]> = {
[CONSOLE_SCOPES.VIEW_BROWSER]: [
's3:ListAllMyBuckets',
's3:ListBucket',
's3:GetBucketLocation',
's3:GetObject',
's3:PutObject',
's3:DeleteObject',
's3:ListObjects',
's3:GetObjectTagging',
's3:PutObjectTagging',
's3:DeleteObjectTagging',
's3:GetBucketTagging',
's3:PutBucketTagging',
's3:DeleteBucketTagging',
's3:GetBucketPolicy',
's3:PutBucketPolicy',
's3:DeleteBucketPolicy',
's3:*',
],
[CONSOLE_SCOPES.VIEW_POLICIES]: [
'admin:ListUserPolicies',
'admin:CreatePolicy',
'admin:DeletePolicy',
'admin:GetPolicy',
'admin:*',
],
[CONSOLE_SCOPES.VIEW_USERS]: [
'admin:ListUsers',
'admin:CreateUser',
'admin:GetUser',
'admin:EnableUser',
'admin:DisableUser',
'admin:DeleteUser',
'admin:*',
],
[CONSOLE_SCOPES.VIEW_USER_GROUPS]: ['admin:ListGroups', 'admin:AddUserToGroup', 'admin:GetGroup', 'admin:*'],
[CONSOLE_SCOPES.VIEW_PERFORMANCE]: ['admin:ServerInfo', 'admin:OBDInfo', 'admin:ServerTrace', 'admin:*'],
[CONSOLE_SCOPES.VIEW_IMPORT_EXPORT]: ['admin:ConfigUpdate', 'admin:*'],
[CONSOLE_SCOPES.VIEW_BUCKET_EVENTS]: [
's3:GetBucketNotification',
's3:PutBucketNotification',
's3:ListenNotification',
's3:ListenBucketNotification',
's3:*',
],
[CONSOLE_SCOPES.VIEW_BUCKET_REPLICATION]: [
's3:GetReplicationConfiguration',
's3:PutReplicationConfiguration',
's3:*',
],
[CONSOLE_SCOPES.VIEW_BUCKET_LIFECYCLE]: ['s3:GetLifecycleConfiguration', 's3:PutLifecycleConfiguration', 's3:*'],
[CONSOLE_SCOPES.VIEW_TIERED_STORAGE]: ['admin:ConfigUpdate', 'admin:*'],
[CONSOLE_SCOPES.VIEW_EVENT_DESTINATIONS]: ['admin:ConfigUpdate', 'admin:*'],
[CONSOLE_SCOPES.VIEW_SSE_SETTINGS]: ['admin:ConfigUpdate', 'admin:*', 'kms:*'],
[CONSOLE_SCOPES.VIEW_LICENSE]: ['admin:ServerInfo', 'admin:*'],
}
/**
* Helper to match resource against patterns (supports wildcards)
*/
function matchResource(policyResources: string[] | undefined, requestResource: string): boolean {
if (!policyResources || policyResources.length === 0) {
// If no resource is specified, we assume it matches (or strictly it should be invalid, but for console actions often resource is implied)
// However, safest is to require resource match if resources are present.
// Let's assume '*' if undefined for console actions flexibility, or stricter?
// AWS IAM requires Resource. Let's assume if undefined, it doesn't match unless the action is special.
// But for simplicity in this project, if Resource is missing in statement, maybe we ignore resource check?
// Let's stick to: if provided in policy, must match. If not provided, match.
return true
}
return policyResources.some(pattern => resourceMatch(pattern, requestResource))
}
/**
* Check if the policy allows the given action on the given resource.
* @param policy The user's policy (merged statements)
* @param action The action to check (e.g. 'console:Browser')
* @param resource The resource to check (default 'console')
*/
export function hasConsolePermission(
policy: ConsolePolicy | ConsoleStatement[] | undefined,
action: string,
resource: string = 'console'
): boolean {
if (!policy) return false
const statements = Array.isArray(policy) ? policy : policy.Statement || []
if (statements.length === 0) return false
// 1. Check for specific Deny
const denied = statements.some(
s => s.Effect === 'Deny' && matchAction(s.Action, action) && matchResource(s.Resource, resource)
)
if (denied) return false
// 2. Check for Allow
// Special case: if action is consoleAdmin (in policy), it grants everything?
// The user said: "if user's policy contains consoleAdmin... means user has all console permissions"
// This likely means if they have 'consoleAdmin' action ALLOWED, they get everything.
// But we are checking for a specific action (e.g. 'console:Browser').
// So we should check if the policy allows 'console:Browser' OR 'consoleAdmin' OR 'console:*'.
// We check if ANY allow statement matches the requested action OR 'consoleAdmin' (if that's considered a super-permission)
const allowed = statements.some(s => {
if (s.Effect !== 'Allow') return false
// Check if statement allows the specific action
const actionMatch = matchAction(s.Action, action)
// Check if statement allows consoleAdmin (superuser for console)
const adminMatch = matchAction(s.Action, CONSOLE_SCOPES.CONSOLE_ADMIN)
// Check if statement allows console:* (common pattern)
const wildcardMatch = matchAction(s.Action, 'console:*')
// Check if statement allows admin:* (superuser)
const adminStarMatch = matchAction(s.Action, 'admin:*')
const explicitMatch =
(actionMatch || adminMatch || wildcardMatch || adminStarMatch) && matchResource(s.Resource, resource)
if (explicitMatch) return true
// Check implied scopes
const impliedActions = IMPLIED_SCOPES[action]
if (impliedActions) {
// If any of the implied actions are allowed by the policy
if (impliedActions.some(implied => matchAction(s.Action, implied))) {
return true
}
}
return false
})
return allowed
}
/**
* Check if user has ALL of the required scopes
*/
export function hasConsoleScopes(
policy: ConsolePolicy | ConsoleStatement[] | undefined,
scopes: string[],
matchAll: boolean = true
): boolean {
if (!scopes || scopes.length === 0) return true
if (matchAll) {
return scopes.every(scope => hasConsolePermission(policy, scope))
} else {
return scopes.some(scope => hasConsolePermission(policy, scope))
}
}