Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSS-9793 Make ranger usersync password configurable #31

Merged
merged 4 commits into from
Jul 23, 2024
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
13 changes: 12 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
AmberCharitos marked this conversation as resolved.
Show resolved Hide resolved
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:
Expand Down Expand Up @@ -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!
25 changes: 24 additions & 1 deletion lib/charms/data_platform_libs/v0/data_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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)

Expand Down
40 changes: 40 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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.

Expand All @@ -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.

Expand Down
27 changes: 26 additions & 1 deletion src/structured_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.")
8 changes: 4 additions & 4 deletions templates/admin-config.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion templates/ranger-usersync-config.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_usersync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(".")
Expand Down
11 changes: 8 additions & 3 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"]
Expand All @@ -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):
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/test_structured_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading