feat: add admin route policy inventory (#3286)
* feat: add admin route policy inventory * fix: align table route policy actions * fix: align load table route policy action
This commit is contained in:
@@ -82,6 +82,7 @@ rustfs-protos = { workspace = true }
|
||||
rustfs-rio.workspace = true
|
||||
rustfs-s3-types = { workspace = true }
|
||||
rustfs-s3-ops = { workspace = true }
|
||||
rustfs-security-governance = { workspace = true }
|
||||
rustfs-data-usage = { workspace = true }
|
||||
rustfs-s3select-api = { workspace = true }
|
||||
rustfs-s3select-query = { workspace = true }
|
||||
|
||||
@@ -16,6 +16,9 @@ mod auth;
|
||||
pub mod console;
|
||||
pub mod handlers;
|
||||
mod plugin_contract;
|
||||
// Contract inventory is validated by tests before later runtime integration.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod route_policy;
|
||||
pub mod router;
|
||||
pub mod service;
|
||||
pub mod site_replication_identity;
|
||||
|
||||
@@ -0,0 +1,879 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use rustfs_security_governance::{
|
||||
AdminActionRef, AdminRouteMatrixError, AdminRouteSpec, HttpMethod, PublicRouteKind, RouteRiskLevel,
|
||||
validate_admin_route_specs,
|
||||
};
|
||||
|
||||
const ALL_ADMIN: AdminActionRef = AdminActionRef::new("AllAdminActions");
|
||||
const ADD_USER_TO_GROUP: AdminActionRef = AdminActionRef::new("AddUserToGroupAdminAction");
|
||||
const ATTACH_POLICY: AdminActionRef = AdminActionRef::new("AttachPolicyAdminAction");
|
||||
const CONFIG_UPDATE: AdminActionRef = AdminActionRef::new("ConfigUpdateAdminAction");
|
||||
const COMMIT_TABLE: AdminActionRef = AdminActionRef::new("CommitTableAction");
|
||||
const CREATE_POLICY: AdminActionRef = AdminActionRef::new("CreatePolicyAdminAction");
|
||||
const CREATE_SERVICE_ACCOUNT: AdminActionRef = AdminActionRef::new("CreateServiceAccountAdminAction");
|
||||
const CREATE_TABLE: AdminActionRef = AdminActionRef::new("CreateTableAction");
|
||||
const DECOMMISSION: AdminActionRef = AdminActionRef::new("DecommissionAdminAction");
|
||||
const DELETE_POLICY: AdminActionRef = AdminActionRef::new("DeletePolicyAdminAction");
|
||||
const DELETE_TABLE: AdminActionRef = AdminActionRef::new("DeleteTableAction");
|
||||
const DELETE_TABLE_NAMESPACE: AdminActionRef = AdminActionRef::new("DeleteTableNamespaceAction");
|
||||
const DELETE_USER: AdminActionRef = AdminActionRef::new("DeleteUserAdminAction");
|
||||
const ENABLE_GROUP: AdminActionRef = AdminActionRef::new("EnableGroupAdminAction");
|
||||
const ENABLE_USER: AdminActionRef = AdminActionRef::new("EnableUserAdminAction");
|
||||
const EXPORT_BUCKET_METADATA: AdminActionRef = AdminActionRef::new("ExportBucketMetadataAction");
|
||||
const EXPORT_IAM: AdminActionRef = AdminActionRef::new("ExportIAMAction");
|
||||
const GET_BUCKET_TARGET: AdminActionRef = AdminActionRef::new("GetBucketTargetAction");
|
||||
const GET_GROUP: AdminActionRef = AdminActionRef::new("GetGroupAdminAction");
|
||||
const GET_POLICY: AdminActionRef = AdminActionRef::new("GetPolicyAdminAction");
|
||||
const GET_REPLICATION_METRICS: AdminActionRef = AdminActionRef::new("GetReplicationMetricsAction");
|
||||
const GET_TABLE: AdminActionRef = AdminActionRef::new("GetTableAction");
|
||||
const GET_TABLE_CATALOG: AdminActionRef = AdminActionRef::new("GetTableCatalogAction");
|
||||
const GET_TABLE_METADATA: AdminActionRef = AdminActionRef::new("GetTableMetadataAction");
|
||||
const GET_TABLE_NAMESPACE: AdminActionRef = AdminActionRef::new("GetTableNamespaceAction");
|
||||
const HEAL: AdminActionRef = AdminActionRef::new("HealAdminAction");
|
||||
const IMPORT_BUCKET_METADATA: AdminActionRef = AdminActionRef::new("ImportBucketMetadataAction");
|
||||
const IMPORT_IAM: AdminActionRef = AdminActionRef::new("ImportIAMAction");
|
||||
const KMS_CREATE_KEY: AdminActionRef = AdminActionRef::new("KMSCreateKeyAdminAction");
|
||||
const KMS_KEY_STATUS: AdminActionRef = AdminActionRef::new("KMSKeyStatusAdminAction");
|
||||
const LIST_GROUPS: AdminActionRef = AdminActionRef::new("ListGroupsAdminAction");
|
||||
const LIST_TEMPORARY_ACCOUNTS: AdminActionRef = AdminActionRef::new("ListTemporaryAccountsAdminAction");
|
||||
const LIST_TIER: AdminActionRef = AdminActionRef::new("ListTierAction");
|
||||
const LIST_USER_POLICIES: AdminActionRef = AdminActionRef::new("ListUserPoliciesAdminAction");
|
||||
const LIST_USERS: AdminActionRef = AdminActionRef::new("ListUsersAdminAction");
|
||||
const PROFILING: AdminActionRef = AdminActionRef::new("ProfilingAdminAction");
|
||||
const REBALANCE: AdminActionRef = AdminActionRef::new("RebalanceAdminAction");
|
||||
const REGISTER_TABLE: AdminActionRef = AdminActionRef::new("RegisterTableAction");
|
||||
const REMOVE_USER_FROM_GROUP: AdminActionRef = AdminActionRef::new("RemoveUserFromGroupAdminAction");
|
||||
const SERVER_INFO: AdminActionRef = AdminActionRef::new("ServerInfoAdminAction");
|
||||
const SET_BUCKET_QUOTA: AdminActionRef = AdminActionRef::new("SetBucketQuotaAdminAction");
|
||||
const SET_BUCKET_TARGET: AdminActionRef = AdminActionRef::new("SetBucketTargetAction");
|
||||
const SET_TABLE: AdminActionRef = AdminActionRef::new("SetTableAction");
|
||||
const SET_TABLE_NAMESPACE: AdminActionRef = AdminActionRef::new("SetTableNamespaceAction");
|
||||
const SET_TIER: AdminActionRef = AdminActionRef::new("SetTierAction");
|
||||
const SITE_REPLICATION_ADD: AdminActionRef = AdminActionRef::new("SiteReplicationAddAction");
|
||||
const SITE_REPLICATION_INFO: AdminActionRef = AdminActionRef::new("SiteReplicationInfoAction");
|
||||
const SITE_REPLICATION_OPERATION: AdminActionRef = AdminActionRef::new("SiteReplicationOperationAction");
|
||||
const SITE_REPLICATION_REMOVE: AdminActionRef = AdminActionRef::new("SiteReplicationRemoveAction");
|
||||
const SITE_REPLICATION_RESYNC: AdminActionRef = AdminActionRef::new("SiteReplicationResyncAction");
|
||||
const STORAGE_INFO: AdminActionRef = AdminActionRef::new("StorageInfoAdminAction");
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum DeferredRoutePolicyReason {
|
||||
ContextualAuthorization,
|
||||
CredentialOnly,
|
||||
MultipleActions,
|
||||
NotImplemented,
|
||||
S3Action,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct DeferredAdminRoutePolicy {
|
||||
method: HttpMethod,
|
||||
path: &'static str,
|
||||
reason: DeferredRoutePolicyReason,
|
||||
}
|
||||
|
||||
impl DeferredAdminRoutePolicy {
|
||||
pub const fn new(method: HttpMethod, path: &'static str, reason: DeferredRoutePolicyReason) -> Self {
|
||||
Self { method, path, reason }
|
||||
}
|
||||
|
||||
pub const fn method(self) -> HttpMethod {
|
||||
self.method
|
||||
}
|
||||
|
||||
pub const fn path(self) -> &'static str {
|
||||
self.path
|
||||
}
|
||||
|
||||
pub const fn reason(self) -> DeferredRoutePolicyReason {
|
||||
self.reason
|
||||
}
|
||||
}
|
||||
|
||||
pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
||||
public(HttpMethod::Get, "/health", PublicRouteKind::Health, RouteRiskLevel::Normal),
|
||||
public(HttpMethod::Head, "/health", PublicRouteKind::Health, RouteRiskLevel::Normal),
|
||||
public(HttpMethod::Get, "/health/ready", PublicRouteKind::Health, RouteRiskLevel::Normal),
|
||||
public(HttpMethod::Head, "/health/ready", PublicRouteKind::Health, RouteRiskLevel::Normal),
|
||||
public(HttpMethod::Post, "/", PublicRouteKind::StsFormPost, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Get, "/profile/cpu", PROFILING, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/profile/memory", PROFILING, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/is-admin", ALL_ADMIN, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/list-users", LIST_USERS, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Delete, "/rustfs/admin/v3/remove-user", DELETE_USER, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Put, "/rustfs/admin/v3/set-user-status", ENABLE_USER, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/groups", LIST_GROUPS, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/group", GET_GROUP, RouteRiskLevel::Sensitive),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/group/{group}",
|
||||
REMOVE_USER_FROM_GROUP,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Put, "/rustfs/admin/v3/set-group-status", ENABLE_GROUP, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/update-group-members",
|
||||
ADD_USER_TO_GROUP,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/temporary-account-info",
|
||||
LIST_TEMPORARY_ACCOUNTS,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/add-service-accounts",
|
||||
CREATE_SERVICE_ACCOUNT,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/add-service-account",
|
||||
CREATE_SERVICE_ACCOUNT,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/export-iam", EXPORT_IAM, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Put, "/rustfs/admin/v3/import-iam", IMPORT_IAM, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/list-canned-policies",
|
||||
LIST_USER_POLICIES,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/info-canned-policy",
|
||||
GET_POLICY,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(HttpMethod::Put, "/rustfs/admin/v3/add-canned-policy", CREATE_POLICY, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/remove-canned-policy",
|
||||
DELETE_POLICY,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/set-user-or-group-policy",
|
||||
ATTACH_POLICY,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Put, "/rustfs/admin/v3/set-policy", ATTACH_POLICY, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/idp/builtin/policy/attach",
|
||||
ATTACH_POLICY,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/idp/builtin/policy/detach",
|
||||
ATTACH_POLICY,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/target/list",
|
||||
GET_BUCKET_TARGET,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/target/{target_type}/{target_name}",
|
||||
SET_BUCKET_TARGET,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/target/{target_type}/{target_name}/reset",
|
||||
SET_BUCKET_TARGET,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/target/arns",
|
||||
GET_BUCKET_TARGET,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/info", SERVER_INFO, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/storageinfo", STORAGE_INFO, RouteRiskLevel::Sensitive),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/pools/decommission",
|
||||
DECOMMISSION,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/pools/cancel", DECOMMISSION, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/rebalance/start", REBALANCE, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/rebalance/status", REBALANCE, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/rebalance/stop", REBALANCE, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/heal/", HEAL, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/heal/{bucket}", HEAL, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/heal/{bucket}/{prefix}", HEAL, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/background-heal/status", HEAL, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/tier", LIST_TIER, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/tier-stats", LIST_TIER, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/tier/{tier}", LIST_TIER, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Delete, "/rustfs/admin/v3/tier/{tiername}", SET_TIER, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Put, "/rustfs/admin/v3/tier", SET_TIER, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/tier/{tiername}", SET_TIER, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/tier/clear", SET_TIER, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/set-bucket-quota",
|
||||
SET_BUCKET_QUOTA,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Put, "/rustfs/admin/v3/quota/{bucket}", SET_BUCKET_QUOTA, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/quota/{bucket}",
|
||||
SET_BUCKET_QUOTA,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/export-bucket-metadata",
|
||||
EXPORT_BUCKET_METADATA,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/export-bucket-metadata",
|
||||
EXPORT_BUCKET_METADATA,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/import-bucket-metadata",
|
||||
IMPORT_BUCKET_METADATA,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/import-bucket-metadata",
|
||||
IMPORT_BUCKET_METADATA,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/get-config-kv", CONFIG_UPDATE, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Put, "/rustfs/admin/v3/set-config-kv", CONFIG_UPDATE, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Delete, "/rustfs/admin/v3/del-config-kv", CONFIG_UPDATE, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/help-config-kv", CONFIG_UPDATE, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/list-config-history-kv",
|
||||
CONFIG_UPDATE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/clear-config-history-kv",
|
||||
CONFIG_UPDATE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/restore-config-history-kv",
|
||||
CONFIG_UPDATE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/config", CONFIG_UPDATE, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Put, "/rustfs/admin/v3/config", CONFIG_UPDATE, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/scanner/status", SERVER_INFO, RouteRiskLevel::Sensitive),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/audit/target/list",
|
||||
GET_BUCKET_TARGET,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/audit/target/{target_type}/{target_name}",
|
||||
SET_BUCKET_TARGET,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/audit/target/{target_type}/{target_name}/reset",
|
||||
SET_BUCKET_TARGET,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/module-switches",
|
||||
SERVER_INFO,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(HttpMethod::Put, "/rustfs/admin/v3/module-switches", CONFIG_UPDATE, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v4/plugins/catalog",
|
||||
SERVER_INFO,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v4/plugins/instances",
|
||||
GET_BUCKET_TARGET,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v4/plugins/instances/{id}",
|
||||
GET_BUCKET_TARGET,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v4/plugins/instances/{id}",
|
||||
SET_BUCKET_TARGET,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v4/plugins/instances/{id}",
|
||||
SET_BUCKET_TARGET,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/replicationmetrics",
|
||||
GET_REPLICATION_METRICS,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/set-remote-target",
|
||||
SET_BUCKET_TARGET,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/remove-remote-target",
|
||||
SET_BUCKET_TARGET,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/add",
|
||||
SITE_REPLICATION_ADD,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/remove",
|
||||
SITE_REPLICATION_REMOVE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/site-replication/info",
|
||||
SITE_REPLICATION_INFO,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/site-replication/metainfo",
|
||||
SITE_REPLICATION_INFO,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/site-replication/status",
|
||||
SITE_REPLICATION_INFO,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/site-replication/devnull",
|
||||
SITE_REPLICATION_INFO,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/site-replication/netperf",
|
||||
SITE_REPLICATION_INFO,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/peer/join",
|
||||
SITE_REPLICATION_ADD,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/peer/bucket-ops",
|
||||
SITE_REPLICATION_OPERATION,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/peer/iam-item",
|
||||
SITE_REPLICATION_OPERATION,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/peer/bucket-meta",
|
||||
SITE_REPLICATION_OPERATION,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/site-replication/peer/idp-settings",
|
||||
SITE_REPLICATION_ADD,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/edit",
|
||||
SITE_REPLICATION_ADD,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/peer/edit",
|
||||
SITE_REPLICATION_ADD,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/peer/remove",
|
||||
SITE_REPLICATION_REMOVE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/resync/op",
|
||||
SITE_REPLICATION_RESYNC,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/site-replication/state/edit",
|
||||
SITE_REPLICATION_OPERATION,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/debug/pprof/profile", PROFILING, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/debug/pprof/status", PROFILING, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/debug/tls/status", PROFILING, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/create-key", KMS_CREATE_KEY, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/key/create", KMS_CREATE_KEY, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/kms/describe-key",
|
||||
KMS_KEY_STATUS,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/kms/key/status",
|
||||
KMS_KEY_STATUS,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/kms/list-keys",
|
||||
KMS_KEY_STATUS,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/kms/generate-data-key",
|
||||
SERVER_INFO,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/kms/status", SERVER_INFO, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/status", SERVER_INFO, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/kms/config", SERVER_INFO, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/clear-cache", SERVER_INFO, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/configure", SERVER_INFO, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/start", SERVER_INFO, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/stop", SERVER_INFO, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/kms/service-status",
|
||||
SERVER_INFO,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/reconfigure", SERVER_INFO, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/keys", KMS_CREATE_KEY, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Delete, "/rustfs/admin/v3/kms/keys/delete", SERVER_INFO, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/kms/keys/cancel-deletion",
|
||||
SERVER_INFO,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/kms/keys", KMS_KEY_STATUS, RouteRiskLevel::Sensitive),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/kms/keys/{key_id}",
|
||||
KMS_KEY_STATUS,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
public(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/oidc/providers",
|
||||
PublicRouteKind::OidcBootstrap,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
public(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/oidc/authorize/{provider_id}",
|
||||
PublicRouteKind::OidcBootstrap,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
public(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/oidc/callback/{provider_id}",
|
||||
PublicRouteKind::OidcBootstrap,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
public(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/oidc/logout",
|
||||
PublicRouteKind::OidcBootstrap,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/oidc/config", SERVER_INFO, RouteRiskLevel::Sensitive),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/oidc/config/{provider_id}",
|
||||
CONFIG_UPDATE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/oidc/config/{provider_id}",
|
||||
CONFIG_UPDATE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/oidc/validate", SERVER_INFO, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Get, "/iceberg/v1/config", GET_TABLE_CATALOG, RouteRiskLevel::Sensitive),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/iceberg/v1/{warehouse}/namespaces",
|
||||
GET_TABLE_NAMESPACE,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/iceberg/v1/{warehouse}/namespaces",
|
||||
SET_TABLE_NAMESPACE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}",
|
||||
GET_TABLE_NAMESPACE,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}",
|
||||
DELETE_TABLE_NAMESPACE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables",
|
||||
GET_TABLE,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables",
|
||||
CREATE_TABLE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/register",
|
||||
REGISTER_TABLE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
GET_TABLE_METADATA,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
COMMIT_TABLE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
DELETE_TABLE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
];
|
||||
|
||||
pub const DEFERRED_ADMIN_ROUTE_POLICIES: &[DeferredAdminRoutePolicy] = &[
|
||||
deferred(HttpMethod::Get, "/rustfs/admin/v3/accountinfo", DeferredRoutePolicyReason::S3Action),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/user-info",
|
||||
DeferredRoutePolicyReason::ContextualAuthorization,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/add-user",
|
||||
DeferredRoutePolicyReason::ContextualAuthorization,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/update-service-account",
|
||||
DeferredRoutePolicyReason::ContextualAuthorization,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/info-service-account",
|
||||
DeferredRoutePolicyReason::ContextualAuthorization,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/info-access-key",
|
||||
DeferredRoutePolicyReason::ContextualAuthorization,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/list-service-accounts",
|
||||
DeferredRoutePolicyReason::ContextualAuthorization,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/list-access-keys-bulk",
|
||||
DeferredRoutePolicyReason::MultipleActions,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/delete-service-accounts",
|
||||
DeferredRoutePolicyReason::ContextualAuthorization,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/delete-service-account",
|
||||
DeferredRoutePolicyReason::ContextualAuthorization,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/idp/builtin/policy-entities",
|
||||
DeferredRoutePolicyReason::MultipleActions,
|
||||
),
|
||||
deferred(HttpMethod::Post, "/rustfs/admin/v3/service", DeferredRoutePolicyReason::NotImplemented),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/inspect-data",
|
||||
DeferredRoutePolicyReason::NotImplemented,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/inspect-data",
|
||||
DeferredRoutePolicyReason::NotImplemented,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/datausageinfo",
|
||||
DeferredRoutePolicyReason::MultipleActions,
|
||||
),
|
||||
deferred(HttpMethod::Get, "/rustfs/admin/v3/metrics", DeferredRoutePolicyReason::CredentialOnly),
|
||||
deferred(HttpMethod::Get, "/rustfs/admin/v3/pools/list", DeferredRoutePolicyReason::MultipleActions),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/pools/status",
|
||||
DeferredRoutePolicyReason::MultipleActions,
|
||||
),
|
||||
deferred(HttpMethod::Get, "/rustfs/admin/v3/get-bucket-quota", DeferredRoutePolicyReason::S3Action),
|
||||
deferred(HttpMethod::Get, "/rustfs/admin/v3/quota/{bucket}", DeferredRoutePolicyReason::S3Action),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/quota-stats/{bucket}",
|
||||
DeferredRoutePolicyReason::S3Action,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/quota-check/{bucket}",
|
||||
DeferredRoutePolicyReason::S3Action,
|
||||
),
|
||||
deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/list-remote-targets",
|
||||
DeferredRoutePolicyReason::CredentialOnly,
|
||||
),
|
||||
];
|
||||
|
||||
pub fn validate_admin_route_policy_specs() -> Result<(), AdminRouteMatrixError> {
|
||||
validate_admin_route_specs(ADMIN_ROUTE_POLICY_SPECS)
|
||||
}
|
||||
|
||||
const fn admin(method: HttpMethod, path: &'static str, action: AdminActionRef, risk_level: RouteRiskLevel) -> AdminRouteSpec {
|
||||
AdminRouteSpec::admin(method, path, action, risk_level)
|
||||
}
|
||||
|
||||
const fn public(method: HttpMethod, path: &'static str, kind: PublicRouteKind, risk_level: RouteRiskLevel) -> AdminRouteSpec {
|
||||
AdminRouteSpec::public(method, path, kind, risk_level)
|
||||
}
|
||||
|
||||
const fn deferred(method: HttpMethod, path: &'static str, reason: DeferredRoutePolicyReason) -> DeferredAdminRoutePolicy {
|
||||
DeferredAdminRoutePolicy::new(method, path, reason)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::admin::router::{AdminOperation, S3Router};
|
||||
use hyper::Method;
|
||||
use serial_test::serial;
|
||||
use std::collections::BTreeSet;
|
||||
use temp_env::with_var;
|
||||
|
||||
#[test]
|
||||
fn admin_route_policy_specs_validate() {
|
||||
assert!(validate_admin_route_policy_specs().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_policy_inventory_has_no_internal_overlap() {
|
||||
let mut seen = BTreeSet::new();
|
||||
|
||||
for spec in ADMIN_ROUTE_POLICY_SPECS {
|
||||
let key = route_key(spec.method(), spec.path());
|
||||
assert!(seen.insert(key), "duplicate direct route policy for {}", spec.path());
|
||||
}
|
||||
|
||||
for policy in DEFERRED_ADMIN_ROUTE_POLICIES {
|
||||
let key = route_key(policy.method(), policy.path());
|
||||
assert!(seen.insert(key), "deferred route overlaps direct policy for {}", policy.path());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn route_policy_inventory_covers_registered_routes() {
|
||||
let router = with_var(rustfs_config::ENV_HEALTH_ENDPOINT_ENABLE, Some("true"), || {
|
||||
let mut router = S3Router::<AdminOperation>::new(false);
|
||||
super::super::register_admin_routes(&mut router).expect("register production admin routes");
|
||||
router
|
||||
});
|
||||
|
||||
let registered = router.registered_routes().iter().cloned().collect::<BTreeSet<_>>();
|
||||
let inventory = route_policy_inventory_keys();
|
||||
|
||||
assert_eq!(inventory, registered, "admin route policy inventory must cover every registered route");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_policy_keeps_public_exceptions_explicit() {
|
||||
assert_public(HttpMethod::Get, "/health", PublicRouteKind::Health);
|
||||
assert_public(HttpMethod::Head, "/health/ready", PublicRouteKind::Health);
|
||||
assert_public(HttpMethod::Post, "/", PublicRouteKind::StsFormPost);
|
||||
assert_public(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/oidc/authorize/{provider_id}",
|
||||
PublicRouteKind::OidcBootstrap,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_policy_keeps_table_catalog_outside_admin_prefix() {
|
||||
let table_specs = ADMIN_ROUTE_POLICY_SPECS
|
||||
.iter()
|
||||
.filter(|spec| spec.path().starts_with("/iceberg/v1"));
|
||||
assert_eq!(table_specs.count(), 11);
|
||||
assert_action(HttpMethod::Get, "/iceberg/v1/{warehouse}/namespaces", GET_TABLE_NAMESPACE);
|
||||
assert_action(HttpMethod::Post, "/iceberg/v1/{warehouse}/namespaces/{namespace}/tables", CREATE_TABLE);
|
||||
assert_action(
|
||||
HttpMethod::Get,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
GET_TABLE_METADATA,
|
||||
);
|
||||
assert_action(
|
||||
HttpMethod::Post,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
COMMIT_TABLE,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_policy_keeps_contextual_auth_deferred() {
|
||||
assert_deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/info-service-account",
|
||||
DeferredRoutePolicyReason::ContextualAuthorization,
|
||||
);
|
||||
assert_deferred(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/datausageinfo",
|
||||
DeferredRoutePolicyReason::MultipleActions,
|
||||
);
|
||||
assert_deferred(HttpMethod::Get, "/rustfs/admin/v3/accountinfo", DeferredRoutePolicyReason::S3Action);
|
||||
}
|
||||
|
||||
fn route_policy_inventory_keys() -> BTreeSet<String> {
|
||||
ADMIN_ROUTE_POLICY_SPECS
|
||||
.iter()
|
||||
.map(|spec| route_key(spec.method(), spec.path()))
|
||||
.chain(
|
||||
DEFERRED_ADMIN_ROUTE_POLICIES
|
||||
.iter()
|
||||
.map(|policy| route_key(policy.method(), policy.path())),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn route_key(method: HttpMethod, path: &str) -> String {
|
||||
format!("{}|{}", method.as_str(), path)
|
||||
}
|
||||
|
||||
fn assert_action(method: HttpMethod, path: &str, action: AdminActionRef) {
|
||||
let spec = ADMIN_ROUTE_POLICY_SPECS
|
||||
.iter()
|
||||
.find(|spec| spec.method() == method && spec.path() == path)
|
||||
.expect("expected direct route policy");
|
||||
assert_eq!(spec.access().admin_action(), Some(action));
|
||||
}
|
||||
|
||||
fn assert_public(method: HttpMethod, path: &str, kind: PublicRouteKind) {
|
||||
let spec = ADMIN_ROUTE_POLICY_SPECS
|
||||
.iter()
|
||||
.find(|spec| spec.method() == method && spec.path() == path)
|
||||
.expect("expected public route policy");
|
||||
assert_eq!(spec.access().public_kind(), Some(kind));
|
||||
}
|
||||
|
||||
fn assert_deferred(method: HttpMethod, path: &str, reason: DeferredRoutePolicyReason) {
|
||||
let policy = DEFERRED_ADMIN_ROUTE_POLICIES
|
||||
.iter()
|
||||
.find(|policy| policy.method() == method && policy.path() == path)
|
||||
.expect("expected deferred route policy");
|
||||
assert_eq!(policy.reason(), reason);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_mapping_matches_registered_route_format() {
|
||||
assert_eq!(route_key(HttpMethod::Get, "/health"), Method::GET.as_str().to_string() + "|/health");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user