diff --git a/config.yaml b/config.yaml index 8ec7653..4ed53a0 100644 --- a/config.yaml +++ b/config.yaml @@ -4,7 +4,10 @@ options: ranger-admin-password: description: | - Password used for the policy manager (Web interface) + The password for Ranger Admin user. + Password can not be changed using this property after initial deployment. + It can be changed in the UI. + Password should be minimum 8 characters with min one alphabet and one numeric. default: "rangerR0cks!" type: string tls-secret-name: @@ -122,3 +125,11 @@ options: functionality for Ranger service in ms. type: int default: 3000 + ranger-usersync-password: + description: | + The password for the user that synchronizes users and groups from LDAP to Ranger admin. + Password can not be changed using this property after initial deployment. + It can be changed in the UI. + Password should be minimum 8 characters with min one alphabet and one numeric. + type: string + default: rangerR0cks! diff --git a/lib/charms/data_platform_libs/v0/data_interfaces.py b/lib/charms/data_platform_libs/v0/data_interfaces.py index 59a9722..a2162aa 100644 --- a/lib/charms/data_platform_libs/v0/data_interfaces.py +++ b/lib/charms/data_platform_libs/v0/data_interfaces.py @@ -331,7 +331,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 37 +LIBPATCH = 38 PYDEPS = ["ops>=2.0.0"] @@ -2606,6 +2606,14 @@ def set_version(self, relation_id: int, version: str) -> None: """ self.update_relation_data(relation_id, {"version": version}) + def set_subordinated(self, relation_id: int) -> None: + """Raises the subordinated flag in the application relation databag. + + Args: + relation_id: the identifier for a particular relation. + """ + self.update_relation_data(relation_id, {"subordinated": "true"}) + class DatabaseProviderEventHandlers(EventHandlers): """Provider-side of the database relation handlers.""" @@ -2842,6 +2850,21 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: """Event emitted when the database relation has changed.""" + is_subordinate = False + remote_unit_data = None + for key in event.relation.data.keys(): + if isinstance(key, Unit) and not key.name.startswith(self.charm.app.name): + remote_unit_data = event.relation.data[key] + elif isinstance(key, Application) and key.name != self.charm.app.name: + is_subordinate = event.relation.data[key].get("subordinated") == "true" + + if is_subordinate: + if not remote_unit_data: + return + + if remote_unit_data.get("state") != "ready": + return + # Check which data has changed to emit customs events. diff = self._diff(event) diff --git a/src/charm.py b/src/charm.py index 678a0a6..180621e 100755 --- a/src/charm.py +++ b/src/charm.py @@ -225,6 +225,7 @@ def _configure_ranger_admin(self, container): "DB_USER": db_conn["user"], "DB_PWD": db_conn["password"], "RANGER_ADMIN_PWD": self.config["ranger-admin-password"], + "RANGER_USERSYNC_PWD": self.config["ranger-usersync-password"], "JAVA_OPTS": "-Duser.timezone=UTC0", } config = render("admin-config.jinja", context) @@ -258,6 +259,7 @@ def _configure_ranger_usersync(self, container): context.update( { "POLICY_MGR_URL": self.config["policy-mgr-url"], + "RANGER_USERSYNC_PWD": self.config["ranger-usersync-password"], } ) config = render("ranger-usersync-config.jinja", context) @@ -268,6 +270,29 @@ def _configure_ranger_usersync(self, container): ) return USERSYNC_ENTRYPOINT, context + def _validate_password(self, password, config_key, state_key): + """Validate that the admin and usersync passwords are not changed after deployment. + + Args: + password: the deployment password. + config_key: the config key for the password. + state_key: the key the password is stored in state. + + Raises: + ValueError: in case the password has been changed. + """ + if password is None: + if self.unit.is_leader(): + setattr(self._state, state_key, self.config[config_key]) + logger.info(self._state.ranger_admin_password) + elif password != self.config[config_key]: + message = ( + f"value of '{config_key}' config cannot be changed after deployment. " + f"Value should be {password}" + ) + logger.error(message) + raise ValueError(message) + def validate(self): """Validate that configuration and relations are valid and ready. @@ -283,6 +308,21 @@ def validate(self): if self.config["charm-function"].value == "usersync": self.ldap.validate() + ranger_admin_password = self._state.ranger_admin_password + ranger_usersync_password = self._state.ranger_usersync_password + + logger.info(ranger_admin_password) + self._validate_password( + ranger_admin_password, + "ranger-admin-password", + "ranger_admin_password", + ) + self._validate_password( + ranger_usersync_password, + "ranger-usersync-password", + "ranger_usersync_password", + ) + def update(self, event): """Update the Ranger server configuration and re-plan its execution. diff --git a/src/structured_config.py b/src/structured_config.py index 3d33d7f..1e15c5a 100644 --- a/src/structured_config.py +++ b/src/structured_config.py @@ -57,7 +57,7 @@ class CharmConfig(BaseConfigModel): sync_ldap_user_group_name_attribute: Optional[str] sync_ldap_deltasync: bool sync_interval: Optional[int] - ranger_usersync_password: Optional[str] + ranger_usersync_password: str policy_mgr_url: str charm_function: FunctionType lookup_timeout: int @@ -133,3 +133,28 @@ def lookup_timeout_validator(cls, value: str) -> Optional[int]: if 1000 <= int_value <= 10000: return int_value raise ValueError("Value out of range.") + + @validator("ranger_admin_password", "ranger_usersync_password") + @classmethod + def password_validator(cls, value: str) -> str: + """Validate if the password meets the following requirements. + + - Minimum 8 characters in length + - Contains at least one alphabetic character + - Contains at least one numeric character + + Args: + value: The password to validate. + + Returns: + value: The validated password if it meets the requirements. + + Raises: + ValueError: If the password does not meet the requirements. + """ + pattern = re.compile( + r"^(?=.*[A-Za-z])(?=.*\d)(?=.*[\W_])[A-Za-z\d\W_]{8,}$" + ) + if pattern.match(value): + return value + raise ValueError("Password does not match requirements.") diff --git a/templates/admin-config.jinja b/templates/admin-config.jinja index 37e9b5b..14518c0 100644 --- a/templates/admin-config.jinja +++ b/templates/admin-config.jinja @@ -72,10 +72,10 @@ db_password={{ DB_PWD }} # change password. Password for below mentioned users can be changed only once using this property. #PLEASE NOTE :: Password should be minimum 8 characters with min one alphabet and one numeric. -rangerAdmin_password={{ RANGER_ADMIN_PWD | default("rangerR0cks!") }} -rangerTagsync_password={{ RANGER_ADMIN_PWD | default("rangerR0cks!") }} -rangerUsersync_password={{ RANGER_ADMIN_PWD | default("rangerR0cks!") }} -keyadmin_password={{ RANGER_ADMIN_PWD | default("rangerR0cks!") }} +rangerAdmin_password={{ RANGER_ADMIN_PWD }} +rangerTagsync_password={{ RANGER_ADMIN_PWD }} +rangerUsersync_password={{ RANGER_USERSYNC_PWD }} +keyadmin_password={{ RANGER_ADMIN_PWD }} #Source for Audit Store. Currently solr and elasticsearch are supported. diff --git a/templates/ranger-usersync-config.jinja b/templates/ranger-usersync-config.jinja index 92de7f3..4424256 100644 --- a/templates/ranger-usersync-config.jinja +++ b/templates/ranger-usersync-config.jinja @@ -27,7 +27,7 @@ unix_group=ranger # This password should be changed from the default. # Please note that this password should be as per rangerusersync user in ranger admin. -rangerUsersync_password= rangerR0cks! +rangerUsersync_password= {{ RANGER_USERSYNC_PWD }} hadoop_conf=/etc/hadoop/conf diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 91f53b3..c626591 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -36,6 +36,7 @@ async def deploy(ops_test: OpsTest): resources=resources, application_name=APP_NAME, num_units=1, + config={"ranger-usersync-password": "P@ssw0rd1234"}, ) await ops_test.model.wait_for_idle( diff --git a/tests/integration/test_usersync.py b/tests/integration/test_usersync.py index 13e7877..147bbca 100644 --- a/tests/integration/test_usersync.py +++ b/tests/integration/test_usersync.py @@ -31,6 +31,7 @@ async def test_user_sync(self, ops_test: OpsTest): ranger_config = { "charm-function": "usersync", + "ranger-usersync-password": "P@ssw0rd1234", } charm = await ops_test.build_charm(".") diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 9181940..89cd266 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -99,6 +99,7 @@ def test_admin_ready(self): "DB_USER": "postgres_user", "DB_PWD": "admin", "RANGER_ADMIN_PWD": "rangerR0cks!", + "RANGER_USERSYNC_PWD": "rangerR0cks!", "JAVA_OPTS": "-Duser.timezone=UTC0", }, } @@ -135,6 +136,7 @@ def test_usersync_ready(self): "startup": "enabled", "environment": { "POLICY_MGR_URL": "http://ranger-k8s:6080", + "RANGER_USERSYNC_PWD": "rangerR0cks!", "SYNC_GROUP_USER_MAP_SYNC_ENABLED": True, "SYNC_GROUP_SEARCH_ENABLED": True, "SYNC_GROUP_SEARCH_BASE": "dc=canonical,dc=dev,dc=com", @@ -172,10 +174,10 @@ def test_config_changed(self): simulate_admin_lifecycle(harness) # Update the config. - self.harness.update_config({"ranger-admin-password": "secure-pass"}) + self.harness.update_config({"ranger-admin-password": "s3cure-pass"}) # The new plan reflects the change. - want_admin_password = "secure-pass" # nosec + want_admin_password = "rangerR0cks!" # nosec got_admin_password = harness.get_container_pebble_plan( "ranger" ).to_dict()["services"]["ranger"]["environment"]["RANGER_ADMIN_PWD"] @@ -185,7 +187,10 @@ def test_config_changed(self): # The ActiveStatus is set with replan message. self.assertEqual( harness.model.unit.status, - MaintenanceStatus("replanning application"), + BlockedStatus( + "value of 'ranger-admin-password' config cannot be changed after deployment. " + "Value should be rangerR0cks!" + ), ) def test_ingress(self): diff --git a/tests/unit/test_structured_config.py b/tests/unit/test_structured_config.py index 808691c..a07d58a 100644 --- a/tests/unit/test_structured_config.py +++ b/tests/unit/test_structured_config.py @@ -49,6 +49,33 @@ def test_string_values(_harness) -> None: check_valid_values(_harness, "sync-ldap-url", accepted_values) +def test_password_fields(_harness) -> None: + """Test password fields validation.""" + erroneous_passwords = [ + "onlyletters", # No numbers + "12345678", # No letters + "NoSpecialChar123", # No special characters + "Short1!", # Too short + ] + + valid_passwords = [ + "Valid1Pass!", + "AnotherValid2#Password", + "Password1$", + "P@ssw0rd1234", + ] + + check_invalid_values( + _harness, "ranger-admin-password", erroneous_passwords + ) + check_valid_values(_harness, "ranger-admin-password", valid_passwords) + + check_invalid_values( + _harness, "ranger-usersync-password", erroneous_passwords + ) + check_valid_values(_harness, "ranger-usersync-password", valid_passwords) + + def check_valid_values(_harness, field: str, accepted_values: list) -> None: """Check the correctness of the passed values for a field.