mirror of
https://github.com/openai/codex.git
synced 2026-05-16 17:23:57 +00:00
windows-sandbox: drive write roots from resolved permissions
This commit is contained in:
@@ -8,7 +8,8 @@ use crate::logging::debug_log;
|
||||
use crate::logging::log_note;
|
||||
use crate::path_normalization::canonical_path_key;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::setup::effective_write_roots_for_setup;
|
||||
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||
use crate::setup::effective_write_roots_for_permissions;
|
||||
use crate::token::LocalSid;
|
||||
use crate::token::world_sid;
|
||||
use anyhow::Result;
|
||||
@@ -259,11 +260,18 @@ pub fn apply_capability_denies_for_world_writable(
|
||||
let cap_path = cap_sid_file(codex_home);
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
std::fs::write(&cap_path, serde_json::to_string(&caps)?)?;
|
||||
let (active_sids, workspace_roots): (Vec<LocalSid>, Vec<PathBuf>) = match sandbox_policy {
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let roots = effective_write_roots_for_setup(
|
||||
sandbox_policy,
|
||||
cwd,
|
||||
if matches!(
|
||||
sandbox_policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
let permissions =
|
||||
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(sandbox_policy, cwd);
|
||||
let (active_sids, workspace_roots): (Vec<LocalSid>, Vec<PathBuf>) =
|
||||
if permissions.uses_write_capabilities_for_cwd(cwd, env_map) {
|
||||
let roots = effective_write_roots_for_permissions(
|
||||
&permissions,
|
||||
cwd,
|
||||
env_map,
|
||||
codex_home,
|
||||
@@ -277,14 +285,9 @@ pub fn apply_capability_denies_for_world_writable(
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
(active_sids, roots)
|
||||
}
|
||||
SandboxPolicy::ReadOnly { .. } => {
|
||||
} else {
|
||||
(vec![LocalSid::from_string(&caps.readonly)?], Vec::new())
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
};
|
||||
for path in flagged {
|
||||
if workspace_roots
|
||||
.iter()
|
||||
|
||||
@@ -39,10 +39,11 @@ mod windows_impl {
|
||||
use crate::logging::log_success;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::policy::parse_policy;
|
||||
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||
use crate::runner_client::spawn_runner_transport;
|
||||
use crate::sandbox_utils::ensure_codex_home_exists;
|
||||
use crate::sandbox_utils::inject_git_safe_directory;
|
||||
use crate::setup::effective_write_roots_for_setup;
|
||||
use crate::setup::effective_write_roots_for_permissions;
|
||||
use crate::token::LocalSid;
|
||||
use anyhow::Result;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -81,6 +82,8 @@ mod windows_impl {
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect::<Vec<_>>();
|
||||
let policy = parse_policy(policy_json_or_preset)?;
|
||||
let permissions =
|
||||
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, cwd);
|
||||
normalize_null_device_env(&mut env_map);
|
||||
ensure_non_interactive_pager(&mut env_map);
|
||||
inherit_path_env(&mut env_map);
|
||||
@@ -112,32 +115,26 @@ mod windows_impl {
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let (sid_for_null, cap_sids) = match &policy {
|
||||
SandboxPolicy::ReadOnly { .. } => {
|
||||
let sid = LocalSid::from_string(&caps.readonly)?;
|
||||
(sid, vec![caps.readonly])
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let write_roots = effective_write_roots_for_setup(
|
||||
&policy,
|
||||
sandbox_policy_cwd,
|
||||
cwd,
|
||||
&env_map,
|
||||
codex_home,
|
||||
write_roots_override,
|
||||
);
|
||||
let cap_sids = write_roots
|
||||
.iter()
|
||||
.map(|root| workspace_write_cap_sid_for_root(codex_home, cwd, root))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
if cap_sids.is_empty() {
|
||||
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
|
||||
}
|
||||
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
let (sid_for_null, cap_sids) = if permissions.uses_write_capabilities_for_cwd(cwd, &env_map)
|
||||
{
|
||||
let write_roots = effective_write_roots_for_permissions(
|
||||
&permissions,
|
||||
cwd,
|
||||
&env_map,
|
||||
codex_home,
|
||||
write_roots_override,
|
||||
);
|
||||
let cap_sids = write_roots
|
||||
.iter()
|
||||
.map(|root| workspace_write_cap_sid_for_root(codex_home, cwd, root))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
if cap_sids.is_empty() {
|
||||
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
|
||||
}
|
||||
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
|
||||
} else {
|
||||
let sid = LocalSid::from_string(&caps.readonly)?;
|
||||
(sid, vec![caps.readonly])
|
||||
};
|
||||
|
||||
unsafe {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::dpapi;
|
||||
use crate::logging::debug_log;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||
use crate::setup::SandboxNetworkIdentity;
|
||||
use crate::setup::SandboxUserRecord;
|
||||
use crate::setup::SandboxUsersFile;
|
||||
use crate::setup::SetupMarker;
|
||||
use crate::setup::gather_read_roots;
|
||||
use crate::setup::gather_write_roots;
|
||||
use crate::setup::gather_write_roots_for_permissions;
|
||||
use crate::setup::offline_proxy_settings_from_env;
|
||||
use crate::setup::run_elevated_setup;
|
||||
use crate::setup::run_setup_refresh_with_overrides;
|
||||
@@ -143,14 +144,16 @@ pub fn require_logon_sandbox_creds(
|
||||
deny_write_paths_override: &[PathBuf],
|
||||
proxy_enforced: bool,
|
||||
) -> Result<SandboxCreds> {
|
||||
let permissions =
|
||||
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, command_cwd);
|
||||
let sandbox_dir = crate::setup::sandbox_dir(codex_home);
|
||||
let needed_read = read_roots_override
|
||||
.map(<[PathBuf]>::to_vec)
|
||||
.unwrap_or_else(|| gather_read_roots(command_cwd, policy, codex_home));
|
||||
let needed_write = write_roots_override
|
||||
.map(<[PathBuf]>::to_vec)
|
||||
.unwrap_or_else(|| gather_write_roots(policy, policy_cwd, command_cwd, env_map));
|
||||
let network_identity = SandboxNetworkIdentity::from_policy(policy, proxy_enforced);
|
||||
.unwrap_or_else(|| gather_write_roots_for_permissions(&permissions, command_cwd, env_map));
|
||||
let network_identity = SandboxNetworkIdentity::from_permissions(&permissions, proxy_enforced);
|
||||
let desired_offline_proxy_settings = offline_proxy_settings_from_env(env_map, network_identity);
|
||||
// NOTE: Do not add CODEX_HOME/.sandbox to `needed_write`; it must remain non-writable by the
|
||||
// restricted capability token. The setup helper's `lock_sandbox_dir` is responsible for
|
||||
|
||||
@@ -94,6 +94,18 @@ impl ResolvedWindowsSandboxPermissions {
|
||||
!self.network.is_enabled()
|
||||
}
|
||||
|
||||
pub(crate) fn network_policy(&self) -> NetworkSandboxPolicy {
|
||||
self.network
|
||||
}
|
||||
|
||||
pub(crate) fn uses_write_capabilities_for_cwd(
|
||||
&self,
|
||||
cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> bool {
|
||||
!self.writable_roots_for_cwd(cwd, env_map).is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn writable_roots_for_cwd(
|
||||
&self,
|
||||
cwd: &Path,
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::logging::log_note;
|
||||
use crate::path_normalization::canonical_path_key;
|
||||
use crate::path_normalization::canonicalize_path;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||
use crate::setup_error::SetupErrorCode;
|
||||
use crate::setup_error::SetupFailure;
|
||||
use crate::setup_error::clear_setup_error_report;
|
||||
@@ -173,8 +174,12 @@ fn run_setup_refresh_inner(
|
||||
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
|
||||
let deny_read_paths = build_payload_deny_read_paths(overrides.deny_read_paths);
|
||||
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
|
||||
let permissions = ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
|
||||
request.policy,
|
||||
request.command_cwd,
|
||||
);
|
||||
let network_identity =
|
||||
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
|
||||
SandboxNetworkIdentity::from_permissions(&permissions, request.proxy_enforced);
|
||||
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
|
||||
let payload = ElevationPayload {
|
||||
version: SETUP_VERSION,
|
||||
@@ -389,15 +394,16 @@ pub(crate) fn gather_read_roots(
|
||||
gather_legacy_full_read_roots(command_cwd, policy, codex_home)
|
||||
}
|
||||
|
||||
pub(crate) fn gather_write_roots(
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
pub(crate) fn gather_write_roots_for_permissions(
|
||||
permissions: &ResolvedWindowsSandboxPermissions,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> Vec<PathBuf> {
|
||||
let AllowDenyPaths { allow, .. } =
|
||||
compute_allow_paths(policy, policy_cwd, command_cwd, env_map);
|
||||
let roots: Vec<PathBuf> = allow.into_iter().collect();
|
||||
let roots = permissions
|
||||
.writable_roots_for_cwd(command_cwd, env_map)
|
||||
.into_iter()
|
||||
.map(|root| root.root)
|
||||
.collect::<Vec<_>>();
|
||||
let mut dedup: HashSet<PathBuf> = HashSet::new();
|
||||
let mut out: Vec<PathBuf> = Vec::new();
|
||||
for r in canonical_existing(&roots) {
|
||||
@@ -410,7 +416,25 @@ pub(crate) fn gather_write_roots(
|
||||
|
||||
pub(crate) fn effective_write_roots_for_setup(
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
codex_home: &Path,
|
||||
write_roots_override: Option<&[PathBuf]>,
|
||||
) -> Vec<PathBuf> {
|
||||
let permissions =
|
||||
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, command_cwd);
|
||||
effective_write_roots_for_permissions(
|
||||
&permissions,
|
||||
command_cwd,
|
||||
env_map,
|
||||
codex_home,
|
||||
write_roots_override,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn effective_write_roots_for_permissions(
|
||||
permissions: &ResolvedWindowsSandboxPermissions,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
codex_home: &Path,
|
||||
@@ -419,7 +443,7 @@ pub(crate) fn effective_write_roots_for_setup(
|
||||
let write_roots = if let Some(roots) = write_roots_override {
|
||||
canonical_existing(roots)
|
||||
} else {
|
||||
gather_write_roots(policy, policy_cwd, command_cwd, env_map)
|
||||
gather_write_roots_for_permissions(permissions, command_cwd, env_map)
|
||||
};
|
||||
let write_roots = expand_user_profile_root(write_roots);
|
||||
let write_roots = filter_user_profile_root(write_roots);
|
||||
@@ -463,8 +487,11 @@ pub(crate) enum SandboxNetworkIdentity {
|
||||
}
|
||||
|
||||
impl SandboxNetworkIdentity {
|
||||
pub(crate) fn from_policy(policy: &SandboxPolicy, proxy_enforced: bool) -> Self {
|
||||
if proxy_enforced || !policy.has_full_network_access() {
|
||||
pub(crate) fn from_permissions(
|
||||
permissions: &ResolvedWindowsSandboxPermissions,
|
||||
proxy_enforced: bool,
|
||||
) -> Self {
|
||||
if proxy_enforced || !permissions.network_policy().is_enabled() {
|
||||
Self::Offline
|
||||
} else {
|
||||
Self::Online
|
||||
@@ -744,8 +771,12 @@ pub fn run_elevated_setup(
|
||||
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
|
||||
let deny_read_paths = build_payload_deny_read_paths(overrides.deny_read_paths);
|
||||
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
|
||||
let permissions = ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
|
||||
request.policy,
|
||||
request.command_cwd,
|
||||
);
|
||||
let network_identity =
|
||||
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
|
||||
SandboxNetworkIdentity::from_permissions(&permissions, request.proxy_enforced);
|
||||
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
|
||||
let payload = ElevationPayload {
|
||||
version: SETUP_VERSION,
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::acl::add_deny_write_ace;
|
||||
use crate::acl::allow_null_device;
|
||||
use crate::allow::AllowDenyPaths;
|
||||
use crate::allow::compute_allow_paths;
|
||||
use crate::allow::compute_allow_paths_for_permissions;
|
||||
use crate::cap::load_or_create_cap_sids;
|
||||
use crate::cap::workspace_write_cap_sid_for_root;
|
||||
use crate::cap::workspace_write_root_contains_path;
|
||||
@@ -23,7 +24,7 @@ use crate::policy::parse_policy;
|
||||
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||
use crate::sandbox_utils::ensure_codex_home_exists;
|
||||
use crate::sandbox_utils::inject_git_safe_directory;
|
||||
use crate::setup::effective_write_roots_for_setup;
|
||||
use crate::setup::effective_write_roots_for_permissions;
|
||||
use crate::token::LocalSid;
|
||||
use crate::token::create_readonly_token_with_cap;
|
||||
use crate::token::create_workspace_write_token_with_caps_from;
|
||||
@@ -43,10 +44,11 @@ use windows_sys::Win32::Foundation::HANDLE;
|
||||
|
||||
pub(crate) struct SpawnContext {
|
||||
pub(crate) policy: SandboxPolicy,
|
||||
pub(crate) permissions: ResolvedWindowsSandboxPermissions,
|
||||
pub(crate) current_dir: PathBuf,
|
||||
pub(crate) sandbox_base: PathBuf,
|
||||
pub(crate) logs_base_dir: Option<PathBuf>,
|
||||
pub(crate) is_workspace_write: bool,
|
||||
pub(crate) uses_write_capabilities: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct ElevatedSpawnContext {
|
||||
@@ -110,14 +112,16 @@ fn prepare_spawn_context_common(
|
||||
let logs_base_dir = Some(sandbox_base.clone());
|
||||
log_start(command, logs_base_dir.as_deref());
|
||||
|
||||
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
|
||||
let permissions = ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, cwd);
|
||||
let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map);
|
||||
|
||||
Ok(SpawnContext {
|
||||
policy,
|
||||
permissions,
|
||||
current_dir: cwd.to_path_buf(),
|
||||
sandbox_base,
|
||||
logs_base_dir,
|
||||
is_workspace_write,
|
||||
uses_write_capabilities,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -139,7 +143,7 @@ pub(crate) fn prepare_legacy_spawn_context(
|
||||
inherit_path,
|
||||
add_git_safe_directory,
|
||||
)?;
|
||||
if should_apply_network_block(&common.policy) {
|
||||
if common.permissions.should_apply_network_block() {
|
||||
apply_no_network_to_env(env_map)?;
|
||||
}
|
||||
Ok(common)
|
||||
@@ -191,19 +195,20 @@ pub(crate) fn prepare_legacy_session_security(
|
||||
|
||||
pub(crate) fn legacy_session_capability_roots(
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
_policy_cwd: &Path,
|
||||
current_dir: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
codex_home: &Path,
|
||||
) -> Vec<PathBuf> {
|
||||
let allow_paths = compute_allow_paths(policy, policy_cwd, current_dir, env_map)
|
||||
let permissions =
|
||||
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, current_dir);
|
||||
let allow_paths = compute_allow_paths_for_permissions(&permissions, current_dir, env_map)
|
||||
.allow
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
effective_write_roots_for_setup(
|
||||
policy,
|
||||
policy_cwd,
|
||||
if permissions.uses_write_capabilities_for_cwd(current_dir, env_map) {
|
||||
effective_write_roots_for_permissions(
|
||||
&permissions,
|
||||
current_dir,
|
||||
env_map,
|
||||
codex_home,
|
||||
@@ -379,7 +384,7 @@ pub(crate) fn apply_legacy_session_acl_rules(
|
||||
allow_null_device(readonly_sid.as_ptr());
|
||||
}
|
||||
if persist_aces
|
||||
&& matches!(policy, SandboxPolicy::WorkspaceWrite { .. })
|
||||
&& !acl_sids.write_root_sids.is_empty()
|
||||
&& let Some(workspace_sid) =
|
||||
matching_root_capability(current_dir, acl_sids.write_root_sids)
|
||||
{
|
||||
@@ -417,24 +422,19 @@ pub(crate) fn prepare_elevated_spawn_context(
|
||||
/*add_git_safe_directory*/ true,
|
||||
)?;
|
||||
|
||||
let AllowDenyPaths { allow, deny } = compute_allow_paths(
|
||||
&common.policy,
|
||||
sandbox_policy_cwd,
|
||||
&common.current_dir,
|
||||
env_map,
|
||||
);
|
||||
let AllowDenyPaths { allow, deny } =
|
||||
compute_allow_paths_for_permissions(&common.permissions, &common.current_dir, env_map);
|
||||
let write_roots: Vec<PathBuf> = allow.into_iter().collect();
|
||||
let deny_write_paths: Vec<PathBuf> = deny.into_iter().collect();
|
||||
let computed_write_roots_override = if common.is_workspace_write {
|
||||
let computed_write_roots_override = if common.uses_write_capabilities {
|
||||
Some(write_roots.as_slice())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let write_roots_for_setup = write_roots_override.or(computed_write_roots_override);
|
||||
let effective_write_roots = if common.is_workspace_write {
|
||||
effective_write_roots_for_setup(
|
||||
&common.policy,
|
||||
sandbox_policy_cwd,
|
||||
let effective_write_roots = if common.uses_write_capabilities {
|
||||
effective_write_roots_for_permissions(
|
||||
&common.permissions,
|
||||
&common.current_dir,
|
||||
env_map,
|
||||
codex_home,
|
||||
@@ -443,7 +443,7 @@ pub(crate) fn prepare_elevated_spawn_context(
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let setup_write_roots_override = if common.is_workspace_write {
|
||||
let setup_write_roots_override = if common.uses_write_capabilities {
|
||||
Some(effective_write_roots.as_slice())
|
||||
} else {
|
||||
write_roots_override
|
||||
@@ -466,24 +466,20 @@ pub(crate) fn prepare_elevated_spawn_context(
|
||||
/*proxy_enforced*/ false,
|
||||
)?;
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let (psid_to_use, cap_sids) = match &common.policy {
|
||||
SandboxPolicy::ReadOnly { .. } => (
|
||||
let (psid_to_use, cap_sids) = if common.uses_write_capabilities {
|
||||
let cap_sids = root_capability_sids(codex_home, cwd, effective_write_roots)?
|
||||
.into_iter()
|
||||
.map(|root_sid| root_sid.sid_str)
|
||||
.collect::<Vec<_>>();
|
||||
if cap_sids.is_empty() {
|
||||
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
|
||||
}
|
||||
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
|
||||
} else {
|
||||
(
|
||||
LocalSid::from_string(&caps.readonly)?,
|
||||
vec![caps.readonly.clone()],
|
||||
),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let cap_sids = root_capability_sids(codex_home, cwd, effective_write_roots)?
|
||||
.into_iter()
|
||||
.map(|root_sid| root_sid.sid_str)
|
||||
.collect::<Vec<_>>();
|
||||
if cap_sids.is_empty() {
|
||||
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
|
||||
}
|
||||
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("dangerous policies rejected before elevated session prep")
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
unsafe {
|
||||
|
||||
Reference in New Issue
Block a user