Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pcs/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ EXTRA_DIST = \
lib/commands/cluster/link.py \
lib/commands/cluster/misc.py \
lib/commands/cluster/node.py \
lib/commands/cluster/permissions.py \
lib/commands/cluster_property.py \
lib/commands/cluster/setup_cluster.py \
lib/commands/cluster/setup_node.py \
Expand Down
26 changes: 26 additions & 0 deletions pcs/common/permissions/dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,29 @@ class PermissionEntryDto(DataTransferObject):
name: str
type: PermissionTargetType
allow: list[PermissionGrantedType]


@dataclass(frozen=True)
class PermissionMetadataTargetTypeDto(DataTransferObject):
code: PermissionTargetType
label: str
description: str


@dataclass(frozen=True)
class PermissionMetadataPermissionTypeDto(DataTransferObject):
code: PermissionGrantedType
label: str
description: str


@dataclass(frozen=True)
class PermissionMetadataDependenciesDto(DataTransferObject):
also_allows: dict[PermissionGrantedType, list[PermissionGrantedType]]


@dataclass(frozen=True)
class PermissionMetadataDto(DataTransferObject):
user_types: list[PermissionMetadataTargetTypeDto]
permission_types: list[PermissionMetadataPermissionTypeDto]
permissions_dependencies: PermissionMetadataDependenciesDto
42 changes: 42 additions & 0 deletions pcs/daemon/app/api_v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
from pcs.common.cluster_dto import ClusterDaemonsInfoDto
from pcs.common.interface.dto import to_dict
from pcs.common.pcs_cfgsync_dto import SyncConfigsDto
from pcs.common.permissions.dto import (
PermissionEntryDto,
PermissionMetadataDependenciesDto,
PermissionMetadataDto,
)
from pcs.common.str_tools import format_list
from pcs.daemon import log
from pcs.daemon.app.api_v0_tools import (
Expand Down Expand Up @@ -598,6 +603,42 @@ async def _handle_request(self) -> None:
self.write("Permissions saved")


class GetPermissionsHandler(_BaseApiV0Handler):
async def _handle_request(self) -> None:
result = await self._run_library_command("cluster.get_permissions", {})
# Ruby compatibility: ignore errors, return empty permissions
#
# 403 is already returned in _run_library_command if the user does not
# have the needed permissions to run this command, so this is safe
user_permissions = result.result if result.success else []

result = await self._run_library_command(
"cluster.get_permissions_metadata", {}
)
permission_metadata = (
cast(PermissionMetadataDto, result.result)
if result.success
else PermissionMetadataDto(
[], [], PermissionMetadataDependenciesDto({})
)
)

# Ruby compatibility: sort the permissions
# sorted by (type, name): GROUP < USER alphabetically, then by name
#
# WebUI displays the permissions in the order they were sent from pcsd
user_permissions.sort(key=lambda p: (p.type, p.name))
self.write(
{
**to_dict(permission_metadata),
"users_permissions": [
to_dict(cast(PermissionEntryDto, entry))
for entry in user_permissions
],
},
)


class KnownHostsChangeHandler(_BaseApiV0Handler):
"""
Input format:
Expand Down Expand Up @@ -757,6 +798,7 @@ def r(url: str) -> str:
),
# permissions
(r("set_permissions"), SetPermissionsHandler, params),
(r("get_permissions"), GetPermissionsHandler, params),
# known hosts
(r("known_hosts_change"), KnownHostsChangeHandler, params),
# check_host
Expand Down
10 changes: 10 additions & 0 deletions pcs/daemon/async_tasks/worker/command_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ class _Cmd:
cmd=cluster.get_host_daemons_info,
required_permission=p.READ,
),
"cluster.get_permissions": _Cmd(
cmd=cluster.get_permissions,
required_permission=p.GRANT,
),
"cluster.get_permissions_metadata": _Cmd(
cmd=cluster.get_permissions_metadata,
required_permission=p.GRANT,
),
"cluster.node_clear": _Cmd(
cmd=cluster.node_clear,
required_permission=p.WRITE,
Expand Down Expand Up @@ -546,6 +554,8 @@ class _Cmd:
LEGACY_API_COMMANDS = (
"auth.known_hosts_change",
"booth.get_config",
"cluster.get_permissions",
"cluster.get_permissions_metadata",
"cluster.set_corosync_conf",
"cluster.set_permissions",
"manage_clusters.add_cluster",
Expand Down
17 changes: 9 additions & 8 deletions pcs/lib/commands/cluster/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@
set_corosync_conf,
)
from .link import add_link, remove_links, update_link
from .misc import (
corosync_authkey_change,
rename,
set_permissions,
verify,
wait_for_pcmk_idle,
)
from .misc import corosync_authkey_change, rename, verify, wait_for_pcmk_idle
from .node import (
get_host_daemons_info,
node_clear,
Expand All @@ -22,6 +16,11 @@
rename_node_cib,
rename_node_corosync,
)
from .permissions import (
get_permissions,
get_permissions_metadata,
set_permissions,
)
from .setup_cluster import setup, setup_local
from .setup_node import add_nodes

Expand All @@ -31,10 +30,12 @@
"config_update",
"config_update_local",
"corosync_authkey_change",
"get_host_daemons_info",
"generate_cluster_uuid",
"generate_cluster_uuid_local",
"get_corosync_conf_struct",
"get_host_daemons_info",
"get_permissions",
"get_permissions_metadata",
"node_clear",
"remove_links",
"remove_nodes",
Expand Down
109 changes: 1 addition & 108 deletions pcs/lib/commands/cluster/misc.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
from typing import Optional, Sequence
from typing import Optional

from lxml.etree import _Element

from pcs import settings
from pcs.common import reports
from pcs.common.permissions.dto import PermissionEntryDto
from pcs.common.permissions.types import PermissionGrantedType
from pcs.lib import node_communication_format
from pcs.lib.auth.types import AuthUser
from pcs.lib.cib import fencing_topology
from pcs.lib.cib.nvpair_multi import NVSET_INSTANCE, find_nvsets
from pcs.lib.cib.resource.bundle import verify as verify_bundles
Expand Down Expand Up @@ -42,18 +39,6 @@
)
from pcs.lib.pacemaker.live import verify as verify_cmd
from pcs.lib.pacemaker.state import ClusterState
from pcs.lib.pcs_cfgsync.sync_files import (
sync_pcs_settings_in_cluster,
update_pcs_settings_locally,
)
from pcs.lib.permissions.checker import PermissionsChecker
from pcs.lib.permissions.config.types import PermissionEntry
from pcs.lib.permissions.tools import (
complete_access_list,
read_pcs_settings_conf,
)
from pcs.lib.permissions.types import PermissionRequiredType
from pcs.lib.permissions.validations import validate_set_permissions
from pcs.lib.resource_agent.types import ResourceAgentName
from pcs.lib.tools import generate_binary_key

Expand Down Expand Up @@ -289,95 +274,3 @@ def warn_gfs2_resources(resources: _Element) -> reports.ReportItemList:

corosync_conf.set_cluster_name(new_name)
env.push_corosync_conf(corosync_conf, skip_offline)


def set_permissions(
env: LibraryEnvironment, permissions: Sequence[PermissionEntryDto]
) -> None:
"""
Replace the current local cluster permissions with provided permissions. If
local node is in cluster, synchronize the updated pcs_settings file.

permissions -- new permissions for the local cluster
"""
# TODO
# Checking user permissions is done in daemon command executor when calling
# lib commands through API - GRANT in this case. If the user has GRANT,
# then this command is called, and the command itself does only "extra"
# permission checks in case the user is trying to change users with FULL
# permissions.
#
# We need to properly check that user has permissions to call this command
# when we eventually want to use this command from CLI through lib_wrapper!

if env.report_processor.report_list(
validate_set_permissions(permissions)
).has_errors:
raise LibraryError()

ensure_live_env(env)

pcs_settings, report_list = read_pcs_settings_conf()
if env.report_processor.report_list(report_list).has_errors:
raise LibraryError()

# TODO: The user_login and user_groups are None when calling
# this command from cli through lib_wrapper -> this will break
auth_user = AuthUser(env.user_login or "", env.user_groups or [])
permissions_checker = PermissionsChecker(env.logger)

new_full_users = set()
new_permission_list = []
for perm in permissions:
if PermissionGrantedType.FULL in perm.allow:
new_full_users.add((perm.name, perm.type))
# Explicitly save dependant permissions. That way if the dependency is
# changed in the future, it won't revoke permissions which were once
# granted
allow = complete_access_list(set(perm.allow))
new_permission_list.append(
PermissionEntry(name=perm.name, type=perm.type, allow=sorted(allow))
)

current_full_users = {
(perm.name, perm.type)
for perm in pcs_settings.get_entries_with_allow_full()
}
if new_full_users != current_full_users:
if not permissions_checker.is_authorized(
auth_user, PermissionRequiredType.FULL
):
env.report_processor.report(
reports.ReportItem.error(
reports.messages.NotAuthorizedToChangeFullPermission()
)
)
raise LibraryError()

# replace all of the the current permissions
pcs_settings.set_permissions(new_permission_list)

if not env.has_corosync_conf:
update_pcs_settings_locally(pcs_settings, env.report_processor)
if env.report_processor.has_errors:
raise LibraryError()
return

# the node is in cluster, sync the updated config to cluster nodes
corosync_conf = env.get_corosync_conf()
local_cluster_name = corosync_conf.get_cluster_name()
local_corosync_nodes, _ = get_existing_nodes_names(corosync_conf)
request_targets = env.get_node_target_factory().get_target_list(
local_corosync_nodes
)
node_communicator = env.get_node_communicator_no_privilege_transition()

sync_pcs_settings_in_cluster(
pcs_settings,
local_cluster_name,
request_targets,
node_communicator,
env.report_processor,
)
if env.report_processor.has_errors:
raise LibraryError()
Loading
Loading