diff --git a/Makefile b/Makefile index 0b0c2dcb54..a03e0421b6 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ APP_IMAGE ?= hub.adsw.io/adcm/adcm APP_TAG ?= $(subst /,_,$(BRANCH_NAME)) SELENOID_HOST ?= 10.92.2.65 SELENOID_PORT ?= 4444 -ADCM_VERSION = "2.1.0" +ADCM_VERSION = "2.1.1" .PHONY: help diff --git a/python/adcm/settings.py b/python/adcm/settings.py index 682183a9c5..9b5012d967 100644 --- a/python/adcm/settings.py +++ b/python/adcm/settings.py @@ -223,6 +223,10 @@ "format": "{asctime} {levelname} {module} {message}", "style": "{", }, + "ldap": { + "format": "{levelname} {module}: {message}", + "style": "{", + }, }, "handlers": { "adcm_file": { @@ -266,6 +270,11 @@ "formatter": "adcm", "stream": "ext://sys.stderr", }, + "ldap_stdout_handler": { + "class": "logging.StreamHandler", + "formatter": "ldap", + "stream": "ext://sys.stdout", + }, }, "loggers": { "adcm": { @@ -297,6 +306,10 @@ "handlers": ["stream_stdout_handler", "stream_stderr_handler"], "level": LOG_LEVEL, }, + "django_auth_ldap": { + "handlers": ["ldap_stdout_handler"], + "level": LOG_LEVEL, + }, }, } diff --git a/python/api_v2/group_config/serializers.py b/python/api_v2/group_config/serializers.py index 88516842dc..f48a563af9 100644 --- a/python/api_v2/group_config/serializers.py +++ b/python/api_v2/group_config/serializers.py @@ -25,11 +25,11 @@ class Meta: fields = ["id", "name", "description", "hosts"] def validate_name(self, value): - model = self.context["view"].get_parent_object() - parent_content_type = ContentType.objects.get_for_model(model=model) - queryset = GroupConfig.objects.filter(name=value, object_type=parent_content_type) + object_ = self.context["view"].get_parent_object() + parent_content_type = ContentType.objects.get_for_model(model=object_) + queryset = GroupConfig.objects.filter(name=value, object_type=parent_content_type, object_id=object_.pk) if queryset.exists(): raise ValidationError( - f"Group config with name {value} already exists for {parent_content_type} {model.name}" + f"Group config with name {value} already exists for {parent_content_type} {object_.name}" ) return value diff --git a/python/api_v2/tests/bundles/cluster_one/config.yaml b/python/api_v2/tests/bundles/cluster_one/config.yaml index 8debd4841a..3206809ed2 100644 --- a/python/api_v2/tests/bundles/cluster_one/config.yaml +++ b/python/api_v2/tests/bundles/cluster_one/config.yaml @@ -94,7 +94,8 @@ export: - boolean -- name: service_1 +- &service_1 + name: service_1 type: service version: *version config: @@ -225,3 +226,6 @@ components: component: {} + +- <<: *service_1 + name: service_1_clone diff --git a/python/api_v2/tests/test_cluster.py b/python/api_v2/tests/test_cluster.py index cb0bd02f19..af4870e617 100644 --- a/python/api_v2/tests/test_cluster.py +++ b/python/api_v2/tests/test_cluster.py @@ -271,11 +271,12 @@ def test_service_prototypes_success(self): ) self.assertEqual(response.status_code, HTTP_200_OK) - self.assertEqual(len(response.json()), 6) + self.assertEqual(len(response.json()), 7) self.assertListEqual( [prototype["displayName"] for prototype in response.json()], [ "service_1", + "service_1_clone", "service_2", "service_3_manual_add", "service_4_save_config_without_required_field", @@ -292,11 +293,12 @@ def test_service_candidates_success(self): ) self.assertEqual(response.status_code, HTTP_200_OK) - self.assertEqual(len(response.json()), 5) + self.assertEqual(len(response.json()), 6) self.assertListEqual( [prototype["displayName"] for prototype in response.json()], [ "service_1", + "service_1_clone", "service_2", "service_4_save_config_without_required_field", "service_5_variant_type_without_values", diff --git a/python/api_v2/tests/test_component.py b/python/api_v2/tests/test_component.py index e9119762dc..1fada0bc90 100644 --- a/python/api_v2/tests/test_component.py +++ b/python/api_v2/tests/test_component.py @@ -31,7 +31,7 @@ def setUp(self) -> None: self.component_2_to_delete = ServiceComponent.objects.get( prototype__name="component_2", service=self.service_1, cluster=self.cluster_1 ) - self.action_1 = Action.objects.get(name="action_1_comp_1") + self.action_1 = Action.objects.get(name="action_1_comp_1", prototype=self.component_1.prototype) def test_list(self): response = self.client.get( diff --git a/python/api_v2/tests/test_group_config.py b/python/api_v2/tests/test_group_config.py index 86053d00ae..446236d4bf 100644 --- a/python/api_v2/tests/test_group_config.py +++ b/python/api_v2/tests/test_group_config.py @@ -79,6 +79,71 @@ def setUp(self) -> None: self.test_user = self.create_user(**self.test_user_credentials) +class TestGroupConfigNaming(BaseServiceGroupConfigTestCase): + def test_create_group_with_same_name_for_different_entities_of_same_type_success(self) -> None: + service_2 = self.add_services_to_cluster(service_names=["service_1_clone"], cluster=self.cluster_1).get() + component_of_service_2 = ServiceComponent.objects.get(service=service_2, prototype__name=self.component_1.name) + + with self.subTest("Cluster"): + self.assertEqual(GroupConfig.objects.filter(name=self.cluster_1_group_config.name).count(), 1) + + response = self.client.post( + path=reverse(viewname="v2:cluster-group-config-list", kwargs={"cluster_pk": self.cluster_2.pk}), + data={"name": self.cluster_1_group_config.name, "description": "group-config-new"}, + ) + + self.assertEqual(response.status_code, HTTP_201_CREATED) + self.assertEqual(GroupConfig.objects.filter(name=self.cluster_1_group_config.name).count(), 2) + + with self.subTest("Service"): + self.assertEqual(GroupConfig.objects.filter(name=self.service_1_group_config.name).count(), 1) + + response = self.client.post( + path=reverse( + viewname="v2:service-group-config-list", + kwargs={"cluster_pk": self.cluster_1.pk, "service_pk": service_2.pk}, + ), + data={"name": self.service_1_group_config.name, "description": "group-config-new"}, + ) + + self.assertEqual(response.status_code, HTTP_201_CREATED) + self.assertEqual(GroupConfig.objects.filter(name=self.service_1_group_config.name).count(), 2) + + with self.subTest("Component"): + name = "component_group" + self.assertEqual(GroupConfig.objects.filter(name=name).count(), 0) + + response = self.client.post( + path=reverse( + viewname="v2:component-group-config-list", + kwargs={ + "cluster_pk": self.cluster_1.pk, + "service_pk": service_2.pk, + "component_pk": component_of_service_2.pk, + }, + ), + data={"name": name, "description": "group-config-new"}, + ) + + self.assertEqual(response.status_code, HTTP_201_CREATED) + self.assertEqual(GroupConfig.objects.filter(name=name).count(), 1) + + response = self.client.post( + path=reverse( + viewname="v2:component-group-config-list", + kwargs={ + "cluster_pk": self.cluster_1.pk, + "service_pk": self.service_1.pk, + "component_pk": self.component_1.pk, + }, + ), + data={"name": name, "description": "group-config-new"}, + ) + + self.assertEqual(response.status_code, HTTP_201_CREATED) + self.assertEqual(GroupConfig.objects.filter(name=name).count(), 2) + + class TestClusterGroupConfig(BaseClusterGroupConfigTestCase): def test_list_success(self): response = self.client.get( diff --git a/python/cm/adcm_config/checks.py b/python/cm/adcm_config/checks.py index 1510eaa32a..ae1a274a60 100644 --- a/python/cm/adcm_config/checks.py +++ b/python/cm/adcm_config/checks.py @@ -16,7 +16,7 @@ from cm.adcm_config.utils import config_is_ro, group_keys_to_flat, proto_ref from cm.checker import FormatError, SchemaError, process_rule -from cm.errors import raise_adcm_ex +from cm.errors import AdcmEx, raise_adcm_ex from cm.models import GroupConfig, Prototype, StagePrototype @@ -255,10 +255,11 @@ def check_config_type( if spec["type"] == "variant": source = spec["limits"]["source"] - if source["strict"] and source["type"] == "inline" and value not in source["value"]: - msg = f'not in variant list: "{source["value"]}"' - raise_adcm_ex(code="CONFIG_VALUE_ERROR", msg=tmpl2.format(msg)) + if source["strict"]: + if source["type"] == "inline" and value not in source["value"]: + msg = f'not in variant list: "{source["value"]}"' + raise AdcmEx(code="CONFIG_VALUE_ERROR", msg=tmpl2.format(msg)) if not default and source["type"] in ("config", "builtin") and value not in source["value"]: msg = f'not in variant list: "{source["value"]}"' - raise_adcm_ex(code="CONFIG_VALUE_ERROR", msg=tmpl2.format(msg)) + raise AdcmEx(code="CONFIG_VALUE_ERROR", msg=tmpl2.format(msg)) diff --git a/python/cm/migrations/0116_fix_null_jsonfield.py b/python/cm/migrations/0116_fix_null_jsonfield.py new file mode 100644 index 0000000000..882c95f91a --- /dev/null +++ b/python/cm/migrations/0116_fix_null_jsonfield.py @@ -0,0 +1,32 @@ +# 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. + +# Generated by Django 3.2.23 on 2024-04-09 11:31 + +from django.db import migrations +from django.db.models import Q + + +def fix_null_jsonfield(apps, schema_editor): + Action = apps.get_model("cm", "Action") + Action.objects.filter(Q(ui_options__isnull=True) | Q(ui_options="{}")).update(ui_options={}) + + +class Migration(migrations.Migration): + dependencies = [ + ("cm", "0059_auto_20200904_0910"), + ("cm", "0115_auto_20231025_1823"), + ] + + operations = [ + migrations.RunPython(code=fix_null_jsonfield, reverse_code=migrations.RunPython.noop), + ] diff --git a/python/rbac/ldap.py b/python/rbac/ldap.py index df117e775a..9d3fdc34e7 100644 --- a/python/rbac/ldap.py +++ b/python/rbac/ldap.py @@ -106,14 +106,17 @@ def get_groups_by_user_dn( scope=user_search.scope, filterstr=user_search.filterstr.replace(replace, search_expr), ) + # Remove references from the Search Result https://www.rfc-editor.org/rfc/rfc4511#section-4.5.3 + users = [user for user in users if user[0] is not None] + if len(users) != 1: err_msg = f"Not one user found by `{search_expr}` search" - return None, err_msg + return [], [], err_msg user_dn_, user_attrs = users[0] if user_dn_.strip().lower() != user_dn.strip().lower(): err_msg = f"Got different user dn: {(user_dn_, user_dn)}. Tune search" - return None, err_msg + return [], [], err_msg group_cns = [] group_dns_lower = [] @@ -251,7 +254,7 @@ def get_user_model(self) -> type[User]: @contextmanager def _ldap_connection(self) -> ldap.ldapobject.LDAPObject: - ldap.set_option(ldap.OPT_REFERRALS, 0) + ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF) conn = ldap.initialize(self.default_settings["SERVER_URI"]) conn.protocol_version = ldap.VERSION3 configure_tls(self.is_tls, os.environ.get(CERT_ENV_KEY, ""), conn)