|
12 | 12 | import typing
|
13 | 13 | import uuid
|
14 | 14 | import warnings
|
15 |
| -from collections.abc import Callable, Collection, Mapping |
| 15 | +from collections.abc import Callable, Collection, Mapping, MutableMapping |
16 | 16 | from datetime import datetime, timezone
|
17 | 17 | from typing import Any, Literal
|
18 | 18 |
|
| 19 | +import marshmallow |
19 | 20 | from cryptography.x509 import CertificateSigningRequest, load_pem_x509_csr
|
20 | 21 | from cryptography.x509.oid import NameOID
|
21 | 22 | from marshmallow import fields as _fields
|
|
32 | 33 | from cmk.utils.livestatus_helpers.tables import Hostgroups, Hosts, Servicegroups
|
33 | 34 | from cmk.utils.livestatus_helpers.types import Column, Table
|
34 | 35 | from cmk.utils.regex import regex, REGEX_ID
|
35 |
| -from cmk.utils.tags import TagGroupID, TagID |
| 36 | +from cmk.utils.tags import TagConfig, TagGroup, TagGroupID, TagID |
36 | 37 | from cmk.utils.user import UserId
|
37 | 38 |
|
38 | 39 | from cmk.gui import sites
|
39 |
| -from cmk.gui.config import builtin_role_ids |
| 40 | +from cmk.gui.config import active_config, builtin_role_ids |
40 | 41 | from cmk.gui.customer import customer_api, SCOPE_GLOBAL
|
41 | 42 | from cmk.gui.exceptions import MKUserError
|
42 | 43 | from cmk.gui.fields.base import BaseSchema, MultiNested, ValueTypedDictSchema
|
|
48 | 49 | from cmk.gui.userdb import load_users
|
49 | 50 | from cmk.gui.watolib import userroles
|
50 | 51 | from cmk.gui.watolib.groups_io import load_group_information
|
51 |
| -from cmk.gui.watolib.host_attributes import host_attribute |
| 52 | +from cmk.gui.watolib.host_attributes import ABCHostAttribute, all_host_attributes, host_attribute |
52 | 53 | from cmk.gui.watolib.hosts_and_folders import Folder, folder_tree, Host
|
53 | 54 | from cmk.gui.watolib.passwords import contact_group_choices, password_exists
|
54 | 55 | from cmk.gui.watolib.sites import site_management_registry
|
55 |
| -from cmk.gui.watolib.tags import load_tag_group |
| 56 | +from cmk.gui.watolib.tags import load_tag_config_read_only, load_tag_group |
56 | 57 |
|
57 | 58 | from cmk.fields import base, Boolean, DateTime, validators
|
58 | 59 |
|
@@ -734,6 +735,90 @@ def validate_custom_host_attributes(
|
734 | 735 | return host_attributes
|
735 | 736 |
|
736 | 737 |
|
| 738 | +class CustomHostAttributesAndTagGroups(BaseSchema): |
| 739 | + class Meta: |
| 740 | + unknown = marshmallow.INCLUDE |
| 741 | + |
| 742 | + # Set it to true on create and update schemas to raise an error if a readonly attribute is passed |
| 743 | + _raise_error_if_attribute_is_readonly = False |
| 744 | + |
| 745 | + @marshmallow.post_load(pass_original=True) |
| 746 | + def _validate_extra_attributes( |
| 747 | + self, |
| 748 | + result_data: dict[str, Any], |
| 749 | + original_data: MutableMapping[str, Any], |
| 750 | + **_unused_args: Any, |
| 751 | + ) -> dict[str, Any]: |
| 752 | + for field in self.fields: |
| 753 | + original_data.pop(field, None) |
| 754 | + |
| 755 | + if not original_data: |
| 756 | + return result_data |
| 757 | + |
| 758 | + host_attributes = all_host_attributes(active_config) |
| 759 | + tag_group_config = load_tag_config_read_only() |
| 760 | + |
| 761 | + for name, value in original_data.items(): |
| 762 | + if tag_group := self._get_custom_tag_group(name, tag_group_config): |
| 763 | + self._validate_tag_group(tag_group, value) |
| 764 | + |
| 765 | + elif host_attribute := self._get_custom_host_attribute(name, host_attributes): |
| 766 | + self._validate_attribute(host_attribute, value) |
| 767 | + |
| 768 | + else: |
| 769 | + self._raise_error(f"Unknown Attribute: {name!r}: {value!r}") |
| 770 | + |
| 771 | + result_data[name] = value |
| 772 | + return result_data |
| 773 | + |
| 774 | + @marshmallow.post_dump(pass_original=True) |
| 775 | + def _add_tags_and_custom_attributes_back( |
| 776 | + self, dump_data: dict[str, Any], original_data: dict[str, Any], **_kwargs: Any |
| 777 | + ) -> dict[str, Any]: |
| 778 | + # Custom attributes and tags are thrown away during validation as they have no field in the schema. |
| 779 | + # So we dump them back in here. |
| 780 | + # TODO: This code complies with the behavior enforced by the test_openapi_host_has_deleted_custom_attributes |
| 781 | + # test. However more research is needed to determine if it should change. |
| 782 | + original_data.update(dump_data) |
| 783 | + return original_data |
| 784 | + |
| 785 | + def _get_custom_tag_group(self, tag_name: str, tag_config: TagConfig) -> TagGroup | None: |
| 786 | + return tag_config.get_tag_group(TagGroupID(tag_name[4:])) |
| 787 | + |
| 788 | + def _get_custom_host_attribute( |
| 789 | + self, attribute_name: str, attributes: dict[str, ABCHostAttribute] |
| 790 | + ) -> ABCHostAttribute | None: |
| 791 | + try: |
| 792 | + attribute = attributes[attribute_name] |
| 793 | + if not attribute.from_config(): |
| 794 | + return None |
| 795 | + |
| 796 | + return attribute |
| 797 | + |
| 798 | + except KeyError: |
| 799 | + return None |
| 800 | + |
| 801 | + def _validate_attribute(self, host_attribute: ABCHostAttribute, value: object) -> None: |
| 802 | + if self._raise_error_if_attribute_is_readonly and not host_attribute.editable(): |
| 803 | + self._raise_error(f"Attribute {host_attribute.name()!r} is readonly.") |
| 804 | + |
| 805 | + if not isinstance(value, str): |
| 806 | + self._raise_error(f"Attribute {host_attribute.name()!r} must be a string.") |
| 807 | + |
| 808 | + try: |
| 809 | + host_attribute.validate_input(value, "") |
| 810 | + |
| 811 | + except MKUserError as exc: |
| 812 | + self._raise_error(f"{host_attribute.name()}: {str(exc)}") |
| 813 | + |
| 814 | + def _validate_tag_group(self, tag_group: TagGroup, value: object) -> None: |
| 815 | + if value not in tag_group.get_tag_ids(): |
| 816 | + self._raise_error(f"Invalid value for tag-group {tag_group.title!r}: {value!r}") |
| 817 | + |
| 818 | + def _raise_error(self, message: str) -> None: |
| 819 | + raise ValidationError(message) |
| 820 | + |
| 821 | + |
737 | 822 | def ensure_string(value):
|
738 | 823 | if not isinstance(value, str):
|
739 | 824 | raise ValidationError(f"Not a string, but a {type(value).__name__}")
|
@@ -1067,7 +1152,7 @@ def _validate(self, value):
|
1067 | 1152 | if (
|
1068 | 1153 | self.presence == "might_not_exist_on_view"
|
1069 | 1154 | and self.context is not None
|
1070 |
| - and self.context["object_context"] == "view" |
| 1155 | + and self.context.get("object_context") == "view" |
1071 | 1156 | ):
|
1072 | 1157 | return
|
1073 | 1158 |
|
|
0 commit comments