From 5ec5b77b3c84b534aa2f8bcfced2557b6413441b Mon Sep 17 00:00:00 2001 From: krishnamuttevi Date: Wed, 5 Aug 2026 16:22:11 +0530 Subject: [PATCH 1/3] RANGER-4897: Extended the test coverage for XUserRest & RoleRest apis --- functional-tests/pytest.ini | 18 +- functional-tests/readme.md | 191 ++- functional-tests/rolerest/__init__.py | 14 + functional-tests/rolerest/conftest.py | 356 ++++ functional-tests/rolerest/readme.md | 137 ++ .../rolerest/test_role_management.py | 1271 ++++++++++++++ .../rolerest/test_role_utility_fun.py | 1340 +++++++++++++++ functional-tests/rolerest/utility/__init__.py | 14 + functional-tests/rolerest/utility/utils.py | 273 +++ functional-tests/run-tests.sh | 190 ++- functional-tests/xuserrest/__init__.py | 14 + functional-tests/xuserrest/conftest.py | 605 +++++++ functional-tests/xuserrest/readme.md | 137 ++ functional-tests/xuserrest/test_groups.py | 1489 +++++++++++++++++ functional-tests/xuserrest/test_permission.py | 1430 ++++++++++++++++ .../xuserrest/test_secure_user.py | 1330 +++++++++++++++ functional-tests/xuserrest/test_ugsync.py | 760 +++++++++ functional-tests/xuserrest/test_user.py | 1058 ++++++++++++ .../xuserrest/test_utility_fun.py | 905 ++++++++++ .../xuserrest/utility/__init__.py | 14 + functional-tests/xuserrest/utility/utils.py | 812 +++++++++ 21 files changed, 12270 insertions(+), 88 deletions(-) create mode 100644 functional-tests/rolerest/__init__.py create mode 100644 functional-tests/rolerest/conftest.py create mode 100644 functional-tests/rolerest/readme.md create mode 100644 functional-tests/rolerest/test_role_management.py create mode 100644 functional-tests/rolerest/test_role_utility_fun.py create mode 100644 functional-tests/rolerest/utility/__init__.py create mode 100644 functional-tests/rolerest/utility/utils.py create mode 100644 functional-tests/xuserrest/__init__.py create mode 100644 functional-tests/xuserrest/conftest.py create mode 100644 functional-tests/xuserrest/readme.md create mode 100644 functional-tests/xuserrest/test_groups.py create mode 100644 functional-tests/xuserrest/test_permission.py create mode 100644 functional-tests/xuserrest/test_secure_user.py create mode 100644 functional-tests/xuserrest/test_ugsync.py create mode 100644 functional-tests/xuserrest/test_user.py create mode 100644 functional-tests/xuserrest/test_utility_fun.py create mode 100644 functional-tests/xuserrest/utility/__init__.py create mode 100644 functional-tests/xuserrest/utility/utils.py diff --git a/functional-tests/pytest.ini b/functional-tests/pytest.ini index 6836906659..a815e6cfe4 100644 --- a/functional-tests/pytest.ini +++ b/functional-tests/pytest.ini @@ -13,17 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. - -# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - [pytest] markers = cleanEZ: clean up the encryption zone createEZ: create encryption zone + get: get methods + post: post methods + put: put methods + delete: delete methods + positive: positive test cases + negative: negative test cases + xuserrest: xuserrest test cases + secure_endpoint: secure endpoint test cases + rolerest: rolerest test cases pythonpath = . \ No newline at end of file diff --git a/functional-tests/readme.md b/functional-tests/readme.md index a155c02e0a..075487f41b 100644 --- a/functional-tests/readme.md +++ b/functional-tests/readme.md @@ -25,72 +25,169 @@ documentation. --> -## Pytest Functional Test-suite +## Pytest Functional Test Suite +This test suite validates REST API endpoints for Apache Ranger services,Admin (rolerest, xuserrest, servicerest), KMS (Key Management Service), and tests HDFS encryption functionalities including key management and file operations within encryption zones. -This test suite validates REST API endpoints for KMS (Key Management Service) and tests HDFS encryption functionalities including key management and file operations within encryption zones. -This framework brings up docker containers and executes test cases +### Available Test Suites -**hdfs :** contains test cases for checking KMS functionality through hdfs encryption lifecycle - -**kms :** contains test cases for checking KMS API functionality +| Suite | Description | +|---|---| +| **hdfs** | Test cases for HDFS encryption lifecycle using KMS | +| **kms** | Test cases for KMS REST API functionality | +| **xuserrest** | Test cases for Ranger Admin User/Group REST APIs | +| **rolerest** | Test cases for Ranger Admin Role REST APIs | +| **servicerest** | Test cases for Ranger Admin Service REST APIs | +--- ### Directory Structure -``` +```text functional-tests/ -├── hdfs/ # Tests on HDFS encryption cycle -├── kms/ # Tests on KMS API -├── pytest.ini # Registers custom pytest markers -├── run_tests.sh # Script to automate test execution -├── requirements.txt -├── readme.md +├── hdfs/ # Tests on HDFS encryption cycle +│ ├── conftest.py # Fixtures and setup for HDFS tests +│ └── test_file.py # HDFS encryption test cases +│ +├── kms/ # Tests on KMS REST API +│ ├── conftest.py # Fixtures and setup for KMS tests +│ └── test_file.py # KMS API test cases +│ +├── xuserrest/ # Tests on Ranger User/Group/Role REST APIs +│ ├──utility/ # Utility Folder contains helper functions +│ | ├── utils.py +│ ├── conftest.py # Fixtures and setup for user REST tests +│ └── test_file.py # User REST API test cases +│ +├── rolerest/ # Tests on Ranger Role REST APIs +│ ├──utility/ # Utility Folder contains helper functions +│ | ├── utils.py +│ ├── conftest.py # Fixtures and setup for role REST tests +│ └── test_file.py # User REST API test cases +│ +├── servicerest/ # Tests on Ranger Service REST APIs +│ ├──utility/ # Utility Folder contains helper functions +│ | ├── utils.py +│ ├── conftest.py # Fixtures and setup for service REST tests +│ ├── test_file.py # Service REST API test cases +│ └── automation.log # logs related to the tests and the conftest files for service rest +│ +├── pytest.ini # Registers custom pytest markers +├── run-tests.sh # Script to automate setup and test execution +├── requirements.txt # Python dependencies +└── readme.md # This documentation + +``` +> **Note:** A Python virtual environment folder named `myenv` will be automatically +> generated upon running the tests for the first time. + + +## Prerequisites +1. Docker & Docker Compose installed and running +2. Python 3.10 or higher +3. Change the working directory to pytest-Tests +```text +cd pytest-Tests/ +``` +4. Make the shell script executable + +```text +chmod +x run-tests.sh +``` + + +## Environment Variables + +Configure container behavior before running the script using the following environment variables: + +1. Fresh Setup & Cleanup: + +Force a clean environment & helps building binaries with local changes (removes old Ranger containers, prunes Docker space, builds fresh, and cleans up after tests): +```text +export CLEAN_CONTAINERS=1 +./run-tests.sh +``` + +After initial setup, disable fresh container creation to speed up subsequent runs (default behavior): +```text +export CLEAN_CONTAINERS=0 +./run-tests.sh ``` -### Running Tests +2. Infrastructure Only (Skip Tests) -#### Container Setup +Start Docker infrastructure without executing Pytest suites: -Before running the tests, configure container behavior using the following environment variable: -~~~ - # To force a fresh container setup: - export CLEAN_CONTAINERS=1 -~~~ +```text +export RUN_TESTS=0 +./run-tests.sh +``` +This is useful when tests fail due to slow container startup. Once all containers are healthy, re-enable tests (default behavior): + +```text +export RUN_TESTS=1 +./run-tests.sh +``` + + +## Running Tests +The run-tests.sh script manages Docker container setup, dependency installation, and test execution. It supports both interactive and argument-based modes. + +1. Interactive Mode: + +Run the script without arguments to be prompted for inputs: +```text +./run-tests.sh +``` +> DB Type: Enter one of postgres, mysql, oracle, mssql. Defaults to postgres. -After the initial setup, you can disable fresh container creation by setting below for the next re-runs: -~~~ - export CLEAN_CONTAINERS=0 -~~~ +> Test Suites: Enter space-separated suite names. Defaults to ALL suites. + +example: +```text + +Available DB types: postgres, mysql, oracle, mssql +Enter DB type (press Enter to default to postgres): postgres + +Available test suites: xuserrest servicerest hdfs kms +Enter test suites space-separated (press Enter to run ALL): kms hdfs +``` + +2. Command-Line Arguments Mode: + +Pass arguments directly to skip prompts: + +```text +./run-tests.sh [db-type] [test-suites...] +``` +db-type — Must be the first argument. Valid values: postgres, mysql, oracle, mssql. -#### Executing Tests +test-suites — Space-separated list: hdfs, kms, xuserrest, servicerest. -Run the test script using: -~~~ - ./run-tests.sh [db-type] [additional-services] - - # valid values for db-type: mysql/postgres/oracle , postgres is the default - # additional-services: multiple services can be specified separated by space - - # e.g for running tests within kms and hdfs use below command: - - ./run-tests.sh postgres hadoop - - # Note: If additional-services are specified, db-type must also be explicitly specified +Examples: -~~~ +```text +# Run all suites with Postgres (default) +./run-tests.sh postgres -#### Note (Optional) +# Run specific suites with Postgres +./run-tests.sh postgres kms hdfs -If you only need to start the infrastructure i.e containers (without running tests): -~~~ - export RUN_TESTS=0 -~~~ -This is useful when tests are failing due to incomplete container setup. +# Run only user REST tests with MySQL +./run-tests.sh mysql xuserrest -After the infrastructure is successfully up, set RUN_TESTS=1 and rerun the script to execute the tests without setup issues. +# Run service REST tests with Oracle +./run-tests.sh oracle servicerest + ``` -#### Note +## Test Reports +After execution, HTML reports are automatically generated for each suite. Open the corresponding file in any browser to view detailed results: -Reports generated after tests execution in html can be viewed in any browser for detailed test results. +| Suite | Report File | +|:------------|:------------------------| +| hdfs | report_hdfs.html | +| kms | report_kms.html | +| xuserrest | report_xuserrest.html | +| rolerest | report_rolerest.html | +| servicerest | report_servicerest.html | \ No newline at end of file diff --git a/functional-tests/rolerest/__init__.py b/functional-tests/rolerest/__init__.py new file mode 100644 index 0000000000..13878a13a7 --- /dev/null +++ b/functional-tests/rolerest/__init__.py @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. \ No newline at end of file diff --git a/functional-tests/rolerest/conftest.py b/functional-tests/rolerest/conftest.py new file mode 100644 index 0000000000..834bd2a3a9 --- /dev/null +++ b/functional-tests/rolerest/conftest.py @@ -0,0 +1,356 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +import random +import string +import uuid +import pytest +import requests +import time +import os + +# Module-level constants — always available globally, even with pytest -n auto +CREDENTIALS = ("admin", "rangerR0cks!") +DEFAULT_HEADERS = { + "Accept": "application/json", + "Content-Type": "application/json" +} +KEYADMIN_CREDENTIALS = ("keyadmin", "rangerR0cks!") + +@pytest.fixture(scope="session") +def credentials(): + return CREDENTIALS + +@pytest.fixture(scope="session") +def default_headers(): + return DEFAULT_HEADERS + +@pytest.fixture(scope="session") +def keyadmin_credentials(): + return KEYADMIN_CREDENTIALS + + +def create_user_with_retry(url, **kwargs): + import time + + for attempt in range(5): + response = requests.post(url, **kwargs) + + if response.status_code == 200: + return response + + time.sleep(1 + attempt) + + raise AssertionError(f"User creation failed: {response.text}") + +@pytest.fixture(scope="function") +def ranger_session(ranger_config): + session = requests.Session() + session.auth = ranger_config["auth"] + session.headers.update(ranger_config["headers"]) + return session + + +@pytest.fixture(scope="session") +def ranger_config(credentials, default_headers): + + return { + "base_url": "http://localhost:6080/service", + "auth": credentials, + "headers": default_headers + } + +@pytest.fixture(scope="session") +def ranger_key_admin_config(keyadmin_credentials, default_headers): + + return { + "id" : 3, + "base_url": "http://localhost:6080/service", + "auth": keyadmin_credentials, + "headers": default_headers + } + + + + +@pytest.fixture(scope="class") +def temp_secure_user(ranger_config): + + created_user_ids = [] + + def _create_user(role_list = None): + + + worker = os.getenv("PYTEST_XDIST_WORKER", "gw0") + username = f"pytest_{worker}_{uuid.uuid4().hex[:8]}" + + role_map = { + "user": "ROLE_USER", + "admin": "ROLE_SYS_ADMIN", + "auditor": "ROLE_ADMIN_AUDITOR" + } + + if role_list: + unique_roles = {role_map.get(role.lower(), "ROLE_USER") for role in role_list} + else: + unique_roles = {"ROLE_USER"} + + final_role_list = list(unique_roles) + + payload = { + "name": username, + "firstName": "Fixture", + "lastName": "User", + "emailAddress": f"{username}@test.com", + "password": "Test@123", + "status": 1, + "isVisible": 1, + "userSource":1, # to ensure it's created as an external user + "userRoleList": final_role_list, + "syncSource":"EXTERNAL_PYTEST", + "otherAttributes":"{\"sync_source\":\"EXTERNAL_PYTEST\"}", + "groupIdList": [], + "groupNameList": [] + } + + response = create_user_with_retry( + f"{ranger_config['base_url']}/xusers/secure/users", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + #time.sleep(0.5) + + assert response.status_code == 200, f"User creation failed: {response.text}" + + created_user = response.json() + created_user_ids.append(created_user["id"]) + + print(f"\n[Fixture] Created user ID: {created_user['id']}, Roles: {final_role_list}") + + return created_user, created_user["id"] + + yield _create_user + + for user_id in created_user_ids: + print(f"[Fixture] Cleaning up user ID: {user_id}") + + response = requests.delete( + f"{ranger_config['base_url']}/xusers/users/{user_id}", + params={"forceDelete": "true"}, + auth=ranger_config["auth"], + headers={**ranger_config["headers"], "X-Requested-By": "ranger"} + ) + print("DELETE RESPONSE:", response.status_code, response.text) + if response.status_code in [200, 204]: + print(f"[Fixture] Deleted user ID: {user_id}") + + elif response.status_code in [400, 404]: + print(f"[Fixture] User ID {user_id} already deleted") + + else: + pytest.fail( + f"Unexpected error deleting user ID {user_id}: {response.text}" + ) + +@pytest.fixture(scope="class") +def temp_keyadmin_user(ranger_config, role = "keyadmin"): + + created_user_ids = [] + + def _create_keyadmin(): + + #username = f"pytest_keyadmin_{uuid.uuid4().hex[:8]}" + worker = os.getenv("PYTEST_XDIST_WORKER", "gw0") + username = f"pytest_keyadmin_{worker}_{uuid.uuid4().hex[:8]}" + + payload = { + "name": username, + "firstName": "Fixture", + "lastName": "KeyAdmin", + "emailAddress": f"{username}@test.com", + "password": "Test@123", + "status": 1, + "isVisible": 1, + "userRoleList": ["ROLE_USER"], + "groupIdList": [], + "groupNameList": [] + } + + response = create_user_with_retry( + f"{ranger_config['base_url']}/xusers/secure/users", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert response.status_code == 200 + + user = response.json() + user_id = user["id"] + + print(f"\n[Fixture] Created keyadmin base: {username} ({user_id})") + + # promote to keyadmin + #requests.put( + response = requests.put( + f"{ranger_config['base_url']}/xusers/secure/users/roles/{user_id}", + json={"vXStrings": [{"value": "ROLE_KEY_ADMIN"}]}, + auth=("keyadmin", "rangerR0cks!"), + headers={**ranger_config["headers"], "X-Requested-By": "ranger"} + ) + #time.sleep(0.5) + + created_user_ids.append(user_id) + + return user, user_id + + yield _create_keyadmin + + # cleanup + for user_id in created_user_ids: + print(f"[Fixture] Cleaning keyadmin {user_id}") + + requests.put( + f"{ranger_config['base_url']}/xusers/secure/users/roles/{user_id}", + json={"vXStrings": [{"value": "ROLE_USER"}]}, + auth=("keyadmin", "rangerR0cks!"), + headers={**ranger_config["headers"], "X-Requested-By": "ranger"} + ) + + requests.delete( + f"{ranger_config['base_url']}/xusers/secure/users/id/{user_id}", + params={"forceDelete": "true"}, + auth=ranger_config["auth"], + headers={**ranger_config["headers"], "X-Requested-By": "ranger"} + ) + +@pytest.fixture(scope="class") +def temp_group(ranger_config): + + created_group_ids = [] + + def _create_group(): + + group_name = f"pytest_group_{uuid.uuid4().hex[:8]}" + + payload = { + "name": group_name, + "groupSource" : 1, # to ensure it's created as an external group + "groupType" : 1, + "syncSource":"EXTERNAL_PYTEST", + "otherAttributes":"{\"sync_source\":\"EXTERNAL_PYTEST\"}", + } + + response = requests.post( + f"{ranger_config['base_url']}/xusers/groups", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert response.status_code in (200, 201), response.text + + data = response.json() + group_id = data["id"] + + created_group_ids.append(group_id) + + print(f"[Fixture] Created group {group_name} ({group_id})") + + return data, group_id + + yield _create_group + + # -------- CLEANUP ---------- + for gid in created_group_ids: + print(f"[Fixture] Cleaning group {gid}") + + requests.delete( + f"{ranger_config['base_url']}/xusers/groups/{gid}", + auth=ranger_config["auth"], + params={"forceDelete": "true"}, + headers={ + **ranger_config["headers"], + "X-Requested-By": "ranger" + } + ) + +@pytest.fixture(scope="class") +def temp_role(ranger_config): + + created_role_ids = [] + + def _create_role(user_list = [], group_list = [], role_list = []): + + role_name = f"pytest_role_{uuid.uuid4().hex[:8]}" + if user_list: + for user in user_list: + assert "name" in user, "User dict must contain 'name' key" + assert "isAdmin" in user, "User dict must contain 'isAdmin' key" + + if group_list: + for group in group_list: + assert "name" in group, "Group dict must contain 'name' key" + assert "isAdmin" in group, "Group dict must contain 'isAdmin' key" + + if role_list: + for role in role_list: + assert "name" in role, "Role dict must contain 'name' key" + assert "isAdmin" in role, "Role dict must contain 'isAdmin' key" + + payload = { + "name": role_name, + "description": "Role created by pytest fixture", + "users": user_list, + "groups": group_list, + "roles": role_list + } + + + + response = requests.post( + f"{ranger_config['base_url']}/roles/roles", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert response.status_code in (200, 201), response.text + + data = response.json() + role_id = data["id"] + + created_role_ids.append(role_id) + + print(f"[Fixture] Created role {role_name} ({role_id})") + + return data, role_id + + yield _create_role + + # # -------- CLEANUP ---------- + for rid in created_role_ids: + print(f"[Fixture] Cleaning role {rid}") + + requests.delete( + f"{ranger_config['base_url']}/roles/roles/{rid}", + auth=ranger_config["auth"], + params={"forceDelete": "true"}, + headers={ + **ranger_config["headers"], + "X-Requested-By": "ranger" + } + ) \ No newline at end of file diff --git a/functional-tests/rolerest/readme.md b/functional-tests/rolerest/readme.md new file mode 100644 index 0000000000..26183f0ae6 --- /dev/null +++ b/functional-tests/rolerest/readme.md @@ -0,0 +1,137 @@ + + +# This is the main directory for running XUSERREST API functionality tests + +This directory contains automated functional tests for the XUSERREST API, covering user management, group management, permissions, secure user operations, and UGSync functionality. + +## Structure +``` +test_xuserrest/ +├── utility/ +│ ├── __init__.py +│ ├── utils.py +├── __init__.py +├── conftest.py +├── test_groups.py +├── test_permission.py +├── test_secure_user.py +├── test_ugsync.py +├── test_user.py +├── test_utility_fun.py +``` + + +## Features and Functionalities Used: + +- **Parametrization:** For running multiple test cases handling the same functionality in a single method. + +- **fetch_logs:** Fetches errors or exceptions from logs when something goes wrong. + +- **cleanup:** Cleans up all resources used while testing, ensuring re-runs of test cases. + +--- + +## `utils.py` + +- Shared utility module for all xuser REST test files — provides reusable helpers, schema validators, and API interaction functions +- Defines global constants: BIGINT_MIN/BIGINT_MAX for boundary checks, RANGER_CONTAINER_NAME/RANGER_LOG_FILE for Docker log access +init_configs() sets global auth configs for all four roles (admin, keyadmin, auditor, user) +assert_response() validates HTTP status codes (single or list) and on failure fetches Ranger logs via fetch_ranger_logs() +fetch_ranger_logs() runs docker exec to pull categorized log sections — recent activity, errors/warns, API calls, and HTTP-code-specific patterns using keyword_map +- Schema validators: validate_user_schema(), validate_secure_user_schema(), validate_xgroup_schema(), validate_external_user_schema(), group_permission_schema() — enforce field types, lengths, and value constraints +CRUD helpers: user_exists(), delete_user(), group_exists(), delete_group(), groupuser_exists(), delete_groupuser() — used for test setup/teardown +- UGSync helpers: validate_auditinfo_schema(), validate_sync_source_info() — validate sync audit responses across unix, file, and ldap sources with source-specific field schemas +- Permission helpers: build_user_permission(), build_group_permission(), build_permission_payload(), user_permission_exists() — construct and verify module permission payloads return_value_ugsync_groupusers() computes expected count of group-user sync operations for assertion in UGSync tests + + +--- + +## `conftest.py` + +- Central pytest configuration file providing shared fixtures and helpers for the entire test suite +- Defines module-level constants: CREDENTIALS, DEFAULT_HEADERS, KEYADMIN_CREDENTIALS +- Includes create_user_with_retry() — retries user POST creation up to 5 times with incremental sleep on failure +- Provides session/function/class scoped fixtures: + 1. Session: credentials, default_headers, keyadmin_credentials, ranger_config, ranger_key_admin_config, client_roles + 2. Function: ranger_session, all_users, all_schema_following_users, get_user_by_id + 3. Class: temp_secure_user, temp_keyadmin_user, temp_permission_module, temp_group, temp_permission_group, temp_permission_user, temp_role — all auto-cleanup after test class + +--- + +## `test_groups.py` + +- Tests for Group management operations across three test classes: TestSecureGroups, TestGroups, and TestGroupUsers +- Each class uses a _setup fixture (class-scoped) that provisions primary & secondary users across all roles (admin, key admin, auditor, user) and two temporary groups with auto-cleanup +- Default response code for auth failure cases is set to 404 to prevent API endpoint discovery due to Spring silent failures + +--- + +## `test_secure_user.py` + +- Tests Secure User CRUD operations via /xusers/secure/users/* endpoints within TestSecureUserEndpoint +- Class-scoped _setup provisions primary & secondary users across all roles (admin, keyadmin, auditor, user) with auto-cleanup +- Defines BIGINT_MIN / BIGINT_MAX constants for boundary value testing +- Covers role-based access control across all operations — verifying both allowed and restricted role/target combinations +- Validates immutability of fields (name, id, createDate) on PUT and silent 204 behavior on malformed bulk DELETE payloads +Uses forceDelete=true and X-Requested-By: ranger headers where required by the API + +--- + +## `test_ugsync.py` + +- Covers userinfo endpoint for user-group association creation with schema validation +- Tests UGSync (User-Group Sync) operations within TestUgsync for syncing users/groups from external sources (LDAP, UNIX, FILE, MULTI_SOURCES) +- Class-scoped _setup provisions primary & secondary users across all roles (admin, keyadmin, auditor, user) and two temp groups with auto-cleanup +- All endpoints are admin-only; unauthorized roles (keyadmin, auditor, user) consistently return 404 due to Spring's silent failure instead of 403 +- Invalid payloads (bad group/user names, missing fields) often return 200 as silent failures — response body is validated instead of status code +- Validates addUsers/delUsers group membership changes are reflected via follow-up GET calls +- Covers sync source audit logging for all source types with schema validation via validate_auditinfo_schema() and validate_sync_source_info() +- Tests group/user visibility toggling (sets isVisible=0) and verifies persistence via follow-up GET +- Handles semi-failure scenarios (mixed valid/invalid users in one payload) and asserts partial success count in response + +--- + +## `test_user.py` + +- Tests User CRUD operations via /xusers/users/* endpoints within TestUsers +- Class-scoped _setup provisions primary & secondary users across all roles (admin, keyadmin, auditor, user) with auto-cleanup +- Covers role-based access control across all operations — verifying both allowed and restricted role/target combinations +- Tests role assignment logic via /roleassignments endpoint — validating priority order: whiteListUser > whiteListGroup > userMap > groupMap > default +- Validates Spring's silent 404 failure pattern for unauthorized POST/DELETE operations (non-admin roles) +- Tests external user creation flow — 204 on first call (new user), 200 on second call (existing user) +- Covers userinfo endpoint for user-group association creation with schema validation +Validates immutability enforcement and invalid role assignment (ROLE_KEY_ADMIN, ROLE_NON_EXISTEN) returning 400/403 + +--- + +## `test_utility_fun.py` + +- Tests Lookup and AuthSession utility endpoints within TestLookup and AuthSession classes +- Class-scoped _setup provisions users across all roles (admin, keyadmin, auditor, user) with auto-cleanup +- All four roles are permitted for every endpoint — no role-based restriction tests +- TestLookup covers /xusers/lookup/users, /xusers/lookup/groups, and /xusers/lookup/principals — validating vXStrings schema when response is non-empty +- AuthSession covers /xusers/authSessions with query param variations — validates vXAuthSessions or totalCount in response diff --git a/functional-tests/rolerest/test_role_management.py b/functional-tests/rolerest/test_role_management.py new file mode 100644 index 0000000000..9f3d20e0e0 --- /dev/null +++ b/functional-tests/rolerest/test_role_management.py @@ -0,0 +1,1271 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from urllib import * +from rolerest.utility.utils import * +from rolerest.utility.utils import assert_response +from xuserrest.utility.utils import * +import uuid +import string +import pytest +import requests +from datetime import datetime +import random + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.rolerest +class TestRoleCRUD: + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_group, + temp_role, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + # ---------- secondary users ---------- + cls.admin1, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin1, _ = temp_keyadmin_user() + cls.audit1, _ = temp_secure_user(["auditor"]) + cls.user1, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config1 = (cls.admin1["name"], "Test@123") + cls.ranger_key_admin_config1 = (cls.ranger_key_admin1["name"], "Test@123") + cls.ranger_auditor_config1 = (cls.audit1["name"], "Test@123") + cls.ranger_user_config1 = (cls.user1["name"], "Test@123") + + cls.ranger_admin_id1 = cls.admin1["id"] + cls.ranger_key_admin_id1 = cls.ranger_key_admin1["id"] + cls.ranger_auditor_id1 = cls.audit1["id"] + cls.ranger_user_id1 = cls.user1["id"] + + + # ---------- group details ---------- + cls.group, cls.group_id = temp_group() + cls.group1, cls.group_id1 = temp_group() + + # ---------- role details ---------- + cls.role, cls.role_id = temp_role() + cls.role1, cls.role_id1 = temp_role() + + # ---------- common ---------- + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config")], + ) + @pytest.mark.parametrize( + "params, test_case", + [ + ({}, "default_request"), + ({"startIndex": 0, "pageSize": 10}, "pagination_basic"), + ({"roleName": "admin"}, "filter_role_name"), + ({"roleId": 1}, "filter_role_id"), + ({"sortBy": "id", "sortType": "asc"}, "sorting_asc"), + ({"sortBy": "id", "sortType": "desc"}, "sorting_desc"), + ({"sortBy": "name", "sortType": "asc"}, "sorting_name_asc"), + ({"startIndex": 0, "pageSize": 10, "sortBy": "id", "sortType": "asc"}, "pagination_with_sort"), + ({"getCount": "false"}, "get_count_false"), + ], + ids=[ + "default", + "pagination-basic", + "filter-role-name", + "filter-role-id", + "sorting-asc", + "sorting-desc", + "sorting-name-asc", + "pagination-with-sort", + "getcount-false", + ], + ) + def test_get_all_roles(self, params, test_case, role, auth): + if role != "admin": + pytest.fail("Admin privileges required to get all roles") + + auth = getattr(self, auth) + response = requests.get( + f"{self.base_url}/roles/roles", + headers=self.headers, + auth=auth, + params=params + ) + assert_response(response, 200, f"Failed to get all roles and got response code {response.status_code}") + + if "pageSize" in params: + assert len(response.json().get("roles", [])) <= params["pageSize"] + + if "roleName" in params: + roles = response.json().get("roles", []) + for r in roles: + assert params["name"].lower() in r["name"].lower(), \ + f"Role {r['name']} doesn't match filter {params['roleName']}" + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("keyadmin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")], + ) + def test_get_role_by_id(self, role, auth): + + auth = getattr(self, auth) + + role_id = self.role_id + + response = requests.get( + f"{self.base_url}/roles/roles/{role_id}", + headers=self.headers, + auth=auth + ) + assert_response(response, 200, f"Failed to get role by ID and got response code {response.status_code}") + data = response.json() + assert data["id"] == role_id, f"Expected role ID {role_id} but got {data['id']}" + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "login_test_case", [ + ("login_user is admin"), + ("login_user is service admin"), + ("login_user is in service admin groups in grouplist"), + #("login_user is service user"), + ] + ) + @pytest.mark.parametrize( + "query_param_user, effective_user, additional_conditions", + [ + ("not exists", "login_user", "login user should not be service user"), + ("exists", "query param userName", "query user is admin"), + ("exists", "query param userName", "query user is service admin"), + ("exists", "query param userName", "query user is in service admin groups in grouplist"), + ]) + def test_get_all_roles_by_name(self, login_test_case, additional_conditions, query_param_user, effective_user, request): + params = {} + if login_test_case == "login_user is admin": + auth = self.ranger_admin_config + del_list = [] + elif login_test_case == "login_user is service admin": + temp_service, temp_service_id = create_service() + temp_s_admin, temp_s_admin_id = request.getfixturevalue("temp_secure_user")("auditor") + assign_service_admin(temp_service_id, temp_service, temp_s_admin['name']) + auth = (temp_s_admin["name"], "Test@123") + params["serviceName"] = temp_service["name"] + del_list = [temp_service_id] + else: + temp_service, temp_service_id = create_service() + temp_s_admin, temp_s_admin_id = request.getfixturevalue("temp_secure_user")("auditor") + assign_service_admin(temp_service_id, temp_service, temp_s_admin['name']) + group, group_id = request.getfixturevalue("temp_group")() + assign_groups_to_user(temp_s_admin["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + assign_service_admin_group(temp_service_id, temp_service, group["name"]) + auth = (temp_s_admin["name"], "Test@123") + params["serviceName"] = temp_service["name"] + del_list = [temp_service_id] + + if query_param_user == "not exists" and login_test_case != "login_user is service user": + pass + else: + if "admin" in additional_conditions: + params["execUser"] = self.admin1["name"] + elif "service admin" in additional_conditions: + if login_test_case == "login_user is service admin": + temp_service, temp_service_id = create_service() + del_list.append(temp_service_id) + s_admin, s_admin_id = request.getfixturevalue("temp_secure_user")("auditor") + assign_service_admin(temp_service_id, temp_service, s_admin['name']) + params["execUser"] = s_admin["name"] + elif "service admin groups" in additional_conditions: + if login_test_case == "login_user is in service admin": + temp_service, temp_service_id = create_service() + del_list.append(temp_service_id) + s_admin, s_admin_id = request.getfixturevalue("temp_secure_user")("auditor") + assign_service_admin(temp_service_id, temp_service, s_admin['name']) + group, group_id = request.getfixturevalue("temp_group")() + assign_groups_to_user(s_admin["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + assign_service_admin_group(temp_service_id, temp_service, group["name"]) + params["execUser"] = s_admin["name"] + + response = requests.get( + f"{self.base_url}/roles/roles/names", + auth=auth, + headers=self.headers, + params=params + ) + + assert_response(response, 200, f"Failed to get role names with different login creds and got response code {response.status_code} with \n response text: {response.text}") + data = response.json() + assert isinstance(data, list), f"Expected response to be a list of role names but got {type(data)} for test case {login_test_case} with additional conditions: {additional_conditions}" + # Cleanup + for item in del_list: + delete_service(item) + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "test_case", [ + ("login_user is not (admin not service admin/group) & role-membership via user"), + ("login_user is not (admin not service admin/group) & role-membership via group"), + ("login_user is not (admin not service admin/group) & role-membership via role-user"), + ("login_user is not (admin not service admin/group) & role-membership via role-group"), + ("login_user is admin"), + ("login_user is service admin"), + ("login_user is in service admin groups in grouplist"), + ] + ) + def test_get_role_by_name_same_creds(self, test_case, request): # here always the login_user = queryparam user + + condition = test_case.removeprefix("login_user is ") + auth, params, role, clean_up_items = ensureRoleAccess(condition, request) + + response = requests.get( + f"{self.base_url}/roles/roles/name/{role['name']}", + auth=auth, + headers=self.headers, + params=params + ) + + assert_response(response, 200, f"Failed to get role by name with same creds and got response code {response.status_code} with \n response text: {response.text}") + + data = response.json() + assert data["id"] == role["id"], f"Expected role ID {role['id']} but got {data['id']} for test case {test_case}" + + # Cleanup + for item in clean_up_items["role_list"]: + delete_role(item) + for item in clean_up_items["service_list"]: + delete_service(item) + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "test_case", [ + "login user is admin", + "login user is service admin", + "login user has service admin groups", + ] + ) + @pytest.mark.parametrize( + "query_param_user, effective_user, additional_conditions", + [ + ("exists", "query param userName", "query user is admin"), + ("exists", "query param userName", "query user is service admin"), + ("exists", "query param userName", "query user is in service admin groups in grouplist"), + ("exists", "query param userName", "query user is not admin or service admin/group but has role membership via user"), + ("exists", "query param userName", "query user is not admin or service admin/group but has role membership via group"), + ("exists", "query param userName", "query user is not admin or service admin/group but has role membership via role-user"), + ("exists", "query param userName", "query user is not admin or service admin/group but has role membership via role-group"), + ("not_exists", "login userName", "None"), + ], + ) + def test_get_role_by_name_with_diff_creds(self, query_param_user, effective_user, test_case, additional_conditions, request): + param = {} + service = None + service_id = None + + + if test_case == "login user is admin": + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["admin"]) + auth = (temp_user["name"], "Test@123") + + elif test_case == "login user is service admin": + service, service_id = create_service() + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + resp = requests.get( + f"{self.base_url}/xusers/users/userName/{temp_user['name']}", + headers=self.headers, + auth=self.ranger_admin_config + ) + print(f"\n before User details for {temp_user['name']} after group assignment and service admin group assignment:\n {resp.json()} \n") + + assign_service_admin(service_id, service, temp_user['name']) + auth = (temp_user["name"], "Test@123") + + + elif test_case == "login user has service admin groups": + + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + group, group_id = request.getfixturevalue("temp_group")() + assign_groups_to_user(temp_user["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + service, service_id = create_service() + assign_service_admin_group(service_id, service, group["name"]) + + + + auth = (temp_user["name"], "Test@123") + + + if query_param_user == "not_exists": + role, role_id = request.getfixturevalue("temp_role")() + params = {} + if service: + params["serviceName"] = service["name"] + clean_up_items = {"role_list": [role_id], "service_list": [service_id] if service_id else []} + else: + condition = additional_conditions.removeprefix("query user is ") + auth_user, params, role, clean_up_items = ensureRoleAccess( + condition, request, + existing_service=[service] if service and "service admin" in test_case else None + ) + + if service: + params["serviceName"] = service["name"] + + + print("\n Auth used: ", auth) + print("\n Query params used: ", params) + response = requests.get( + f"{self.base_url}/roles/roles/name/{role['name']}", + auth=auth, # auth is admin but execUser in query param has service admin group permissions + headers=self.headers, + params=params + ) + + assert_response(response, 200, f"Failed to get role by name with different creds and got response code {response.status_code} with \n response text: {response.text}") + + + data = response.json() + assert data["id"] == role["id"], f"Expected role ID {role['id']} but got {data['id']} for test case {test_case}" + print(f"Response data: {data} for test case {test_case}") + # Cleanup + for item in clean_up_items["role_list"]: + delete_role(item) + for item in clean_up_items["service_list"]: + delete_service(item) + # Cleanup login user's service if not already cleaned up + if service_id and service_id not in clean_up_items["service_list"]: + delete_service(service_id) + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "u_role", ["admin", "user", "key_admin", "auditor"],) + @pytest.mark.parametrize( + "roles, auth", + [("admin", "ranger_admin_config"),("user", "ranger_user_config"), ("key_admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config")],) + @pytest.mark.parametrize( + "test_case", [ + "user directly assigned to roles", + "user assigned to roles via group membership", + ]) + def test_get_roles_for_user_by_userName(self, test_case, roles, auth, u_role, request): + + + if u_role == "key_admin": + test_user, test_user_id = request.getfixturevalue("temp_keyadmin_user")() + auth = getattr(self, auth) + + else: + test_user, test_user_id = request.getfixturevalue("temp_secure_user")([u_role]) + auth = getattr(self, auth) + if test_case == "user directly assigned to roles": + role, role_id = request.getfixturevalue("temp_role")(user_list=[{"name": test_user["name"], "isAdmin": True}]) + elif test_case == "user assigned to roles via group membership": + group, group_id = request.getfixturevalue("temp_group")() + assign_groups_to_user(test_user["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + role, role_id = request.getfixturevalue("temp_role")(group_list=[{"name": group["name"], "isAdmin": True}]) + response = requests.get( + f"{self.base_url}/roles/roles/user/{test_user['name']}", + auth=auth, + headers=self.headers + ) + assert_response(response, 200, f"Failed to get roles for user by userName and got response code {response.status_code} with \n response text: {response.text}") + data = response.json() + assert isinstance(data, list), f"Expected response to be a list of roles but got {type(data)} for test case {test_case}" + assert role['name'] in data, f"Expected role {role['name']} to be in the response but got {data} for test case {test_case}" + + @pytest.mark.get + @pytest.mark.positive + def test_get_roles_for_user_by_userName_special_case(self, request): + temp_user, temp_user_id = self.user, self.ranger_user_id + group, group_id = self.group, self.group_id + + assign_groups_to_user(temp_user["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + + role, role_id = request.getfixturevalue("temp_role")(group_list=[{"name": group["name"], "isAdmin": True}]) + + auth = (temp_user["name"], "Test@123") + response = requests.get( + f"{self.base_url}/roles/roles/user/{temp_user['name']}", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, f"Failed to get roles for user by userName in special case and got response code {response.status_code} with \n response text: {response.text}") + + data = response.json() + assert isinstance(data, list), f"Expected response to be a list of roles but got {type(data)} for special test case" + assert role['name'] in data, f"Expected role {role['name']} to be in the response but got {data} for special test case" + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "test_case, auth", [ + ("login user is admin", "ranger_admin_config"), + ("login user is auditor", "ranger_auditor_config"), + ("login user is key admin", "ranger_key_admin_config"), + ("login user is user - checking direct role membership only", "ranger_user_config"), + ("login user is user - checking direct group role membership only", "ranger_user_config"), + ("login user is user - with another login_role in userRoleList", "create"), + ]) + def test_get_lookup_roles(self, test_case, auth, request): + if "is user" in test_case: + if test_case.endswith("checking direct role membership only"): + test_user, test_user_id = request.getfixturevalue("temp_secure_user")(["user"]) + assert len(test_user["userRoleList"]) == 1 , "User should have only one role for this test case" + elif test_case.endswith("checking direct group role membership only"): + test_user, test_user_id = request.getfixturevalue("temp_secure_user")(["user"]) + assert len(test_user["userRoleList"]) == 1 , "User should have only one role for this test case" + else: + test_user, test_user_id = request.getfixturevalue("temp_secure_user")(["user", "admin"]) + assert len(test_user["userRoleList"]) >1 , "User should have multiple roles for this test case" + role, role_id = request.getfixturevalue("temp_role")(user_list=[{"name": test_user["name"], "isAdmin": False}]) + auth = (test_user["name"], "Test@123") + else: + auth = getattr(self, auth) + role, role_id = request.getfixturevalue("temp_role")() + + role1, role_id1 = request.getfixturevalue("temp_role")() # independent role + + response = requests.get( + f"{self.base_url}/roles/lookup/roles", + auth=auth, + headers=self.headers + ) + + # validation + assert_response(response, 200, f"Failed to get lookup roles and got response code {response.status_code} with \n response text: {response.text}") + data = response.json() + assert isinstance(data, dict), f"Expected response to be a dictionary of roles but got {type(data)} for test case {test_case}" + + role_names = [r['name'] for r in data.get('roles', [])] + if "checking direct" in test_case: + assert role1['name'] not in role_names, f"Expected independent role {role1['name']} to not be in the response but got {role_names} for test case {test_case}" + assert role['name'] in role_names, f"Expected role {role['name']} to be in the response but got {role_names} for test case {test_case}" + else: + assert role["name"] in role_names and role1["name"] in role_names, f"Expected roles {role['name']} and {role1['name']} to be in the response but got {role_names} for test case {test_case}" + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "auth", ["ranger_user_config", "ranger_auditor_config", "ranger_key_admin_config", "ranger_admin_config"]) + def test_get_lookup_rolenames(self, auth, request): + role, role_id = request.getfixturevalue("temp_role")() + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/roles/lookup/roles/names", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, f"Failed to get lookup role names and got response code {response.status_code} with \n response text: {response.text}") + data = response.json() + assert isinstance(data, dict), f"Expected response to be a dictionary but got {type(data)} for test case with auth {auth}" + role_names = [r['value'] for r in data["vXStrings"]] + assert role["name"] in role_names, f"Expected role {role['name']} to be in the response but got {role_names} for test case with auth {auth}" + + @pytest.mark.post + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth, test_case", + [("admin", "ranger_admin_config", "minimal_request"), + ("admin", "ranger_admin_config", "with_users_groups"), + ("admin", "ranger_admin_config", "with_create_non_exist_user_group"), + ("service_admin","","with_service_name") + ], + ) + def test_create_role(self, role, auth, test_case, request): + if role == "service_admin": + service, service_id = create_service() + s_admin, s_admin_id = request.getfixturevalue("temp_secure_user")("auditor") + assign_service_admin(service_id, service, s_admin['name']) + auth = (s_admin['name'], "Test@123") + elif role == "admin": + auth = getattr(self, auth) + else: + pytest.fail("Invalid role for this test") + + + param = {} + + if test_case == "minimal_request": + payload = { + "name": f"test-role-{uuid.uuid4()}" + } + elif test_case == "with_users_groups": + payload = { + "name": f"test-role-{uuid.uuid4()}", + "users": [{"name": self.admin["name"], "isAdmin": True}], + "groups": [{"name": self.group["name"], "isAdmin": False}] + } + elif test_case == "with_create_non_exist_user_group": + param = {"createNonExistUserGroup": "true"} + payload = { + "name": f"test-role-{uuid.uuid4()}", + "users": [{"name": f"newuser_{uuid.uuid4().hex[:6]}", "isAdmin": False}], + "groups": [{"name": f"newgroup_{uuid.uuid4().hex[:6]}", "isAdmin": False}] + } + + elif test_case == "with_service_name" : + param = {"serviceName": service["name"]} + payload = { + "name": f"test-role-{uuid.uuid4()}" + } + response = requests.post( + f"{self.base_url}/roles/roles", + headers=self.headers, + auth=auth, + json=payload, + params=param + ) + + assert_response(response, 200, f"Expected 200 for {test_case} but got {response.status_code}") + + response_data = response.json() + role_id = response_data["id"] + print(f"[+] Role created successfully with id={role_id} for test case {test_case}") + print(f" /n /n Response data: {response_data}. /n /n") + delete_role(role_id) + + if test_case == "with_service_name": + # Cleanup the created service as well + delete_service(service_id) + + if test_case == "with_create_non_exist_user_group": + # Cleanup the created user and group + new_user = payload["users"][0]["name"] + new_group = payload["groups"][0]["name"] + + + requests.delete( + f"{self.base_url}/xusers/users/userName/{new_user}", + params={"forceDelete": "true"}, + auth=self.ranger_admin_config, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert_response(response, [200, 204], f"Expected 200 for user deletion but got {response.status_code}") + + + requests.delete( + f"{self.base_url}/xusers/groups/groupName/{new_group}", + params={"forceDelete": "true"}, + auth=self.ranger_admin_config, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert_response(response, [200, 204], f"Expected 200 for group deletion but got {response.status_code}") + + @pytest.mark.put + @pytest.mark.positive + @pytest.mark.parametrize( + "role", ["admin", "auditor", "keyadmin", "user", "any admin"], + ) + @pytest.mark.parametrize( + "test_case", + ["update via user membership", "update via group membership", "update via role membership - user", "update via role membership - group"] + ) + def test_update_role(self, request, role, test_case): + + if role == "any admin": + user, user_id = request.getfixturevalue("temp_secure_user")("admin") + auth= self.ranger_admin_config + else: + user, user_id = request.getfixturevalue("temp_secure_user")(role) + auth=(user["name"], "Test@123") + + if test_case == "update via user membership": + role, r_id = request.getfixturevalue("temp_role")(user_list=[{"name": user["name"], "isAdmin": True}]) + + elif test_case == "update via group membership": + group, group_id = request.getfixturevalue("temp_group")() + group_list=[{"name": group["name"], "isAdmin": True}] + assert group_list[0]["isAdmin"] == True, "Group should be admin in this test case" + role, r_id = request.getfixturevalue("temp_role")(group_list=group_list) + assign_groups_to_user(user["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + + elif test_case == "update via role membership - user": + c_role, c_id = request.getfixturevalue("temp_role")(user_list=[{"name": user["name"], "isAdmin": True}]) + role_list = [{"name": c_role["name"], "isAdmin": True}] + assert role_list[0]["isAdmin"] == True, "Role should be admin in this test case" + role, r_id = request.getfixturevalue("temp_role")(role_list=role_list) + + elif test_case == "update via role membership - group": + group, group_id = request.getfixturevalue("temp_group")() + c_role, c_id = request.getfixturevalue("temp_role")(group_list=[{"name": group["name"], "isAdmin": True}]) + role_list = [{"name": c_role["name"], "isAdmin": True}] + assert role_list[0]["isAdmin"] == True, "Role should be admin in this test case" + role, r_id = request.getfixturevalue("temp_role")(role_list=role_list) + assign_groups_to_user(user["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + + + payload = { + "id": r_id, + "name": role["name"], + "description": f"Updated description for {test_case}", + "users": [{"name": user["name"], "isAdmin": "false"}], + } + + response = requests.put( + f"{self.base_url}/roles/roles/{r_id}", + headers=self.headers, + auth=auth, + json=payload + ) + assert_response(response, 200, f"Expected 200 for {test_case} but got {response.status_code}") + + data = response.json() + assert data["description"] == payload["description"], f"Description not updated for {test_case}" + # Cleanup should be done after the role dependency is removed + delete_role(r_id) + if test_case.startswith("update via role membership"): + delete_role(c_id) + + if "group" in test_case: + delete_group(group_id, self.ranger_admin_config, self.base_url, self.headers) + + delete_user(user_id, self.ranger_admin_config, self.base_url, self.headers) + + + @pytest.mark.delete + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"),]) + def test_delete_role(self, role, auth, request): + + if role != "admin": + pytest.fail("Deletion of role requires admin priviliges") + + auth = getattr(self, auth) + + role, r_id = request.getfixturevalue("temp_role")() + + response = requests.delete( + f'{self.base_url}/roles/roles/{r_id}', + auth = auth, + headers= self.headers + ) + + assert_response(response, 204, f'Expected the test_case to be returing 204, but got {response.status_code}') + + @pytest.mark.delete + @pytest.mark.positive + @pytest.mark.parametrize( + "test_case", [ + "login user is admin", + "login user is service admin", + "login user has service admin groups", + ] + ) + @pytest.mark.parametrize( + "query_param_user, effective_user, additional_conditions", + [ + ("exists", "query param userName", "query user is admin"), + ("exists", "query param userName", "query user is service admin"), + ("exists", "query param userName", "query user is in service admin groups in grouplist"), + ("not_exists", "login userName", "None"), + ], + ) + def test_delete_role_by_name(self, test_case, query_param_user, effective_user, additional_conditions, request): + params = {} + if test_case == "login user is admin": + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["admin"]) + auth = (temp_user["name"], "Test@123") + elif test_case == "login user is service admin": + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + service, service_id = create_service() + assign_service_admin(service_id, service, temp_user['name']) + auth = (temp_user["name"], "Test@123") + params["serviceName"] = service["name"] + elif test_case == "login user has service admin groups": + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + group, group_id = request.getfixturevalue("temp_group")() + assign_groups_to_user(temp_user["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + service, service_id = create_service() + assign_service_admin_group(service_id, service, group["name"]) + auth = (temp_user["name"], "Test@123") + params["serviceName"] = service["name"] + + if query_param_user == "not_exists": + pass + else: + if "admin" in additional_conditions: + params["execUser"] = self.admin1["name"] + elif "service admin" in additional_conditions: + s_admin, s_admin_id = request.getfixturevalue("temp_secure_user")("auditor") + # if service admin exists in params use same else create + if "serviceName" not in params: + service, service_id = create_service() + params["serviceName"] = service["name"] + assign_service_admin(service_id, service, s_admin['name']) + params["execUser"] = s_admin["name"] + elif "service admin groups" in additional_conditions: + s_admin, s_admin_id = request.getfixturevalue("temp_secure_user")("auditor") + if "serviceName" not in params: + service, service_id = create_service() + params["serviceName"] = service["name"] + group, group_id = request.getfixturevalue("temp_group")() + assign_groups_to_user(s_admin["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + assign_service_admin_group(service_id, service, group["name"]) + params["execUser"] = s_admin["name"] + + role, r_id = request.getfixturevalue("temp_role")() + response = requests.delete( + f'{self.base_url}/roles/roles/name/{role["name"]}', + auth = auth, + headers= self.headers, + params=params + ) + assert_response(response, 204, f'Expected the test_case to be returing 204, but got {response.status_code} with \n response text: {response.text}') + + if params.get("serviceName"): + delete_service(service_id) + + # NEGATIVE TESTS + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [("user", "ranger_user_config"), ("auditor", "ranger_auditor_config"), ("keyadmin", "ranger_key_admin_config")], + ) + def test_get_all_roles_negative(self, role, auth): + auth = getattr(self, auth) + response = requests.get( + f"{self.base_url}/roles/roles", + headers=self.headers, + auth=auth + ) + assert response.status_code == 400, f"Expected 400 for {role} but got {response.status_code}" + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case", ["invalid_role_id"]) + def test_get_role_by_id_negative(self, test_case): + invalid_role_id = 9999999 # Assuming this role ID doesn't exist + response = requests.get( + f"{self.base_url}/roles/roles/{invalid_role_id}", + headers=self.headers, + auth=self.ranger_admin_config + ) + assert response.status_code == 400, f"Expected 400 for {test_case} but got {response.status_code}" + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case", [ + "login user is not (admin not service admin/group) - user creds", + "login user is not (admin not service admin/group) - auditor creds", + "login user is not (admin not service admin/group) - key admin creds", + "login user passes but query param user is not admin or service admin/group - user creds", + "login user passes but query param user is not admin or service admin/group - auditor creds", + "login user passes but query param user is not admin or service admin/group - key admin creds", + ]) + def test_get_all_roles_by_name_negative(self, test_case, request): + params = {} + if "login user is not (admin not service admin/group)" in test_case: + if "user creds" in test_case: + auth = self.ranger_user_config + elif "auditor creds" in test_case: + auth = self.ranger_auditor_config + elif "key admin creds" in test_case: + auth = self.ranger_key_admin_config + else: + auth = self.ranger_admin_config + if "user creds" in test_case: + params["execUser"] = self.ranger_user_config[0] + elif "auditor creds" in test_case: + params["execUser"] = self.ranger_auditor_config[0] + elif "key admin creds" in test_case: + params["execUser"] = self.ranger_key_admin_config[0] + response = requests.get( + f"{self.base_url}/roles/roles/names", + headers=self.headers, + auth=auth, + params=params + ) + assert_response(response, 400, f"Expected 400 for {test_case} but got {response.status_code} with \n response text: {response.text}") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case", [ + ("unauthorized by non service admin/group - user access"), + ("unauthorized by non service admin/group - auditor access"), + ("unauthorized by non service admin/group - key admin access"), + ("invalid payload - wrong service name in params"), + ("invalid payload - wrong param user(non exist user)"), + ] + ) + def test_get_role_by_name_same_creds_negative(self, test_case, request): + + auth = self.ranger_admin_config # Default to admin auth, will be overridden for specific negative cases + if test_case.startswith("unauthorized by non service admin/group"): + + if "user" in test_case: + auth = self.ranger_user_config + elif "auditor" in test_case: + auth = self.ranger_auditor_config + elif "key admin" in test_case: + auth = self.ranger_key_admin_config + + name = self.role["name"] + params = {"execUser": self.admin["name"]} # Default param for all test cases + if test_case == "invalid payload - wrong service name in params": + params= {"serviceName": "invalid_service_name", "execUser": self.admin["name"]} + + response = requests.get( + f"{self.base_url}/roles/roles/name/{name}", + auth=auth, + headers=self.headers, + params=params + ) + if "invalid payload" in test_case: + assert_response(response, 200, "dd") + data = response.json() + print(f"Response data for invalid payload case: {data}") + return + + + assert_response(response, 400, f"Expected 400 for {test_case} but got {response.status_code} with \n response text: {response.text}") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "login_test_case", [ + "login user is neither admin nor service admin/group - user creds", + "login user is neither admin nor service admin/group - auditor creds", + "login user is neither admin nor service admin/group - key admin creds", + ] + ) + def test_get_role_by_name_with_diff_creds_negative_login(self, login_test_case, request): + auth = self.ranger_admin_config # Default to admin auth, will be overridden for specific negative cases + if "user" in login_test_case: + auth = self.ranger_user_config + elif "auditor" in login_test_case: + auth = self.ranger_auditor_config + elif "key admin" in login_test_case: + auth = self.ranger_key_admin_config + + role, role_id = request.getfixturevalue("temp_role")() + params = {"execUser": self.admin["name"]} # Using admin as execUser in query param + + response = requests.get( + f"{self.base_url}/roles/roles/name/{role['name']}", + auth=auth, + headers=self.headers, + params=params + ) + + assert_response(response, 400, f"Expected 400 for {login_test_case} but got {response.status_code} with \n response text: {response.text}") + + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "query_user, test_case", [ + #("non exists", "login user exists and it becomes effective user but is service user"), + ("not exists", "login user passes but query param user is non exist user"), + ("exists", "login user is admin but query param user is non admin or service admin and has no role membership - user creds"), + ("exists", "login user is admin but query param user is non admin or service admin and has no role membership - auditor creds"), + ("exists", "login user is admin but query param user is non admin or service admin and has no role membership - key admin creds"), + ] + ) + def test_get_role_by_name_with_diff_creds_negative_query_user(self, query_user, test_case, request): + auth = self.ranger_admin_config # Using admin for authentication in these negative test cases + params = {} + role, role_id = request.getfixturevalue("temp_role")() + + if query_user == "not exists": + params["execUser"] = f"nonexistuser_{uuid.uuid4().hex[:6]}" + else: + if "user creds" in test_case: + params["execUser"] = self.ranger_user_config + elif "auditor creds" in test_case: + params["execUser"] = self.ranger_auditor_config + elif "key admin creds" in test_case: + params["execUser"] = self.ranger_key_admin_config + + response = requests.get( + f"{self.base_url}/roles/roles/name/{role['name']}", + auth=auth, + headers=self.headers, + params=params + ) + + assert_response(response, 400, f"Expected 400 for {test_case} but got {response.status_code} with \n response text: {response.text}") + + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case", [ + "invalid user_name", + ]) + def test_get_roles_for_user_by_userName_negative(self, test_case): + auth = self.ranger_admin_config + invalid_user_name = f"nonexistuser_{uuid.uuid4().hex[:6]}" + response = requests.get( + f"{self.base_url}/roles/roles/user/{invalid_user_name}", + auth=auth, + headers=self.headers + ) + assert_response(response, 400, f"Expected 400 for {test_case} but got {response.status_code} with \n response text: {response.text}") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, response_code", [ + ("invalid auth creds", 401), + ("single user with no membership", 200), # silent failure + ]) + def test_get_lookup_roles_negative(self, test_case, response_code): + if test_case == "single user with no membership": + temp_user, temp_user_id = self.user1, self.ranger_user_id1 + auth = (temp_user["name"], "Test@123") + elif test_case == "invalid auth creds": + auth = ("invalid_user", "invalid_pass") + response = requests.get( + f"{self.base_url}/roles/lookup/roles", + auth=auth, + headers=self.headers + ) + assert_response(response, response_code, f"Expected {response_code} for {test_case} but got {response.status_code} with \n response text: {response.text}") + if test_case == "single user with no membership": + data = response.json() + assert isinstance(data, dict), f"Expected response to be a dictionary of roles but got {type(data)} for test case {test_case}" + assert data.get("roles") == [], f"Expected empty roles list for user with no membership but got {data.get('roles')} for test case {test_case}" + + + @pytest.mark.post + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth_name, expected_status", + [ + ("duplicate_role_name", "ranger_admin_config", 400), + ("invalid_member_owner", "ranger_admin_config", 400), + ("null_role_name", "ranger_admin_config", 400), + ("blank_role_name", "ranger_admin_config", 400), + ("no_create_non_exist_user_with_new_user", "ranger_admin_config", 400), + ("empty_body", "ranger_admin_config", 400), + ("unauthorized_user", "ranger_user_config", 400), + ("unauthorized_auditor", "ranger_auditor_config", 400), + ("unauthorized_key_admin", "ranger_key_admin_config", 400), + ("wrong_service_name", "ranger_key_admin_config", 400), + + ], + ) + def test_create_role_negative(self, test_case, auth_name, expected_status, request): + param = {} + payload = {} + auth = getattr(self, auth_name) + + if test_case == "duplicate_role_name": + temp_role_name = f"test-role-{uuid.uuid4()}" + requests.post( + f"{self.base_url}/roles/roles", + headers=self.headers, + auth=auth, + json={"name": temp_role_name} + ) + payload = {"name": temp_role_name} + role_to_delete = temp_role_name + + elif test_case == "invalid_member_owner": + payload = { + "name": f"test-role-{uuid.uuid4()}", + "users": [{"name": "{OWNER}", "isAdmin": True}] + } + + elif test_case == "null_role_name": + payload = {"name": None} + + elif test_case == "blank_role_name": + payload = {"name": " "} + + elif test_case == "no_create_non_exist_user_with_new_user": + param = {"createNonExistUserGroup": "false"} + payload = { + "name": f"test-role-{uuid.uuid4()}", + "users": [{"name": f"newuser_{uuid.uuid4().hex[:6]}", "isAdmin": False}] + } + + elif test_case == "empty_body": + payload = {} + + elif test_case.startswith("unauthorized_user"): + payload = {"name": f"test-role-{uuid.uuid4()}"} + + elif test_case == "wrong_service_name": + service, service_id = create_service() + other_service, _ = create_service() + s_admin, _ = request.getfixturevalue("temp_secure_user")("auditor") + assign_service_admin(service_id, service, s_admin['name']) + auth = (s_admin['name'], "Test@123") + param = {"serviceName": other_service["name"]} + payload = {"name": f"test-role-{uuid.uuid4()}"} + + response = requests.post( + f"{self.base_url}/roles/roles", + headers=self.headers, + auth=auth, + json=payload, + params=param + ) + + assert_response(response, expected_status, f"Expected {expected_status} for {test_case} but got {response.status_code}") + + if test_case == "wrong_service_name": + delete_service(service_id) + delete_service(other_service["id"]) + if test_case == "duplicate_role_name": + # Cleanup the created role + roles_resp = requests.get( + f"{self.base_url}/roles/roles", + headers=self.headers, + auth=auth, + params={"roleName": role_to_delete} + ) + if roles_resp.status_code == 200 and roles_resp.json().get("roles"): + role_id = roles_resp.json()["roles"][0]["id"] + delete_role(role_id) + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, expected_status", [ + ("update_non_exist_role", 400), + ("update_via_id_mismatch", 400), + ("update_via_non_member_global_user", 400), + ("update_via_non_member_global_auditor", 400), + ("update_via_non_member_global_keyadmin", 400), + ("update via invalid payload with out required membership fields", 400), + ("invalid update of role name when role is linked with other role", 400) + ] + ) + def test_update_role_negative(self, test_case, request, expected_status): + # For all negative test cases we will use admin user to avoid 403 and focus on the specific negative scenario + auth = self.ranger_admin_config + + if test_case == "update_non_exist_role": + r_id = 999999 # Assuming this role ID doesn't exist + payload = { + "id": r_id, + "name": f"test-role-{uuid.uuid4()}", + "description": "Trying to update non-existent role" + } + + elif test_case == "update_via_id_mismatch": + role, r_id = request.getfixturevalue("temp_role")() + payload = { + "id": r_id + 1, # Mismatching ID + "name": role["name"], + "description": "Trying to update with ID mismatch" + } + + elif test_case == "update_via_non_member_global_user": + role, r_id = request.getfixturevalue("temp_role")() + payload = { + "id": r_id, + "name": role["name"], + "description": "Trying to update via non-member global user" + } + auth = self.ranger_user_config # Non-member global user + + elif test_case == "update_via_non_member_global_auditor": + role, r_id = request.getfixturevalue("temp_role")() + payload = { + "id": r_id, + "name": role["name"], + "description": "Trying to update via non-member global auditor" + } + auth = self.ranger_auditor_config # Non-member global auditor + + elif test_case == "update_via_non_member_global_keyadmin": + role, r_id = request.getfixturevalue("temp_role")() + payload = { + "id": r_id, + "name": role["name"], + "description": "Trying to update via non-member global keyadmin" + } + auth = self.ranger_key_admin_config # Non-member global keyadmin + + elif test_case == "update via invalid payload with out required membership fields": + role, r_id = request.getfixturevalue("temp_role")() + payload = { + "id": r_id, + # "name is required for update, so we will not provide it to create invalid payload" + "description": "Trying to update with invalid payload" + } + + elif test_case == "invalid update of role name when role is linked with other role": + # Create a parent role + parent_role, parent_role_id = request.getfixturevalue("temp_role")() + # Create a child role linked to parent role + child_role, child_role_id = request.getfixturevalue("temp_role")(role_list=[{"name": parent_role["name"], "isAdmin": True}]) + payload = { + "id": parent_role_id, + "name": child_role["name"], # Trying to update parent role name to child role name which should fail due to name conflict + "description": "Trying to update role name to an existing linked role name" + } + r_id = parent_role_id + + response = requests.put( + f"{self.base_url}/roles/roles/{r_id}", + headers=self.headers, + auth=auth, + json=payload + ) + assert_response(response, expected_status, f"Expected {expected_status} for {test_case}") + + if test_case == "invalid update of role name when role is linked with other role": + # Cleanup the created roles + delete_role(child_role_id) + delete_role(parent_role_id) + elif test_case != "update_non_exist_role": + delete_role(r_id) + + @pytest.mark.delete + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth_name, expected_status", + [ + ("delete_non_exist_role", "ranger_admin_config", 400), + ("unauthorized_user", "ranger_user_config", 400), + ("unauthorized_auditor", "ranger_auditor_config", 400), + ("unauthorized_key_admin", "ranger_key_admin_config", 400), + ("delete role linked with other role and has role membership which should prevent deletion", "ranger_admin_config", 400) + ] + ) + def test_delete_role_negative(self, test_case, auth_name, expected_status, request): + auth = getattr(self, auth_name) + + if test_case == "delete_non_exist_role": + r_id = 9999999 # Assuming this role ID doesn't exist + + elif test_case.startswith("unauthorized"): + role, r_id = request.getfixturevalue("temp_role")() + + elif test_case == "delete role linked with other role and has role membership which should prevent deletion": + # Create a parent role + parent_role, parent_role_id = request.getfixturevalue("temp_role")() + # Create a child role linked to parent role + child_role, child_role_id = request.getfixturevalue("temp_role")(role_list=[{"name": parent_role["name"], "isAdmin": True}]) + r_id = parent_role_id + response = requests.delete( + f'{self.base_url}/roles/roles/{r_id}', + auth = auth, + headers= self.headers + ) + + assert_response(response, expected_status, f'Expected {expected_status} for {test_case} but got {response.status_code}') + + if test_case == "delete role linked with other role and has role membership which should prevent deletion": + # Cleanup the created roles + delete_role(child_role_id) + if test_case.startswith("unauthorized"): + # Cleanup the created role + delete_role(r_id) + + @pytest.mark.delete + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case", [ + "login user is not (admin not service admin/group) - user creds", + "login user is not (admin not service admin/group) - auditor creds", + "login user is not (admin not service admin/group) - key admin creds", + "login user passes but query param user is not admin or service admin/group - user creds", + "login user passes but query param user is not admin or service admin/group - auditor creds", + "login user passes but query param user is not admin or service admin/group - key admin creds", + "all corect but the role is linked with other role and has role membership which should prevent deletion" + ] + ) + def test_delete_role_by_name_negative(self, test_case, request): + + + params = {} + if "login user is not (admin not service admin/group)" in test_case: + if "user creds" in test_case: + auth = self.ranger_user_config + elif "auditor creds" in test_case: + auth = self.ranger_auditor_config + elif "key admin creds" in test_case: + auth = self.ranger_key_admin_config + else: + auth = self.ranger_admin_config + if "user creds" in test_case: + params["execUser"] = self.ranger_user_config[0] + elif "auditor creds" in test_case: + params["execUser"] = self.ranger_auditor_config[0] + elif "key admin creds" in test_case: + params["execUser"] = self.ranger_key_admin_config[0] + if test_case == "all corect but the role is linked with other role and has role membership which should prevent deletion": + auth = self.ranger_admin_config + # parent role + role, role_id = request.getfixturevalue("temp_role")() + # parent role linked with child role + child, child_id = request.getfixturevalue("temp_role")(role_list=[{"name": role["name"], "isAdmin": True}]) + else: + role, role_id = request.getfixturevalue("temp_role")() + + response = requests.delete( + f'{self.base_url}/roles/roles/name/{role["name"]}', + auth = auth, + headers= self.headers, + params=params + ) + assert_response(response, 400, f"Expected 400 for {test_case} but got {response.status_code} with \n response text: {response.text}") + + if test_case == "all corect but the role is linked with other role and has role membership which should prevent deletion": + # Cleanup the created roles + delete_role(child_id) + + delete_role(role_id) \ No newline at end of file diff --git a/functional-tests/rolerest/test_role_utility_fun.py b/functional-tests/rolerest/test_role_utility_fun.py new file mode 100644 index 0000000000..e479567c44 --- /dev/null +++ b/functional-tests/rolerest/test_role_utility_fun.py @@ -0,0 +1,1340 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from urllib import * +from rolerest.utility.utils import * +from rolerest.utility.utils import assert_response +from xuserrest.utility.utils import * +import io +import os +import json +import uuid +import string +import pytest +import requests +from datetime import datetime +import random + + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.rolerest +class TestRoleUtilityFun: + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_group, + temp_role, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + # ---------- secondary users ---------- + cls.admin1, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin1, _ = temp_keyadmin_user() + cls.audit1, _ = temp_secure_user(["auditor"]) + cls.user1, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config1 = (cls.admin1["name"], "Test@123") + cls.ranger_key_admin_config1 = (cls.ranger_key_admin1["name"], "Test@123") + cls.ranger_auditor_config1 = (cls.audit1["name"], "Test@123") + cls.ranger_user_config1 = (cls.user1["name"], "Test@123") + + cls.ranger_admin_id1 = cls.admin1["id"] + cls.ranger_key_admin_id1 = cls.ranger_key_admin1["id"] + cls.ranger_auditor_id1 = cls.audit1["id"] + cls.ranger_user_id1 = cls.user1["id"] + + + # ---------- group details ---------- + cls.group, cls.group_id = temp_group() + cls.group1, cls.group_id1 = temp_group() + + # ---------- role details ---------- + cls.role, cls.role_id = temp_role() + cls.role1, cls.role_id1 = temp_role() + + # ---------- common ---------- + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "auth", ["ranger_admin_config"]) + def test_get_rokes_exportJson(self, auth): + auth_creds = getattr(self, auth) + export_filename = "roles_export.json" + + response = requests.get( + f"{self.base_url}/roles/roles/exportJson", + headers=self.headers, + auth=auth_creds + ) + + assert_response(response, [200, 204], f"Expected 200 or 204 for exporting roles but got {response.status_code}") + + # Save the response to a file, mimicking the curl -o behavior + with open(export_filename, "wb") as f: + f.write(response.content) + + try: + if response.status_code == 200: + with open(export_filename, "r") as f: + data = json.load(f) + assert "roles" in data, "Exported JSON should contain 'roles' key" + assert isinstance(data["roles"], list), "'roles' should be a list in the exported file" + + elif response.status_code == 204: + # No roles exist in the system, and we get a 204. The downloaded file should be exactly 0 bytes. + assert os.path.getsize(export_filename) == 0, "Expected empty file for 204 status code" + + finally: + if os.path.exists(export_filename): + os.remove(export_filename) + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth, service_name", + [ + ("admin", "ranger_admin_config", "dev_hdfs"), + ("key_admin", "ranger_key_admin_config", "dev_kms"), + ]) + @pytest.mark.parametrize( + "test_case, last_known_version, expected_status", + [ + ("valid-service-no-change", -1, 304), + ("valid-service-with-update", 0, 200), + ("valid-service-old-version", 1, 200), + ]) + def test_get_secure_roles_download_by_serviceName(self, test_case, auth, service_name, last_known_version, expected_status, role): + + auth = getattr(self, auth) + + base_params = { + "lastActivationTime": 0, + "clusterName": "mycluster", + "pluginId": "hdfs@host1" + } + + if test_case == "valid-service-no-change": + # Fetch the current role version to simulate a "no-change" 304 scenario + seed_response = requests.get( + f"{self.base_url}/roles/secure/download/{service_name}", + params={ + **base_params, + "lastKnownRoleVersion": -1 + }, + auth=auth, + headers=self.headers + ) + + assert seed_response.status_code == 200, \ + f"Seed request failed: {seed_response.status_code}" + + # Extract roleVersion from RangerRoles object + last_known_version = seed_response.json().get("roleVersion") + + response = requests.get( + f"{self.base_url}/roles/secure/download/{service_name}", + params={ + **base_params, + "lastKnownRoleVersion": last_known_version + }, + auth=auth, + headers=self.headers + ) + + assert_response(response, expected_status, test_case) + + if expected_status == 200: + data = response.json() + + assert "roleVersion" in data, "Response missing 'roleVersion'" + assert "rangerRoles" in data, "Response missing 'rangerRoles'" + + assert data.get("serviceName") == service_name, f"Expected serviceName {service_name}" + + + @pytest.mark.skip(reason = "Skipping download tests due to existing code changes in rolerest") + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [ + ("admin", "ranger_admin_config"), + ("key_admin", "ranger_key_admin_config"), + ("auditor", "ranger_auditor_config"), + ("user", "ranger_user_config"), + ] + ) + @pytest.mark.parametrize( + "test_case, last_known_version, expected_status", + [ + ("valid-service-no-change", -1, 304), # will be updated in future + ("valid-service-with-update", 0, 200), + ("valid-service-old-version", 1, 200), + ]) + def test_get_download_by_serviceName(self, test_case, auth, last_known_version, expected_status, role): + auth = getattr(self, auth) + service_name = "dev_hdfs" # valid existing service + + if test_case == "valid-service-no-change": + seed_response = requests.get( + f"{self.base_url}/roles/download/{service_name}", + params={"lastKnownUserStoreVersion": -1, "lastActivationTime": 0, + "clusterName": "mycluster", "pluginId": "hdfs@host1"}, + auth=auth, + headers=self.headers + ) + assert seed_response.status_code == 200, f"Seed request failed: {seed_response.status_code}" + current_version = seed_response.json().get("userStoreVersion") + last_known_version = current_version # now use the actual version → should get 304 + + params = { + "lastKnownUserStoreVersion": last_known_version, + "lastActivationTime": 0, + "clusterName": "mycluster", + "pluginId": f"hdfs@host1" + } + + response = requests.get( + f"{self.base_url}/roles/download/{service_name}", + params=params, + auth=auth, + headers=self.headers + ) + assert_response(response, expected_status, test_case) + if expected_status == 200: + data = response.json() + assert "userStoreVersion" in data, f"Response missing 'userStoreVersion' for {test_case}" + assert "userGroupMapping" in data or "users" in data, f"Response missing user/group data for {test_case}" + + + + @pytest.mark.post + @pytest.mark.positive + @pytest.mark.parametrize( + "auth", ["ranger_admin_config"] + ) + @pytest.mark.parametrize( + "test_case", + [ + "create new role", + "update existing role - updateIfExists=true", + "skip existing role modified - updateIfExists=false", + "skip existing role identical - updateIfExists=true", + ] + ) + def test_post_import_roles_from_file(self, request, auth, test_case): + auth_creds = getattr(self, auth) + r_id = None + new_role_name = "test_imported_role_new_" + "".join(random.choices(string.ascii_lowercase + string.digits, k=5)) + flg, _ = get_role_by_name(new_role_name, auth_creds) + assert not flg, f"Role with name {new_role_name} already exists, cannot proceed with the test case for creating new role." + + import_payload = { + "metaDataInfo": { + "Host name": "test-host", + "Exported by": "test-admin" + }, + "roles": [] + } + + params = { + "createNonExistUserGroupRole": "true" + } + + if test_case == "create new role": + params["updateIfExists"] = "false" + import_payload["roles"].append({ + "name": new_role_name, + "users": [{"name": "admin", "isAdmin": False}], + "groups": [], + "roles": [] + }) + + elif "existing role" in test_case: + role, r_id = request.getfixturevalue("temp_role")( + user_list=[{"name": "admin", "isAdmin": False}] + ) + + # FIX: Copy the full role object so Java's .equals() matches correctly + role_payload = role.copy() + role_payload["id"] = r_id + + if "identical" not in test_case: + # Safely append user if we want to intentionally trigger an update + if "users" not in role_payload: + role_payload["users"] = [] + role_payload["users"].append({"name": "test_user_new", "isAdmin": True}) + + import_payload["roles"].append(role_payload) + + if "updateIfExists=true" in test_case: + params["updateIfExists"] = "true" + else: + params["updateIfExists"] = "false" + + req_headers = self.headers.copy() + + # Cleaned up header manipulation + if "Content-Type" in req_headers: + del req_headers["Content-Type"] + + req_headers["Accept"] = "application/json" + + json_str = json.dumps(import_payload) + file_obj = io.BytesIO(json_str.encode('utf-8')) + + files = { + 'file': ('roles.json', file_obj, 'application/json') + } + + response = requests.post( + f"{self.base_url}/roles/roles/importRolesFromFile", + headers=req_headers, + auth=auth_creds, + params=params, + files=files + ) + + assert_response(response, 200, f"Expected 200 for {test_case} but got {response.status_code}") + + data_n = response.json() + print(data_n) + + # FIX: Parse the msgDesc safely by checking the keys, avoiding index assumption errors + data = {'Total Role Created': 0, 'Total Role Updated': 0, 'Total Role Unchanged': 0} + msg_parts = data_n.get('msgDesc', '').split(',') + + for part in msg_parts: + if 'Total Role Created' in part: + data['Total Role Created'] = int(part.split('=')[1].strip()) + elif 'Total Role Updated' in part: + data['Total Role Updated'] = int(part.split('=')[1].strip()) + elif 'Total Role Unchanged' in part: + data['Total Role Unchanged'] = int(part.split('=')[1].strip()) + + if test_case == "create new role": + assert data['Total Role Created'] == 1, f"Expected 1 role to be created but got {data['Total Role Created']}" + get_role_by_name_flg, role_data = get_role_by_name(new_role_name, auth_creds) + assert get_role_by_name_flg, "Newly created role not found when fetched by name." + assert role_data["name"] == new_role_name, f"Expected role name to be {new_role_name} but got {role_data['name']}" + delete_role(role_data["id"]) + + elif "update existing role - updateIfExists=true" in test_case: + assert data['Total Role Updated'] == 1, f"Expected 1 role to be updated but got {data['Total Role Updated']}" + get_role_by_name_flg, role_data = get_role_by_name(role["name"], auth_creds) + assert get_role_by_name_flg, "Updated role not found when fetched by name." + returned_users = {u["name"]: u.get("isAdmin", False) for u in role_data.get("users", [])} + assert "test_user_new" in returned_users, "New user added in the update payload is not present in the updated role." + assert returned_users["test_user_new"] is True, "Admin privilege for the new user should be true as per the update payload." + + elif "skip existing role modified - updateIfExists=false" in test_case: + assert data['Total Role Updated'] == 0, f"Expected 0 roles to be updated but got {data['Total Role Updated']}" + get_role_by_name_flg, role_data = get_role_by_name(role["name"], auth_creds) + assert get_role_by_name_flg, "Existing role not found when fetched by name." + returned_users = {u["name"]: u.get("isAdmin", False) for u in role_data.get("users", [])} + assert "test_user_new" not in returned_users, "User that was supposed to be added in the update payload is present even though updateIfExists was false." + + elif "skip existing role identical - updateIfExists=true" in test_case: + assert data['Total Role Updated'] == 0, f"Expected 0 roles to be updated but got {data['Total Role Updated']}" + + + @pytest.mark.put + @pytest.mark.positive + @pytest.mark.parametrize( + "auth", ["ranger_admin_config"] + ) + @pytest.mark.parametrize( + "isAdmin", ["true", "false"] + ) + @pytest.mark.parametrize( + "test_case", + [ + "existing users are admin", + "existing groups are admin", + "existing users are not admin", + "existing groups are not admin", + ] + ) + def test_put_role_add_users_and_groups(self, request, auth, isAdmin, test_case): + auth_creds = getattr(self, auth) + user_id = None + group_id = None + param_user_id = None + param_group_id = None + params = {"isAdmin": isAdmin} + + if "user" in test_case: + user, user_id = request.getfixturevalue("temp_secure_user")("user") + is_initial_admin = False if "not admin" in test_case else True + + role, r_id = request.getfixturevalue("temp_role")( + user_list=[{"name": user["name"], "isAdmin": is_initial_admin}] + ) + param_user, param_user_id = request.getfixturevalue("temp_secure_user")("user") + params["users"] = [param_user["name"], user["name"]] + + elif "group" in test_case: + group, group_id = request.getfixturevalue("temp_group")() + is_initial_admin = False if "not admin" in test_case else True + + role, r_id = request.getfixturevalue("temp_role")( + group_list=[{"name": group["name"], "isAdmin": is_initial_admin}] + ) + param_group, param_group_id = request.getfixturevalue("temp_group")() + params["groups"] = [param_group["name"], group["name"]] + + response = requests.put( + f"{self.base_url}/roles/roles/{r_id}/addUsersAndGroups", + headers=self.headers, + auth=auth_creds, + params=params, + json={} + ) + + assert_response(response, 200, f"Expected 200 for {test_case} but got {response.status_code}") + + data = response.json() + returned_users = [u["name"] for u in data.get("users", [])] + returned_groups = [g["name"] for g in data.get("groups", [])] + + if "user" in test_case: + if isAdmin == 'false': + # drops the existing user because grantAdmin is false, + # and skips adding them again because they are in existingUsernames. + assert len(returned_users) == 1, \ + f"Expected only 1 user in the updated role but got {len(returned_users)}" + assert param_user["name"] in returned_users, "New user was not added." + assert user["name"] not in returned_users, "Existing user should have been dropped." + else: + # retains the existing user and adds the new user. + assert len(returned_users) == 2, \ + f"Expected 2 users in the updated role but got {len(returned_users)}" + assert param_user["name"] in returned_users and user["name"] in returned_users, \ + "Expected both the new and existing user to be present." + + if "group" in test_case: + # for groups simply loops through all effectiveGroups and adds them unconditionally. + # Since both groups are passed in params, both will always be returned. + assert len(returned_groups) == 2, \ + f"Expected 2 groups in the updated role but got {len(returned_groups)}" + assert param_group["name"] in returned_groups and group["name"] in returned_groups, \ + "Expected both the new and existing group to be present." + + + @pytest.mark.put + @pytest.mark.positive + @pytest.mark.parametrize( + "auth", ["ranger_admin_config"] + ) + @pytest.mark.parametrize( + "test_case", + [ + "remove existing user", + "remove existing group", + "remove non-existent user", + "remove non-existent group" + ] + ) + def test_put_role_remove_users_and_groups(self, request, auth, test_case): + auth_creds = getattr(self, auth) + params = {} + + if "user" in test_case: + user, _ = request.getfixturevalue("temp_secure_user")("user") + + role, r_id = request.getfixturevalue("temp_role")( + user_list=[{"name": user["name"], "isAdmin": True}] + ) + if "non-existent" in test_case: + params["users"] = ["some_fake_user_name"] + else: + params["users"] = [user["name"]] + + elif "group" in test_case: + group, _ = request.getfixturevalue("temp_group")() + + role, r_id = request.getfixturevalue("temp_role")( + group_list=[{"name": group["name"], "isAdmin": True}] + ) + + if "non-existent" in test_case: + params["groups"] = ["some_fake_group_name"] + else: + params["groups"] = [group["name"]] + + response = requests.put( + f"{self.base_url}/roles/roles/{r_id}/removeUsersAndGroups", + headers=self.headers, + auth=auth_creds, + params=params, + json={} + ) + + assert_response(response, 200, f"Expected 200 for {test_case} but got {response.status_code}") + + data = response.json() + returned_users = [u["name"] for u in data.get("users", [])] + returned_groups = [g["name"] for g in data.get("groups", [])] + + if "user" in test_case: + if "non-existent" in test_case: + assert user["name"] in returned_users, "Original user should remain if a different user was removed." + else: + assert user["name"] not in returned_users, "Existing user should have been removed from the role." + + if "group" in test_case: + if "non-existent" in test_case: + assert group["name"] in returned_groups, "Original group should remain if a different group was removed." + else: + assert group["name"] not in returned_groups, "Existing group should have been removed from the role." + + + @pytest.mark.put + @pytest.mark.positive + @pytest.mark.parametrize( + "auth", ["ranger_admin_config"] + ) + @pytest.mark.parametrize( + "test_case", + [ + "existing user is admin", + "existing user is not admin", + "existing group is admin", + "existing group is not admin", + ] + ) + def test_put_role_remove_admin_from_users_and_groups(self, request, auth, test_case): + auth_creds = getattr(self, auth) + params = {} + if "user" in test_case: + user, _ = request.getfixturevalue("temp_secure_user")("user") + is_initial_admin = False if "not admin" in test_case else True + + role, r_id = request.getfixturevalue("temp_role")( + user_list=[{"name": user["name"], "isAdmin": is_initial_admin}] + ) + params["users"] = [user["name"]] + + elif "group" in test_case: + group, _ = request.getfixturevalue("temp_group")() + is_initial_admin = False if "not admin" in test_case else True + + role, r_id = request.getfixturevalue("temp_role")( + group_list=[{"name": group["name"], "isAdmin": is_initial_admin}] + ) + params["groups"] = [group["name"]] + + response = requests.put( + f"{self.base_url}/roles/roles/{r_id}/removeAdminFromUsersAndGroups", + headers=self.headers, + auth=auth_creds, + params=params, + json={} + ) + + assert_response(response, 200, f"Expected 200 for {test_case} but got {response.status_code}") + data = response.json() + + returned_users = {u["name"]: u.get("isAdmin", False) for u in data.get("users", [])} + returned_groups = {g["name"]: g.get("isAdmin", False) for g in data.get("groups", [])} + + if "user" in test_case: + assert user["name"] in returned_users, "User should not be removed from the role, only modified." + assert returned_users[user["name"]] is False, "Admin privilege should be false after removal." + + if "group" in test_case: + assert group["name"] in returned_groups, "Group should not be removed from the role, only modified." + assert returned_groups[group["name"]] is False, "Admin privilege should be false after removal." + + @pytest.mark.put + @pytest.mark.positive + # @pytest.mark.parametrize( # since all thses are just variations of login user we are not testing all + # "test_case", [ + # "login user is admin", + # "login user is service admin", + # "login user has service admin groups", + # ] + # ) + # @pytest.mark.parametrize( + # "grantOption", ["true", "false"] # to reduce the number of combinations randomly assigned + # ) + # @pytest.mark.parametrize( + # "payload_test_case", + # ["users adition", "groups addition", "roles addition"], # to cover all 3 possibilities of adding users/groups/roles in the payload in different test runs without combinatorial explosion of test cases + # ) + @pytest.mark.parametrize( + "test_case, query_param_user, effective_user, additional_conditions, grantOption, payload_test_case", + [ + ("login user is service admin", "exists", "query param userName", "query user is admin", "true", "users adition"), + ("login user has service admin groups", "exists", "query param userName", "query user is admin", "true", "groups addition"), + ("login user is admin","exists", "query param userName", "query user is admin", "true", "roles addition"), + ("login user is service admin", "exists", "query param userName", "query user is service admin", "true", "users adition"), + ("login user has service admin groups","exists", "query param userName", "query user is in service admin groups in grouplist", "true", "groups addition"), + ("login user is admin","exists", "query param userName", "query user is not admin or service admin/group but has role membership via user", "true", "roles addition"), + ("login user is admin","exists", "query param userName", "query user is not admin or service admin/group but has role membership via group", "true", "groups addition"), + ("login user is admin","exists", "query param userName", "query user is not admin or service admin/group but has role membership via role-user", "true", "roles addition"), + ("login user is admin","exists", "query param userName", "query user is not admin or service admin/group but has role membership via role-group", "true", "groups addition"), + ], + ) + def test_put_role_grant(self, request, test_case, query_param_user, effective_user, additional_conditions, grantOption, payload_test_case): + param = {} + service = None + service_id = None + + + if test_case == "login user is admin": + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["admin"]) + auth = (temp_user["name"], "Test@123") + + elif test_case == "login user is service admin": + + service, service_id = create_service() + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + assign_service_admin(service_id, service, temp_user['name']) + auth = (temp_user["name"], "Test@123") + + + elif test_case == "login user has service admin groups": + + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + group, group_id = request.getfixturevalue("temp_group")() + assign_groups_to_user(temp_user["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + service, service_id = create_service() + assign_service_admin_group(service_id, service, group["name"]) + auth = (temp_user["name"], "Test@123") + + + + if query_param_user == "not_exists": + role, role_id = request.getfixturevalue("temp_role")() + params = {} + if service: + params["serviceName"] = service["name"] + clean_up_items = {"role_list": [role_id], "service_list": [service_id] if service_id else []} + else: + condition = additional_conditions.removeprefix("query user is ") + auth_user, params, role, clean_up_items = ensureRoleAccess( + condition, request, + existing_service=[service] if service and "service admin" in test_case else None + ) + + if service: + params["serviceName"] = service["name"] + + if 'serviceName' in params: + path_param = params['serviceName'] + else: + path_param = 'nonexistentService' + + if 'execUser' in params: + payload = { + 'grantor' : params['execUser'], + } + if payload_test_case == "users adition": + payload_user, payload_user_id = request.getfixturevalue("temp_secure_user")() + payload["users"] = [payload_user["name"]] + clean_up_items["user_list"] = [payload_user_id] + elif payload_test_case == "groups addition": + payload_group, payload_group_id = request.getfixturevalue("temp_group")() + payload["groups"] = [payload_group["name"]] + clean_up_items["group_list"] = [payload_group_id] + elif payload_test_case == "roles addition": + payload_role, payload_role_id = request.getfixturevalue("temp_role")() + payload["roles"] = [payload_role["name"]] + #clean_up_items["role_list"] = [payload_role_id] + + payload["grantOption"] = grantOption + payload["targetRoles"] = [role["name"]] + + print("\n Auth used: ", auth) + print("\n payload used: ", payload) + response = requests.put( + f"{self.base_url}/roles/roles/grant/{path_param}", + auth=auth, # auth is admin but execUser in query param has service admin group permissions + headers=self.headers, + json = payload + ) + + assert_response(response, 200, f"Failed to update roles and got response code {response.status_code} with \n response text: {response.text}") + + + if payload_test_case == "users adition": + get_role_by_name_flg, role_data = get_role_by_name(role["name"]) + assert get_role_by_name_flg, "Role not found when fetched by name after granting role to user." + returned_users = {u["name"]: u.get("isAdmin", False) for u in role_data.get("users", [])} + assert payload_user["name"] in returned_users, "New user added in the grant payload is not present in the updated role." + elif payload_test_case == "groups addition": + get_role_by_name_flg, role_data = get_role_by_name(role["name"]) + assert get_role_by_name_flg, "Role not found when fetched by name after granting role to group." + returned_groups = {g["name"]: g.get("isAdmin", False) for g in role_data.get("groups", [])} + assert payload_group["name"] in returned_groups, "New group added in the grant payload is not present in the updated role." + elif payload_test_case == "roles addition": + get_role_by_name_flg, role_data = get_role_by_name(role["name"]) + assert get_role_by_name_flg, "Role not found when fetched by name after granting role to role." + returned_roles = [r["name"] for r in role_data.get("roles", [])] + assert payload_role["name"] in returned_roles, "New role added in the grant payload is not present in the updated role." + + + # # Cleanup + if service_id is not None and service_id not in clean_up_items.get("service_list", []): + clean_up_items["service_list"].append(service_id) + + # 1. Delete the target roles first (removes the references) + for item in clean_up_items.get("role_list", []): + delete_role(item) + + # 2. Delete the payload role now that it's no longer referenced + if payload_test_case == "roles addition": + delete_role(payload_role_id) + + # 3. Clean up services + for item in clean_up_items.get("service_list", []): + delete_service(item) + + @pytest.mark.put + @pytest.mark.positive + @pytest.mark.parametrize( + "test_case, query_param_user, effective_user, additional_conditions, grantOption, payload_test_case", + [ + ("login user is service admin", "exists", "query param userName", "query user is admin", "true", "users adition"), + ("login user has service admin groups", "exists", "query param userName", "query user is admin", "true", "groups addition"), + ("login user is admin","exists", "query param userName", "query user is admin", "true", "roles addition"), + ("login user is service admin", "exists", "query param userName", "query user is service admin", "false", "users adition"), # Switched to false to test complete removal + ("login user has service admin groups","exists", "query param userName", "query user is in service admin groups in grouplist", "false", "groups addition"), + ("login user is admin","exists", "query param userName", "query user is not admin or service admin/group but has role membership via user", "false", "roles addition"), + ("login user is admin","exists", "query param userName", "query user is not admin or service admin/group but has role membership via group", "true", "groups addition"), + ("login user is admin","exists", "query param userName", "query user is not admin or service admin/group but has role membership via role-user", "true", "roles addition"), + ("login user is admin","exists", "query param userName", "query user is not admin or service admin/group but has role membership via role-group", "true", "groups addition"), + ], + ) + def test_put_role_revoke(self, request, test_case, query_param_user, effective_user, additional_conditions, grantOption, payload_test_case): + param = {} + service = None + service_id = None + + if test_case == "login user is admin": + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["admin"]) + auth = (temp_user["name"], "Test@123") + + elif test_case == "login user is service admin": + service, service_id = create_service() + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + assign_service_admin(service_id, service, temp_user['name']) + auth = (temp_user["name"], "Test@123") + + elif test_case == "login user has service admin groups": + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + group, group_id = request.getfixturevalue("temp_group")() + assign_groups_to_user(temp_user["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) + service, service_id = create_service() + assign_service_admin_group(service_id, service, group["name"]) + auth = (temp_user["name"], "Test@123") + + if query_param_user == "not_exists": + role, role_id = request.getfixturevalue("temp_role")() + params = {} + if service: + params["serviceName"] = service["name"] + clean_up_items = {"role_list": [role_id], "service_list": [service_id] if service_id else []} + else: + condition = additional_conditions.removeprefix("query user is ") + auth_user, params, role, clean_up_items = ensureRoleAccess( + condition, request, + existing_service=[service] if service and "service admin" in test_case else None + ) + + if service: + params["serviceName"] = service["name"] + + if 'serviceName' in params: + path_param = params['serviceName'] + else: + path_param = 'nonexistentService' + + if 'execUser' in params: + payload = { + 'grantor' : params['execUser'], + } + + if payload_test_case == "users adition": + payload_user, payload_user_id = request.getfixturevalue("temp_secure_user")() + payload["users"] = [payload_user["name"]] + clean_up_items["user_list"] = [payload_user_id] + elif payload_test_case == "groups addition": + payload_group, payload_group_id = request.getfixturevalue("temp_group")() + payload["groups"] = [payload_group["name"]] + clean_up_items["group_list"] = [payload_group_id] + elif payload_test_case == "roles addition": + payload_role, payload_role_id = request.getfixturevalue("temp_role")() + payload["roles"] = [payload_role["name"]] + + payload["targetRoles"] = [role["name"]] + + pre_grant_payload = payload.copy() + pre_grant_payload["grantOption"] = "true" + + requests.put( + f"{self.base_url}/roles/roles/grant/{path_param}", + auth=auth, + headers=self.headers, + json=pre_grant_payload + ) + + # REVOKE STEP + payload["grantOption"] = grantOption + + print("\n Auth used: ", auth) + print("\n payload used: ", payload) + response = requests.put( + f"{self.base_url}/roles/roles/revoke/{path_param}", + auth=auth, + headers=self.headers, + json=payload + ) + + assert_response(response, 200, f"Failed to revoke roles and got response code {response.status_code} with \n response text: {response.text}") + + + get_role_by_name_flg, role_data = get_role_by_name(role["name"]) + assert get_role_by_name_flg, "Role not found when fetched by name after revoking role." + + is_grant_option_true = grantOption.lower() == "true" + + if payload_test_case == "users adition": + returned_users = {u["name"]: u.get("isAdmin", False) for u in role_data.get("users", [])} + if is_grant_option_true: + assert payload_user["name"] in returned_users, "User should remain in role, but have isAdmin revoked." + assert not returned_users[payload_user["name"]], "User's isAdmin flag should be False after revoke." + else: + assert payload_user["name"] not in returned_users, "User should be completely removed from the role." + + elif payload_test_case == "groups addition": + returned_groups = {g["name"]: g.get("isAdmin", False) for g in role_data.get("groups", [])} + if is_grant_option_true: + assert payload_group["name"] in returned_groups, "Group should remain in role, but have isAdmin revoked." + assert not returned_groups[payload_group["name"]], "Group's isAdmin flag should be False after revoke." + else: + assert payload_group["name"] not in returned_groups, "Group should be completely removed from the role." + + elif payload_test_case == "roles addition": + returned_roles = {r["name"]: r.get("isAdmin", False) for r in role_data.get("roles", [])} + if is_grant_option_true: + assert payload_role["name"] in returned_roles, "Role should remain in target role, but have isAdmin revoked." + assert not returned_roles[payload_role["name"]], "Role's isAdmin flag should be False after revoke." + else: + assert payload_role["name"] not in returned_roles, "Role should be completely removed from the target role." + + if service_id is not None and service_id not in clean_up_items.get("service_list", []): + clean_up_items["service_list"].append(service_id) + + for item in clean_up_items.get("role_list", []): + delete_role(item) + + if payload_test_case == "roles addition": + delete_role(payload_role_id) + + for item in clean_up_items.get("service_list", []): + delete_service(item) + + + + # NEGATIVE TEST CASES + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "auth", ["ranger_key_admin_config", "ranger_auditor_config", "ranger_user_config"] + ) + def test_get_rokes_exportJson_unauthorized(self, auth): + auth_creds = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/roles/roles/exportJson", + headers=self.headers, + auth=auth_creds + ) + + assert_response(response, 403, f"Expected 403 for unauthorized user but got {response.status_code}") + + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth, service_name, expected_status", + [ + ("invalid-service-name", "ranger_admin_config", "nonExistentService_XYZ", 404), + ("empty-service-name", "ranger_admin_config", "", 404), + ("admin-accessing-kms", "ranger_admin_config", "dev_kms", 403), + ("unauthorized-auditor-access", "ranger_auditor_config", "dev_kms", 403), + ("unauthorized-user-access", "ranger_user_config", "dev_kms", 403), + ("keyadmin-nonkms-access", "ranger_key_admin_config", "dev_hdfs", 400), + ] + ) + def test_get_secure_roles_download_by_serviceName_negative(self, test_case, auth, service_name, expected_status): + + auth_credentials = getattr(self, auth) + + params = { + "lastKnownUserStoreVersion": -1, + "lastActivationTime": 0, + "clusterName": "mycluster", + } + + if test_case != "missing-plugin-id": + params["pluginId"] = "hdfs@host1" + + response = requests.get( + f"{self.base_url}/roles/secure/download/{service_name}", + params=params, + auth=auth_credentials, + headers=self.headers + ) + + assert_response(response, expected_status, test_case) + + @pytest.mark.skip(reason = "Skipping download tests due to existing code changes in rolerest") + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth, expected_status", + [ + #("unauthorized-access-user", "ranger_user_config", 403), + ("invalid-service-name", "ranger_admin_config", 404), + ("empty-service-name", "ranger_admin_config", 404), + ("missing-plugin-id", "ranger_admin_config", 200), + ] + ) + def test_get_download_by_serviceName_negative(self, test_case, auth, expected_status): + auth_credentials = getattr(self, auth) + + if test_case == "invalid-service-name": + service_name = "nonExistentService_XYZ" + elif test_case == "empty-service-name": + service_name = "" + else: + service_name = "dev_hdfs" # Assuming this is your valid seed service + + + params = { + "lastKnownUserStoreVersion": -1, + "lastActivationTime": 0, + "clusterName": "mycluster", + } + + if test_case != "missing-plugin-id": + params["pluginId"] = "hdfs@host1" + + response = requests.get( + f"{self.base_url}/xusers/download/{service_name}", + params=params, + auth=auth_credentials, + headers=self.headers + ) + + assert_response(response, expected_status, test_case) + + + @pytest.mark.post + @pytest.mark.negative + @pytest.mark.parametrize( + "auth", ["ranger_key_admin_config", "ranger_auditor_config", "ranger_user_config"] + ) + def test_post_import_roles_from_file_unauthorized(self, auth): + auth_creds = getattr(self, auth) + new_role_name = "test_imported_role_new" + + import_payload = { + "metaDataInfo": { + "Host name": "test-host", + "Exported by": "test-admin" + }, + "roles": [{ + "name": new_role_name, + "users": [{"name": "admin", "isAdmin": False}], + "groups": [], + "roles": [] + }] + } + + req_headers = self.headers.copy() + + if "Content-Type" in req_headers: + del req_headers["Content-Type"] + + req_headers["Accept"] = "application/json" + + json_str = json.dumps(import_payload) + file_obj = io.BytesIO(json_str.encode('utf-8')) + + files = { + 'file': ('roles.json', file_obj, 'application/json') + } + + response = requests.post( + f"{self.base_url}/roles/roles/importRolesFromFile", + headers=req_headers, + auth=auth_creds, + params={"createNonExistUserGroupRole": "true", "updateIfExists": "false"}, + files=files + ) + + assert_response(response, 403, f"Expected 403 for unauthorized user but got {response.status_code}") + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "auth", ["ranger_key_admin_config", "ranger_auditor_config", "ranger_user_config"] + ) + def test_put_role_add_users_and_groups_unauthorized(self, auth): + auth_creds = getattr(self, auth) + role, r_id = self.role, self.role_id + + response = requests.put( + f"{self.base_url}/roles/roles/{r_id}/addUsersAndGroups", + headers=self.headers, + auth=auth_creds, + params={"isAdmin": "true", "users": [self.user["name"]], "groups": [self.group["name"]]}, + json={} + ) + + assert_response(response, 400, f"Expected 400 for unauthorized user but got {response.status_code}") + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "auth", ["ranger_key_admin_config", "ranger_auditor_config", "ranger_user_config"] + ) + def test_put_role_remove_users_and_groups_unauthorized(self, auth): + auth_creds = getattr(self, auth) + r_id = self.role_id + + response = requests.put( + f"{self.base_url}/roles/roles/{r_id}/removeUsersAndGroups", + headers=self.headers, + auth=auth_creds, + params={"users": [self.user["name"]], "groups": [self.group["name"]]}, + json={} + ) + + assert_response(response, 400, f"Expected 400 for unauthorized user but got {response.status_code}") + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "login_test_case", [ + "login user is neither admin nor service admin/group - user creds", + "login user is neither admin nor service admin/group - auditor creds", + "login user is neither admin nor service admin/group - key admin creds", + ] + ) + def test_put_role_grant_with_diff_creds_negative_login(self, login_test_case, request): + auth = self.ranger_admin_config # Default to admin auth, will be overridden for specific negative cases + if "user" in login_test_case: + auth = self.ranger_user_config + elif "auditor" in login_test_case: + auth = self.ranger_auditor_config + elif "key admin" in login_test_case: + auth = self.ranger_key_admin_config + + role, role_id = request.getfixturevalue("temp_role")() + params = {"execUser": self.admin["name"]} # Using admin as execUser in query param + + payload = { + "grantor": self.admin["name"], + "users": [self.user["name"]], + "groups": [self.group["name"]], + "roles": [], + "grantOption": "true", + "targetRoles": [role["name"]] + } + + try: + response = requests.put( + f"{self.base_url}/roles/roles/grant/nonexistentService", + auth=auth, + headers=self.headers, + params=params, + json=payload + ) + + assert_response(response, 400, f"Expected 400 for {login_test_case} but got {response.status_code} with \n response text: {response.text}") + finally: + delete_role(role_id) + + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "query_user, test_case", [ + ("non exists", "login user exists and it becomes effective user but is service user"), + ("not exists", "login user is admin but query param user is non exist user"), + ("exists", "login user is admin but query param user is non admin or service admin and has no role membership - user creds"), + ("exists", "login user is admin but query param user is non admin or service admin and has no role membership - auditor creds"), + ("exists", "login user is admin but query param user is non admin or service admin and has no role membership - key admin creds"), + ]) + def test_put_role_grant_with_diff_creds_negative_query_user(self, query_user, test_case, request): + auth = self.ranger_admin_config + params = {} + role, role_id = request.getfixturevalue("temp_role")() + + if query_user in ["non exists", "not exists"]: + params["execUser"] = f"nonexistuser_{uuid.uuid4().hex[:6]}" + else: + if "user creds" in test_case: + params["execUser"] = self.ranger_user_config[0] + elif "auditor creds" in test_case: + params["execUser"] = self.ranger_auditor_config[0] + elif "key admin creds" in test_case: + params["execUser"] = self.ranger_key_admin_config[0] + + payload = { + "grantor": params.get("execUser", ""), + "users": [self.user["name"]], + "groups": [self.group["name"]], + "roles": [], + "grantOption": "true", + "targetRoles": [role["name"]] + } + + try: + response = requests.put( + f"{self.base_url}/roles/roles/grant/nonexistentService", + auth=auth, + headers=self.headers, + params=params, + json=payload + ) + + assert_response(response, 400, f"Expected 400 for {test_case} but got {response.status_code} with \n response text: {response.text}") + finally: + delete_role(role_id) + + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case", [ + "adding non existent user to role", + "adding non existent group to role", + "adding non existent role to role", + ]) + def test_put_role_grant_with_diff_creds_negative_payload(self, test_case, request): + auth = self.ranger_admin_config + role, role_id = request.getfixturevalue("temp_role")() + params = {"execUser": self.admin["name"]} + + payload = { + "grantor": self.admin["name"], + "users": [], + "groups": [], + "roles": [], + "grantOption": "true", + "targetRoles": [role["name"]] + } + + if test_case == "adding non existent user to role": + payload["users"] = [f"nonexistentuser_{uuid.uuid4().hex[:6]}"] + elif test_case == "adding non existent group to role": + payload["groups"] = [f"nonexistentgroup_{uuid.uuid4().hex[:6]}"] + elif test_case == "adding non existent role to role": + payload["roles"] = [f"nonexistentrole_{uuid.uuid4().hex[:6]}"] + + try: + response = requests.put( + f"{self.base_url}/roles/roles/grant/nonexistentService", + auth=auth, + headers=self.headers, + params=params, + json=payload + ) + + assert_response(response, 400, f"Expected 400 for {test_case} but got {response.status_code} with \n response text: {response.text}") + finally: + delete_role(role_id) + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "login_test_case", [ + "login user is neither admin nor service admin/group - user creds", + "login user is neither admin nor service admin/group - auditor creds", + "login user is neither admin nor service admin/group - key admin creds", + ] + ) + def test_put_role_revoke_with_diff_creds_negative_login(self, login_test_case, request): + auth = self.ranger_admin_config # Default to admin auth, will be overridden for specific negative cases + if "user" in login_test_case: + auth = self.ranger_user_config + elif "auditor" in login_test_case: + auth = self.ranger_auditor_config + elif "key admin" in login_test_case: + auth = self.ranger_key_admin_config + + role, role_id = request.getfixturevalue("temp_role")() + params = {"execUser": self.admin["name"]} # Using admin as execUser in query param + + payload = { + "grantor": self.admin["name"], + "users": [self.user["name"]], + "groups": [self.group["name"]], + "roles": [], + "grantOption": "true", + "targetRoles": [role["name"]] + } + + try: + response = requests.put( + f"{self.base_url}/roles/roles/revoke/nonexistentService", + auth=auth, + headers=self.headers, + params=params, + json=payload + ) + + assert_response(response, 400, f"Expected 400 for {login_test_case} but got {response.status_code} with \n response text: {response.text}") + finally: + delete_role(role_id) + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "query_user, test_case", [ + ("non exists", "login user exists and it becomes effective user but is service user"), + ("not exists", "login user is admin but query param user is non exist user"), + ("exists", "login user is admin but query param user is non admin or service admin and has no role membership - user creds"), + ("exists", "login user is admin but query param user is non admin or service admin and has no role membership - auditor creds"), + ("exists", "login user is admin but query param user is non admin or service admin and has no role membership - key admin creds"), + ]) + def test_put_role_revoke_with_diff_creds_negative_query_user(self, query_user, test_case, request): + auth = self.ranger_admin_config + params = {} + role, role_id = request.getfixturevalue("temp_role")() + + if query_user in ["non exists", "not exists"]: + params["execUser"] = f"nonexistuser_{uuid.uuid4().hex[:6]}" + else: + if "user creds" in test_case: + params["execUser"] = self.ranger_user_config[0] + elif "auditor creds" in test_case: + params["execUser"] = self.ranger_auditor_config[0] + elif "key admin creds" in test_case: + params["execUser"] = self.ranger_key_admin_config[0] + + payload = { + "grantor": params.get("execUser", ""), + "users": [self.user["name"]], + "groups": [self.group["name"]], + "roles": [], + "grantOption": "true", + "targetRoles": [role["name"]] + } + + try: + response = requests.put( + f"{self.base_url}/roles/roles/revoke/nonexistentService", + auth=auth, + headers=self.headers, + params=params, + json=payload + ) + + assert_response(response, 400, f"Expected 400 for {test_case} but got {response.status_code} with \n response text: {response.text}") + finally: + delete_role(role_id) + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case", [ + "removing non existent user from role", + "removing non existent group from role", + "removing non existent role from role", + ]) + def test_put_role_revoke_with_diff_creds_negative_payload(self, test_case, request): + auth = self.ranger_admin_config + role, role_id = request.getfixturevalue("temp_role")() + params = {"execUser": self.admin["name"]} + + payload = { + "grantor": self.admin["name"], + "users": [], + "groups": [], + "roles": [], + "grantOption": "true", + "targetRoles": [role["name"]] + } + + if test_case == "removing non existent user from role": + payload["users"] = [f"nonexistentuser_{uuid.uuid4().hex[:6]}"] + elif test_case == "removing non existent group from role": + payload["groups"] = [f"nonexistentgroup_{uuid.uuid4().hex[:6]}"] + elif test_case == "removing non existent role from role": + payload["roles"] = [f"nonexistentrole_{uuid.uuid4().hex[:6]}"] + + try: + response = requests.put( + f"{self.base_url}/roles/roles/revoke/nonexistentService", + auth=auth, + headers=self.headers, + params=params, + json=payload + ) + + assert_response(response, 200, f"Expected 200 for {test_case} but got {response.status_code} with \n response text: {response.text}") + + get_role_by_name_flg, role_data = get_role_by_name(role["name"]) + assert get_role_by_name_flg, "Role not found when fetched by name after revoke with non-existent user/group/role in payload." + assert role_data.get("users", []) == [], "Users list should be empty as the only user in revoke payload was non-existent." + assert role_data.get("groups", []) == [], "Groups list should be empty as the only group in revoke payload was non-existent." + assert role_data.get("roles", []) == [], "Roles list should be empty as the only role in revoke payload was non-existent." + finally: + delete_role(role_id) \ No newline at end of file diff --git a/functional-tests/rolerest/utility/__init__.py b/functional-tests/rolerest/utility/__init__.py new file mode 100644 index 0000000000..13878a13a7 --- /dev/null +++ b/functional-tests/rolerest/utility/__init__.py @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. \ No newline at end of file diff --git a/functional-tests/rolerest/utility/utils.py b/functional-tests/rolerest/utility/utils.py new file mode 100644 index 0000000000..f73f235d60 --- /dev/null +++ b/functional-tests/rolerest/utility/utils.py @@ -0,0 +1,273 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +import json +import uuid +import pytest +import requests +from datetime import datetime +import inspect +import subprocess +import xuserrest.utility.utils as xutils + + +BASE_URL = "http://localhost:6080/service" +AUTH = ("admin", "rangerR0cks!") +HEADERS = {"Content-Type": "application/json"} + +RANGER_CONTAINER_NAME = "ranger" +RANGER_LOG_FILE = "/var/log/ranger/ranger-admin-ranger.rangernw-.log" + +BIGINT_MIN = -9223372036854775808 +BIGINT_MAX = 9223372036854775807 + +SERVICE_NAME = "admin" + +RANGER_CONFIG = None +RANGER_KEY_ADMIN_CONFIG = None +RANGER_AUDITOR_CONFIG = None +RANGER_USER_CONFIG = None + + +def init_configs(ranger_config, ranger_key_admin_config, ranger_auditor_config, ranger_user_config): + global RANGER_CONFIG, RANGER_KEY_ADMIN_CONFIG, RANGER_AUDITOR_CONFIG, RANGER_USER_CONFIG + + RANGER_CONFIG = ranger_config + RANGER_KEY_ADMIN_CONFIG = ranger_key_admin_config + RANGER_AUDITOR_CONFIG = ranger_auditor_config + RANGER_USER_CONFIG = ranger_user_config + + + + +def create_service(user_name = "hdfs", password = "hdfs"): + + unique_name = f"test_service_{uuid.uuid4().hex[:8]}" + # Create service + service_payload = { + "name": unique_name, + "displayName": unique_name, + "type": "hdfs", + "isEnabled": True, + "configs": { + "username": user_name, + "password": password, + "fs.default.name": "hdfs://localhost:9000", + "hadoop.security.authentication": "simple", + "hadoop.security.authorization": "true", + # "policy.download.auth.users": user_name, + # "tag.download.auth.users": user_name, + # "userstore.download.auth.users": user_name + } + } + svc_resp = requests.post( + f"{BASE_URL}/plugins/services", + auth=AUTH, + headers=HEADERS, + data=json.dumps(service_payload) + ) + svc_resp.raise_for_status() + service = svc_resp.json() + service_id = service["id"] + print(f"[+] Service created: name={service['name']}, id={service_id}") + + + return service, service_id + + +def assign_service_admin(service_id, service, username): + configs = service.get("configs", {}) + + # Ranger stores service admins as a comma-separated string in configs + existing_admins = configs.get("service.admin.users", "") + admin_set = set(filter(None, existing_admins.split(","))) + admin_set.add(username) + + configs["service.admin.users"] = ",".join(admin_set) + + update_payload = { + "id": service_id, + "name": service["name"], + "displayName": service.get("displayName", service["name"]), + "type": service["type"], + "isEnabled": service.get("isEnabled", True), + "configs": configs + } + + resp = requests.put( + f"{BASE_URL}/plugins/services/{service_id}", + auth=AUTH, + headers=HEADERS, + json=update_payload + ) + #resp.raise_for_status() + print(f"[+] User '{username}' added as service admin for service id={service_id}") + print("\n [+] Response of updated service admin : ", resp.json(), "\n") + +def assign_service_admin_group(service_id, service, groupname): + configs = service.get("configs", {}) + + # Ranger stores service admin groups as a comma-separated string in configs + existing_groups = configs.get("service.admin.groups", "") + group_set = set(filter(None, existing_groups.split(","))) + group_set.add(groupname) + + configs["service.admin.groups"] = ",".join(group_set) + + update_payload = { + "id": service_id, + "name": service["name"], + "displayName": service.get("displayName", service["name"]), + "type": service["type"], + "isEnabled": service.get("isEnabled", True), + "configs": configs + } + + resp = requests.put( + f"{BASE_URL}/plugins/services/{service_id}", + auth=AUTH, + headers=HEADERS, + json=update_payload + ) + #resp.raise_for_status() + print(f"[+] Group '{groupname}' added as service admin group for service id={service_id}") + print("\n [+] Response of updated service admin group : ", resp.json(), "\n") + +def delete_service(service_id): + resp = requests.delete( + f"{BASE_URL}/plugins/services/{service_id}", + auth=AUTH, + headers=HEADERS + ) + resp.raise_for_status() + print(f"[+] Service deleted: id={service_id}") + + + +def assert_response(response, expected_status, text = None, service_name=SERVICE_NAME): + xutils.assert_response(response, expected_status, text, service_name) + +def get_role_by_name(role_name, auth=AUTH): + response = requests.get( + f"{BASE_URL}/roles/roles/name/{role_name}", + auth=auth, + headers=HEADERS, + ) + + return response.status_code == 200, response.json() + +# Remove Content-Type for DELETE requests +def delete_role(role_id): + resp = requests.delete( + f"{BASE_URL}/roles/roles/{role_id}", + auth=AUTH, + headers={"Accept": "application/json"} # Changed from string to dictionary + ) + #assert_response(resp, [200, 204], f"Failed to delete role id={role_id}: {resp.text}") + assert resp.status_code in [200, 204], f"Failed to delete role id={role_id}: {resp.text}" + print(f" /n /n [+] Role deleted successfully with id={role_id}. Response status: {resp.status_code}. Response text: {resp.text} /n /n") + + + +def ensureRoleAccess(test_case, request, existing_service=None): + + service_list = [] + + if test_case == "admin": + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["admin"]) + auth = (temp_user["name"], "Test@123") + role, role_id = request.getfixturevalue("temp_role")() + role_list = [role_id] + + elif test_case == "service admin": + if existing_service is None: + service, service_id = create_service() + else: + service = existing_service[0] + service_id = service["id"] + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")("auditor") + assign_service_admin(service_id, service, temp_user['name']) + auth = (temp_user["name"], "Test@123") + role, role_id = request.getfixturevalue("temp_role")() + service_list = [service_id] + role_list = [role_id] + + elif test_case == "in service admin groups in grouplist": + if existing_service is None: + service, service_id = create_service() + else: + service = existing_service[0] + service_id = service["id"] + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")("auditor") + group, group_id = request.getfixturevalue("temp_group")() + assign_service_admin_group(service_id, service, group["name"]) + xutils.assign_groups_to_user(temp_user["name"], [group["name"]], AUTH, BASE_URL, HEADERS) + auth = (temp_user["name"], "Test@123") + role, role_id = request.getfixturevalue("temp_role")() + service_list = [service_id] + role_list = [role_id] + + req = requests.get(f"{BASE_URL}/plugins/services/{service_id}", auth=AUTH, headers=HEADERS) + print("\n [+] Service details after assigning service admin group: ", req.json(), "\n") + + req = requests.get(f"{BASE_URL}/xusers/users/{temp_user_id}", auth=AUTH, headers=HEADERS) + print("\n [+] User details after assigning service admin group: ", req.json(), "\n") + + req = requests.get(f"{BASE_URL}/xusers/groups/{group_id}", auth=AUTH, headers=HEADERS) + print("\n [+] Group details after assigning service admin group: ", req.json(), "\n") + + + elif test_case.endswith("role-user"): # must be before endswith("user") + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")("user") + c_role, c_id = request.getfixturevalue("temp_role")(user_list=[{"name": temp_user["name"], "isAdmin": True}]) + role, role_id = request.getfixturevalue("temp_role")(role_list=[{"name": c_role["name"], "isAdmin": True}]) + auth = (temp_user["name"], "Test@123") + role_list = [role_id, c_id] + + elif test_case.endswith("role-group"): # must be before endswith("group") + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")("user") + group, group_id = request.getfixturevalue("temp_group")() + xutils.assign_groups_to_user(temp_user["name"], [group["name"]], ("admin", "rangerR0cks!"), "http://localhost:6080/service", { + "Accept": "application/json", + "Content-Type": "application/json" + }) + c_role, c_id = request.getfixturevalue("temp_role")(group_list=[{"name": group["name"], "isAdmin": True}]) + role, role_id = request.getfixturevalue("temp_role")(role_list=[{"name": c_role["name"], "isAdmin": True}]) + auth = (temp_user["name"], "Test@123") + role_list = [role_id, c_id] + + elif test_case.endswith("user"): + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")("user") + role, role_id = request.getfixturevalue("temp_role")(user_list=[{"name": temp_user["name"], "isAdmin": True}]) + auth = (temp_user["name"], "Test@123") + role_list = [role_id] + + elif test_case.endswith("group"): + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")("user") + group, group_id = request.getfixturevalue("temp_group")() + xutils.assign_groups_to_user(temp_user["name"], [group["name"]], AUTH, BASE_URL, HEADERS) + role, role_id = request.getfixturevalue("temp_role")(group_list=[{"name": group["name"], "isAdmin": True}]) + auth = (temp_user["name"], "Test@123") + role_list = [role_id] + + else: + raise ValueError(f"Unknown test_case: '{test_case}'") # catches mismatches early + + params = {"execUser": temp_user["name"]} + if test_case in ["service admin", "in service admin groups in grouplist"]: + params = {"serviceName": service["name"], "execUser": temp_user["name"]} + + clean_up_items = {"role_list": role_list, "service_list": service_list} + return auth, params, role, clean_up_items diff --git a/functional-tests/run-tests.sh b/functional-tests/run-tests.sh index 4368ce7d3b..28a1d6f63f 100755 --- a/functional-tests/run-tests.sh +++ b/functional-tests/run-tests.sh @@ -24,19 +24,45 @@ #!/bin/bash +# All available test suites (pytest folders) +ALL_TEST_SUITES=(rolerest xuserrest servicerest hdfs kms) + +# Suites that have actual docker-compose services +DOCKER_SERVICES=(hdfs kms) + +# Test-only suites (no docker-compose file needed) +TEST_ONLY_SUITES=(rolerest xuserrest servicerest) + #handle input -DB_TYPE="${1:-postgres}" +DB_TYPE="${1:-}" shift || true EXTRA_SERVICES=("$@") -# Cleanup is optional if you need Fresh containers install(export CLEAN_CONTAINERS=1 to force fresh start) -CLEAN_CONTAINERS="${CLEAN_CONTAINERS:-0}" +# Prompt for DB_TYPE if not provided +if [[ -z "${DB_TYPE}" ]]; then + echo "" + echo "Available DB types: postgres, mysql, oracle" + read -rp "Enter DB type (press Enter to default to postgres): " input_db + DB_TYPE="${input_db:-postgres}" +fi -echo "Using DB type: ${DB_TYPE}" -if [ "${#EXTRA_SERVICES[@]}" -gt 0 ]; then - echo "Extra services: ${EXTRA_SERVICES[*]}" +# Prompt for EXTRA_SERVICES / TEST SUITES if not provided +if [[ "${#EXTRA_SERVICES[@]}" -eq 0 ]]; then + echo "" + echo "Available test suites: ${ALL_TEST_SUITES[*]}" + read -rp "Enter test suites space-separated (press Enter to run ALL): " -a input_services + if [[ "${#input_services[@]}" -gt 0 && -n "${input_services[0]}" ]]; then + EXTRA_SERVICES=("${input_services[@]}") + else + echo "No input given. Running ALL test suites..." + EXTRA_SERVICES=("${ALL_TEST_SUITES[@]}") + fi fi +echo "" +echo "DB Type : ${DB_TYPE}" +echo "Test Suites : ${EXTRA_SERVICES[*]}" +echo "" echo "CLEAN_CONTAINERS=${CLEAN_CONTAINERS}" # Remove all containers and clean up docker space @@ -47,17 +73,40 @@ fi #path setup RANGER_DOCKER_PATH="../dev-support/ranger-docker" -TESTS_PATH="../../pytest-Tests" +TESTS_PATH="../../functional-tests" -cd "$RANGER_DOCKER_PATH"|| exit 1 +cd "$RANGER_DOCKER_PATH" || exit 1 # Ensure scripts are executable chmod +x scripts/**/*.sh || true chmod +x download-archives.sh || true -# Download archives -if [ "${#EXTRA_SERVICES[@]}" -gt 0 ]; then - ./download-archives.sh "${EXTRA_SERVICES[@]}" +# Download archives — only for docker-backed services +DOCKER_BACKED=() +for service in "${EXTRA_SERVICES[@]}"; do + for ds in "${DOCKER_SERVICES[@]}"; do + if [[ "$service" == "$ds" ]]; then + case "$service" in + hdfs) + # 'hive' arg downloads hadoop + tez (both needed by Dockerfile.ranger-hadoop) + # 'hadoop' arg alone does NOT download tez + DOCKER_BACKED+=("hive") + ;; + *) + DOCKER_BACKED+=("$service") + ;; + esac + fi + done +done + +# Deduplicatec +DOCKER_BACKED+=("kms") +DOCKER_BACKED=($(printf '%s\n' "${DOCKER_BACKED[@]}" | sort -u)) + +if [[ "${#DOCKER_BACKED[@]}" -gt 0 ]]; then + echo "Downloading archives for: ${DOCKER_BACKED[*]}" + ./download-archives.sh "${DOCKER_BACKED[@]}" fi export RANGER_DB_TYPE="${DB_TYPE}" @@ -73,12 +122,31 @@ elif [[ -z "$(docker compose -f docker-compose.ranger-build.yml ps -q "${ADMIN_S fi if [[ "${admin_missing}" == "true" ]]; then - echo "Admin service (${ADMIN_SERVICE}) missing. Building and starting" + echo "Admin service (${ADMIN_SERVICE}) missing." + # Remove leftover 'version' directory from previous build to prevent mv error + if [[ "${CLEAN_CONTAINERS}" == "1" ]]; then + rm -rf dist/version + fi + docker compose -f docker-compose.ranger-build.yml build - docker compose -f docker-compose.ranger-build.yml up -d + if [[ $? -ne 0 ]]; then + echo "ERROR: Ranger build failed. Exiting..." + exit 1 + fi + + if [[ "${CLEAN_CONTAINERS}" == "1" ]]; then + echo "Starting containers because CLEAN_CONTAINERS=1" + docker compose -f docker-compose.ranger-build.yml up + + if [[ $? -ne 0 ]]; then + echo "ERROR: Ranger startup failed. Exiting..." + exit 1 + fi + else + echo "Skipping 'docker compose up' because CLEAN_CONTAINERS is not 1" + fi else echo "Admin service (${ADMIN_SERVICE}) already exists. Skipping build/up." - docker compose -f docker-compose.ranger-build.yml up -d fi # Bring up basic services @@ -87,19 +155,35 @@ DOCKER_FILES=( "-f" "docker-compose.ranger-usersync.yml" "-f" "docker-compose.ranger-tagsync.yml" "-f" "docker-compose.ranger-kms.yml" - ) +) -# add extra service from input -for service in "${EXTRA_SERVICES[@]}" ; do - DOCKER_FILES+=("-f" "docker-compose.ranger-${service}.yml") +# Add compose files based on requested suites +for service in "${EXTRA_SERVICES[@]}"; do + case "$service" in + hdfs) + DOCKER_FILES+=("-f" "docker-compose.ranger-hadoop.yml") + ;; + kms) + # already included in base + ;; + esac done -BASE_SERVICES=(ranger ranger-${RANGER_DB_TYPE} ranger-zk ranger-solr ranger-usersync ranger-tagsync ranger-kms) +# Build ALL_SERVICES list — only docker-backed services get container checks +BASE_SERVICES=(ranger ranger-${RANGER_DB_TYPE} ranger-zk ranger-solr ranger-kms) ALL_SERVICES=("${BASE_SERVICES[@]}") for service in "${EXTRA_SERVICES[@]}"; do - ALL_SERVICES+=("ranger-${service}") + case "$service" in + hdfs) + ALL_SERVICES+=("ranger-hadoop") + ;; + kms) + : # already in BASE_SERVICES + ;; + esac done + # only create/build if containers do not exist missing=false for container in "${ALL_SERVICES[@]}"; do @@ -119,22 +203,50 @@ fi echo "Waiting for containers to start..." if [[ "${missing}" == "true" || "${admin_missing}" == "true" ]]; then - sleep 60 + sleep 20 else echo "No rebuild/start needed. Skipping wait." fi - echo "Checking container status..." flag=true + for container in "${ALL_SERVICES[@]}"; do if [[ $(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) == "true" ]]; then echo "Container $container is running!" else - echo "Container $container is NOT running!" - flag=false + echo "Container $container is NOT running! Attempting restart..." + + docker restart "$container" >/dev/null 2>&1 + + echo "Waiting 5 seconds before re-check..." + sleep 5 + + if [[ $(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) == "true" ]]; then + echo "Container $container successfully restarted!" + # DO NOTHING → keep flag=true + else + echo "Container $container FAILED to restart!" + flag=false + break + fi fi done +if [ "$flag" = false ]; then + echo "Some containers failed to start. Exiting..." + exit 1 +else + echo "All containers are running fine!" +fi + + +echo -e "\n\n\n\n\n\n\n\n\n\n-------------------------------------------" +echo "Please wait while all services are fully initialized. This may take a few minutes..." +echo "-------------------------------------------" + +sleep 60 # Wait for 60 seconds to allow services to initialize + + #RUN TESTS-------- #Use export RUN_TESTS=0 to only bring up infra and allow all services to initialise properly (NOTE: It'll skip tests to avoid early startup failures). RUN_TESTS="${RUN_TESTS:-1}" @@ -149,19 +261,33 @@ if [[ $flag == true ]]; then pip install --upgrade pip pip install -r requirements.txt || { echo "Failed to install requirements"; exit 1; } # Install dependencies - pytest -vs hdfs/ --html=report_hdfs.html # Runs all tests in the hdfs directory - pytest -vs kms/ --html=report_kms.html # Runs all tests in the kms directory + echo "" + echo "Running tests for suites: ${EXTRA_SERVICES[*]}" + echo "" + + for suite in "${EXTRA_SERVICES[@]}"; do + if [[ -d "${suite}" ]]; then + echo "-------------------------------------------" + echo "Running tests for: ${suite}" + echo "-------------------------------------------" + pytest -vs "${suite}/" --html="report_${suite}.html" + else + echo "WARNING: No test folder found for '${suite}', skipping..." + fi + done + else - echo "All required containers are up. Skipping tests (TESTS_RUN=${TESTS_RUN})." + echo "All required containers are up. Skipping tests (RUN_TESTS=${RUN_TESTS})." fi else echo "Some containers failed to start. Exiting..." if [[ "${CLEAN_CONTAINERS}" == "1" ]]; then - docker stop $(docker ps -q --filter "name=ranger") 2>/dev/null || true - docker rm $(docker ps -aq --filter "name=ranger") 2>/dev/null || true - fi - exit 1 + docker stop $(docker ps -q --filter "name=ranger") 2>/dev/null || true + docker rm $(docker ps -aq --filter "name=ranger") 2>/dev/null || true fi + exit 1 +fi + if [[ "${CLEAN_CONTAINERS}" == "1" ]]; then echo "Cleaning up containers..." @@ -172,4 +298,4 @@ else fi echo "Test execution complete!" -exit 0 +exit 0 \ No newline at end of file diff --git a/functional-tests/xuserrest/__init__.py b/functional-tests/xuserrest/__init__.py new file mode 100644 index 0000000000..13878a13a7 --- /dev/null +++ b/functional-tests/xuserrest/__init__.py @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. \ No newline at end of file diff --git a/functional-tests/xuserrest/conftest.py b/functional-tests/xuserrest/conftest.py new file mode 100644 index 0000000000..4de7c884c2 --- /dev/null +++ b/functional-tests/xuserrest/conftest.py @@ -0,0 +1,605 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +import random +import string +import uuid +import pytest +import requests +import time +import os + +# Module-level constants — always available globally, even with pytest -n auto +CREDENTIALS = ("admin", "rangerR0cks!") +DEFAULT_HEADERS = { + "Accept": "application/json", + "Content-Type": "application/json" +} +KEYADMIN_CREDENTIALS = ("keyadmin", "rangerR0cks!") + +@pytest.fixture(scope="session") +def credentials(): + return CREDENTIALS + +@pytest.fixture(scope="session") +def default_headers(): + return DEFAULT_HEADERS + +@pytest.fixture(scope="session") +def keyadmin_credentials(): + return KEYADMIN_CREDENTIALS + + +def create_user_with_retry(url, **kwargs): + import time + + for attempt in range(5): + response = requests.post(url, **kwargs) + + if response.status_code == 200: + return response + + time.sleep(1 + attempt) + + raise AssertionError(f"User creation failed: {response.text}") + +@pytest.fixture(scope="function") +def ranger_session(ranger_config): + session = requests.Session() + session.auth = ranger_config["auth"] + session.headers.update(ranger_config["headers"]) + return session + + +@pytest.fixture(scope="session") +def ranger_config(credentials, default_headers): + + return { + "base_url": "http://localhost:6080/service", + "auth": credentials, + "headers": default_headers + } + +@pytest.fixture(scope="session") +def ranger_key_admin_config(keyadmin_credentials, default_headers): + + return { + "id" : 3, + "base_url": "http://localhost:6080/service", + "auth": keyadmin_credentials, + "headers": default_headers + } + +@pytest.fixture() +def all_users(ranger_config): + + url = f"{ranger_config['base_url']}/xusers/users/" + + response = requests.get( + url, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert response.status_code == 200, f"Failed to fetch users list: {response.status_code}" + + data = response.json() + assert "vXUsers" in data, "Invalid users list schema" + + users = data["vXUsers"] + assert isinstance(users, list) + assert len(users) > 0, "No users found in Ranger" + + return users + + +@pytest.fixture() +def all_schema_following_users(ranger_config): + + url = f"{ranger_config['base_url']}/xusers/users/" + response = requests.get( + url, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert response.status_code == 200, f"Failed to fetch users list: {response.status_code}" + + data = response.json() + users = data["vXUsers"] + + return [ + user for user in users + if not (7 <= user["id"] <= 18) + ] + +@pytest.fixture(scope="session") +def client_roles(ranger_config): + + username = ranger_config["auth"][0] + + url = f"{ranger_config['base_url']}/xusers/users/userName/{username}" + + response = requests.get( + url, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert response.status_code == 200, \ + f"Failed to fetch user details for {username}" + + return response.json().get("userRoleList", []) + + + +@pytest.fixture() +def get_user_by_id(ranger_config): + + def _get_user(user_id): + response = requests.get( + f"{ranger_config['base_url']}/xusers/secure/users/{user_id}", + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + if response.status_code == 200: + return response.json() + elif response.status_code in [400, 404]: + return None + else: + pytest.fail( + f"Unexpected response: {response.status_code} - {response.text}" + ) + + return _get_user + +@pytest.fixture(scope="class") +def temp_secure_user(ranger_config, client_roles): + + if "ROLE_SYS_ADMIN" not in client_roles: + pytest.fail("Admin privileges required to create secure user") + + created_user_ids = [] + + def _create_user(role_list = None): + + + worker = os.getenv("PYTEST_XDIST_WORKER", "gw0") + username = f"pytest_{worker}_{uuid.uuid4().hex[:8]}" + + role_map = { + "user": "ROLE_USER", + "admin": "ROLE_SYS_ADMIN", + "auditor": "ROLE_ADMIN_AUDITOR" + } + + if role_list: + unique_roles = {role_map.get(role.lower(), "ROLE_USER") for role in role_list} + else: + unique_roles = {"ROLE_USER"} + + final_role_list = list(unique_roles) + + payload = { + "name": username, + "firstName": "Fixture", + "lastName": "User", + "emailAddress": f"{username}@test.com", + "password": "Test@123", + "status": 1, + "isVisible": 1, + "userSource":1, # to ensure it's created as an external user + "userRoleList": final_role_list, + "syncSource":"EXTERNAL_PYTEST", + "otherAttributes":"{\"sync_source\":\"EXTERNAL_PYTEST\"}", + "groupIdList": [], + "groupNameList": [] + } + + response = create_user_with_retry( + f"{ranger_config['base_url']}/xusers/secure/users", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + #time.sleep(0.5) + + assert response.status_code == 200, f"User creation failed: {response.text}" + + created_user = response.json() + created_user_ids.append(created_user["id"]) + + print(f"\n[Fixture] Created user ID: {created_user['id']}, Roles: {final_role_list}") + + return created_user, created_user["id"] + + yield _create_user + + for user_id in created_user_ids: + print(f"[Fixture] Cleaning up user ID: {user_id}") + + response = requests.delete( + f"{ranger_config['base_url']}/xusers/users/{user_id}", + params={"forceDelete": "true"}, + auth=ranger_config["auth"], + headers={**ranger_config["headers"], "X-Requested-By": "ranger"} + ) + print("DELETE RESPONSE:", response.status_code, response.text) + if response.status_code in [200, 204]: + print(f"[Fixture] Deleted user ID: {user_id}") + + elif response.status_code in [400, 404]: + print(f"[Fixture] User ID {user_id} already deleted") + + else: + pytest.fail( + f"Unexpected error deleting user ID {user_id}: {response.text}" + ) + +@pytest.fixture(scope="class") +def temp_keyadmin_user(ranger_config, client_roles, role = "keyadmin"): + + if "ROLE_SYS_ADMIN" not in client_roles: + pytest.fail("Admin privileges required") + + created_user_ids = [] + + def _create_keyadmin(): + + #username = f"pytest_keyadmin_{uuid.uuid4().hex[:8]}" + worker = os.getenv("PYTEST_XDIST_WORKER", "gw0") + username = f"pytest_keyadmin_{worker}_{uuid.uuid4().hex[:8]}" + + payload = { + "name": username, + "firstName": "Fixture", + "lastName": "KeyAdmin", + "emailAddress": f"{username}@test.com", + "password": "Test@123", + "status": 1, + "isVisible": 1, + "userRoleList": ["ROLE_USER"], + "groupIdList": [], + "groupNameList": [] + } + + response = create_user_with_retry( + f"{ranger_config['base_url']}/xusers/secure/users", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert response.status_code == 200 + + user = response.json() + user_id = user["id"] + + print(f"\n[Fixture] Created keyadmin base: {username} ({user_id})") + + # promote to keyadmin + #requests.put( + response = requests.put( + f"{ranger_config['base_url']}/xusers/secure/users/roles/{user_id}", + json={"vXStrings": [{"value": "ROLE_KEY_ADMIN"}]}, + auth=("keyadmin", "rangerR0cks!"), + headers={**ranger_config["headers"], "X-Requested-By": "ranger"} + ) + #time.sleep(0.5) + + created_user_ids.append(user_id) + + return user, user_id + + yield _create_keyadmin + + # cleanup + for user_id in created_user_ids: + print(f"[Fixture] Cleaning keyadmin {user_id}") + + requests.put( + f"{ranger_config['base_url']}/xusers/secure/users/roles/{user_id}", + json={"vXStrings": [{"value": "ROLE_USER"}]}, + auth=("keyadmin", "rangerR0cks!"), + headers={**ranger_config["headers"], "X-Requested-By": "ranger"} + ) + + requests.delete( + f"{ranger_config['base_url']}/xusers/secure/users/id/{user_id}", + params={"forceDelete": "true"}, + auth=ranger_config["auth"], + headers={**ranger_config["headers"], "X-Requested-By": "ranger"} + ) + +@pytest.fixture(scope="class") +def temp_permission_module(ranger_config, client_roles): + + if "ROLE_SYS_ADMIN" not in client_roles: + pytest.fail("Admin privileges required to create permission module") + + created_module_ids = [] + + def _create_permission_module(): + + + module_name = f"pytest_permission_{uuid.uuid4().hex[:8]}" + payload = { + "module": module_name, + } + + # Fetch existing permissions to get a valid module and permission ID + response = requests.post( + f"{ranger_config['base_url']}/xusers/permission", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + + assert response.status_code in (200, 201), \ + f"Failed to create permission module: {response.text}" + + module = response.json() + + # Ranger may return id OR moduleId depending on version + module_id = module.get("id") or module.get("moduleId") + + created_module_ids.append(module_id) + + print(f"\n[Fixture] Created permission module: {module_name} ({module_id})") + + return module, module_id + + yield _create_permission_module + + # ---------------- CLEANUP ---------------- + for module_id in created_module_ids: + print(f"[Fixture] Cleaning permission module {module_id}") + + response = requests.delete( + f"{ranger_config['base_url']}/xusers/permission/{module_id}", + auth=ranger_config["auth"], + headers={ + **ranger_config["headers"], + "X-Requested-By": "ranger" + } + ) + + if response.status_code in (200, 204): + print(f"[Fixture] Deleted permission module {module_id}") + elif response.status_code == 404: + print(f"[Fixture] Permission module {module_id} already deleted") + else: + pytest.fail( + f"Unexpected error deleting permission module {module_id}: {response.text}" + ) + +@pytest.fixture(scope="class") +def temp_group(ranger_config): + + created_group_ids = [] + + def _create_group(): + + group_name = f"pytest_group_{uuid.uuid4().hex[:8]}" + + payload = { + "name": group_name, + "groupSource" : 1, # to ensure it's created as an external group + "groupType" : 1, + "syncSource":"EXTERNAL_PYTEST", + "otherAttributes":"{\"sync_source\":\"EXTERNAL_PYTEST\"}", + } + + response = requests.post( + f"{ranger_config['base_url']}/xusers/groups", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert response.status_code in (200, 201), response.text + + data = response.json() + group_id = data["id"] + + created_group_ids.append(group_id) + + print(f"[Fixture] Created group {group_name} ({group_id})") + + return data, group_id + + yield _create_group + + # -------- CLEANUP ---------- + for gid in created_group_ids: + print(f"[Fixture] Cleaning group {gid}") + + requests.delete( + f"{ranger_config['base_url']}/xusers/groups/{gid}", + auth=ranger_config["auth"], + params={"forceDelete": "true"}, + headers={ + **ranger_config["headers"], + "X-Requested-By": "ranger" + } + ) + +@pytest.fixture(scope="class") +def temp_permission_group(ranger_config): + + created_ids = [] + + def _create_permission_group(group_id, module_id): + + payload = { + "groupId": group_id, + "moduleId": module_id, + "isAllowed": 1 + } + + response = requests.post( + f"{ranger_config['base_url']}/xusers/permission/group", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + print("CREATE PERMISSION GROUP:", + response.status_code, response.text) + + if response.status_code in (200, 201): + data = response.json() + created_ids.append(data["id"]) + return data, data["id"] + + if response.status_code == 400 and "duplicate" in response.text.lower(): + print("[Fixture] Permission already exists — reusing") + + get_resp = requests.get( + f"{ranger_config['base_url']}/xusers/permission/group", + params={ + "groupId": group_id, + "moduleId": module_id + }, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert get_resp.status_code == 200 + + data = get_resp.json()["vXGroupPermissions"][0] + return data, data["id"] + + pytest.fail(response.text) + + yield _create_permission_group + # cleanup + for pid in created_ids: + requests.delete( + f"{ranger_config['base_url']}/xusers/permission/group/{pid}", + auth=ranger_config["auth"], + headers={ + **ranger_config["headers"], + "X-Requested-By": "ranger" + } + ) + +@pytest.fixture(scope="class") +def temp_permission_user(ranger_config): + + created_ids = [] + + def _create_permission_user(user_id, module_id): + + payload = { + "userId": user_id, + "moduleId": module_id, + "isAllowed": 1 + } + + response = requests.post( + f"{ranger_config['base_url']}/xusers/permission/user", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + print("CREATE PERMISSION USER:", + response.status_code, response.text) + + if response.status_code in (200, 201): + data = response.json() + created_ids.append(data["id"]) + return data, data["id"] + + if response.status_code == 400 and "duplicate" in response.text.lower(): + print("[Fixture] Permission already exists — reusing") + + get_resp = requests.get( + f"{ranger_config['base_url']}/xusers/permission/user", + params={ + "userId": user_id, + "moduleId": module_id + }, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert get_resp.status_code == 200 + + data = get_resp.json()["vXUserPermissions"][0] + return data, data["id"] + + pytest.fail(response.text) + + yield _create_permission_user + # cleanup + for pid in created_ids: + requests.delete( + f"{ranger_config['base_url']}/xusers/permission/user/{pid}", + auth=ranger_config["auth"], + headers={ + **ranger_config["headers"], + "X-Requested-By": "ranger" + } + ) + +@pytest.fixture(scope="class") +def temp_role(ranger_config): + + created_role_ids = [] + + def _create_role(): + + role_name = f"pytest_role_{uuid.uuid4().hex[:8]}" + + payload = { + "name": role_name + } + + response = requests.post( + f"{ranger_config['base_url']}/roles/roles", + json=payload, + auth=ranger_config["auth"], + headers=ranger_config["headers"] + ) + + assert response.status_code in (200, 201), response.text + + data = response.json() + role_id = data["id"] + + created_role_ids.append(role_id) + + print(f"[Fixture] Created role {role_name} ({role_id})") + + return data, role_id + + yield _create_role + + # -------- CLEANUP ---------- + for rid in created_role_ids: + print(f"[Fixture] Cleaning role {rid}") + + requests.delete( + f"{ranger_config['base_url']}/roles/roles/{rid}", + auth=ranger_config["auth"], + headers={ + **ranger_config["headers"], + "X-Requested-By": "ranger" + } + ) \ No newline at end of file diff --git a/functional-tests/xuserrest/readme.md b/functional-tests/xuserrest/readme.md new file mode 100644 index 0000000000..26183f0ae6 --- /dev/null +++ b/functional-tests/xuserrest/readme.md @@ -0,0 +1,137 @@ + + +# This is the main directory for running XUSERREST API functionality tests + +This directory contains automated functional tests for the XUSERREST API, covering user management, group management, permissions, secure user operations, and UGSync functionality. + +## Structure +``` +test_xuserrest/ +├── utility/ +│ ├── __init__.py +│ ├── utils.py +├── __init__.py +├── conftest.py +├── test_groups.py +├── test_permission.py +├── test_secure_user.py +├── test_ugsync.py +├── test_user.py +├── test_utility_fun.py +``` + + +## Features and Functionalities Used: + +- **Parametrization:** For running multiple test cases handling the same functionality in a single method. + +- **fetch_logs:** Fetches errors or exceptions from logs when something goes wrong. + +- **cleanup:** Cleans up all resources used while testing, ensuring re-runs of test cases. + +--- + +## `utils.py` + +- Shared utility module for all xuser REST test files — provides reusable helpers, schema validators, and API interaction functions +- Defines global constants: BIGINT_MIN/BIGINT_MAX for boundary checks, RANGER_CONTAINER_NAME/RANGER_LOG_FILE for Docker log access +init_configs() sets global auth configs for all four roles (admin, keyadmin, auditor, user) +assert_response() validates HTTP status codes (single or list) and on failure fetches Ranger logs via fetch_ranger_logs() +fetch_ranger_logs() runs docker exec to pull categorized log sections — recent activity, errors/warns, API calls, and HTTP-code-specific patterns using keyword_map +- Schema validators: validate_user_schema(), validate_secure_user_schema(), validate_xgroup_schema(), validate_external_user_schema(), group_permission_schema() — enforce field types, lengths, and value constraints +CRUD helpers: user_exists(), delete_user(), group_exists(), delete_group(), groupuser_exists(), delete_groupuser() — used for test setup/teardown +- UGSync helpers: validate_auditinfo_schema(), validate_sync_source_info() — validate sync audit responses across unix, file, and ldap sources with source-specific field schemas +- Permission helpers: build_user_permission(), build_group_permission(), build_permission_payload(), user_permission_exists() — construct and verify module permission payloads return_value_ugsync_groupusers() computes expected count of group-user sync operations for assertion in UGSync tests + + +--- + +## `conftest.py` + +- Central pytest configuration file providing shared fixtures and helpers for the entire test suite +- Defines module-level constants: CREDENTIALS, DEFAULT_HEADERS, KEYADMIN_CREDENTIALS +- Includes create_user_with_retry() — retries user POST creation up to 5 times with incremental sleep on failure +- Provides session/function/class scoped fixtures: + 1. Session: credentials, default_headers, keyadmin_credentials, ranger_config, ranger_key_admin_config, client_roles + 2. Function: ranger_session, all_users, all_schema_following_users, get_user_by_id + 3. Class: temp_secure_user, temp_keyadmin_user, temp_permission_module, temp_group, temp_permission_group, temp_permission_user, temp_role — all auto-cleanup after test class + +--- + +## `test_groups.py` + +- Tests for Group management operations across three test classes: TestSecureGroups, TestGroups, and TestGroupUsers +- Each class uses a _setup fixture (class-scoped) that provisions primary & secondary users across all roles (admin, key admin, auditor, user) and two temporary groups with auto-cleanup +- Default response code for auth failure cases is set to 404 to prevent API endpoint discovery due to Spring silent failures + +--- + +## `test_secure_user.py` + +- Tests Secure User CRUD operations via /xusers/secure/users/* endpoints within TestSecureUserEndpoint +- Class-scoped _setup provisions primary & secondary users across all roles (admin, keyadmin, auditor, user) with auto-cleanup +- Defines BIGINT_MIN / BIGINT_MAX constants for boundary value testing +- Covers role-based access control across all operations — verifying both allowed and restricted role/target combinations +- Validates immutability of fields (name, id, createDate) on PUT and silent 204 behavior on malformed bulk DELETE payloads +Uses forceDelete=true and X-Requested-By: ranger headers where required by the API + +--- + +## `test_ugsync.py` + +- Covers userinfo endpoint for user-group association creation with schema validation +- Tests UGSync (User-Group Sync) operations within TestUgsync for syncing users/groups from external sources (LDAP, UNIX, FILE, MULTI_SOURCES) +- Class-scoped _setup provisions primary & secondary users across all roles (admin, keyadmin, auditor, user) and two temp groups with auto-cleanup +- All endpoints are admin-only; unauthorized roles (keyadmin, auditor, user) consistently return 404 due to Spring's silent failure instead of 403 +- Invalid payloads (bad group/user names, missing fields) often return 200 as silent failures — response body is validated instead of status code +- Validates addUsers/delUsers group membership changes are reflected via follow-up GET calls +- Covers sync source audit logging for all source types with schema validation via validate_auditinfo_schema() and validate_sync_source_info() +- Tests group/user visibility toggling (sets isVisible=0) and verifies persistence via follow-up GET +- Handles semi-failure scenarios (mixed valid/invalid users in one payload) and asserts partial success count in response + +--- + +## `test_user.py` + +- Tests User CRUD operations via /xusers/users/* endpoints within TestUsers +- Class-scoped _setup provisions primary & secondary users across all roles (admin, keyadmin, auditor, user) with auto-cleanup +- Covers role-based access control across all operations — verifying both allowed and restricted role/target combinations +- Tests role assignment logic via /roleassignments endpoint — validating priority order: whiteListUser > whiteListGroup > userMap > groupMap > default +- Validates Spring's silent 404 failure pattern for unauthorized POST/DELETE operations (non-admin roles) +- Tests external user creation flow — 204 on first call (new user), 200 on second call (existing user) +- Covers userinfo endpoint for user-group association creation with schema validation +Validates immutability enforcement and invalid role assignment (ROLE_KEY_ADMIN, ROLE_NON_EXISTEN) returning 400/403 + +--- + +## `test_utility_fun.py` + +- Tests Lookup and AuthSession utility endpoints within TestLookup and AuthSession classes +- Class-scoped _setup provisions users across all roles (admin, keyadmin, auditor, user) with auto-cleanup +- All four roles are permitted for every endpoint — no role-based restriction tests +- TestLookup covers /xusers/lookup/users, /xusers/lookup/groups, and /xusers/lookup/principals — validating vXStrings schema when response is non-empty +- AuthSession covers /xusers/authSessions with query param variations — validates vXAuthSessions or totalCount in response diff --git a/functional-tests/xuserrest/test_groups.py b/functional-tests/xuserrest/test_groups.py new file mode 100644 index 0000000000..6552e18d70 --- /dev/null +++ b/functional-tests/xuserrest/test_groups.py @@ -0,0 +1,1489 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from urllib import * +from xuserrest.utility.utils import * +import uuid +import string +import pytest +import requests +from datetime import datetime +import random + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +class TestSecureGroups: + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_group, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + # ---------- secondary users ---------- + cls.admin1, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin1, _ = temp_keyadmin_user() + cls.audit1, _ = temp_secure_user(["auditor"]) + cls.user1, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config1 = (cls.admin1["name"], "Test@123") + cls.ranger_key_admin_config1 = (cls.ranger_key_admin1["name"], "Test@123") + cls.ranger_auditor_config1 = (cls.audit1["name"], "Test@123") + cls.ranger_user_config1 = (cls.user1["name"], "Test@123") + + cls.ranger_admin_id1 = cls.admin1["id"] + cls.ranger_key_admin_id1 = cls.ranger_key_admin1["id"] + cls.ranger_auditor_id1 = cls.audit1["id"] + cls.ranger_user_id1 = cls.user1["id"] + + + # ---------- group details ---------- + cls.group, cls.group_id = temp_group() + cls.group1, cls.group_id1 = temp_group() + + # ---------- common ---------- + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")]) + def test_get_groups_by_id(self, role, auth): + auth = getattr(self, auth) + + if role == "user": + assign_groups_to_user(self.user["name"], [self.group["name"]], self.ranger_admin_config, self.base_url, self.headers) + data = fetch_groups_for_user_id(self.user["id"], self.ranger_admin_config, self.base_url, self.headers) + assert check_group_in_user_groups(self.group_id, data), f"Group with id {self.group_id} is not assigned to user with id {self.user['id']}" + + group_exists(self.group_id, self.ranger_admin_config, self.base_url, self.headers) + + response = requests.get( + f"{self.base_url}/xusers/secure/groups/{self.group_id}", + auth = auth, + headers=self.headers, + ) + assert_response(response, 200, f"Get group by id failed and got {response.status_code} instead of 200 for role {role}") + response_data = response.json() + validate_xgroup_schema(response_data) + assert response_data["id"] == self.group_id + assert response_data["name"] == self.group["name"] + + + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + + def test_create_group(self, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to create group") + auth = getattr(self, auth) + group_name = "test_group_" + str(uuid.uuid4())[:8] + + mandatory_fields = ["name"] + + payload ={ + "name": group_name, + "groupSource": 0 + } + + mandatory_field_check_in_response(payload, mandatory_fields) + + + response = requests.post( + f"{self.base_url}/xusers/secure/groups", + auth = auth, + json=payload, + headers=self.headers, + ) + assert_response(response, 200, f"Group creation failed and got {response.status_code} instead of 200") + response_data = response.json() + validate_xgroup_schema(response_data) + if 'description' in payload: + assert response_data["description"] == payload["description"] + else: + assert response_data["description"] == payload["name"] + + delete_group(response_data["id"], auth, self.base_url, self.headers) + print(f"Group with name {group_name} created successfully with id {response_data['id']}") + + + @pytest.mark.positive + @pytest.mark.put + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_update_group(self, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to update group") + auth = getattr(self, auth) + + mandatory_fields = ["id","name"] + payload ={ + "name": self.group["name"], + "id": self.group_id, + "groupSource": 0 + } + + assert mandatory_field_check_in_response(payload, mandatory_fields), f"Mandatory fields {mandatory_fields} are not present in payload for update group" + assert payload["name"] == self.group["name"], f"Name field in payload is not same as existing group name for update group" + + group_exists(self.group_id, self.ranger_admin_config, self.base_url, self.headers) + + response = requests.put( + f"{self.base_url}/xusers/secure/groups/{self.group_id}", + auth = auth, + json=payload, + headers=self.headers, + ) + assert_response(response, 200, f"Group update failed and got {response.status_code} instead of 200") + response_data = response.json() + validate_xgroup_schema(response_data) + assert response_data["groupSource"] == 0, f"Group source is not same as expected after update group" + + + @pytest.mark.positive + @pytest.mark.put + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_update_group_visibility(self, role, auth, request): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to update group") + auth = getattr(self, auth) + + temp_group, temp_group_id = request.getfixturevalue("temp_group")() + temp_group1, temp_group_id1 = request.getfixturevalue("temp_group")() + payload = { + str(temp_group_id): 0, + str(temp_group_id1): 0 + } + for group_id in payload.keys(): + flag, data = group_exists(int(group_id), self.ranger_admin_config, self.base_url, self.headers) + assert flag, f"Group with id {group_id} does not exist before update visibility for group which is expected for test case: {role}" + assert data["isVisible"] != 0, f"Group with id {group_id} is not visible before update visibility for group which is expected for test case: {role}" + + response = requests.put( + f"{self.base_url}/xusers/secure/groups/visibility", + auth = auth, + json=payload, + headers=self.headers, + ) + assert_response(response, 204, f"Group update failed and got {response.status_code} instead of 204") + + # checking if groups are updated to invisible after update visibility for group + flag, data = group_exists(temp_group_id, self.ranger_admin_config, self.base_url, self.headers) + assert data["isVisible"] == 0, f"Group with id {temp_group_id} is not updated to invisible after update visibility for group which is expected for test case" + + + @pytest.mark.positive + @pytest.mark.delete + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_delete_group_by_id(self, role, auth, request): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to delete group") + auth = getattr(self, auth) + + temp_group, temp_group_id = request.getfixturevalue("temp_group")() + response = requests.delete( + f"{self.base_url}/xusers/secure/groups/id/{temp_group_id}", + auth = auth, + headers=self.headers, + ) + assert_response(response, 204, f"Group deletion failed and got {response.status_code} instead of 204 for role {role}") + flag, data = group_exists(temp_group_id, self.ranger_admin_config, self.base_url, self.headers) + assert not flag, f"Group with id {temp_group_id} still exists after deletion which is not expected for test case: {role}" + + + @pytest.mark.positive + @pytest.mark.delete + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_delete_group_by_userName(self, role, auth, request): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to delete group") + auth = getattr(self, auth) + + temp_group, temp_group_id = request.getfixturevalue("temp_group")() + flag = group_exists_by_name(temp_group["name"], self.ranger_admin_config, self.base_url, self.headers) + assert flag, f"Group with name {temp_group['name']} does not exist before deletion which is expected for test case: {role}" + response = requests.delete( + f"{self.base_url}/xusers/secure/groups/{temp_group['name']}", + auth = auth, + headers=self.headers, + ) + assert_response(response, 204, f"Group deletion failed and got {response.status_code} instead of 204 for role {role}") + flag = group_exists_by_name(temp_group["name"], self.ranger_admin_config, self.base_url, self.headers) + assert not flag, f"Group with name {temp_group['name']} still exists after deletion which is not expected for test case: {role}" + + + @pytest.mark.positive + @pytest.mark.delete + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_delete_group_by_userName_bulk(self, role, auth, request): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to delete group") + auth = getattr(self, auth) + + temp_group, temp_group_id = request.getfixturevalue("temp_group")() + temp_group1, temp_group_id1 = request.getfixturevalue("temp_group")() + + names = [temp_group["name"], temp_group1["name"]] + + for name in names: + flag = group_exists_by_name(name, self.ranger_admin_config, self.base_url, self.headers) + assert flag, f"Group with name {name} does not exist before deletion which is expected for test case: {role}" + + payload = { + "vXStrings": [{"value": name} for name in names] + } + + response = requests.delete( + f"{self.base_url}/xusers/secure/groups/delete?forceDelete=true", + auth=auth, + json=payload, + headers=self.headers, + ) + assert_response(response, 204, f"Group deletion failed and got {response.status_code} instead of 204 for role {role}") + + for name in names: + flag = group_exists_by_name(name, self.ranger_admin_config, self.base_url, self.headers) + assert not flag, f"Group with name {name} still exists after deletion which is not expected for test case: {role}" + + + + # NEGATIVE TEST CASES + + @pytest.mark.negative + @pytest.mark.get + @pytest.mark.parametrize( + "test_case",[("invalid access by user with out group membership")]) + def test_get_group_by_id_negative(self, test_case, request): + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")() + auth = (temp_user["name"], "Test@123") + data = fetch_groups_for_user_id(temp_user_id, self.ranger_admin_config, self.base_url, self.headers) + assert not check_group_in_user_groups(self.group_id, data), f"Group with id {self.group_id} is assigned to user with id {temp_user_id} which is not expected for test case: {test_case}" + if test_case == "invalid access by user with out group membership": + response = requests.get( + f"{self.base_url}/xusers/secure/groups/{self.group_id}", + auth = auth, + headers=self.headers, + ) + assert_response(response, 403, f"Get group by id should have failed with 403 but got {response.status_code} instead for test case: {test_case}") + + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize( + "test_case, auth", + [("invalid auth by keyadmin", "ranger_key_admin_config"), ("invalid auth by auditor", "ranger_auditor_config"), ("invalid auth by user", "ranger_user_config"), + ("missing mandatory field name", "ranger_admin_config"), ("malformed format no name value(empty string)", "ranger_admin_config"), ("invalid format for mandatory field", "ranger_admin_config")]) + def test_create_group_negative(self, test_case, auth): + auth = getattr(self, auth) + group_name = "test_group_" + str(uuid.uuid4())[:8] + payload ={ + "name": group_name, + "groupSource": 0 + } + response_code = 404 # Set default response to 404 for auth failures to prevent API endpoint discovery due to spring slient failure. + mandatory_fields = ["name"] + + if test_case == "missing mandatory field name": + del payload["name"] + response_code = 404 + + elif test_case == "malformed format no name value(empty string)": + payload = r'{"name": "" ,"groupSource": 0}' + response_code = 400 + + elif test_case == "invalid format for mandatory field": + payload = r'{"name": RANDOM ,"groupSource": 0}' + response_code = 400 + + response = requests.post( + f"{self.base_url}/xusers/secure/groups", + auth = auth, + json=payload, + headers=self.headers, + ) + + assert_response(response, response_code, f"Group creation should have failed with {response_code} but got {response.status_code} instead for test case: {test_case}") + + @pytest.mark.negative + @pytest.mark.put + @pytest.mark.parametrize( + "test_case, auth", + [("invalid auth by keyadmin", "ranger_key_admin_config"), ("invalid auth by auditor", "ranger_auditor_config"), ("invalid auth by user", "ranger_user_config"), + ("missing mandatory field name", "ranger_admin_config"), ("missing mandatory field id", "ranger_admin_config"),("updating immutable field username", "ranger_admin_config")]) + def test_update_group_negative(self, test_case, auth): + auth = getattr(self, auth) + payload ={ + "name": self.group["name"], + "id": self.group_id, + "groupSource": 0 + } + response_code = 403 + mandatory_fields = ["id","name"] + + if test_case == "missing mandatory field name": + del payload["name"] + response_code = 404 + + elif test_case == "missing mandatory field id": + del payload["id"] + response_code = 400 + + elif test_case == "updating immutable field username": + payload["name"] = "new_username" + assert payload["name"] != self.group["name"], f"Name field in payload is same as existing group name for update group which is not expected for test case: {test_case}" + response_code = 400 + + response = requests.put( + f"{self.base_url}/xusers/secure/groups/{self.group_id}", + auth = auth, + json=payload, + headers=self.headers, + ) + assert_response(response, response_code, f"Group update should have failed with {response_code} but got {response.status_code} instead for test case: {test_case}") + + @pytest.mark.negative + @pytest.mark.put + @pytest.mark.parametrize( + "test_case, auth", + [("invalid auth by keyadmin", "ranger_key_admin_config"), ("invalid auth by auditor", "ranger_auditor_config"), ("invalid auth by user", "ranger_user_config"), + ("updating visibility with invalid group id", "ranger_admin_config")]) + def test_update_group_visibility_negative(self, test_case, auth): + auth = getattr(self, auth) + + if test_case == "updating visibility with invalid group id": + rand_id = random.randint(999999, 9999999) + flg, data = group_exists(rand_id, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group with id {rand_id} exists which is not expected for test case: {test_case}" + group_id = rand_id + response_code = 404 + + else: + group_id = self.group_id + response_code = 403 + + payload = { + str(group_id): 0 + } + + response = requests.put( + f"{self.base_url}/xusers/secure/groups/visibility", + auth = auth, + json=payload, + headers=self.headers, + ) + assert_response(response, response_code, f"Group update should have failed with {response_code} but got {response.status_code} instead for test case: {test_case}") + + @pytest.mark.negative + @pytest.mark.delete + @pytest.mark.parametrize("test_case, auth", [("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config"), ("non existent id", "ranger_admin_config")]) + def test_delete_group_by_id_negative(self, test_case, auth, request): + auth = getattr(self, auth) + if test_case == "non existent id": + rand_id = random.randint(999999, 9999999) + flg, data = group_exists(rand_id, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group with id {rand_id} exists which is not expected for test case: {test_case}" + temp_group_id = rand_id + expected_response_code = 404 + else: + temp_group_id = self.group_id + expected_response_code = 404 # Set default response to 404 for auth failures to prevent API endpoint discovery due to spring slient failure. + response = requests.delete( + f"{self.base_url}/xusers/secure/groups/id/{temp_group_id}", + auth = auth, + headers=self.headers, + ) + assert_response(response, expected_response_code, f"Group deletion failed and got {response.status_code} instead of {expected_response_code} for test case {test_case}") + + @pytest.mark.negative + @pytest.mark.delete + @pytest.mark.parametrize("test_case, auth", [("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config"), ("non existent name", "ranger_admin_config")]) + def test_delete_group_by_userName_negative(self, test_case, auth, request): + auth = getattr(self, auth) + if test_case == "non existent name": + name = "non_existent_group_name_" + str(uuid.uuid4())[:8] + flg = group_exists_by_name(name, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group with name {name} exists which is not expected for test case: {test_case}" + expected_response_code = 400 + else: + temp_group = self.group + name = temp_group["name"] + expected_response_code = 404 # Set default response to 404 for auth failures to prevent API endpoint discovery due to spring slient failure. + + response = requests.delete( + f"{self.base_url}/xusers/secure/groups/{name}", + auth = auth, + headers=self.headers, + ) + assert_response(response, expected_response_code, f"Group deletion failed and got {response.status_code} instead of {expected_response_code} for test case {test_case}") + + @pytest.mark.negative + @pytest.mark.delete + @pytest.mark.parametrize("test_case, auth", [("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config"), + ("non existent name", "ranger_admin_config")]) + def test_delete_group_by_userName_bulk_negative(self, test_case, auth, request): + auth = getattr(self, auth) + + temp_group, temp_group_id = request.getfixturevalue("temp_group")() + temp_group1, temp_group_id1 = request.getfixturevalue("temp_group")() + if test_case == "non existent name": + name = "non_existent_group_name_" + str(uuid.uuid4())[:8] + flg = group_exists_by_name(name, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group with name {name} exists which is not expected for test case: {test_case}" + names = [name] + expexted_response_code = 400 + + else: + names = [temp_group["name"], temp_group1["name"]] + + for name in names: + flag = group_exists_by_name(name, self.ranger_admin_config, self.base_url, self.headers) + assert flag, f"Group with name {name} does not exist before deletion which is expected for test case: {test_case}" + expexted_response_code = 404 # Set default response to 404 for auth failures to prevent API endpoint discovery due to spring slient failure. + payload = { + "vXStrings": [{"value": name} for name in names] + } + + response = requests.delete( + f"{self.base_url}/xusers/secure/groups/delete?forceDelete=true", + auth=auth, + json=payload, + headers=self.headers, + ) + assert_response(response, expexted_response_code, f"Group deletion failed and got {response.status_code} instead of {expexted_response_code} for test case {test_case}") + + + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +class TestGroups: + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_group, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + # ---------- secondary users ---------- + cls.admin1, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin1, _ = temp_keyadmin_user() + cls.audit1, _ = temp_secure_user(["auditor"]) + cls.user1, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config1 = (cls.admin1["name"], "Test@123") + cls.ranger_key_admin_config1 = (cls.ranger_key_admin1["name"], "Test@123") + cls.ranger_auditor_config1 = (cls.audit1["name"], "Test@123") + cls.ranger_user_config1 = (cls.user1["name"], "Test@123") + + cls.ranger_admin_id1 = cls.admin1["id"] + cls.ranger_key_admin_id1 = cls.ranger_key_admin1["id"] + cls.ranger_auditor_id1 = cls.audit1["id"] + cls.ranger_user_id1 = cls.user1["id"] + + + # ---------- group details ---------- + cls.group, cls.group_id = temp_group() + cls.group1, cls.group_id1 = temp_group() + + # ---------- common ---------- + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")]) + def test_get_groups_by_id(self, role, auth): + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + group_exists(temp_group_id, self.ranger_admin_config, self.base_url, self.headers) + + if role == "user": + assign_groups_to_user(self.user["name"], [temp_group["name"]], self.ranger_admin_config, self.base_url, self.headers) + data = fetch_groups_for_user_id(self.user["id"], self.ranger_admin_config, self.base_url, self.headers) + assert check_group_in_user_groups(temp_group_id, data), f"Group with id {temp_group_id} is not assigned to user with id {self.user['id']} which is not expected for test case: {role}" + + response = requests.get( + f"{self.base_url}/xusers/groups/{temp_group_id}", + auth = auth, + headers=self.headers, + ) + assert_response(response, 200, f"Get group by id failed and got {response.status_code} instead of 200") + response_data = response.json() + validate_xgroup_schema(response_data) + assert response_data["id"] == temp_group_id + assert response_data["name"] == temp_group["name"] + if role == "user": + assert response_data["updatedBy"] == "*****", f"Group with id {temp_group_id} should be masked for user role but it did not, which is not expected for test case: {role}" + + @pytest.mark.positive + @pytest.mark.parametrize( + "params, test_case", + [ + ({}, "default_request"), + ({"startIndex": 0, "pageSize": 10}, "pagination_basic"), + ({"name": "public"}, "filter_name"), + ({"isVisible": 1}, "filter_is_visible"), + ({"groupSource": 0}, "filter_group_source_internal"), + ({"groupSource": 1}, "filter_group_source_external"), + ({"syncSource": "Unix"}, "filter_sync_source"), + ({"name": "public", "isVisible": 1}, "filter_combined"), + ({"sortBy": "name", "sortType": "asc"}, "sorting_asc"), + ({"sortBy": "name", "sortType": "desc"}, "sorting_desc"), + ({"startIndex": 0, "pageSize": 5, "name": "public"}, "pagination_with_filter"), + ], + ids=[ + "default", + "pagination-basic", + "filter-name", + "filter-isVisible", + "filter-groupSource-internal", + "filter-groupSource-external", + "filter-syncSource", + "filter-combined", + "sorting-asc", + "sorting-desc", + "pagination-with-filter", + ], + ) + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("key admin", "ranger_key_admin_config"),("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")], + ids=["admin", "key admin", "auditor", "user"], + ) + def test_get_group(self, params, test_case, role, auth): + + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/groups", + params=params, + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, f"Failed for case: {test_case}") + + data = response.json() + + assert "startIndex" in data and isinstance(data["startIndex"], int) + assert "pageSize" in data and isinstance(data["pageSize"], int) + assert "totalCount" in data and isinstance(data["totalCount"], int) + assert "resultSize" in data and isinstance(data["resultSize"], int) + assert isinstance(data, dict), f"Invalid response format in {test_case}" + + assert "vXGroups" in data, f"'vXGroups' not found in {test_case}" + assert isinstance(data["vXGroups"], list) + + groups = data["vXGroups"] + + if not groups: + assert data.get("totalCount", 0) == 0 + assert data.get("resultSize", 0) == 0 + return + + + validate_xgroup_schema(groups[0]) # checking for first group in the list, as all groups in the response should have same schema + + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config"), ("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")]) + def test_get_group_count(self, role, auth): + auth = getattr(self, auth) + response = requests.get( + f"{self.base_url}/xusers/groups/count", + auth=auth, + headers=self.headers + ) + assert_response(response, 200, f"Failed to get group count and got {response.status_code} instead of 200 for role {role}") + data = response.json() + assert "value" in data and isinstance(data["value"], int), f"Invalid response format for group count for role {role}" + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")]) + def test_get_groups_by_groupName(self, role, auth): + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + group_exists(temp_group_id, self.ranger_admin_config, self.base_url, self.headers) + + if role == "user": + assign_groups_to_user(self.user["name"], [temp_group["name"]], self.ranger_admin_config, self.base_url, self.headers) + data = fetch_groups_for_user_id(self.user["id"], self.ranger_admin_config, self.base_url, self.headers) + assert check_group_in_user_groups(temp_group_id, data), f"Group with id {temp_group_id} is not assigned to user with id {self.user['id']} which is not expected for test case: {role}" + + response = requests.get( + f"{self.base_url}/xusers/groups/groupName/{temp_group['name']}", + auth = auth, + headers=self.headers, + ) + assert_response(response, 200, f"Get group by name failed and got {response.status_code} instead of 200") + response_data = response.json() + validate_xgroup_schema(response_data) + assert response_data["id"] == temp_group_id + assert response_data["name"] == temp_group["name"] + # if role == "user": + # assert response_data["updatedBy"] == "*****", f"Group with id {temp_group_id} should be masked for user role but it did not, which is not expected for test case: {role}" + + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize( + "testcase, ,role, auth", + [("non existing group name", "admin", "ranger_admin_config"), ("existing group name", "admin", "ranger_admin_config")],) + def test_create_group_by_name_positive(self, testcase, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to create group") + auth = getattr(self, auth) + if testcase == "non existing group name": + group_name = "non_existing_group_name_" + str(uuid.uuid4())[:8] + flg = group_exists_by_name(group_name, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group with name {group_name} exists which is not expected for test case: {testcase}" + else: + group_name = self.group["name"] + flg= group_exists_by_name(group_name, self.ranger_admin_config, self.base_url, self.headers) + assert flg, f"Group with name {group_name} does not exist which is not expected for test case: {testcase}" + + payload = { + "name": group_name, + "description": f"Description for {group_name} via pytest fun()" # since description changes we can validate that it is updating + } + + response = requests.post( + f"{self.base_url}/xusers/groups", + auth=auth, + json=payload, + headers=self.headers + ) + assert_response(response, 200, f"Failed to get group by name and got {response.status_code} instead of 200 for test case: {testcase}") + response_data = response.json() + validate_xgroup_schema(response_data) + assert response_data["name"] == group_name, f"Group name in response is not same as requested group name for test case: {testcase}" + assert response_data["description"] == payload["description"], f"Group description in response is not same as requested group description for test case: {testcase}" + if testcase == "non existing group name": + delete_group(response_data["id"], auth, self.base_url, self.headers) + print(f"Group with name {group_name} created successfully with id {response_data['id']} and deleted successfully for test case: {testcase}") + else: + assert response_data["id"] == self.group_id, f"Group id in response is not same as existing group id for test case: {testcase}" + + + + @pytest.mark.positive + @pytest.mark.put + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_update_group(self, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to update group") + auth = getattr(self, auth) + + mandatory_fields = ["id","name"] + payload ={ + "name": self.group["name"], + "id": self.group_id, + "groupSource": 0 + } + + assert mandatory_field_check_in_response(payload, mandatory_fields), f"Mandatory fields {mandatory_fields} are not present in payload for update group" + assert payload["name"] == self.group["name"], f"Name field in payload is not same as existing group name for update group" + + group_exists(self.group_id, self.ranger_admin_config, self.base_url, self.headers) + + response = requests.put( + f"{self.base_url}/xusers/groups", + auth = auth, + json=payload, + headers=self.headers, + ) + assert_response(response, 200, f"Group update failed and got {response.status_code} instead of 200") + response_data = response.json() + validate_xgroup_schema(response_data) + assert response_data["groupSource"] == 0, f"Group source is not same as expected after update group" + + @pytest.mark.positive + @pytest.mark.delete + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_delete_group_by_id(self, role, auth, request): + grp, grp_id = request.getfixturevalue("temp_group")() + auth = getattr(self, auth) + + response = requests.delete( + f"{self.base_url}/xusers/groups/{grp_id}", + auth = auth, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert_response(response, 204, f"Group deletion failed and got {response.status_code} instead of 204 for role {role}") + flg, dat = group_exists(grp_id, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group with id {grp_id} still exists after deletion which is not expected for test case: {role}" + + + @pytest.mark.positive + @pytest.mark.delete + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_delete_group_by_groupName(self, role, auth, request): + grp, grp_id = request.getfixturevalue("temp_group")() + auth = getattr(self, auth) + name = grp["name"] + flag = group_exists_by_name(name, self.ranger_admin_config, self.base_url, self.headers) + assert flag, f"Group with name {name} does not exist before deletion which is expected for test case: {role}" + response = requests.delete( + f"{self.base_url}/xusers/groups/groupName/{name}", + auth = auth, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert_response(response, 204, f"Group deletion failed and got {response.status_code} instead of 204 for role {role}") + flag = group_exists_by_name(name, self.ranger_admin_config, self.base_url, self.headers) + assert not flag, f"Group with name {name} still exists after deletion which is not expected for test case: {role}" + + + @pytest.mark.positive + @pytest.mark.delete + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_delete_group_user_by_name(self, role, auth, request): + grp, grp_id = request.getfixturevalue("temp_group")() + auth = getattr(self, auth) + name = grp["name"] + flag = group_exists_by_name(name, self.ranger_admin_config, self.base_url, self.headers) + assert flag, f"Group with name {name} does not exist before deletion which is expected for test case: {role}" + assign_groups_to_user(self.user["name"], [name], self.ranger_admin_config, self.base_url, self.headers) + response = requests.delete( + f"{self.base_url}/xusers/group/{name}/user/{self.user['name']}", + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"}, + ) + + assert_response(response, 204, f"Group deletion failed and got {response.status_code} instead of 204 for role {role}") + data = fetch_groups_for_user_id(self.user["id"], self.ranger_admin_config, self.base_url, self.headers) + assert not check_group_in_user_groups(grp_id, data), f"Group with id {grp_id} is assigned to user with id {self.user['id']} which is not expected for test case: {role}" + + + # NEGATIVE TEST CASES + + @pytest.mark.negative + @pytest.mark.get + @pytest.mark.parametrize( + "test_case",[("invalid access by user with out group membership")]) + def test_get_group_by_id_negative(self, test_case, request): + temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")() + auth = (temp_user["name"], "Test@123") + data = fetch_groups_for_user_id(temp_user_id, self.ranger_admin_config, self.base_url, self.headers) + assert not check_group_in_user_groups(self.group_id, data), f"Group with id {self.group_id} is assigned to user with id {temp_user_id} which is not expected for test case: {test_case}" + if test_case == "invalid access by user with out group membership": + response = requests.get( + f"{self.base_url}/xusers/groups/{self.group_id}", + auth = auth, + headers=self.headers, + ) + assert_response(response, 403, f"Get group by id should have failed with 403 but got {response.status_code} instead for test case: {test_case}") + + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize( + "test_case, auth", + [("invalid auth by keyadmin", "ranger_key_admin_config"), ("invalid auth by auditor", "ranger_auditor_config"), ("invalid auth by user", "ranger_user_config"), + ("missing mandatory field name", "ranger_admin_config")]) + def test_create_group_by_name_negative(self, test_case, auth): + auth = getattr(self, auth) + group_name = "test_group_" + str(uuid.uuid4())[:8] + payload = { + "name": group_name + } + response_code = 404 # Set default response to 404 for auth failures to prevent API endpoint discovery due to spring slient failure. + + if test_case == "missing mandatory field name": + del payload["name"] + response_code = 404 + + response = requests.post( + f"{self.base_url}/xusers/groups", + auth=auth, + json=payload, + headers=self.headers + ) + assert_response(response, response_code, f"Group creation should have failed with {response_code} but got {response.status_code} instead for test case: {test_case}") + + + @pytest.mark.negative + @pytest.mark.put + @pytest.mark.parametrize( + "test_case, auth", + [("invalid auth by keyadmin", "ranger_key_admin_config"), ("invalid auth by auditor", "ranger_auditor_config"), ("invalid auth by user", "ranger_user_config"), + ("missing mandatory field name", "ranger_admin_config"), ("missing mandatory field id", "ranger_admin_config"),("updating immutable field username", "ranger_admin_config")]) + def test_update_group_negative(self, test_case, auth): + auth = getattr(self, auth) + payload ={ + "name": self.group["name"], + "id": self.group_id, + "groupSource": 0 + } + response_code = 403 + mandatory_fields = ["id","name"] + + if test_case == "missing mandatory field name": + del payload["name"] + response_code = 404 + + elif test_case == "missing mandatory field id": + del payload["id"] + response_code = 400 + + elif test_case == "updating immutable field username": + payload["name"] = "new_username" + assert payload["name"] != self.group["name"], f"Name field in payload is same as existing group name for update group which is not expected for test case: {test_case}" + response_code = 400 + + response = requests.put( + f"{self.base_url}/xusers/groups", + auth = auth, + json=payload, + headers=self.headers, + ) + assert_response(response, response_code, f"Group update should have failed with {response_code} but got {response.status_code} instead for test case: {test_case}") + + + @pytest.mark.negative + @pytest.mark.delete + @pytest.mark.parametrize( + "test_case, auth", [("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config"), + ("non existent id", "ranger_admin_config"), ("group - assigned to role", "ranger_admin_config")]) + def test_delete_group_negative(self, test_case, auth, request): + auth = getattr(self, auth) + if test_case == "non existent id": + rand_id = random.randint(999999, 9999999) + flg, data = group_exists(rand_id, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group with id {rand_id} exists which is not expected for test case: {test_case}" + temp_group_id = rand_id + expected_response_code = 404 + elif test_case == "group - assigned to role": + temp_group, temp_group_id = request.getfixturevalue("temp_group")() + temp_role, temp_role_id = request.getfixturevalue("temp_role")() + assign_role_to_group(temp_group["name"], temp_role, self.ranger_admin_config, self.base_url, self.headers) + expected_response_code = 400 + print(" it is executed for test case: ", test_case) + else: + temp_group_id = self.group_id + expected_response_code = 404 # Set default response to 404 for auth failures to prevent API endpoint discovery due to spring slient failure. + + response = requests.delete( + f"{self.base_url}/xusers/groups/{temp_group_id}", + auth = auth, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert_response(response, expected_response_code, f"Group deletion failed and got {response.status_code} instead of {expected_response_code} for test case {test_case}") + + + @pytest.mark.negative + @pytest.mark.delete + @pytest.mark.parametrize( + "test_case, auth", [("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config"), + ("non existent name", "ranger_admin_config"), ("group - assigned to role", "ranger_admin_config")]) + def test_delete_group_by_groupName_negative(self, test_case, auth, request): + auth = getattr(self, auth) + if test_case == "non existent name": + name = "non_existent_group_name_" + str(uuid.uuid4())[:8] + flg = group_exists_by_name(name, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group with name {name} exists which is not expected for test case: {test_case}" + expected_response_code = 400 + elif test_case == "group - assigned to role": + temp_group, temp_group_id = request.getfixturevalue("temp_group")() + temp_role, temp_role_id = request.getfixturevalue("temp_role")() + assign_role_to_group(temp_group["name"], temp_role, self.ranger_admin_config, self.base_url, self.headers) + name = temp_group["name"] + expected_response_code = 400 + print(" it is executed for test case: ", test_case) + else: + temp_group = self.group + name = temp_group["name"] + expected_response_code = 404 # Set default response to 404 for auth failures to prevent API endpoint discovery due to spring slient failure. + + response = requests.delete( + f"{self.base_url}/xusers/groups/groupName/{name}", + auth = auth, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert_response(response, expected_response_code, f"Group deletion failed and got {response.status_code} instead of {expected_response_code} for test case {test_case}") + + + @pytest.mark.negative + @pytest.mark.delete + @pytest.mark.parametrize( + "test_case, auth", [ + ("invalid_auth by user", "ranger_user_config"), ("invalid auth by keyadmin", "ranger_key_admin_config"), ("invalid auth by auditor", "ranger_auditor_config"), + ("non existent user name", "ranger_admin_config"), ("non existent group name", "ranger_admin_config")]) + def test_delete_group_user_by_name_negative(self, test_case, auth, request): + auth = getattr(self, auth) + if test_case == "non existent user name": + user_name = "non_existent_user_name_" + str(uuid.uuid4())[:8] + flg = user_exists_by_name(user_name, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"User with name {user_name} exists which is not expected for test case: {test_case}" + expected_response_code = 400 + group_name = self.group["name"] + elif test_case == "non existent group name": + group_name = "non_existent_group_name_" + str(uuid.uuid4())[:8] + flg = group_exists_by_name(group_name, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group with name {group_name} exists which is not expected for test case: {test_case}" + expected_response_code = 400 + user_name = self.user["name"] + else: + user_name = self.user["name"] + group_name = self.group["name"] + expected_response_code = 404 # Set default response to 404 for auth failures to prevent API endpoint discovery due to spring slient failure. + + response = requests.delete( + f"{self.base_url}/xusers/group/{group_name}/user/{user_name}", + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"}, + ) + + assert_response(response, expected_response_code, f"Group deletion failed and got {response.status_code} instead of {expected_response_code} for test case {test_case}") + + + + + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +class TestGroupUsers: + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_group, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + # ---------- secondary users ---------- + cls.admin1, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin1, _ = temp_keyadmin_user() + cls.audit1, _ = temp_secure_user(["auditor"]) + cls.user1, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config1 = (cls.admin1["name"], "Test@123") + cls.ranger_key_admin_config1 = (cls.ranger_key_admin1["name"], "Test@123") + cls.ranger_auditor_config1 = (cls.audit1["name"], "Test@123") + cls.ranger_user_config1 = (cls.user1["name"], "Test@123") + + cls.ranger_admin_id1 = cls.admin1["id"] + cls.ranger_key_admin_id1 = cls.ranger_key_admin1["id"] + cls.ranger_auditor_id1 = cls.audit1["id"] + cls.ranger_user_id1 = cls.user1["id"] + + + # ---------- group details ---------- + cls.group, cls.group_id = temp_group() + cls.group1, cls.group_id1 = temp_group() + + # ---------- common ---------- + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config"), ("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")]) + def test_get_groupusers_by_id(self, role, auth): + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + temp_user_id = self.ranger_user_id1 + + grp_user = create_groupuser(temp_group["name"], temp_user_id, self.ranger_admin_config, self.base_url, self.headers) + grp_user_id = grp_user["id"] + + assert groupuser_exists(grp_user_id, self.ranger_admin_config, self.base_url, self.headers), f"Group-User association with id {grp_user_id} does not exist which is required for test case: {role}" + response = requests.get( + f"{self.base_url}/xusers/groupusers/{grp_user_id}", + auth = auth, + headers=self.headers, + ) + assert_response(response, 200, f"Get group-user association by id failed and got {response.status_code} instead of 200 for role {role}") + response_data = response.json() + assert response_data["parentGroupId"] == temp_group_id + assert response_data["userId"] == temp_user_id + delete_groupuser(grp_user_id, self.ranger_admin_config, self.base_url, self.headers) + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize("role, auth", + [("admin", "ranger_admin_config"), ("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")]) + def test_get_groupusers_count(self, role, auth): + auth = getattr(self, auth) + response = requests.get( + f"{self.base_url}/xusers/groupusers/count", + auth=auth, + headers=self.headers + ) + assert_response(response, 200, f"Failed to get group-user association count and got {response.status_code} instead of 200 for role {role}") + data = response.json() + assert "value" in data and isinstance(data["value"], int), f"Invalid response format for group-user association count for role {role}" + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config"), ("key admin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")]) + def test_get_groupusers_list(self, role, auth): + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + temp_user_id = self.ranger_user_id1 + + grp_user = create_groupuser(temp_group["name"], temp_user_id, self.ranger_admin_config, self.base_url, self.headers) + grp_user_id = grp_user["id"] + + assert groupuser_exists(grp_user_id, self.ranger_admin_config, self.base_url, self.headers), f"Group-User association with id {grp_user_id} does not exist which is required for test case: {role}" + response = requests.get( + f"{self.base_url}/xusers/groupusers", + auth = auth, + headers=self.headers, + ) + assert_response(response, 200, f"Get group-user association list failed and got {response.status_code} instead of 200 for role {role}") + response_data = response.json() + assert "vXGroupUsers" in response_data and isinstance(response_data["vXGroupUsers"], list), f"Invalid response format for group-user association list for role {role}" + data = response_data["vXGroupUsers"] + if data != []: + assert isinstance(data[0]['id'], int), f"Invalid id format in group-user association list for role {role}" + delete_groupuser(grp_user_id, self.ranger_admin_config, self.base_url, self.headers) + + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_get_groupusers_by_groupName(self, role, auth): + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + temp_user_id = self.ranger_user_id1 + + grp_user = create_groupuser(temp_group["name"], temp_user_id, self.ranger_admin_config, self.base_url, self.headers) + grp_user_id = grp_user["id"] + + assert groupuser_exists(grp_user_id, self.ranger_admin_config, self.base_url, self.headers), f"Group-User association with id {grp_user_id} does not exist which is required for test case: {role}" + + groupname = temp_group["name"] + + response = requests.get( + f"{self.base_url}/xusers/groupusers/groupName/{groupname}", + auth = auth, + headers=self.headers, + ) + assert_response(response, 200, f"Get group-user association by group name failed and got {response.status_code} instead of 200 for role {role}") + response_data = response.json() + assert "xgroupInfo" in response_data + data = response_data["xgroupInfo"] + validate_xgroup_schema(data) + assert "xuserInfo" in response_data + delete_groupuser(grp_user_id, self.ranger_admin_config, self.base_url, self.headers) + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_create_groupuser(self, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to create group-user association") + + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + temp_user_id = self.ranger_user_id1 + + payload = { + "name": temp_group["name"], + "parentGroupId": temp_group_id, + "userId": temp_user_id + } + + + assert payload != {}, f"Payload is empty for test case: {role}" + assert payload["name"] != "", f"Name field in payload is empty for test case: {role}" + assert payload["userId"] != "", f"UserId field in payload is empty for test case: {role}" + flg, _ = group_exists(temp_group_id, self.ranger_admin_config, self.base_url, self.headers) + assert flg, f"Group with id {temp_group_id} does not exist which is required for test case: {role}" + assert user_exists(temp_user_id, self.ranger_admin_config, self.base_url, self.headers), f"User with id {temp_user_id} does not exist which is required for test case: {role}" + + response = requests.post( + f"{self.base_url}/xusers/groupusers", + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"}, + json=payload + ) + + returned_data = response.json() + assert_response(response, 200, f"Group-User association creation failed and got {response.status_code} instead of 200 for role {role}") + data = fetch_groups_for_user_id(temp_user_id, self.ranger_admin_config, self.base_url, self.headers) + assert check_group_in_user_groups(temp_group_id, data), f"Group with id {temp_group_id} is not assigned to user with id {temp_user_id} which is not expected for test case: {role}" + + print(returned_data) + delete_groupuser(returned_data["id"], self.ranger_admin_config, self.base_url, self.headers) + + + @pytest.mark.positive + @pytest.mark.put + @pytest.mark.parametrize( + "test_case, role, auth", + [("Updation of the user deatils","admin", "ranger_admin_config"), ("Updation of the group details","admin", "ranger_admin_config")]) + def test_update_groupuser(self, test_case, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to update group-user association") + + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + temp_user_id = self.ranger_user_id1 + + grp_user = create_groupuser(temp_group["name"], temp_user_id, self.ranger_admin_config, self.base_url, self.headers) + grp_user_id = grp_user["id"] + + assert groupuser_exists(grp_user_id, self.ranger_admin_config, self.base_url, self.headers), f"Group-User association with id {grp_user_id} does not exist which is required for test case: {role}" + + validation_grp_name = temp_group["name"] + validation_user_id = temp_user_id + + if test_case == "Updation of the group details": + payload = { + "id": grp_user_id, + "name": self.group1['name'], + "userId": temp_user_id + } + validation_grp_name = self.group1['name'] + elif test_case == "Updation of the user deatils": + payload = { + "id": grp_user_id, + "name": temp_group["name"], + "userId": self.ranger_user_id1 + } + validation_user_id = self.ranger_user_id1 + + + response = requests.put( + f"{self.base_url}/xusers/groupusers", + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"}, + json=payload + ) + + assert_response(response, 200, f"Group-User association update failed and got {response.status_code} instead of 200 for role {role}") + returned_data = response.json() + assert returned_data["id"] == grp_user_id + assert returned_data["name"] == validation_grp_name + assert returned_data["userId"] == validation_user_id + + delete_groupuser(grp_user_id, self.ranger_admin_config, self.base_url, self.headers) + + @pytest.mark.positive + @pytest.mark.delete + @pytest.mark.parametrize("role, auth", [("admin", "ranger_admin_config")]) + def test_delete_groupuser(self, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not authorized to delete group-user association") + + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + temp_user_id = self.ranger_user_id1 + + grp_user = create_groupuser(temp_group["name"], temp_user_id, self.ranger_admin_config, self.base_url, self.headers) + grp_user_id = grp_user["id"] + + assert groupuser_exists(grp_user_id, self.ranger_admin_config, self.base_url, self.headers), f"Group-User association with id {grp_user_id} does not exist which is required for test case: {role}" + + response = requests.delete( + f"{self.base_url}/xusers/groupusers/{grp_user_id}", + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"}, + ) + + assert_response(response, 204, f"Group-User association deletion failed and got {response.status_code} instead of 204 for role {role}") + assert not groupuser_exists(grp_user_id, self.ranger_admin_config, self.base_url, self.headers), f"Group-User association with id {grp_user_id} still exists after deletion which is not expected for test case: {role}" + + + + # NEGATIVE TEST CASES + + @pytest.mark.negative + @pytest.mark.get + @pytest.mark.parametrize( + "test_case",[("invalid access by non existing id")]) + def test_get_groupusers_by_id_negative(self, test_case): + auth = self.ranger_admin_config + rand_id = random.randint(999999, 9999999) + flg = groupuser_exists(rand_id, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group-User association with id {rand_id} exists which is not expected for test case: {test_case}" + response = requests.get( + f"{self.base_url}/xusers/groupusers/{rand_id}", + auth = auth, + headers=self.headers, + ) + assert_response(response, 400, f"Get group-user association by id should have failed with 400 but got {response.status_code} instead for test case: {test_case}") + + @pytest.mark.negative + @pytest.mark.get + @pytest.mark.parametrize( + "test_case, auth", + [("invalid access by non existing group name", "ranger_admin_config"), ("invalid auth by user", "ranger_user_config"), ("invalid auth by keyadmin", "ranger_key_admin_config"), ("invalid auth by auditor", "ranger_auditor_config")]) + def test_get_groupusers_by_groupName_negative(self, test_case, auth): + auth = getattr(self, auth) + if test_case == "invalid access by non existing group name": + groupname = "non_existent_group_name_" + str(uuid.uuid4())[:8] + flg = group_exists_by_name(groupname, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group with name {groupname} exists which is not expected for test case: {test_case}" + expected_response_code = 200 + else: + groupname = self.group["name"] + expected_response_code = 403 + response = requests.get( + f"{self.base_url}/xusers/groupusers/groupName/{groupname}", + auth = auth, + headers=self.headers, + ) + assert_response(response, expected_response_code, f"Get group-user association by group name should have failed with {expected_response_code} but got {response.status_code} instead for test case: {test_case}") + if test_case == "invalid access by non existing group name": + assert response.json() == {}, f"Response body is not empty for non existing group name which is not expected for test case: {test_case}" + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize( + "test_case, auth", + [ + ("invalid auth by keyadmin", "ranger_key_admin_config"), + ("invalid auth by auditor", "ranger_auditor_config"), + ("invalid auth by user", "ranger_user_config"), + ("missing mandatory field name", "ranger_admin_config"), + ("missing mandatory field userId", "ranger_admin_config"), + ("invalid payload - non existent user id", "ranger_admin_config"), + ]) + def test_create_groupuser_negative(self, test_case, auth): + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + temp_user_id = self.ranger_user_id1 + + payload = { + "name": temp_group["name"], + "parentGroupId": temp_group_id, + "userId": temp_user_id + } + response_code = 404 # Set default response to 404 for auth failures to prevent API endpoint discovery due to spring slient failure. + + if test_case == "missing mandatory field name": + del payload["name"] + response_code = 400 + + elif test_case == "missing mandatory field userId": + del payload["userId"] + response_code = 400 + + elif test_case == "invalid payload - non existent user id": + payload["userId"] = -999999 + response_code = 404 + + assert user_exists(temp_user_id, self.ranger_admin_config, self.base_url, self.headers) or test_case == "invalid payload - non existent user id", \ + f"User with id {temp_user_id} does not exist which is required for test case: {test_case}" + + response = requests.post( + f"{self.base_url}/xusers/groupusers", + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"}, + json=payload + ) + + assert_response(response, response_code, f"Group-User association creation should have failed with {response_code} but got {response.status_code} instead for test case: {test_case}") + + + @pytest.mark.negative + @pytest.mark.put + @pytest.mark.parametrize( + "test_case, auth",[ + ("invalid auth by keyadmin", "ranger_key_admin_config"), + ("invalid auth by auditor", "ranger_auditor_config"), + ("invalid auth by user", "ranger_user_config"), + ("missing mandatory field", "ranger_admin_config"), + ("invalid payload - non existent user id", "ranger_admin_config"), + ("invalid payload - non existent group name", "ranger_admin_config"), + ("invalid payload - non existent id", "ranger_admin_config") ]) + def test_update_groupuser_negative(self, test_case, auth): + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + temp_user_id = self.ranger_user_id1 + grp_user = create_groupuser(temp_group["name"], temp_user_id, self.ranger_admin_config, self.base_url, self.headers) + grp_user_id = grp_user["id"] + assert groupuser_exists(grp_user_id, self.ranger_admin_config, self.base_url, self.headers), f"Group-User association with id {grp_user_id} does not exist which is required for test case: {test_case}" + if test_case == "missing mandatory field": + payload = { + "id": grp_user_id, + "name": temp_group["name"], + "userId": temp_user_id + } + del payload["name"] + response_code = 400 + elif test_case == "invalid payload - non existent user id": + payload = { + "id": grp_user_id, + "name": temp_group["name"], + "userId": -9999 + } + response_code = 400 + elif test_case == "invalid payload - non existent group name": + payload = { + "id": grp_user_id, + "name": "non_existent_group_name_ugn_" + str(uuid.uuid4())[:8], + "userId": temp_user_id + } + response_code = 400 + elif test_case == "invalid payload - non existent id": + payload = { + "id": -999999, + "name": temp_group["name"], + "userId": temp_user_id + } + response_code = 400 + elif test_case.startswith("invalid auth"): + payload = { + "id": grp_user_id, + "name": temp_group["name"], + "userId": temp_user_id + } + response_code = 403 + response = requests.put( + f"{self.base_url}/xusers/groupusers", + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"}, + json=payload + ) + assert_response(response, response_code, f"Group-User association update should have failed with {response_code} but got {response.status_code} instead for test case: {test_case}") + delete_groupuser(grp_user_id, self.ranger_admin_config, self.base_url, self.headers) + + + @pytest.mark.negative + @pytest.mark.delete + @pytest.mark.parametrize( + "test_case, auth",[ + ("invalid auth by keyadmin", "ranger_key_admin_config"), + ("invalid auth by auditor", "ranger_auditor_config"), + ("invalid auth by user", "ranger_user_config"), + ("non existent id", "ranger_admin_config")]) + def test_delete_groupuser_negative(self, test_case, auth): + auth = getattr(self, auth) + temp_group, temp_group_id = self.group, self.group_id + temp_user_id = self.ranger_user_id1 + grp_user = create_groupuser(temp_group["name"], temp_user_id, self.ranger_admin_config, self.base_url, self.headers) + grp_user_id = grp_user["id"] + assert groupuser_exists(grp_user_id, self.ranger_admin_config, self.base_url, self.headers), f"Group-User association with id {grp_user_id} does not exist which is required for test case: {test_case}" + if test_case == "non existent id": + rand_id = random.randint(999999, 9999999) + flg = groupuser_exists(rand_id, self.ranger_admin_config, self.base_url, self.headers) + assert not flg, f"Group-User association with id {rand_id} exists which is not expected for test case: {test_case}" + target_id = rand_id + expected_response_code = 400 + else: + target_id = grp_user_id + expected_response_code = 404 # Set default response to 404 for auth failures to prevent API endpoint discovery due to spring slient failure. + + response = requests.delete( + f"{self.base_url}/xusers/groupusers/{target_id}", + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"}, + ) + + assert_response(response, expected_response_code, f"Group-User association deletion should have failed with {expected_response_code} but got {response.status_code} instead for test case: {test_case}") + delete_groupuser(grp_user_id, self.ranger_admin_config, self.base_url, self.headers) \ No newline at end of file diff --git a/functional-tests/xuserrest/test_permission.py b/functional-tests/xuserrest/test_permission.py new file mode 100644 index 0000000000..9b51452925 --- /dev/null +++ b/functional-tests/xuserrest/test_permission.py @@ -0,0 +1,1430 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +import string +from xuserrest.utility.utils import * +import uuid +import pytest +import requests +from datetime import datetime +import random + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +class TestPermissions: + + SERVICE_NAME = "admin" + + + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_permission_module, + temp_permission_group, + temp_group, + temp_permission_user, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + # ---------- secondary users ---------- + cls.admin1, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin1, _ = temp_keyadmin_user() + cls.audit1, _ = temp_secure_user(["auditor"]) + cls.user1, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config1 = (cls.admin1["name"], "Test@123") + cls.ranger_key_admin_config1 = (cls.ranger_key_admin1["name"], "Test@123") + cls.ranger_auditor_config1 = (cls.audit1["name"], "Test@123") + cls.ranger_user_config1 = (cls.user1["name"], "Test@123") + + cls.ranger_admin_id1 = cls.admin1["id"] + cls.ranger_key_admin_id1 = cls.ranger_key_admin1["id"] + cls.ranger_auditor_id1 = cls.audit1["id"] + cls.ranger_user_id1 = cls.user1["id"] + + + # ---------- permissions setup ---------- + cls.permission_module, cls.permission_module_id = temp_permission_module() + cls.permission_module1, cls.permission_module_id1 = temp_permission_module() + + cls.permission_module_name = cls.permission_module["module"] + cls.permission_module_name1 = cls.permission_module1["module"] + + # create group + cls.group, cls.group_id = temp_group() + + cls.permission_group, cls.permission_group_id = temp_permission_group( + cls.group_id, + cls.permission_module_id + ) + + # create user + cls.permission_user, cls.permission_user_id = temp_permission_user( + cls.ranger_user_id, cls.permission_module_id + ) + cls.permission_user1, cls.permission_user_id1 = temp_permission_user( + cls.ranger_user_id1, cls.permission_module_id + ) + # ---------- common ---------- + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "params, test_case", + [ + ({}, "default_request"), + ({"startIndex": 0, "pageSize": 10}, "pagination_basic"), + ({"startIndex": 5, "pageSize": 5}, "pagination_offset"), + ({"module": "admin"}, "filter_module"), + ({"userName": "admin"}, "filter_user"), + ({"groupName": "public"}, "filter_group"), + ({"module": "admin", "userName": "admin"}, "filter_combined"), + ({"sortBy": "id", "sortType": "asc"}, "sorting_asc"), + ({"sortBy": "id", "sortType": "desc"}, "sorting_desc"), + ({"getCount": "false"}, "get_count_false"), + ], + ids=[ + "default", + "pagination-basic", + "pagination-offset", + "filter-module", + "filter-user", + "filter-group", + "filter-combined", + "sorting-asc", + "sorting-desc", + "getcount-false", + ], + ) + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("auditor", "ranger_auditor_config")], + ids=["admin", "auditor"], + ) + def test_get_permission(self, params, test_case, role, auth): + if role not in ["admin", "auditor"]: + assert False, f"Role {role} should not have access to permissions, but got access in {test_case}" + + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permission", + params=params, + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, f"Failed for case: {test_case}") + + data = response.json() + + assert isinstance(data, dict), f"Invalid response format in {test_case}" + + assert "vXModuleDef" in data, f"'vXModuleDef' not found in {test_case}" + assert isinstance(data["vXModuleDef"], list) + + modules = data["vXModuleDef"] + + if not modules: + assert data.get("totalCount", 0) == 0 + assert data.get("resultSize", 0) == 0 + return + + module = modules[0] + + + if module["userPermList"]: + assert "id" in module["userPermList"][0] + # group permissions (optional — API dependent) + if "groupPermList" in module: + assert isinstance(module["groupPermList"], list) + + if "resultSize" in data: + assert isinstance(data["resultSize"], int) + + if "listSize" in data: + assert isinstance(data["listSize"], int) + + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "params, test_case", + [ + ({}, "default_request"), + ({"startIndex": 0, "pageSize": 10}, "pagination_basic"), + ({"startIndex": 5, "pageSize": 5}, "pagination_offset"), + ({"module": "admin"}, "filter_module"), + ({"userName": "admin"}, "filter_user"), + ({"groupName": "public"}, "filter_group"), + ({"module": "admin", "userName": "admin"}, "filter_combined"), + ({"sortBy": "id", "sortType": "asc"}, "sorting_asc"), + ({"sortBy": "id", "sortType": "desc"}, "sorting_desc"), + ({"getCount": "false"}, "get_count_false"), + ], + ids=[ + "default", + "pagination-basic", + "pagination-offset", + "filter-module", + "filter-user", + "filter-group", + "filter-combined", + "sorting-asc", + "sorting-desc", + "getcount-false", + ], + ) + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("auditor", "ranger_auditor_config")], + ids=["admin", "auditor"], + ) + def test_get_permissionlist(self, params, test_case, role, auth): + if role not in ["admin", "auditor"]: + assert False, f"Role {role} should not have access to permission list, but got access in {test_case}" + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permissionlist", + params=params, + auth=auth, + headers=self.headers + ) + + + assert_response(response, 200, f"Failed for case: {test_case}") + + data = response.json() + + assert isinstance(data, dict), f"Invalid response format in {test_case}" + + + assert "vXModulePermissionList" in data, f"'vXModulePermissionList' not found in {test_case}" + assert isinstance(data["vXModulePermissionList"], list) + + modules = data["vXModulePermissionList"] + + if not modules: + assert data.get("totalCount", 0) == 0 + assert data.get("resultSize", 0) == 0 + return + + assert "userNameList" in modules[0], f"'userNameList' not found in {test_case}" + assert isinstance(modules[0]["userNameList"], list) + + if len(modules[0]["userNameList"]) > 0: + assert isinstance(modules[0]["userNameList"][0], str) + if "groupPermList" in modules[0]: + assert isinstance(modules[0]["groupPermList"], list) + + if "resultSize" in data: + assert isinstance(data["resultSize"], int) + + if "listSize" in data: + assert isinstance(data["listSize"], int) + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("auditor", "ranger_auditor_config")], + ids=["admin", "auditor"], + ) + def test_get_permission_count(self, role, auth): + if role not in ["admin", "auditor"]: + assert False, f"Role {role} should not have access to permission count, but got access" + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permission/count", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200) + + data = response.json() + + assert isinstance(data["value"], int), "Invalid response format" + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("auditor", "ranger_auditor_config")], + ids=["admin", "auditor"], + ) + def test_get_permission_by_id(self, role, auth): + if role not in ["admin", "auditor"]: + assert False, f"Role {role} should not have access to permission by ID, but got access" + auth = getattr(self, auth) + + permission_id = self.permission_module_id + + # Now fetch permission by ID + response = requests.get( + f"{self.base_url}/xusers/permission/{permission_id}", + auth=auth, + headers=self.headers + ) + assert_response(response, 200, "Failed to fetch permission by ID") + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("auditor", "ranger_auditor_config")], + ids=["admin", "auditor"], + ) + def test_get_permission_group(self, role, auth): # since the same searchUtil.extractCommonCriterias already checked test_get_permission with the for sortfields so we are not checking them again. + if role not in ["admin", "auditor"]: + assert False, f"Role {role} should not have access to permission group, but got access" + + auth = getattr(self, auth) + + resp = requests.get( + f"{self.base_url}/xusers/permission/group", + auth=auth, + headers=self.headers + ) + assert_response(resp, 200, "Unable to fetch groups") + + data = resp.json() + print(data) + group_permission_schema(data) + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("auditor", "ranger_auditor_config")], + ids=["admin", "auditor"], + ) + def test_get_permission_group_count(self, role, auth): + if role not in ["admin", "auditor"]: + assert False, f"Role {role} should not have access to permission group count, but got access" + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permission/group/count", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200) + + data = response.json() + + assert isinstance(data["value"], int), "Invalid response format" + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("auditor", "ranger_auditor_config")], + ids=["admin", "auditor"], + ) + def test_get_permission_group_by_id(self, role, auth): + if role not in ["admin", "auditor"]: + assert False, f"Role {role} should not have access to permission group by ID, but got access" + + auth = getattr(self, auth) + + group_permission_id = self.permission_group_id + + response = requests.get( + f"{self.base_url}/xusers/permission/group/{group_permission_id}", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, "Failed to fetch permission group by ID") + data = response.json() + assert data["id"] == group_permission_id + assert data["moduleId"] == self.permission_module_id + assert "groupId" in data + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("auditor", "ranger_auditor_config")], + ids=["admin", "auditor"], + ) + def test_get_permission_user(self, role, auth): # since the same searchUtil.extractCommonCriterias already checked test_get_permission with the for sortfields so we are not checking them again. + if role not in ["admin", "auditor"]: + assert False, f"Role {role} should not have access to permission user, but got access" + + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permission/user", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, "Failed to fetch permission user") + + data = response.json() + + assert isinstance(data, dict), "Invalid response format" + assert "vXUserPermission" in data, "'vXUserPermission' not found in response" + assert isinstance(data["vXUserPermission"], list) + + modules = data["vXUserPermission"] + + if not modules: + assert data.get("totalCount", 0) == 0 + assert data.get("resultSize", 0) == 0 + return + mod = modules[0] + assert "userName" in mod, "'userName' not found in response" + assert isinstance(mod["userName"], str) + assert isinstance(mod['id'], int) + + if "resultSize" in data: + assert isinstance(data["resultSize"], int) + + if "listSize" in data: + assert isinstance(data["listSize"], int) + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("auditor", "ranger_auditor_config")], + ids=["admin", "auditor"], + ) + def test_get_permission_user_count(self, role, auth): + if role not in ["admin", "auditor"]: + assert False, f"Role {role} should not have access to permission user count" + + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permission/user/count", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, "Failed to fetch permission user count") + data = response.json() + assert isinstance(data.get("value"), int), "Invalid response format" + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"), ("auditor", "ranger_auditor_config")], + ids=["admin", "auditor"], + ) + def test_get_permission_user_by_id(self, role, auth, request): + if role not in ["admin", "auditor"]: + assert False, f"Role {role} should not have access to permission user details" + auth = getattr(self, auth) + module_id = self.permission_module_id + permission_user_id = self.permission_user_id + response = requests.get( + f"{self.base_url}/xusers/permission/user/{permission_user_id}", + auth=auth, + headers=self.headers + ) + assert_response(response, 200, "Failed to fetch permission user by ID") + data = response.json() + assert data["id"] == permission_user_id + assert data["userId"] == self.ranger_user_id + assert data["moduleId"] == module_id + + + @pytest.mark.post + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config")], + ids=["admin"], + ) + def test_create_permissions(self, role, auth): + if role != "admin": + assert False, f"Role {role} should not have access to create permissions" + + auth = getattr(self, auth) + + module_name = f"pytest_permission_{uuid.uuid4().hex[:8]}" + payload = { + "module": module_name, + } + + if permission_module_exists(module_name, self.ranger_admin_config, self.base_url, self.headers): + assert False, f"Module name {module_name} should not exist so, cannot assign permissions for existing module" + + # Fetch existing permissions to get a valid module and permission ID + response = requests.post( + f"{self.base_url}/xusers/permission", + json=payload, + auth=auth, + headers=self.headers + ) + assert_response(response, 200, "Failed to fetch permissions for assignment") + + + + @pytest.mark.post + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config")], + ids=["admin"], + ) + def test_create_permissions_group(self, role, auth, request): + if role != "admin": + assert False, f"Role {role} should not have access to create permissions group" + auth = getattr(self, auth) + + module, module_id = request.getfixturevalue("temp_permission_module")() + module_check = requests.get( + f"{self.base_url}/xusers/permission/{module_id}", + auth=auth, + headers=self.headers + ) + assert_response(module_check, 200, "Failed to fetch module for group permission assignment") + + group, group_id = request.getfixturevalue("temp_group")() + group_check = requests.get( + f"{self.base_url}/xusers/groups/{group_id}", + auth=auth, + headers=self.headers + ) + assert_response(group_check, 200, "Failed to fetch group for permission assignment") + + payload = { + "groupId": group_id, + "moduleId": module_id, + "isAllowed": 1 + } + + response = requests.post( + f"{self.base_url}/xusers/permission/group", + json=payload, + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, "Failed to create permissions for assignment") + + + @pytest.mark.post + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config")], + ids=["admin"], + ) + def test_create_permissions_user(self, role, auth, request): + if role != "admin": + assert False, f"Role {role} should not have access to create permission user" + auth = getattr(self, auth) + + module, module_id = request.getfixturevalue("temp_permission_module")() + user, user_id = request.getfixturevalue("temp_secure_user")(["user"]) + params = { + "userId": user_id, + "moduleId": module_id, + "isAllowed": 1 + } + + assert not user_permission_exists(user_id, module_id, self.ranger_admin_config, self.base_url, self.headers), "Permission user should not exist before creation" + module_check = requests.post( + f"{self.base_url}/xusers/permission/user", + json=params, + auth=auth, + headers=self.headers + ) + + assert_response(module_check, 200, "Failed to create permission for user") + assert user_permission_exists(user_id, module_id, self.ranger_admin_config, self.base_url, self.headers), "Permission user should exist after creation" + module_check_data = module_check.json() + assert module_check_data["userId"] == user_id, "Created permission user ID does not match expected user ID" + assert module_check_data["moduleId"] == module_id, "Created permission module ID does not match expected module ID" + + @pytest.mark.put + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth, test_case", + [ + ("admin", "ranger_admin_config", "existing_user_different_data"), + ("admin", "ranger_admin_config", "existing_user_same_data"), + ("admin", "ranger_admin_config", "new_user_permission"), + ("admin", "ranger_admin_config", "existing_group_different_data"), + ("admin", "ranger_admin_config", "existing_group_same_data"), + ("admin", "ranger_admin_config", "new_group_permission"), + ("admin", "ranger_admin_config", "user_and_group_update_together"), + ("admin", "ranger_admin_config", "empty_permission_lists"), + ]) + def test_update_permissions(self, role, auth, test_case): + if role != "admin": + assert False, f"Role {role} should not have access to update permissions" + + auth_config = getattr(self, auth) + perm_id = self.permission_module_id + + # Fetch groups + resp = requests.get(f"{self.base_url}/xusers/groups", auth=auth_config, headers=self.headers) + assert_response(resp, 200, f"Unable to fetch groups: {resp.text}") + groups = resp.json().get("vXGroups", []) + assert groups, "No groups found in the system." + + u_exist, u_new = self.ranger_user_id1, self.ranger_user_id + g_exist, g_new = groups[0]["id"], groups[1]["id"] if len(groups) > 1 else groups[0]["id"] + + # Helper functions + u_perm = lambda uid, allow: build_user_permission(uid, perm_id, allow) + g_perm = lambda gid, allow: build_group_permission(gid, perm_id, allow) + + # Map test cases to tuple: (userPermList, groupPermList) + cases = { + "existing_user_different_data": ([u_perm(u_exist, 0)], []), + "existing_user_same_data": ([u_perm(u_exist, 1)], []), + "new_user_permission": ([u_perm(u_new, 1)], []), + "existing_group_different_data": ([], [g_perm(g_exist, 0)]), + "existing_group_same_data": ([], [g_perm(g_exist, 1)]), + "new_group_permission": ([], [g_perm(g_new, 1)]), + "user_and_group_update_together": ([u_perm(u_exist, 0)], [g_perm(g_exist, 0)]), + "empty_permission_lists": ([], []) + } + + user_list, group_list = cases[test_case] + + payload = build_permission_payload( + module_id=perm_id, + module_name=self.permission_module_name, + user_perm_list=user_list, + group_perm_list=group_list + ) + + response = requests.put( + f"{self.base_url}/xusers/permission/{perm_id}", + json=payload, + auth=auth_config, + headers=self.headers + ) + + assert_response(response, 200, f"Permission update failed for {test_case}: {response.text}") + + data = response.json() + assert data["id"] == perm_id + assert data["module"] == self.permission_module_name + + if payload["userPermList"]: + assert "userPermList" in data + assert isinstance(data["userPermList"], list) + + if payload["groupPermList"]: + assert "groupPermList" in data + assert isinstance(data["groupPermList"], list) + + + @pytest.mark.put + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth, test_case", + [ + ("admin", "ranger_admin_config", "with payload id"), + ("admin", "ranger_admin_config", "without payload id"), + ], + ids=["admin-with_id", "admin-without_id"] + ) + def test_update_permissions_group(self, request, role, auth, test_case): + if role != "admin": + assert False, f"Role {role} should not have access to update permission group" + + auth_config = getattr(self, auth) + module, module_id = request.getfixturevalue("temp_permission_module")() + grp, grp_id = request.getfixturevalue("temp_group")() + + perm_grp, perm_id = request.getfixturevalue("temp_permission_group")(grp_id, module_id) + + if test_case == "without payload id": + payload ={ + "groupId": self.group_id, + "moduleId": module_id, + "isAllowed": 1 + } + assert "id" not in payload, "Payload should not contain 'id' for this test case" + + elif test_case == "with payload id": + + payload ={ + "id": perm_id, + "groupId": self.group_id, + "moduleId": module_id, + "isAllowed": 1 + + } + assert payload["id"] == perm_id, "Payload groupId does not match expected group ID for update test" + + + assert grp_id != self.group_id, "Precondition failed: Need a different group ID to test update of group permission" + assert perm_grp["groupId"] == grp_id, "Precondition failed: Permission group is not linked to the expected group" + + + response = requests.put( + f"{self.base_url}/xusers/permission/group/{perm_id}", + json=payload, + auth=auth_config, + headers=self.headers + ) + + assert_response(response,200,f"Permission update failed for empty lists: {response.text}") + response_data = response.json() + assert response_data["groupId"] == self.group_id, "Updated groupId does not match expected group ID" + + @pytest.mark.put + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config")], + ids=["admin"] + ) + def test_update_permissions_user(self, request, role, auth): + if role != "admin": + assert False, f"Role {role} should not have access to update permission user" + + auth_config = getattr(self, auth) + module, module_id = request.getfixturevalue("temp_permission_module")() + user, user_id = request.getfixturevalue("temp_secure_user")(["user"]) + + perm_user, perm_user_id = request.getfixturevalue("temp_permission_user")(user_id, module_id) + + + payload = { + "id": perm_user_id, + "userId": user_id, + "moduleId": module_id, + "isAllowed": 0 # only we can change this field in update + } + + + assert perm_user["userId"] == user_id, "Precondition failed: Permission user is not linked to the expected user" + assert perm_user["moduleId"] == module_id, "Precondition failed: Permission user is not linked to the expected module" + assert user_permission_exists(user_id, module_id, self.ranger_admin_config, self.base_url, self.headers, user_perm_id = perm_user_id), "Permission user should exist before update" + assert payload["isAllowed"] != perm_user["isAllowed"], "Precondition failed: Need different isAllowed value to test update of user permission" + + + response = requests.put( + f"{self.base_url}/xusers/permission/user/{perm_user_id}", + json=payload, + auth=auth_config, + headers=self.headers + ) + + assert_response(response, 200, f"Permission user update failed: {response.text}") + response_data = response.json() + assert response_data["isAllowed"] == 0, "Updated isAllowed does not match expected value" + + @pytest.mark.delete + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config")], + ids=["admin"] + ) + def test_delete_permissions(self, role, auth): + if role != "admin": + assert False, f"Role {role} should not have access to delete permissions" + auth_config = getattr(self, auth) + # First create a permission to ensure we have a valid ID to delete + module_name = f"pytest_permission_delete_{uuid.uuid4().hex[:8]}" + payload = { + "module": module_name, + } + response = requests.post( + f"{self.base_url}/xusers/permission", + json=payload, + auth=auth_config, + headers=self.headers + ) + assert_response(response, 200, "Failed to create permission for deletion test") + perm_id = response.json().get("id") + assert perm_id, "No permission ID returned after creation, cannot proceed with deletion test" + + response = requests.delete( + f"{self.base_url}/xusers/permission/{perm_id}", + auth=auth_config, + headers=self.headers + ) + + assert_response( + response, + 204, + f"Permission deletion failed: {response.text}" + ) + + @pytest.mark.delete + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config")], + ids=["admin"] + ) + def test_delete_permission_group(self, role, auth, request): + if role != "admin": + assert False, f"Role {role} should not have access to delete permission group" + auth_config = getattr(self, auth) + module, module_id = request.getfixturevalue("temp_permission_module")() + grp, grp_id = request.getfixturevalue("temp_group")() + + perm_grp, perm_id = request.getfixturevalue("temp_permission_group")(grp_id, module_id) + + response = requests.delete( + f"{self.base_url}/xusers/permission/group/{perm_id}", + auth=auth_config, + headers=self.headers + ) + + assert_response( + response, + 204, + f"Permission group deletion failed: {response.text}" + ) + + + @pytest.mark.delete + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config")], + ids=["admin"] + ) + def test_delete_permission_user(self, role, auth, request): + if role != "admin": + assert False, f"Role {role} should not have access to delete permission user" + auth_config = getattr(self, auth) + module, module_id = request.getfixturevalue("temp_permission_module")() + user, user_id = request.getfixturevalue("temp_secure_user")(["user"]) + + perm_user, perm_user_id = request.getfixturevalue("temp_permission_user")(user_id, module_id) + + response = requests.delete( + f"{self.base_url}/xusers/permission/user/{perm_user_id}", + auth=auth_config, + headers=self.headers + ) + + assert_response( + response, + 204, + f"Permission user deletion failed: {response.text}" + ) + + + # NEGATIVE TESTS + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [("user", "ranger_user_config"), ("key_admin", "ranger_key_admin_config")], + ids=["user", "key_admin"], + ) + def test_get_permission_with_invalid_role(self, role, auth): + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permission", + auth=auth, + headers=self.headers + ) + assert_response(response, 403, f"{role} should not have permission to view permissions, but got {response.status_code}") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [("user", "ranger_user_config"), ("key_admin", "ranger_key_admin_config")], + ids=["user", "key_admin"], + ) + def test_get_permissionlist_with_invalid_role(self, role, auth): + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permissionlist", + auth=auth, + headers=self.headers + ) + assert_response(response, 403, f"{role} should not have permission to view permission list, but got {response.status_code}") + + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [("keyadmin", "ranger_key_admin_config"), ("user", "ranger_user_config")], + ids=["keyadmin", "user"], + ) + def test_get_permission_count_with_invalid_role(self, role, auth): + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permission/count", + auth=auth, + headers=self.headers + ) + + assert_response(response, 403, f"{role} should not have permission to view permission count, but got {response.status_code}") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case", + ["invalid_id", ("keyadmin", "ranger_key_admin_config"), ("user", "ranger_user_config")], + ids=["invalid_id", "invalid_role_keyadmin", "invalid_role_user"], + ) + def test_invalid_get_permission_by_id(self, test_case): + + if test_case == "invalid_id": + permission_id = -93402 # Assuming this ID does not exist + auth = self.ranger_admin_config + expected_status = 404 + + else: + permission_id = self.permission_module_id + auth = getattr(self, test_case[1]) + expected_status = 403 + + response = requests.get( + f"{self.base_url}/xusers/permission/{permission_id}", + auth=auth, + headers=self.headers + ) + assert_response(response, expected_status, "Failed to fetch permission by ID") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [("user", "ranger_user_config"), ("key_admin", "ranger_key_admin_config")], + ids=["user", "key_admin"], + ) + def test_get_permission_group_with_invalid_role(self, role, auth): + + auth = getattr(self, auth) + + resp = requests.get( + f"{self.base_url}/xusers/permission/group", + auth=auth, + headers=self.headers + ) + assert_response(resp, 403, f"{role} should not have permission to view permission groups, but got {resp.status_code}") + + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [("user", "ranger_user_config"), ("key_admin", "ranger_key_admin_config")], + ids=["user", "key_admin"], + ) + def test_get_permission_group_count_with_invalid_role(self, role, auth): + + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permission/group/count", + auth=auth, + headers=self.headers + ) + + assert_response(response, 403, f"{role} should not have permission to view permission group count, but got {response.status_code}") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [("user", "ranger_user_config"), ("key_admin", "ranger_key_admin_config")], + ids=["user", "key_admin"], + ) + def test_get_permission_group_by_id_with_invalid_role(self, role, auth): + auth = getattr(self, auth) + + group_permission_id = self.permission_group_id + + response = requests.get( + f"{self.base_url}/xusers/permission/group/{group_permission_id}", + auth=auth, + headers=self.headers + ) + + assert_response(response, 403, f"{role} should not have permission to view permission group by ID, but got {response.status_code}") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [("user", "ranger_user_config"), ("key_admin", "ranger_key_admin_config")], + ids=["user", "key_admin"], + ) + def test_get_permission_user_with_invalid_role(self, role, auth): + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permission/user", + auth=auth, + headers=self.headers + ) + + assert_response(response, 403, f"{role} should not have permission to view permission users, but got {response.status_code}") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [("user", "ranger_user_config"), ("key_admin", "ranger_key_admin_config")], + ids=["user", "key_admin"], + ) + def test_get_permission_user_count_with_invalid_role(self, role, auth): + auth = getattr(self, auth) + + response = requests.get( + f"{self.base_url}/xusers/permission/user/count", + auth=auth, + headers=self.headers + ) + + assert_response(response, 403, f"{role} should not have permission to view permission user count, but got {response.status_code}") + + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "Test_case", + ["invalid_id", "invalid_role_keyadmin", "invalid_role_user"], + ids=["invalid_id", "invalid_role_keyadmin", "invalid_role_user"], + ) + def test_get_permission_user_by_id_with_invalid_test_case(self, Test_case, request): + if Test_case == "invalid_id": + permission_user_id = -93402 # Assuming this ID does not exist + auth = self.ranger_admin_config + expected_status = 404 + elif Test_case == "invalid_role_keyadmin": + permission_user_id = self.permission_user_id + auth = self.ranger_key_admin_config + expected_status = 403 + elif Test_case == "invalid_role_user": + permission_user_id = self.permission_user_id + auth = self.ranger_user_config + expected_status = 403 + + response = requests.get( + f"{self.base_url}/xusers/permission/user/{permission_user_id}", + auth=auth, + headers=self.headers + ) + assert_response(response, expected_status, f"Invalid test case {Test_case}: Expected status {expected_status}, got {response.status_code}") + + @pytest.mark.post + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth, test_case", + [("admin", "ranger_admin_config", "creating permission with existing module name"), ("auditor", "ranger_auditor_config", "invalid auth by auditor"), ("keyadmin", "ranger_key_admin_config", "invalid auth by keyadmin"), ("user", "ranger_user_config", "invalid auth by user")], + ids=["admin-existing_module", "auditor-invalid_auth", "keyadmin-invalid_auth", "user-invalid_auth"], + ) + def test_create_permissions_with_negative_testcases(self, role, auth, test_case): + + auth = getattr(self, auth) + module_name = self.permission_module_name + + if test_case == "creating permission with existing module name": + assert permission_module_exists(module_name, self.ranger_admin_config, self.base_url, self.headers), f"Module name {module_name} should exist for this test case to be valid" + expected_status = 400 + else: + expected_status = 403 + payload = { + "module": module_name, + } + + response = requests.post( + f"{self.base_url}/xusers/permission", + json=payload, + auth=auth, + headers=self.headers + ) + assert_response(response, expected_status, f"{test_case}: Expected status {expected_status}, got {response.status_code}") + + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [("keyadmin", "ranger_key_admin_config"), ("user", "ranger_user_config")], + ids=["keyadmin", "user"], + ) + + def test_update_permissions_using_invalid_role(self, role, auth): + + auth_config = getattr(self, auth) + perm_id = self.permission_module_id + + payload = build_permission_payload( + module_id=perm_id, + module_name=self.permission_module_name, + user_perm_list=[], + group_perm_list=[] + ) + + response = requests.put( + f"{self.base_url}/xusers/permission/{perm_id}", + json=payload, + auth=auth_config, + headers=self.headers + ) + + assert_response( + response, + 403, + f"Permission update failed for: {response.text}" + ) + + @pytest.mark.post + @pytest.mark.negative + @pytest.mark.parametrize("test_case", [ + "nonexistent_module", "nonexistent_group_id", "preexisting_permission_group_payload", + "invalid_auth_by_role", "invalid_payload_missing_fields" + ]) + def test_create_permissions_group_with_invalid_cases(self, test_case, request): + + get_g = lambda: request.getfixturevalue("temp_group")()[1] + get_m = lambda: request.getfixturevalue("temp_permission_module")()[1] + + cases = { + "nonexistent_module": ({"groupId": get_g(), "moduleId": -99999, "isAllowed": 1}, self.ranger_admin_config, 404), + "nonexistent_group_id": ({"groupId": -99999, "moduleId": get_m(), "isAllowed": 1}, self.ranger_admin_config, 404), + "preexisting_permission_group_payload": ({"groupId": get_g(), "moduleId": get_m(), "isAllowed": 1}, self.ranger_admin_config, 400), + "invalid_auth_by_role": ({"groupId": get_g(), "moduleId": get_m(), "isAllowed": 1}, self.ranger_user_config, 403), + "invalid_payload_missing_fields": ({"moduleId": self.permission_module_id, "isAllowed": 1}, self.ranger_admin_config, 400), + } + + payload, auth, expected_status = cases[test_case] + + if test_case == "preexisting_permission_group_payload": + requests.post(f"{self.base_url}/xusers/permission/group", json=payload, auth=auth, headers=self.headers) + + response = requests.post(f"{self.base_url}/xusers/permission/group", json=payload, auth=auth, headers=self.headers) + assert_response(response, expected_status, f"Case {test_case} failed: {response.status_code}") + + + @pytest.mark.post + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth_name, test_case, expected_status", + [ + ("admin", "ranger_admin_config", "nonexistent_module", 404), + ("admin", "ranger_admin_config", "nonexistent_user", 400), + ("admin", "ranger_admin_config", "preexisting_permission_user_payload", 400), + ("user", "ranger_user_config", "invalid_auth_by_role_user", 403), + ("keyadmin", "ranger_key_admin_config", "invalid_auth_by_role_keyadmin", 403), + ("auditor", "ranger_auditor_config", "invalid_auth_by_role_auditor", 403), + ("admin", "ranger_admin_config", "invalid_payload_missing_fields", 400), + ], + ids=lambda x: x # Automatically uses the test_case name for the ID + ) + def test_create_permissions_user_with_invalid_roless(self, role, auth_name, test_case, expected_status, request): + auth = getattr(self, auth_name) + + payload = { + "userId": self.ranger_user_id, + "moduleId": self.permission_module_id, + "isAllowed": 1 + } + + # Apply specific overrides for unique cases + overrides = { + "nonexistent_module": {"moduleId": -99999}, + "nonexistent_user": {"userId": -99999}, + "invalid_payload_missing_fields": {"isAllowed": None} # This will trigger the pop below + } + + if test_case in overrides: + payload.update(overrides[test_case]) + if payload.get("isAllowed") is None: payload.pop("isAllowed") + + if test_case == "preexisting_permission_user_payload": + assert user_permission_exists(self.ranger_user_id, self.permission_module_id, self.ranger_admin_config, self.base_url, self.headers), "Precondition failed" + + response = requests.post( + f"{self.base_url}/xusers/permission/user", + json=payload, + auth=auth, + headers=self.headers + ) + + assert_response(response, expected_status, f"Failed {test_case}: {response.text}") + + @pytest.mark.put + @pytest.mark.negative + def test_update_permissions_with_nonexistent_id(self): + + auth_config = self.ranger_admin_config + perm_id = -93402 # Assuming this ID does not exist + + payload = build_permission_payload( + module_id=perm_id, + module_name=self.permission_module_name, + user_perm_list=[], + group_perm_list=[] + ) + + response = requests.put( + f"{self.base_url}/xusers/permission/{perm_id}", + json=payload, + auth=auth_config, + headers=self.headers + ) + + assert_response( + response, + 404, + f"Permission update should fail for nonexistent ID: {response.text}" + ) + + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth_name, expected_status", + [ + ("non_matching_payload_id", "ranger_admin_config", 400), + ("missing_mandatory_fields", "ranger_admin_config", 400), + ("invalid_auth_by_user", "ranger_user_config", 403), + ("invalid_auth_by_keyadmin", "ranger_key_admin_config", 403), + ], + ids=lambda x: str(x) + ) + def test_update_permissions_group(self, request, test_case, auth_name, expected_status): + + module, module_id = request.getfixturevalue("temp_permission_module")() + grp, grp_id = request.getfixturevalue("temp_group")() + + perm_grp, perm_id = request.getfixturevalue("temp_permission_group")(grp_id, module_id) + + auth_config = getattr(self, auth_name) + + payload = { + "id" : perm_id, + "groupId": self.group_id, + "moduleId": module_id, + "isAllowed": 1 + } + + if test_case == "non_matching_payload_id": + payload["id"] = self.permission_group_id # using a different existing permission group ID to cause mismatch + assert payload["id"] != perm_id, "Payload ID should not match the permission group ID for this test case" + + elif test_case == "missing_mandatory_fields": + del payload["groupId"] + + response = requests.put( + f"{self.base_url}/xusers/permission/group/{perm_id}", + json=payload, + auth=auth_config, + headers=self.headers + ) + assert_response(response, expected_status, f"Permission update should fail for test case '{test_case}': but got {response.text}") + + del_response = requests.delete( + f"{self.base_url}/xusers/permission/group/{perm_id}", + auth=self.ranger_admin_config, + headers=self.headers + ) + assert_response(del_response, 204, f"Cleanup failed for permission group ID {perm_id}: {del_response.text}") + + + @pytest.mark.put + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth, test_case", + [ + ("admin", "ranger_admin_config", "invalid payload missing id"), + ("admin", "ranger_admin_config", "invalid payload missing userId"), + ("admin", "ranger_admin_config", "invalid payload missing isAllowed"), + ("admin", "ranger_admin_config", "invalid payload editing non editable fields - userId"), + ("admin", "ranger_admin_config", "invalid payload editing non editable fields - moduleId"), + ("user", "ranger_user_config", "invalid auth by user"), + ("key_admin", "ranger_key_admin_config", "invalid auth by keyadmin"), + ("auditor", "ranger_auditor_config", "invalid auth by auditor"), + ], + ids=["invalid payload missing id", "invalid payload missing userId", "invalid payload missing isAllowed", "invalid payload (editing non editable fields - userId)", "invalid payload (editing non editable fields - moduleId)", "invalid auth by user", "invalid auth by keyadmin", "invalid auth by auditor"], + ) + def test_update_permissions_user_with_invalid_testcases(self, request, role, auth, test_case): + + if role == "admin": + auth_config = self.ranger_admin_config + module, module_id = request.getfixturevalue("temp_permission_module")() + user, user_id = request.getfixturevalue("temp_secure_user")(["user"]) + perm_user, perm_user_id = request.getfixturevalue("temp_permission_user")(user_id, module_id) + payload = {"id": perm_user_id, "userId": user_id, "moduleId": module_id, "isAllowed": 0} + else: + perm_user_id = self.permission_user_id + + if test_case.startswith("invalid payload missing"): + field = test_case.split("missing ")[1] + payload.pop(field, None) + + status_map = {"id": 400, "userId": 400, "moduleId": 400, "isAllowed": 404} + expected_status = status_map.get(field, 400) + + + elif test_case.startswith("invalid payload editing non editable fields"): + field = test_case.split("- ")[1] + payload[field] = random.randint(10000, 99999) + expected_status = 400 if field == "userId" else 404 + + elif test_case.startswith("invalid auth by "): + payload = { + "id": perm_user_id, + "userId": self.ranger_user_id, + "moduleId": self.permission_module_id, + "isAllowed": 0 + } + auth_config = getattr(self, f"ranger_{role}_config") + expected_status = 403 + + response = requests.put( + f"{self.base_url}/xusers/permission/user/{perm_user_id}", + json=payload, + auth=auth_config, + headers=self.headers + ) + + assert_response(response, expected_status, f"Permission user update failed: {response.text}") + + @pytest.mark.delete + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + [ + ("keyadmin", "ranger_key_admin_config"), + ("auditor", "ranger_auditor_config"), + ("user", "ranger_user_config"), + ], + ids=[ "keyadmin", "auditor", "user"],) + def test_delete_permissions_with_invalid_roles(self, role, auth): + + auth_config = getattr(self, auth) + + + perm_id = self.permission_module_id + + response = requests.delete( + f"{self.base_url}/xusers/permission/{perm_id}", + auth=auth_config, + headers=self.headers + ) + + assert_response( + response, + 403, + f"Permission deletion failed: {response.text}" + ) + + @pytest.mark.delete + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + (["keyadmin", "ranger_key_admin_config"], ["auditor", "ranger_auditor_config"],["user", "ranger_user_config"]), + ids=["keyadmin", "auditor", "user"],) + def test_delete_permission_group_with_invalid_roles(self, role, auth, request): + auth_config = getattr(self, auth) + module, module_id = request.getfixturevalue("temp_permission_module")() + grp, grp_id = request.getfixturevalue("temp_group")() + + perm_grp, perm_id = request.getfixturevalue("temp_permission_group")(grp_id, module_id) + + response = requests.delete( + f"{self.base_url}/xusers/permission/group/{perm_id}", + auth=auth_config, + headers=self.headers + ) + + assert_response( + response, + 403, + f"Permission group deletion should fail for role {role} but got {response.status_code}" + ) + + @pytest.mark.delete + @pytest.mark.negative + @pytest.mark.parametrize( + "role, auth", + (["keyadmin", "ranger_key_admin_config"], ["auditor", "ranger_auditor_config"], ["user", "ranger_user_config"]), + ids=["keyadmin", "auditor", "user"], + ) + def test_delete_permission_user(self, role, auth, request): + + auth_config = getattr(self, auth) + module_id = self.permission_module_id + user_id = self.ranger_user_id + + perm_user_id = self.permission_user_id + + response = requests.delete( + f"{self.base_url}/xusers/permission/user/{perm_user_id}", + auth=auth_config, + headers=self.headers + ) + + assert_response( + response, + 403, + f"Permission user deletion should fail for role {role} but got {response.status_code}" + ) \ No newline at end of file diff --git a/functional-tests/xuserrest/test_secure_user.py b/functional-tests/xuserrest/test_secure_user.py new file mode 100644 index 0000000000..2723972df7 --- /dev/null +++ b/functional-tests/xuserrest/test_secure_user.py @@ -0,0 +1,1330 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from urllib import * +import pytest +import requests +import json +from datetime import datetime +import random +import string +from xuserrest.utility.utils import * + +BIGINT_MIN = -9223372036854775808 +BIGINT_MAX = 9223372036854775807 + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +@pytest.mark.secure_endpoint +class TestSecureUserEndpoint: + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + # ---------- secondary users ---------- + cls.admin1, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin1, _ = temp_keyadmin_user() + cls.audit1, _ = temp_secure_user(["auditor"]) + cls.user1, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config1 = (cls.admin1["name"], "Test@123") + cls.ranger_key_admin_config1 = (cls.ranger_key_admin1["name"], "Test@123") + cls.ranger_auditor_config1 = (cls.audit1["name"], "Test@123") + cls.ranger_user_config1 = (cls.user1["name"], "Test@123") + + cls.ranger_admin_id1 = cls.admin1["id"] + cls.ranger_key_admin_id1 = cls.ranger_key_admin1["id"] + cls.ranger_auditor_id1 = cls.audit1["id"] + cls.ranger_user_id1 = cls.user1["id"] + + # ---------- common ---------- + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + + # POSITIVE TESTS + @pytest.mark.get + @pytest.mark.positive + def test_get_secure_user_schema_validation(self, client_roles, all_schema_following_users): + + if client_roles == ["ROLE_USER"]: + assert False, "Test requires elevated privileges to access all users" + + """ check only if you need to verify all users + for user in all_schema_following_users: + + response = requests.get( + f"{self.base_url}/xusers/secure/users/{user['id']}", + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert_response(response, 200) + + assert response.headers["Content-Type"].startswith("application/json") + assert response.elapsed.total_seconds() < 2 + + data = response.json() + + validate_secure_user_schema(data) + + assert data["id"] == user["id"]""" + user_id = 1 + response = requests.get( + f"{self.base_url}/xusers/secure/users/{user_id}", + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert_response(response, 200) + + + data = response.json() + + validate_secure_user_schema(data) + + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize("auth_role, target_role", [ + ("admin", "admin"), + ("admin", "auditor"), + ("auditor", "admin"), + ("auditor", "auditor"), + ("keyadmin", "keyadmin"), + ("keyadmin", "user"), + ("s_auditor", "auditor"), + + ]) + def test_get_secure_external_user(self, client_roles, auth_role, target_role, request): + + if not any(r in client_roles for r in ["ROLE_SYS_ADMIN", "ROLE_ADMIN_AUDITOR","ROLE_KEY_ADMIN"]): + pytest.skip("Test requires admin or auditor privileges") + if target_role == "keyadmin": + target_id = self.ranger_key_admin_id + assert user_exists(target_id, self.ranger_key_admin_config, self.base_url, self.headers), "Pre-requisite failed: Expected keyadmin user does not exist or inaccessible" + elif target_role == "admin": + target_id = self.ranger_admin_id + elif target_role == "auditor": + target_id = self.ranger_auditor_id + elif target_role == "user": + target_id = self.ranger_user_id + + + if auth_role == "admin": + authorization = self.ranger_admin_config + + elif auth_role == "auditor": + auth_id = self.ranger_auditor_id1 + authorization = self.ranger_auditor_config1 + elif auth_role == "s_auditor": + auth_id = self.ranger_auditor_id + authorization = self.ranger_auditor_config + elif auth_role == "keyadmin": + authorization = self.ranger_key_admin_config + + + response = requests.get( + f"{self.base_url}/xusers/secure/users/external/{target_id}", + auth=authorization, + headers=self.headers + ) + + assert_response(response, 200) + + data = response.json() + + if auth_role == "admin": + validate_external_user_schema(data) + + elif auth_role == "auditor": + # If auditor fetching their own data + if auth_id == target_id: + validate_external_user_schema(data) + else: + assert data["vXStrings"] == [], \ + "Auditor should not see other users' external roles" + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize("auth_role, target_role", [ + ("admin", "admin"), + ("admin", "auditor"), + ("keyadmin", "keyadmin"), + ("keyadmin", "user"), + ("s_auditor", "auditor"), + ]) + def test_get_secure_user_roles_by_username(self, client_roles, auth_role, target_role, request): + #admin cant access keyadmin, can access the users, auditor, admin + # keyadmin cant access admin, can access the users, keyadmin + # auditor if it is with same name and creds it will return this is flaw in internal repo + + if not any(r in client_roles for r in ["ROLE_SYS_ADMIN", "ROLE_ADMIN_AUDITOR","ROLE_KEY_ADMIN"]): + pytest.skip("Test requires admin or auditor privileges") + if target_role == "auditor": + target_user, target_id = self.audit, self.ranger_auditor_id + target_name = target_user["name"] + elif target_role == "keyadmin": + target_name = self.ranger_key_admin["name"] + assert user_exists(self.ranger_key_admin_id, self.ranger_key_admin_config, self.base_url, self.headers), "Pre-requisite failed: Expected keyadmin user does not exist or inaccessible" + elif target_role == "admin": + target_name = self.admin["name"] + elif target_role == "user": + target_name = self.user["name"] + else: + pytest.fail(f"Unhandled target_role in test setup: {target_role}") + + if auth_role == "admin": + authorization = self.ranger_admin_config + elif auth_role == "auditor": + auth_user, auth_id = self.audit1, self.ranger_auditor_id1 + authorization = self.ranger_auditor_config1 + elif auth_role == "s_auditor": + auth_user, auth_id = self.audit, self.ranger_auditor_id + authorization = self.ranger_auditor_config + target_name = auth_user["name"] # Auditor can access only their own roles, so target_name should be same as auth_user's name + elif auth_role == "keyadmin": + authorization = self.ranger_key_admin_config + response = requests.get( + f"{self.base_url}/xusers/secure/users/roles/userName/{target_name}", + auth=authorization, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + print("Response text:", response.text, "Response code:", response.status_code, " for auth_role:", auth_role, " target_role:", target_role) + + assert_response(response, 200) + + data = response.json() + + if auth_role == "admin": + validate_external_user_schema(data) + + elif auth_role == "auditor": + # If auditor fetching their own data + if auth_id == target_id: + validate_external_user_schema(data) + else: + assert data["vXStrings"] == [], \ + "Auditor should not see other users' external roles" + + + @pytest.mark.post + @pytest.mark.positive + def test_create_secure_user(self, client_roles): + + if "ROLE_SYS_ADMIN" not in client_roles: + assert False, "Test requires admin privileges" + + print("\nCreating a new secure user") + + random_suffix = ''.join(random.choices(string.ascii_lowercase, k=5)) + username = f"pytest_user_{random_suffix}" + + payload = { + "name": username, + "firstName": "PyTest", + "lastName": "User", + "emailAddress": f"{username}@test.com", + "password": "Test@123", + "status": 1, + "isVisible": 1, + "userRoleList": ["ROLE_USER"], + "groupIdList": [], + "groupNameList": [] + } + + response = requests.post( + f"{self.base_url}/xusers/secure/users", + json=payload, + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert_response(response, 200) + data = response.json() + user_id = data["id"] + + print(f"\nCreated user: {username} | ID: {user_id}") + + try: + validate_secure_user_schema(data) + assert data["name"] == username + # Verify persistence + verify_response = requests.get( + f"{self.base_url}/xusers/secure/users/{user_id}", + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert_response(verify_response, 200) + + verify_data = verify_response.json() + assert verify_data["id"] == user_id + finally: + if user_id: + delete_user(user_id, force=True, ranger_admin_config=self.ranger_admin_config, base_url=self.base_url, headers=self.headers) + + @pytest.mark.put + @pytest.mark.positive + def test_update_secure_user_flow(self, request, client_roles): + + if not any(role in client_roles for role in ["ROLE_SYS_ADMIN", "ROLE_KEY_ADMIN"]): + assert False, "Test requires admin or key-admin privileges" + + created_user, user_id = request.getfixturevalue("temp_secure_user")() + + print(f"Created user ID: {user_id}, firstName: {created_user['firstName']}") + + print("\n Updating created user") + update_payload = created_user.copy() + update_payload["firstName"] = "Updatedname" + + update_response = requests.put( + f"{self.base_url}/xusers/secure/users/{user_id}", + json=update_payload, + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert_response(update_response, 200) + updated_data = update_response.json() + + validate_secure_user_schema(updated_data) + assert updated_data["firstName"] == "Updatedname" + + print(f"Update successful for user ID: {user_id}, firstName: {updated_data['firstName']}") + + @pytest.mark.put + @pytest.mark.positive + def test_update_secure_user_active_status(self, request, client_roles): + + if "ROLE_SYS_ADMIN" not in client_roles: + assert False, "Test requires admin privileges" + + created_user1, user_id1 = request.getfixturevalue("temp_secure_user")() + created_user2, user_id2 = request.getfixturevalue("temp_secure_user")() + + print(f"\nTesting active status update") + + update_payload = { + str(user_id1): 0, + str(user_id2): 0 + } + + response = requests.put( + f"{self.base_url}/xusers/secure/users/activestatus", + json=update_payload, + auth=(self.ranger_admin_config), + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + assert_response(response, 204, f"Failed to update active status") + response1 = requests.get( + f"{self.base_url}/xusers/secure/users/{user_id1}", + auth=self.ranger_admin_config, + headers=self.headers + ) + response2 = requests.get( + f"{self.base_url}/xusers/secure/users/{user_id2}", + auth=self.ranger_admin_config, + headers=self.headers + ) + assert response1.json()["status"] == 0, f"User ID {user_id1} active status not updated" + assert response2.json()["status"] == 0, f"User ID {user_id2} active status not updated" + + @pytest.mark.put + @pytest.mark.positive + def test_update_secure_user_visibility(self, request, client_roles): + + if "ROLE_SYS_ADMIN" not in client_roles: + assert False, "Test requires admin privileges" + + created_user1, user_id1 = request.getfixturevalue("temp_secure_user")() + created_user2, user_id2 = request.getfixturevalue("temp_secure_user")() + + print(f"\nTesting visibility update") + + update_payload = { + str(user_id1): 0, + str(user_id2): 0 + } + + response = requests.put( + f"{self.base_url}/xusers/secure/users/visibility", + json=update_payload, + auth=(self.ranger_admin_config), + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + assert_response(response, 204, f"Failed to update visibility") + + response1 = requests.get( + f"{self.base_url}/xusers/secure/users/{user_id1}", + auth=self.ranger_admin_config, + headers=self.headers + ) + response2 = requests.get( + f"{self.base_url}/xusers/secure/users/{user_id2}", + auth=self.ranger_admin_config, + headers=self.headers + ) + assert response1.json()["isVisible"] == 0, f"User ID {user_id1} visibility not updated" + assert response2.json()["isVisible"] == 0, f"User ID {user_id2} visibility not updated" + + @pytest.mark.parametrize("auth_role, target_role", [ + ("admin", "admin"), + ("admin", "auditor"), + ("admin", "user"), + ("keyadmin", "keyadmin"), + ("keyadmin", "user") + ]) + @pytest.mark.put + @pytest.mark.positive + def test_update_secure_role_using_username(self, auth_role, target_role, client_roles, request): + + if not any(role in client_roles for role in ["ROLE_SYS_ADMIN", "ROLE_KEY_ADMIN"]): + assert False, "Test requires admin or key-admin privileges" + + if target_role == "keyadmin": + target_user, target_id = request.getfixturevalue("temp_keyadmin_user")() + + auth_user = self.ranger_key_admin + + assert auth_user["name"] != target_user["name"], \ + "Auth user and target user must be different" + + authorization = (auth_user["name"], 'Test@123') + else: + target_user, target_id = request.getfixturevalue("temp_secure_user")([target_role]) + + if auth_role == "admin": + authorization = self.ranger_admin_config + elif auth_role == "keyadmin" and target_role != "keyadmin": + authorization = self.ranger_key_admin_config + elif auth_role == "auditor": + authorization = self.ranger_auditor_config + elif auth_role == "user": + authorization = self.ranger_user_config + + + print(f"\nTesting role update for user: {target_user['name']}") + + update_payload = { + "vXStrings": [ + {"value": "ROLE_USER"} + ] + } + + response = requests.put( + f"{self.base_url}/xusers/secure/users/roles/userName/{target_user['name']}", + json=update_payload, + auth=authorization, + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 200) + + updated_data = response.json() + validate_external_user_schema(updated_data) + assert "ROLE_USER" in updated_data["vXStrings"][0]["value"] + + + @pytest.mark.parametrize("auth_role, target_role", [ + ("admin", "admin"), + ("admin", "auditor"), + ("admin", "user"), + ("keyadmin", "keyadmin"), + ("keyadmin", "user") + ]) + @pytest.mark.put + @pytest.mark.positive + def test_update_secure_role_using_id(self, auth_role, target_role, client_roles, request): + + if not any(role in client_roles for role in ["ROLE_SYS_ADMIN", "ROLE_KEY_ADMIN"]): + assert False, "Test requires admin or key-admin privileges" + + if target_role == "keyadmin": + target_user,target_id = request.getfixturevalue("temp_keyadmin_user")() + assert user_exists(target_id, self.ranger_key_admin_config, self.base_url, self.headers), "Pre-requisite failed: Expected keyadmin user does not exist or inaccessible" + + else: + target_user, target_id = request.getfixturevalue("temp_secure_user")([target_role]) + + if auth_role == "admin": + authorization = self.ranger_admin_config + elif auth_role == "keyadmin": + authorization = self.ranger_key_admin_config + elif auth_role == "auditor": + authorization = self.ranger_auditor_config + elif auth_role == "user": + authorization = self.ranger_user_config + + + print(f"\nTesting role update for user: {target_user['name']}") + + update_payload = { + "vXStrings": [ + {"value": "ROLE_USER"} + ] + } + + response = requests.put( + f"{self.base_url}/xusers/secure/users/roles/{target_id}", + json=update_payload, + auth=authorization, + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 200) + + updated_data = response.json() + validate_external_user_schema(updated_data) + assert "ROLE_USER" in updated_data["vXStrings"][0]["value"] + + @pytest.mark.delete + @pytest.mark.positive + def test_delete_secure_user_by_id(self, request, client_roles): + + if "ROLE_SYS_ADMIN" not in client_roles: + pytest.fail("Test requires admin privileges") + + target_user, user_id = request.getfixturevalue("temp_secure_user")(["ROLE_USER"]) + + resp = requests.delete( + f"{self.base_url}/xusers/secure/users/id/{user_id}?forceDelete=true", + auth=self.ranger_admin_config, + headers={ + **self.headers, + "X-Requested-By": "ranger" # Mandatory for DELETE + } + ) + + assert_response(resp, 204, f"Failed to delete user: {target_user['name']}") + + + @pytest.mark.delete + @pytest.mark.positive + def test_delete_secure_bulk_users(self, request, client_roles): + + if "ROLE_SYS_ADMIN" not in client_roles: + pytest.fail("Test requires admin privileges") + + created_user1, user_id1 = request.getfixturevalue("temp_secure_user")() + created_user2, user_id2 = request.getfixturevalue("temp_secure_user")() + + delete_payload = { + "vXStrings": [ + {"value": created_user1["name"]}, + {"value": created_user2["name"]} + ] + } + + response = requests.delete( + f"{self.base_url}/xusers/secure/users/delete?forceDelete=true", + json=delete_payload, + auth=self.ranger_admin_config, + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 204) + assert not user_exists(user_id1, self.ranger_admin_config, self.base_url, self.headers), f"User ID {user_id1} should be deleted but still exists" + assert not user_exists(user_id2, self.ranger_admin_config, self.base_url, self.headers), f"User ID {user_id2} should be deleted but still exists" + + @pytest.mark.delete + @pytest.mark.positive + def test_get_deleted_user_by_name(self, request, client_roles): + if "ROLE_SYS_ADMIN" not in client_roles: + pytest.fail("Test requires admin privileges") + + target_user, _ = request.getfixturevalue("temp_secure_user")(["ROLE_USER"]) + user_name = target_user["name"] + + resp =requests.delete( + f"{self.base_url}/xusers/secure/users/{user_name}", + auth=self.ranger_admin_config, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + + assert_response(resp, 204, f"Failed to delete user: {target_user['name']}") + + response = requests.get( + f"{self.base_url}/xusers/secure/users/{user_name}", + auth=self.ranger_admin_config + ) + assert_response(response, 404) + + + # NEGATIVE TESTS + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "user_id, auth, expected_codes", + [ + (1, ("wrong_user", "wrong_pass"), {401, 403}), + (1, ("admin", "wrong_pass"), {401, 403}), + (1, ("", ""), {401, 403}), + (1, None, {401, 403}), + (-999999, "valid", {400, 404}), + (-12345, "valid", {400, 404}), + ("abc", "valid", {400, 404}), + ("east or west ranger is the best", "valid", {400, 404}), + ], + ids=[ + "wrong-credentials", + "wrong-password", + "empty-credentials", + "missing-auth", + "non-existing-id", + "negative-id", + "string-id", + "garbage-input", + ], + ) + def test_negative_access(self, user_id, auth, expected_codes): + + if auth == "valid": + auth = self.ranger_admin_config + + response = requests.get( + f"{self.base_url}/xusers/secure/users/{user_id}", + auth=auth, + headers=self.headers, + ) + + assert_response(response, expected_codes, f"Expected status codes {expected_codes} but got {response.status_code} for user_id: {user_id} with auth: {auth}") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "role, password, user_id", + [ + ("user", "Admin@123", 28), # wrong password + ("keyadmin", None, 1), # missing auth + ], + ids=["role-user", "role-keyadmin"], + ) + def test_role_access_restrictions(self, role, password, user_id, ranger_config, request): + + if role == "keyadmin": + create_keyadmin = request.getfixturevalue("temp_keyadmin_user") + keyadmin_user, _ = create_keyadmin() + + if password is None: + auth = None + else: + auth = (keyadmin_user["name"], password) + else: + new_user, _ = request.getfixturevalue("temp_secure_user")([role]) + auth = (new_user["name"], password) + + response = requests.get( + f"{self.base_url}/xusers/secure/users/{user_id}", + auth=auth, + headers=self.headers, + ) + assert_response(response, [401, 403], f"Unauthorized role accessed secure endpoint: Role={role}, User ID={user_id}") + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize("target_role", ("user", "keyadmin"),) + def test_get_secure_external_user_invalid_role(self, request, target_role): + + if target_role == "keyadmin": + authorization = self.ranger_key_admin_config + # keyadmin cann't access admin + target_id = self.ranger_admin_id1 + + else: + target_id = self.ranger_user_id1 + authorization = self.ranger_user_config + + response = requests.get( + f"{self.base_url}/xusers/secure/users/external/{target_id}", + auth=authorization, + headers=self.headers + ) + + assert_response(response, 403, f"{target_role} should not access external user endpoint") + + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize("auth_role, target_role", [ + ("admin", "keyadmin"), + ("keyadmin", "auditor"), + ("auditor", "auditor"), + ("auditor", "admin"), + ("auditor", "keyadmin"), + ("auditor", "user"), + ("user", "user"), + ("user", "auditor"), + ]) + def test_get_secure_user_roles_by_username_by_invalid_role(self, request, client_roles, auth_role, target_role): + # admin cant access keyadmin, can access the users, auditor, admin + # keyadmin cant access admin, can access the users, keyadmin + # auditor if it is with same name and creds it will return this is flaw in internal repo + + if not any(r in client_roles for r in ["ROLE_SYS_ADMIN", "ROLE_ADMIN_AUDITOR","ROLE_KEY_ADMIN"]): + pytest.fail("Test requires admin or auditor privileges") + if target_role == "keyadmin": + target_user,target_id = self.ranger_key_admin, self.ranger_key_admin_id + assert user_exists(target_id, self.ranger_key_admin_config, self.base_url, self.headers), "Pre-requisite failed: Expected keyadmin user does not exist or inaccessible" + target_name = target_user["name"] + elif target_role == "auditor": + target_user, target_id = self.audit, self.ranger_auditor_id + target_name = target_user["name"] + elif target_role == "admin": + target_user, target_id = self.admin, self.ranger_admin_id + target_name = target_user["name"] + elif target_role == "user": + target_user, target_id = self.user, self.ranger_user_id + target_name = target_user["name"] + + if auth_role == "admin": + authorization = self.ranger_admin_config1 + elif auth_role == "auditor": + authorization = self.ranger_auditor_config1 + elif auth_role == "keyadmin": + authorization = self.ranger_key_admin_config1 + elif auth_role == "user": + authorization = self.ranger_user_config1 + + response = requests.get( + f"{self.base_url}/xusers/secure/users/roles/userName/{target_name}", + auth=authorization, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + print("Response text:", response.text, "Response code:", response.status_code, " for auth_role:", auth_role, " target_role:", target_role) + + if auth_role == "auditor" and target_role != "keyadmin": + assert_response(response, 400) # it will show invalid input data + + else: + assert_response(response, 403) + + + @pytest.mark.post + @pytest.mark.negative + def test_create_secure_user_missing_name(self): + + payload = { + # "name" missing + "firstName": "PyTest", + "lastName": "User", + "emailAddress": "missingname@test.com", + "password": "Test@123", + "status": 1, + "isVisible": 1, + "userRoleList": ["ROLE_USER"], + "groupIdList": [], + "groupNameList": [] + } + + response = requests.post( + f"{self.base_url}/xusers/secure/users", + json=payload, + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert_response(response, 400) + + + @pytest.mark.post + @pytest.mark.negative + def test_create_secure_user_duplicate_username(self): + + + random_suffix = ''.join(random.choices(string.ascii_lowercase, k=5)) + username = f"duplicate_test_user_{random_suffix}" + + payload = { + "name": username, + "firstName": "PyTest", + "lastName": "User", + "emailAddress": f"{username}@test.com", + "password": "Test@123", + "status": 1, + "isVisible": 1, + "userRoleList": ["ROLE_USER"], + "groupIdList": [], + "groupNameList": [] + } + + user_id = None + + try: + # First creation → should succeed + first_response = requests.post( + f"{self.base_url}/xusers/secure/users", + json=payload, + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert_response(first_response, 200) + + user_id = first_response.json().get("id") + assert user_id is not None, "User ID not returned" + + # Second creation → should fail + second_response = requests.post( + f"{self.base_url}/xusers/secure/users", + json=payload, + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert_response(second_response, 400) + + finally: + delete_user(user_id, force=True, ranger_admin_config=self.ranger_admin_config, base_url=self.base_url, headers=self.headers) + + @pytest.mark.post + @pytest.mark.negative + def test_create_secure_user_via_invalid_roles(self, request): + + normal_user, n_id = request.getfixturevalue("temp_secure_user")(["user"]) + + auditor_user, a_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + + + users_to_test = [ + (normal_user["name"], "Test@123"), + (auditor_user["name"], "Test@123"), + self.ranger_key_admin_config, + ] + + for username, password in users_to_test: + + random_suffix = ''.join(random.choices(string.ascii_lowercase, k=5)) + blocked_username = f"blocked_{random_suffix}" + + payload = { + "name": blocked_username, + "firstName": "Blocked", + "lastName": "User", + "emailAddress": f"{blocked_username}@test.com", + "password": "Test@123", + "status": 1, + "isVisible": 1, + "userRoleList": ["ROLE_USER"], + "groupIdList": [], + "groupNameList": [] + } + + response = requests.post( + f"{self.base_url}/xusers/secure/users", + json=payload, + auth=(username, password), + headers=self.headers + ) + + print(f"{username} → {response.status_code}") + assert_response(response, [403,404] , f"{username} should not have permission to create secure users, but got {response.status_code}") + + + @pytest.mark.put + @pytest.mark.negative + def test_edit_secure_user_using_invalid_id(self): + + invalid_id = -999999 + update_payload = { + "firstName": "ShouldFail" + } + + + update_response = requests.put( + f"{self.base_url}/xusers/secure/users/{invalid_id}", + json=update_payload, + auth=self.ranger_admin_config, + headers=self.headers + ) + assert_response(update_response, 404, "Expected status code 404 for non-existing user ID") + + @pytest.mark.put + @pytest.mark.negative + def test_edit_secure_user_using_invalid_roles(self, request): + + normal_user, n_id = self.user1, self.ranger_user_id1 + auditor_user, a_id = self.audit1, self.ranger_auditor_id1 + + users_to_test = [ + self.ranger_user_config1, + self.ranger_auditor_config1, + ] + + + update_payload = normal_user.copy() + update_payload["firstName"] = "ShouldNotUpdate" + + for username, password in users_to_test: + + update_response = requests.put( + f"{self.base_url}/xusers/secure/users/{n_id}", + json=update_payload, + auth=(username, password), + headers=self.headers + ) + + print("we got response", update_response.status_code, " for username, ", username) + + assert_response(update_response, 403, f"{username} should not have permission to edit secure users, but got {update_response.status_code}") + + @pytest.mark.put + @pytest.mark.negative + def test_edit_secure_user_using_invalid_payload(self, request): + + created_user, user_id = request.getfixturevalue("temp_secure_user")(["user"]) # can use existing but for simplicity creating new user + + invalid_payload = { + "name": created_user["name"], + # missing required fields like firstName, lastName etc. + } + + update_response = requests.put( + f"{self.base_url}/xusers/secure/users/{user_id}", + json=invalid_payload, + auth=(created_user["name"], "Test@123"), + headers=self.headers + ) + + assert update_response.status_code == 400, "Expected status code 400 for invalid payload" + + @pytest.mark.parametrize("field", [ + "name", + "id", + "createDate" + ]) + @pytest.mark.put + @pytest.mark.negative + def test_edit_secure_mandatory_fields(self, request, field): + + created_user, user_id = request.getfixturevalue("temp_secure_user")(["user"]) + + invalid_payload = created_user.copy() + + # Apply invalid modification depending on field type + if field == "name": + invalid_payload[field] = "shld not update" + elif field == "id": + invalid_payload[field] = 1 # must be an existing ID, but not the one being updated + else: + invalid_payload[field] = "4566:00:00Z" + update_response = requests.put( + f"{self.base_url}/xusers/secure/users/{user_id}", + json=invalid_payload, + auth=self.ranger_admin_config, # must use admin to test validation + headers=self.headers + ) + + print(field, "update response code:", update_response.status_code) + + assert_response(update_response, 400, f"Expected status code 400 when trying to modify mandatory field: {field}") + + + @pytest.mark.put + @pytest.mark.negative + def test_update_secure_user_active_status_with_invalid_roles(self, request): + + target_user, target_id = request.getfixturevalue("temp_secure_user")() + normal_user, n_id = request.getfixturevalue("temp_secure_user")(["user"]) + auditor_user, a_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + + users_to_test = [ + (normal_user["name"], "Test@123"), + (auditor_user["name"], "Test@123"), + self.ranger_key_admin_config, + ] + + print(f"\nTesting active status update with unauthorized roles") + + update_payload = { + str(target_id): 0 + } + + for username, password in users_to_test: + print(f"Attempting update with user: {username}") + + response = requests.put( + f"{self.base_url}/xusers/secure/users/activestatus", + json=update_payload, + auth=(username, password), # Use the specific user's credentials + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 403, f"{username} should not have permission to update active status, but got {response.status_code}") + + verify_response = requests.get( + f"{self.base_url}/xusers/secure/users/{target_id}", + auth=self.ranger_admin_config, + headers=self.headers + ) + assert verify_response.json().get("status") != 0, "Security failure: Status was actually updated despite error code!" + + @pytest.mark.put + @pytest.mark.negative + def test_update_secure_user_visibility_with_invalid_roles(self, request): + + target_user, target_id = request.getfixturevalue("temp_secure_user")() + normal_user, n_id = request.getfixturevalue("temp_secure_user")(["user"]) + auditor_user, a_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + + users_to_test = [ + (normal_user["name"], "Test@123"), + (auditor_user["name"], "Test@123"), + self.ranger_key_admin_config, + ] + + print(f"\nTesting active status update with unauthorized roles") + + update_payload = { + str(target_id): 0 + } + + for username, password in users_to_test: + print(f"Attempting update with user: {username}") + + response = requests.put( + f"{self.base_url}/xusers/secure/users/visibility", + json=update_payload, + auth=(username, password), # Use the specific user's credentials + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 403, f"{username} should not have permission to update visibility, but got {response.status_code}") + + verify_response = requests.get( + f"{self.base_url}/xusers/secure/users/{target_id}", + auth=self.ranger_admin_config, + headers=self.headers + ) + assert verify_response.json().get("isVisible") != 0, "Security failure: Visibility was actually updated despite error code!" + + @pytest.mark.parametrize("auth_role, target_role", [ + ("admin", "keyadmin"), + ("keyadmin", "admin"), + ("keyadmin", "auditor"), + ("auditor", "admin"), + ("auditor", "user"), + ("auditor", "keyadmin"), + ("user", "user"), + ("user", "admin"), + ("user", "auditor"), + ("user", "keyadmin"), + ]) + @pytest.mark.put + @pytest.mark.negative + def test_update_secure_role_using_id_with_invalid_roles(self, auth_role, target_role, request): + + + if target_role == "keyadmin": + target_user,target_id = request.getfixturevalue("temp_keyadmin_user")() + assert user_exists(target_id, self.ranger_key_admin_config, self.base_url, self.headers), "Pre-requisite failed: Expected keyadmin user does not exist or inaccessible" + + else: + target_user, target_id = request.getfixturevalue("temp_secure_user")([target_role]) + + if auth_role == "admin": + authorization = self.ranger_admin_config + elif auth_role == "keyadmin": + authorization = self.ranger_key_admin_config + else: + auth_user, auth_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + authorization = (auth_user["name"], "Test@123") + + + print(f"\nTesting role update for id: {target_id}") + + update_payload = { + "vXStrings": [ + {"value": "ROLE_USER"} + ] + } + + response = requests.put( + f"{self.base_url}/xusers/secure/users/roles/{target_id}", + json=update_payload, + auth=authorization, + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 403) + + + + @pytest.mark.parametrize("auth_role, target_role", [ + ("admin", "keyadmin"), + ("keyadmin", "admin"), + ("keyadmin", "auditor"), + ("auditor", "admin"), + ("auditor", "user"), + ("auditor", "keyadmin"), + ("user", "user"), + ("user", "admin"), + ("user", "auditor"), + ("user", "keyadmin"), + ]) + @pytest.mark.put + @pytest.mark.negative + def test_update_secure_role_using_username_with_invalid_roles(self, auth_role, target_role, request): + + + if target_role == "keyadmin": + target_user,target_id = request.getfixturevalue("temp_keyadmin_user")() + assert user_exists(target_id, self.ranger_key_admin_config, self.base_url, self.headers), "Pre-requisite failed: Expected keyadmin user with ID 3 does not exist" + + else: + target_user, target_id = request.getfixturevalue("temp_secure_user")([target_role]) + + if auth_role == "admin": + authorization = self.ranger_admin_config + elif auth_role == "keyadmin": + authorization = self.ranger_key_admin_config + else: + auth_user, auth_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + authorization = (auth_user["name"], "Test@123") + + + print(f"\nTesting role update for user: {target_user['name']}") + + update_payload = { + "vXStrings": [ + {"value": "ROLE_USER"} + ] + } + + response = requests.put( + f"{self.base_url}/xusers/secure/users/roles/userName/{target_user['name']}", + json=update_payload, + auth=authorization, + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 403) + + + @pytest.mark.delete + @pytest.mark.negative + def test_delete_secure_users_invalid_payload(self, client_roles): + + if "ROLE_SYS_ADMIN" not in client_roles: + assert False, "Test requires admin privileges" + + print("\nTesting bulk delete with invalid payload") + + invalid_payload = { + "invalid": "data" + } + + response = requests.delete( + f"{self.base_url}/xusers/secure/users/delete", + auth=self.ranger_admin_config, + headers={ + **self.headers, + "X-Requested-By": "ranger" + }, + json=invalid_payload + ) + + assert_response(response, 204, f"Expected 204 silent failure for invalid bulk delete payload since the API gives no indication that the payload was wrong. {response.status_code}. Response: {response.text}") + + @pytest.mark.delete + @pytest.mark.negative + def test_delete_secure_user_using_invalid_id(self, client_roles): + + if "ROLE_SYS_ADMIN" not in client_roles: + assert False, "Test requires admin privileges" + + print("\nTesting delete with invalid user ID") + + invalid_user_id = -999999 + + response = requests.delete( + f"{self.base_url}/xusers/secure/users/id/{invalid_user_id}", + auth=self.ranger_admin_config, + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 404) + + @pytest.mark.delete + @pytest.mark.negative + @pytest.mark.parametrize("role",["user", "auditor", "keyadmin"]) + def test_delete_secure_user_using_invalid_role(self, request, role): + + normal_user, n_id = request.getfixturevalue("temp_secure_user")(role) + print(f"\nTesting secure delete with role: {role}") + print(f"User ID: {n_id}") + + response = requests.delete( + f"{self.base_url}/xusers/secure/users/id/{n_id}", + auth=(normal_user["name"], "Test@123"), + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 404, f"The api should return 404 because the spring intentially does not allow and invalid auth to reach but recieved {response.status_code}") + + @pytest.mark.delete + @pytest.mark.negative + def test_delete_secure_user_using_invalid_payload(self, request): + created_user, user_id = request.getfixturevalue("temp_secure_user")(["user"]) + + print("\nTesting delete with invalid payload") + + invalid_payload = { + "invalid": "data" + } + + response = requests.delete( + f"{self.base_url}/xusers/secure/users/delete", + auth=(created_user["name"], "Test@123"), + headers={ + **self.headers, + "X-Requested-By": "ranger" + }, + json=invalid_payload + ) + + assert_response(response, 404, f"Expected 404 since the API should not recognize the payload and thus not find any users to delete. Got {response.status_code}. Response: {response.text}") + + @pytest.mark.delete + @pytest.mark.negative + @pytest.mark.parametrize("fatal_payload", [ + {"vXStrings": "this_is_a_string_not_a_list"}, + {"vXStrings": [{"value": 12345}]}, + {"vXStrings": [{"value": {"unexpected": "dictionary"}}]}, + [{"vXStrings": [{"value": "user1"}]}] + ]) + def test_delete_secure_users_malformed_items(self, ranger_config, client_roles, fatal_payload): + if "ROLE_SYS_ADMIN" not in client_roles: + pytest.skip("Test requires admin privileges") + + response = requests.delete( + f"{ranger_config['base_url']}/xusers/secure/users/delete?forceDelete=true", + json=fatal_payload, + auth=ranger_config["auth"], + headers={**ranger_config["headers"], "X-Requested-By": "ranger"} + ) + + assert_response(response, 400) + + @pytest.mark.delete + @pytest.mark.negative + def test_delete_secure_user_using_invalid_username(self, client_roles): + + if "ROLE_SYS_ADMIN" not in client_roles: + assert False, "Test requires admin privileges" + + print("\nTesting delete with invalid username") + + invalid_username = "invalid_user7890#mkc" + + response = requests.delete( + f"{self.base_url}/xusers/secure/users/{invalid_username}", + auth=self.ranger_admin_config, + headers={ + **self.headers, + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 400, f"Expected 400 for invalid username, but got {response.status_code}. Response: {response.text}") + + @pytest.mark.delete + @pytest.mark.negative + @pytest.mark.parametrize("role",["user", "auditor", "keyadmin"]) + def test_delete_secure_user_by_username_using_invalid_role(self, ranger_config, request, role): + + normal_user, n_id = request.getfixturevalue("temp_secure_user")(role) + print(f"\nTesting secure delete with role: {role}") + print(f"User name: {normal_user['name']}") + + response = requests.delete( + f"{ranger_config['base_url']}/xusers/secure/users/{normal_user['name']}", + auth=(normal_user["name"], "Test@123"), + headers={ + **ranger_config["headers"], + "X-Requested-By": "ranger" + } + ) + + assert_response(response, 404, f"The api should return 404 because the spring intentionally does not allow and invalid auth to reach but received {response.status_code}") \ No newline at end of file diff --git a/functional-tests/xuserrest/test_ugsync.py b/functional-tests/xuserrest/test_ugsync.py new file mode 100644 index 0000000000..52cf30d27c --- /dev/null +++ b/functional-tests/xuserrest/test_ugsync.py @@ -0,0 +1,760 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from urllib import * +from wsgiref import headers +from xuserrest.utility.utils import * +import uuid +import string +import pytest +import requests +from datetime import datetime +import random + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +class TestUgsync: + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_group, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + # ---------- secondary users ---------- + cls.admin1, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin1, _ = temp_keyadmin_user() + cls.audit1, _ = temp_secure_user(["auditor"]) + cls.user1, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config1 = (cls.admin1["name"], "Test@123") + cls.ranger_key_admin_config1 = (cls.ranger_key_admin1["name"], "Test@123") + cls.ranger_auditor_config1 = (cls.audit1["name"], "Test@123") + cls.ranger_user_config1 = (cls.user1["name"], "Test@123") + + cls.ranger_admin_id1 = cls.admin1["id"] + cls.ranger_key_admin_id1 = cls.ranger_key_admin1["id"] + cls.ranger_auditor_id1 = cls.audit1["id"] + cls.ranger_user_id1 = cls.user1["id"] + + + # ---------- group details ---------- + cls.group, cls.group_id = temp_group() + cls.group1, cls.group_id1 = temp_group() + + # ---------- common ---------- + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize("role, auth", [ + ("admin", "ranger_admin_config") + ]) + def test_get_ugsync_groupusers(self, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not expected to have access to get group users list") + + auth = getattr(self, auth) + response = requests.get( + f"{self.base_url}/xusers/ugsync/groupusers", + auth = auth, + headers = self.headers + ) + assert_response(response, 200, f"Expected status code 200, got {response.status_code}") + data = response.json() + if data: + assert isinstance(data, dict), f"Expected response to be a dict, got {type(data)}" + + first_key = next(iter(data)) + assert isinstance(first_key, str) and len(first_key) <= 767, "Expected first key to be non-empty" + + first_value = data[first_key] + assert isinstance(first_value, list), f"Expected first value to be a list, got {type(first_value)}" + + if first_value: + assert isinstance(first_value[0], str) and len(first_value[0]) <= 767, f"Expected elements of the list to be strings, got {type(first_value[0])}" + + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("role, auth", [ + ("admin", "ranger_admin_config") + ]) + def test_create_ugsync_groupusers(self, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not expected to have access to create group users list") + auth = getattr(self, auth) + + flg,_ = group_exists(self.group1["id"], auth, self.base_url, self.headers) + assert flg, f"Group with name {self.group1['name']} does not exist which is not expected for test case: {role}" + + # assign group1 to user1 + assign_groups_to_user(self.user1["name"], [self.group1["name"]], auth, self.base_url, self.headers) + data = fetch_groups_for_user_id(self.user1["id"], self.ranger_admin_config, self.base_url, self.headers) + assert check_group_in_user_groups(self.group1['id'], data), f"Group with id {self.group1['id']} is not assigned to user with id {self.user['id']} which is not expected for test case: {role}" + + payload = [ + { + "groupName": self.group1["name"], + "addUsers": [self.user["name"]], + "delUsers": [self.user1["name"]] + } + ] + + return_value = return_value_ugsync_groupusers(payload, auth, self.base_url, self.headers) + + response = requests.post( + f"{self.base_url}/xusers/ugsync/groupusers", + auth = auth, + json = payload, + headers = self.headers + ) + assert_response(response, 200, f"Expected status code 200, got {response.status_code}") + + data = response.json() + assert data == return_value, f"Expected response to be {return_value}, got {data}" + + data = fetch_groups_for_user_id(self.user["id"], self.ranger_admin_config, self.base_url, self.headers) + assert check_group_in_user_groups(self.group1['id'], data), \ + f"Group with id {self.group1['id']} is not assigned to user with id {self.user['id']} which is not expected for test case: {role}" + + data = fetch_groups_for_user_id(self.user1["id"], self.ranger_admin_config, self.base_url, self.headers) + assert not check_group_in_user_groups(self.group1['id'], data), \ + f"Group with id {self.group1['id']} is still assigned to user1 with id {self.user1['id']} which is not expected for test case: {role}" + + + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("role, auth, test_case", [ + ("admin", "ranger_admin_config", "ugsync via ldap"), + ("admin", "ranger_admin_config", "ugsync via unix"), + ("admin", "ranger_admin_config", "ugsync via file"), + ("admin", "ranger_admin_config", "ugsync via multi sources") + + ]) + def test_create_ugsync_auditinfo(self, role, auth, test_case): + if role != "admin": + pytest.fail(f"Role {role} is not expected to have access to create audit info") + auth = getattr(self, auth) + + + payload = { + "syncSource": "", + "noOfNewUsers": 10, + "noOfNewGroups": 5, + "noOfModifiedUsers": 3, + "noOfModifiedGroups": 2, + } + + if test_case == "ugsync via ldap": + payload["syncSource"] = "LDAP" + payload["ldapSyncSourceInfo"] = {} + elif test_case == "ugsync via unix": + payload["syncSource"] = "UNIX" + payload["unixSyncSourceInfo"] = {} + elif test_case == "ugsync via file": + payload["syncSource"] = "FILE" + payload["fileSyncSourceInfo"] = {} + elif test_case == "ugsync via multi sources": + payload["syncSource"] = "MULTI_SOURCES" + payload["ldapSyncSourceInfo"] = {} + payload["unixSyncSourceInfo"] = {} + payload["fileSyncSourceInfo"] = {} + + validate_auditinfo_schema(payload) + + response = requests.post( + f"{self.base_url}/xusers/ugsync/auditinfo", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, 200, f"Expected status code 200, got {response.status_code}") + data = response.json() + validate_auditinfo_schema(data) + validate_sync_source_info(payload, data) + + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("role, auth, test_case", [ + ("admin", "ranger_admin_config","existing groups in payload"), + ("admin", "ranger_admin_config","new groups in payload") + ]) + def test_create_ugsync_group(self, role, auth, test_case): + if role != "admin": + pytest.fail(f"Role {role} is not expected to have access to create group users list") + + auth = getattr(self, auth) + + payload = { + "vXGroups":[ + { + "name": "testgroupugsync"+str(random.randint(1000,9999)), + "description": "test description for group created via ugsync" + } + ] + } + + if test_case == "existing groups in payload": + payload["vXGroups"][0]["name"] = self.group1["name"] + flg, resp = group_exists(self.group1["id"], auth, self.base_url, self.headers) + + assert flg, f"Group with name {self.group1['name']} does not exist..." + cur_description = resp.get("description") + assert cur_description != payload["vXGroups"][0]["description"], f"Group description is already same as payload for existing group in payload which is not expected for test case: {test_case}" + + + for i in payload["vXGroups"]: + validate_xgroup_schema(i) # Removed the trailing comma issue + + try: + response = requests.post( + f"{self.base_url}/xusers/ugsync/groups", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, 200, f"Expected status code 200, got {response.status_code}") + data = response.json() + assert data == len(payload["vXGroups"]), f"Expected response to be {len(payload['vXGroups'])}, got {data}" + + for group in payload["vXGroups"]: + group_name = group["name"] + + resp = requests.get( + f"{self.base_url}/xusers/groups/groupName/{group_name}", + auth=auth, + headers=self.headers + ) + assert resp.status_code == 200, f"Group '{group_name}' missing after sync" + + if test_case == "existing groups in payload": + synced_group = resp.json() + assert synced_group.get("description") == group["description"], "Group description failed to update during sync!" + + finally: + # Cleanup only the random groups we created. + if test_case == "new groups in payload": + for group in payload["vXGroups"]: + resp = requests.get( + f"{self.base_url}/xusers/groups/groupName/{group['name']}", + auth=self.ranger_admin_config, + headers=self.headers + ) + if resp.status_code == 200: + delete_group(resp.json().get("id"), self.ranger_admin_config, self.base_url, self.headers) + + + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("role, auth", [ + ("admin", "ranger_admin_config") + ]) + def test_create_ugsync_group_visibility(self, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not expected to have access to create group users list") + + auth = getattr(self, auth) + + payload = [self.group1["name"]] + + return_value = len(set(payload)) + + response = requests.post( + f"{self.base_url}/xusers/ugsync/groups/visibility", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, 200, f"Expected status code 200, got {response.status_code}") + data = response.json() + assert data == return_value, f"Expected response to be {return_value}, got {data}" + + flg, resp = group_exists(self.group1["id"], auth, self.base_url, self.headers) + assert flg and resp.get("isVisible") == 0, f"Group visibility is not updated to 0 as expected for test case: {role}" + + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("role, auth, testcase", [ + ("admin", "ranger_admin_config", "single user in payload"), + ("admin", "ranger_admin_config", "multiple users in payload"), + ("admin", "ranger_admin_config", "existing user in payload") + ]) + def test_create_ugsync_users(self, role, auth, testcase): + if role != "admin": + pytest.fail(f"Role {role} is not expected to have access to create group users list") + + auth = getattr(self, auth) + + payload = { + "vXUsers":[ + { + "name": "testuserugsync"+str(random.randint(1000,9999)), + "firstName": "test", + "userRoleList": ["ROLE_USER"], + } + ] + } + + if testcase in ["multiple users in payload", "existing user in payload"]: + payload["vXUsers"].append( + payload["vXUsers"][0].copy() + ) + + return_value = 0 + for i in payload["vXUsers"]: + assert mandatory_field_check_in_response(i, ["name", "firstName", "userRoleList"]), f"Mandatory fields missing in payload user {i} for test case: {role}" + validate_user_schema(i) + return_value += 1 + + try: + response = requests.post( + f"{self.base_url}/xusers/ugsync/users", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, 200, f"Expected status code 200, got {response.status_code}") + data = response.json() + assert data == return_value, f"Expected response to be {return_value}, got {data}" + + finally: + deleted_users = set() + for i in payload["vXUsers"]: + username = i['name'] + + if username in deleted_users: + continue + + resp = requests.get( + f"{self.base_url}/xusers/users/userName/{username}", + auth = self.ranger_admin_config, + headers = self.headers, + ) + + if resp.status_code == 200: + delete_user(resp.json().get("id"), self.ranger_admin_config, self.base_url, self.headers) + deleted_users.add(username) + + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("role, auth", [ + ("admin", "ranger_admin_config") + ]) + def test_create_ugsync_users_visibility(self, role, auth): + if role != "admin": + pytest.fail(f"Role {role} is not expected to have access to create group users list") + + auth = getattr(self, auth) + + payload = [self.user["name"]] + + return_value = len(set(payload)) + + response = requests.post( + f"{self.base_url}/xusers/ugsync/users/visibility", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, 200, f"Expected status code 200, got {response.status_code}") + data = response.json() + assert data == return_value, f"Expected response to be {return_value}, got {data}" + + resp = requests.get( + f"{self.base_url}/xusers/users/userName/{self.user['name']}", + auth=auth, + headers=self.headers + ) + data = resp.json() + assert resp.status_code == 200 and data.get("isVisible") == 0, f"User visibility is not updated to 0 as expected for test case: {role}" + + + + # NEGATIVE TESTS + + @pytest.mark.negative + @pytest.mark.get + @pytest.mark.parametrize("role, auth", [ + ("key admin", "ranger_key_admin_config"), + ("auditor", "ranger_auditor_config"), + ("user", "ranger_user_config") + ]) + def test_get_ugsync_groupusers_unauthorized(self, role, auth): + auth = getattr(self, auth) + response = requests.get( + f"{self.base_url}/xusers/ugsync/groupusers", + auth = auth, + headers = self.headers + ) + assert_response(response, 404, f"Expected status code 404 for role {role} due to spring's silent failure, got {response.status_code}") + + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("auth, testcase", [ + ("ranger_key_admin_config", "unauthorized access by key admin"), + ("ranger_auditor_config", "unauthorized access by auditor"), + ("ranger_user_config", "unauthorized access by user"), + ("ranger_admin_config", "invalid group name in payload"), + ("ranger_admin_config", "invalid user name in addUser payload"), + ("ranger_admin_config", "invalid user name in delUser payload"), + ]) + def test_create_ugsync_groupusers_negative(self, auth, testcase): + auth = getattr(self, auth) + invalid_name = "invalid_user_name"+str(random.randint(1000,9999)) + + payload = [ + { + "groupName": self.group1["name"], + "addUsers": [self.user["name"]], + "delUsers": [] + } + ] + + if testcase == "invalid group name in payload": + payload[0]["groupName"] = invalid_name + response_code = 200 + assert not group_exists_by_name(invalid_name, self.ranger_admin_config, self.base_url, self.headers) + + + elif testcase == "invalid user name in addUser payload": + payload[0]["addUsers"] = [invalid_name] + response_code = 200 # silent failure + assert not user_exists_by_name(invalid_name, self.ranger_admin_config, self.base_url, self.headers) + + elif testcase == "invalid user name in delUser payload": + payload[0]["delUsers"] = [invalid_name] + response_code = 200 # silent failure + + else: + response_code = 404 # spring silent failure for unauthorized access to this API is returning 404 instead of 403 which is not expected but we are asserting based on actual response due to this reason + + response = requests.post( + f"{self.base_url}/xusers/ugsync/groupusers", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, response_code, f"Expected status code {response_code} for test case: {testcase}, got {response.status_code}") + if testcase.startswith("unauthorized access"): + return + + response_data = return_value_ugsync_groupusers(payload, self.ranger_admin_config, self.base_url, self.headers) + data = response.json() + assert data == response_data, f"Expected response to be {response_data} for test case: {testcase}, got {data}" + + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("auth, testcase", [ + ("ranger_key_admin_config", "unauthorized access by key admin"), + ("ranger_auditor_config", "unauthorized access by auditor"), + ("ranger_user_config", "unauthorized access by user"), + ("ranger_admin_config", "missing fields in payload") + ]) + def test_create_ugsync_auditinfo_unauthorized(self, auth, testcase): + auth = getattr(self, auth) + + payload = { + "syncSource": "LDAP", + "ldapSyncSourceInfo": {}, + "noOfNewUsers": 10, + "noOfNewGroups": 5, + "noOfModifiedUsers": 3, + "noOfModifiedGroups": 2, + } + + if testcase == "missing fields in payload": + mandatory_fields = ["syncSource", "noOfNewUsers", "noOfNewGroups", "noOfModifiedUsers", "noOfModifiedGroups"] + if "ldapSyncSourceInfo" in payload: + mandatory_fields.append("ldapSyncSourceInfo") + if "unixSyncSourceInfo" in payload: + mandatory_fields.append("unixSyncSourceInfo") + if "fileSyncSourceInfo" in payload: + mandatory_fields.append("fileSyncSourceInfo") + #field_to_remove = random.choice(mandatory_fields) + field_to_remove = "syncSource" + print(f"Removing field {field_to_remove} from payload for test case: {testcase}") + del payload[field_to_remove] + expected_status = 404 + else: + expected_status = 404 # spring silent failure for unauthorized access to this API is returning 404 instead of 403 which is not expected but we are asserting based on actual response due to this reason + + response = requests.post( + f"{self.base_url}/xusers/ugsync/auditinfo", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, expected_status, f"Expected status code {expected_status} for test case: {testcase}, got {response.status_code}") + + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("auth, testcase", [ + ("ranger_key_admin_config", "unauthorized access by key admin"), + ("ranger_auditor_config", "unauthorized access by auditor"), + ("ranger_user_config", "unauthorized access by user"), + ("ranger_admin_config", "invalid payload in Vxgroups"), + ("ranger_admin_config", "invalid group name in payload") + ]) + def test_create_ugsync_group_negative(self, auth, testcase): + auth = getattr(self, auth) + + payload = { + "vXGroups":[ + { + "name": "testgroupugsync"+str(random.randint(1000,9999)), + "description": "test description for group created via ugsync" + } + ] + } + + if testcase == "invalid payload in Vxgroups": + payload["vXGroups"][0]["isVisible"] = "invalidValue_expected bool" + expected_status = 400 + elif testcase == "invalid group name in payload": + payload["vXGroups"][0]["name"] = "" + expected_status = 200 # silent failure but group should not be created + else: + expected_status = 404 # spring silent failure for unauthorized access to this API is returning 404 instead of 403 which is not expected but we are asserting based on actual response due to this reason + + response = requests.post( + f"{self.base_url}/xusers/ugsync/groups", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, expected_status, f"Expected status code {expected_status} for test case: {testcase}, got {response.status_code}") + + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("auth, testcase", [ + ("ranger_key_admin_config", "unauthorized access by key admin"), + ("ranger_auditor_config", "unauthorized access by auditor"), + ("ranger_user_config", "unauthorized access by user"), + ("ranger_admin_config", "invalid group name in payload - repetition in list"), + ("ranger_admin_config", "invalid group name in payload - non existing group"), + ]) + def test_create_ugsync_group_visibility_negative(self, auth, testcase): + auth = getattr(self, auth) + + payload = [self.group1["name"]] + + if testcase == "invalid group name in payload - repetition in list": + payload.append(self.group1["name"]) + expected_status = 200 # silent failure but visibility should be updated for the group + elif testcase == "invalid group name in payload - non existing group": + payload[0] = "non_existing_group"+str(random.randint(1000,9999)) + expected_status = 200 # silent failure but no group should be created + else: + expected_status = 404 # spring silent failure for unauthorized access to this API is returning 404 instead of 403 which is not expected but we are asserting based on actual response due to this reason + + response = requests.post( + f"{self.base_url}/xusers/ugsync/groups/visibility", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, expected_status, f"Expected status code {expected_status} for test case: {testcase}, got {response.status_code}") + + if testcase.startswith("unauthorized access"): + return + return_value = len(set(payload)) + assert return_value == response.json(), f"Expected response to be {return_value} for test case: {testcase}, got {response.json()}" + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("auth, testcase", [ + ("ranger_key_admin_config", "unauthorized access by key admin"), + ("ranger_auditor_config", "unauthorized access by auditor"), + ("ranger_user_config", "unauthorized access by user"), + ("ranger_admin_config", "invalid payload in Vxusers - missing mandatory field"), + ("ranger_admin_config", "invalid payload in Vxusers - missing mandatory field - userRoleList"), + ("ranger_admin_config", "invalid payload in Vxusers - keyadmin role in userRoleList"), + ("ranger_admin_config", "invalid payload in Vxusers - empty user name"), + ("ranger_admin_config", "invalid payload in Vxusers - empty first name"), + ("ranger_admin_config", "semi failure - invalid and valid users in payload") + ]) + def test_create_ugsync_users_negative(self, auth, testcase): + auth = getattr(self, auth) + + payload = { + "vXUsers":[ + { + "name": "testuserugsync"+str(random.randint(1000,9999)), + "firstName": "test", + "userRoleList": ["ROLE_USER"], + } + ] + } + + return_value = 0 + + if testcase == "invalid payload in Vxusers - missing mandatory field": + del payload["vXUsers"][0]["firstName"] + expected_status = 200 # silent failure but user should not be created + return_value -=1 + + elif testcase == "invalid payload in Vxusers - missing mandatory field - userRoleList": + del payload["vXUsers"][0]["userRoleList"] + expected_status = 403 # userRoleList is mandatory field for user creation via ugsync and should throw 403 if missing in payload + + elif testcase == "invalid payload in Vxusers - keyadmin role in userRoleList": + payload["vXUsers"][0]["userRoleList"] = ["ROLE_KEY_ADMIN"] + expected_status = 403 # cant assign key admin role to user via ugsync + + elif testcase == "invalid payload in Vxusers - empty user name": + payload["vXUsers"][0]["name"] = "" + expected_status = 200 # silent failure but user should not be created + return_value -=1 + + elif testcase == "invalid payload in Vxusers - empty first name": + payload["vXUsers"][0]["firstName"] = "" + expected_status = 200 # silent failure but user should not be created + return_value -=1 + + elif testcase == "semi failure - invalid and valid users in payload": + payload = { + "vXUsers":[ + { + "name": "testuserugsync"+str(random.randint(1000,9999)), + "firstName": "test", + "userRoleList": ["ROLE_USER"], + }, + { + "name": "", # invalid user with empty name + "firstName": "test", + "userRoleList": ["ROLE_USER"], + } + ] + } + temp_usr= payload["vXUsers"][0]["name"] + expected_status = 200 # silent failure but user should not be created + return_value -= 1 # only the valid user in payload should be created + + else: + expected_status = 404 # spring silent failure for unauthorized access to this API is returning 404 instead of 403 which is not expected but we are asserting based on actual response due to this reason + + response = requests.post( + f"{self.base_url}/xusers/ugsync/users", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, expected_status, f"Expected status code {expected_status} for test case: {testcase}, got {response.status_code}") + + if testcase.startswith("unauthorized access") or testcase.endswith("userRoleList"): + return + + return_value += len(payload["vXUsers"]) + + assert response.json() == return_value , f"Expected response to be {return_value} for test case: {testcase}, got {response.json()}" + + if testcase == "semi failure - invalid and valid users in payload": + assert user_exists_by_name(temp_usr, self.ranger_admin_config, self.base_url, self.headers), f"Valid user in payload is not created for test case: {testcase}" + resp = requests.get( + f"{self.base_url}/xusers/users/userName/{temp_usr}", + auth = self.ranger_admin_config, + headers = self.headers + ) + assert resp.status_code == 200, f"User '{temp_usr}' missing after sync for test case: {testcase}" + u_id = resp.json().get("id") + delete_user(u_id, self.ranger_admin_config, self.base_url, self.headers) + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("auth, testcase", [ + ("ranger_key_admin_config", "unauthorized access by key admin"), + ("ranger_auditor_config", "unauthorized access by auditor"), + ("ranger_user_config", "unauthorized access by user"), + ("ranger_admin_config", "invalid user name in payload - repetition in list"), + ("ranger_admin_config", "invalid user name in payload - non existing user"), + ]) + def test_create_ugsync_users_visibility_negative(self, auth, testcase): + auth = getattr(self, auth) + + payload = [self.group1["name"]] + + if testcase == "invalid user name in payload - repetition in list": + payload.append(self.group1["name"]) + expected_status = 200 # silent failure but visibility should be updated for the user if it exists + elif testcase == "invalid user name in payload - non existing user": + payload[0] = "non_existing_user"+str(random.randint(1000,9999)) + expected_status = 200 # silent failure but no user should be created + else: + expected_status = 404 # spring silent failure for unauthorized access to this API is returning 404 instead of 403 which is not expected but we are asserting based on actual response due to this reason + + response = requests.post( + f"{self.base_url}/xusers/ugsync/users/visibility", + auth = auth, + json = payload, + headers = self.headers + ) + + assert_response(response, expected_status, f"Expected status code {expected_status} for test case: {testcase}, got {response.status_code}") + + if testcase.startswith("unauthorized access"): + return + return_value = len(set(payload)) + assert return_value == response.json(), f"Expected response to be {return_value} for test case: {testcase}, got {response.json()}" diff --git a/functional-tests/xuserrest/test_user.py b/functional-tests/xuserrest/test_user.py new file mode 100644 index 0000000000..81589947ab --- /dev/null +++ b/functional-tests/xuserrest/test_user.py @@ -0,0 +1,1058 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from urllib import * +from xuserrest.utility.utils import * +import uuid +import string +import pytest +import requests +from datetime import datetime +import random + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +class TestUsers: + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + # ---------- secondary users ---------- + cls.admin1, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin1, _ = temp_keyadmin_user() + cls.audit1, _ = temp_secure_user(["auditor"]) + cls.user1, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config1 = (cls.admin1["name"], "Test@123") + cls.ranger_key_admin_config1 = (cls.ranger_key_admin1["name"], "Test@123") + cls.ranger_auditor_config1 = (cls.audit1["name"], "Test@123") + cls.ranger_user_config1 = (cls.user1["name"], "Test@123") + + cls.ranger_admin_id1 = cls.admin1["id"] + cls.ranger_key_admin_id1 = cls.ranger_key_admin1["id"] + cls.ranger_auditor_id1 = cls.audit1["id"] + cls.ranger_user_id1 = cls.user1["id"] + + # ---------- common ---------- + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + # Positive Test Cases + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"),("keyadmin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")], + ids=["admin", "keyadmin", "auditor", "user"], + ) + def test_get_users(self, role, auth, request): + auth = getattr(self, auth) + + url = f"{self.base_url}/xusers/users" + response = requests.get( + url, + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, f"Failed to fetch users list: {response.status_code}") + + data = response.json() + assert "vXUsers" in data, "Invalid users list schema" + + users = data["vXUsers"] + assert isinstance(users, list) + assert len(users) > 0, "No users found in Ranger" + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize( + "role, auth", + [("admin", "ranger_admin_config"),("keyadmin", "ranger_key_admin_config"), ("auditor", "ranger_auditor_config"), ("user", "ranger_user_config")], + ids=["admin", "keyadmin", "auditor", "user"], + ) + def test_get_user_count(self, role, auth, request): + auth = getattr(self, auth) + + url = f"{self.base_url}/xusers/users/count" + response = requests.get( + url, + auth = auth, + headers=self.headers + ) + + assert_response(response, 200, f"Failed to fetch user count: {response.status_code}") + + data = response.json() + print(f"Total user count response: {data['value']}, auth : {auth[0]}") + assert isinstance(data["value"], int), "User count not found in response" + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize("auth_role, user_role, auth", [ + ("admin", "user", "ranger_admin_config"), + ("admin", "auditor", "ranger_admin_config"), + ("admin", "admin", "ranger_admin_config"), + ("keyadmin", "keyadmin", "ranger_key_admin_config"), + ("keyadmin", "user", "ranger_key_admin_config"), + ("auditor", "admin", "ranger_auditor_config"), + ("auditor", "auditor", "ranger_auditor_config"), + ("auditor", "user", "ranger_auditor_config"), + ("user", "same_user", "ranger_user_config") +]) + def test_get_user_by_id(self, auth_role, user_role, auth): + auth = getattr(self, auth) + + if user_role == "same_user": + user, user_id = self.user, self.ranger_user_id + elif user_role == "keyadmin": + user, user_id = self.ranger_key_admin1, self.ranger_key_admin_id1 + elif user_role == "admin": + user, user_id = self.admin1, self.ranger_admin_id1 + elif user_role == "auditor": + user, user_id = self.audit1, self.ranger_auditor_id1 + elif user_role == "user": + user, user_id = self.user1, self.ranger_user_id1 + + response = requests.get( + f"{self.base_url}/xusers/users/{user_id}", + auth = auth, + headers=self.headers + ) + + assert_response(response, 200, f"Failed to fetch user of role {user_role} using {auth_role}") + + data = response.json() + validate_user_schema(data) + assert data["id"] == user_id, "Fetched user ID does not match requested ID" + + + @pytest.mark.positive + @pytest.mark.get + @pytest.mark.parametrize("auth_role, user_role, auth", [ + ("admin", "user", "ranger_admin_config"), + ("admin", "auditor", "ranger_admin_config"), + ("admin", "admin", "ranger_admin_config"), + ("keyadmin", "keyadmin", "ranger_key_admin_config"), + ("keyadmin", "user", "ranger_key_admin_config"), + ("auditor", "admin", "ranger_auditor_config"), + ("auditor", "auditor", "ranger_auditor_config"), + ("auditor", "user", "ranger_auditor_config"), + ("user", "same_user", "ranger_user_config") +]) + def test_get_users_by_username(self, auth_role, user_role, auth): + auth = getattr(self, auth) + + if user_role == "same_user": + user, user_id = self.user, self.ranger_user_id + elif user_role == "keyadmin": + user, user_id = self.ranger_key_admin1, self.ranger_key_admin_id1 + elif user_role == "admin": + user, user_id = self.admin1, self.ranger_admin_id1 + elif user_role == "auditor": + user, user_id = self.audit1, self.ranger_auditor_id1 + elif user_role == "user": + user, user_id = self.user1, self.ranger_user_id1 + + response = requests.get( + f"{self.base_url}/xusers/users/userName/{user['name']}", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, f"Failed to fetch user of role {user_role} using {auth_role}") + + data = response.json() + validate_user_schema(data) + assert data["id"] == user_id, "Fetched user ID does not match requested ID" + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("auth_role, auth", [("admin", "ranger_admin_config")]) + def test_create_user(self, auth_role, auth): + if auth_role != "admin": + assert False, "Test requires admin privileges" + + auth = getattr(self, auth) + + print("\nCreating a new secure user") + + username = f"mine_pytest_fixture_{uuid.uuid4().hex[:8]}" + + payload = { + "name": username, + "password": "Test@123" + } + + response = requests.post( + f"{self.base_url}/xusers/users", + json=payload, + auth=auth, + headers=self.headers + ) + + assert_response(response, 200) + data = response.json() + user_id = data["id"] + + print(f"\nCreated user: {username} | ID: {user_id}") + + try: + validate_user_schema(data) # since it will always return default users skip this + assert data["name"] == username + # Verify persistence + verify_response = requests.get( + f"{self.base_url}/xusers/users/{user_id}", + auth=auth, + headers=self.headers + ) + + assert_response(verify_response, 200) + + verify_data = verify_response.json() + assert verify_data["id"] == user_id + print(response.json()) + finally: + + delete_user(user_id, auth, self.base_url, self.headers) + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("auth_role, auth", [("admin", "ranger_admin_config")]) + def test_create_users_external(self, auth_role, auth): + if auth_role != "admin": + assert False, "Test requires admin privileges" + + auth = getattr(self, auth) + + print("\nCreating a new external user") + + username = f"external_pytest_fixture_{uuid.uuid4().hex[:8]}" + + payload = { + "name": username + } + + response = requests.post( + f"{self.base_url}/xusers/users/external", + json=payload, + auth=auth, + headers=self.headers + ) + + assert_response(response, 204) # since it wont return anything for new users. + + try : + response = requests.post( + f"{self.base_url}/xusers/users/external", + json=payload, + auth=auth, + headers=self.headers + ) + + data = response.json() + assert_response(response, 200) # since it will return existing user details for existing users. + user_id = data["id"] + + print(f"\nCreated external user: {username} | ID: {user_id}") + finally: + response = requests.delete( + f"{self.base_url}/xusers/users/{user_id}", + params={"forceDelete": "true"}, + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert_response(response, 204) + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("auth_role, auth", [("admin", "ranger_admin_config")]) + def test_create_user_userinfo(self, request, auth_role, auth): + + if auth_role != "admin": + assert False, "Test requires admin privileges" + + auth = getattr(self, auth) + + test_user, test_id = request.getfixturevalue("temp_secure_user")(["user"]) + + print(f"\nCreating a new secure user with userinfo endpoint") + assert user_exists(test_id, auth, self.base_url, self.headers), "User should exist before performing userinfo based creation" + + grp_name = random.choice(string.ascii_letters) + ''.join(random.choices(string.ascii_letters + string.digits, k=7)) + payload = { + "xuserInfo": { + "name": test_user["name"] + }, + "xgroupInfo": [ + { + "name": grp_name, + "groupType": 1, + "groupSource": 1, + "description": "this is for test" + + } + ] + } + + assert payload["xuserInfo"]["name"] == test_user["name"], "User info name should match the test user name" + assert len(payload["xgroupInfo"]) >= 1, "There should be at least one group info" + + response = requests.post( + f"{self.base_url}/xusers/users/userinfo", + json=payload, + auth=auth, + headers=self.headers + ) + + + assert_response(response, 200) + + data = response.json() + + validate_xgroup_schema(data["xgroupInfo"][0]) + + + assert data["xgroupInfo"][0]["name"] == payload["xgroupInfo"][0]["name"], "Response group name should match the test user name" + + grp_id = data["xgroupInfo"][0]["id"] + # cleanup + params = {"forceDelete": "true"} + del_req = requests.delete( + f"{self.base_url}/xusers/groups/{grp_id}", + auth=auth, + headers=self.headers, + params=params + ) + print(f"Deleted group {grp_name} created during test, status code: {del_req.status_code}") + + + @pytest.mark.positive + @pytest.mark.post + @pytest.mark.parametrize("auth_role, auth", [("admin", "ranger_admin_config")]) + def test_create_roleassignment(self, auth_role, auth, request): + + if auth_role != "admin": + pytest.fail("Test requires admin privileges") + auth = getattr(self, auth) + + users = [request.getfixturevalue("temp_secure_user")(["user"])[0] for _ in range(8)] + for u in users: + assert u["userSource"] == 1, f"{u['name']} should be external user" + + uA, uB, uC, uD, uE, uF, uG, uH = [u["name"] for u in users] + + group_admin = f"grp_admin_{uuid.uuid4().hex[:6]}" + group_auditor = f"grp_aud_{uuid.uuid4().hex[:6]}" + group_user = f"grp_user_{uuid.uuid4().hex[:6]}" + + assign_groups_to_user(uB, [group_auditor], auth, self.base_url, self.headers) # groupMap + assign_groups_to_user(uE, [group_admin], auth, self.base_url, self.headers) # wlGroup + assign_groups_to_user(uF, [group_admin], auth, self.base_url, self.headers) # override userMap + assign_groups_to_user(uG, [group_user], auth, self.base_url, self.headers) # override groupMap + assign_groups_to_user(uH, [group_admin], auth, self.base_url, self.headers) # wlUser vs wlGroup + + user_map = { + uA: "ROLE_SYS_ADMIN", + uF: "ROLE_ADMIN_AUDITOR", # will be overridden by whitelist group + } + + group_map = { + group_auditor: "ROLE_ADMIN_AUDITOR", + } + + white_user_map = { + uD: "ROLE_ADMIN_AUDITOR", + uH: "ROLE_SYS_ADMIN", # overrides whitelist group + } + + white_group_map = { + group_admin: "ROLE_SYS_ADMIN", + group_user: "ROLE_USER", + } + + payload = { + "users": [uA, uB, uC, uD, uE, uF, uG, uH], + "userRoleAssignments": user_map, + "groupRoleAssignments": group_map, + "whiteListUserRoleAssignments": white_user_map, + "whiteListGroupRoleAssignments": white_group_map, + "isReset": False, + "isLastPage": False + } + + response = requests.post( + f"{self.base_url}/xusers/users/roleassignments", + json=payload, + auth=auth, + headers=self.headers + ) + + assert_response(response, 200) + + expected = { + uA: "ROLE_SYS_ADMIN", # userMap + uB: "ROLE_ADMIN_AUDITOR", # groupMap + uC: "ROLE_USER", # default + uD: "ROLE_ADMIN_AUDITOR", # whitelist user + uE: "ROLE_SYS_ADMIN", # whitelist group + uF: "ROLE_SYS_ADMIN", # wlGroup > userMap + uG: "ROLE_USER", # wlGroup > groupMap + uH: "ROLE_SYS_ADMIN", # wlUser > wlGroup + } + + for user, expected_role in expected.items(): + resp = requests.get( + f"{self.base_url}/xusers/users/userName/{user}", + auth=auth, + headers=self.headers + ) + + assert resp.status_code == 200, f"Failed to fetch {user}" + data = resp.json() + validate_user_schema(data) + + actual_roles = data["userRoleList"] + print(f"{user} → {actual_roles}") + assert expected_role in actual_roles, \ + f"{user}: expected {expected_role}, got {actual_roles}" + + + # cleanup of created groups + for group in [group_admin, group_auditor, group_user]: + resp = requests.get( + f"{self.base_url}/xusers/groups/groupName/{group}", + auth=auth, + headers=self.headers + ) + if resp.status_code == 200: + group_id = resp.json()["id"] + params = {"forceDelete": "true"} + requests.delete( + f"{self.base_url}/xusers/groups/{group_id}", + params=params, + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + print(f"Deleted group {group} created during test") + assert_response(resp, 200, f"Failed to delete group {group} during cleanup") + + @pytest.mark.positive + @pytest.mark.put + @pytest.mark.parametrize("auth_role", ["admin", "keyadmin"]) + def test_update_user(self, auth_role, request): + + user, user_id = request.getfixturevalue("temp_secure_user")(["user"]) + + print(f"\nUpdating user: {user['name']} | ID: {user_id}") + + payload = { + "id": user_id, + "name": user["name"], + "firstName": "updatedfirstname", + "lastName": "updatedlastname", + "emailAddress": f"updated_{user['emailAddress']}", + "status": 1, + "isVisible": 1, + "userRoleList": ["ROLE_USER"], + "groupIdList": [], + "groupNameList": [] + } + + mandatory_fields = ["id", "name", "userRoleList", "firstName"] + missing = [k for k in mandatory_fields if k not in payload] + assert not missing, f"Missing mandatory fields in payload: {missing}" + + if auth_role == "admin": + auth = self.ranger_admin_config + assert "ROLE_KEY_ADMIN" not in payload["userRoleList"], "Admin can not promote any one to keyadmin role" + assert "ROLE_KEY_ADMIN" not in user["userRoleList"], "Admin can not update keyadmin role" + elif auth_role == "keyadmin": + auth = self.ranger_key_admin_config + assert "ROLE_SYS_ADMIN" not in payload["userRoleList"], "Key Admin can not promote any one to admin role" + assert "ROLE_ADMIN_AUDITOR" not in payload["userRoleList"], "Key Admin can not promote any one to auditor role" + assert "ROLE_SYS_ADMIN" not in user["userRoleList"], "Key Admin can not update admin role" + assert "ROLE_ADMIN_AUDITOR" not in user["userRoleList"], "Key Admin can not update auditor role" + + + resp = requests.get( + f"{self.base_url}/xusers/users/{user_id}", + auth=self.ranger_admin_config, + headers=self.headers + ) + resp = resp.json() + + for i in ["id", "name"]: + assert resp[i] == payload[i], f"Miss-match mandatory field in response: {i}" + + response = requests.put( + f"{self.base_url}/xusers/users", + json=payload, + auth=auth, + headers=self.headers + ) + + assert_response(response, 200) + + data = response.json() + validate_user_schema(data) + assert data["firstName"].lower() == "updatedfirstname" + assert data["lastName"].lower() == "updatedlastname" + assert data["emailAddress"].lower() == f"updated_{user['emailAddress']}".lower() + + @pytest.mark.positive + @pytest.mark.delete + def test_delete_user(self, request, client_roles): + if "ROLE_SYS_ADMIN" not in client_roles: + assert False, "Test requires admin privileges" + + user, user_id = request.getfixturevalue("temp_secure_user")(["user"]) + + print(f"\nDeleting user: {user['name']} | ID: {user_id}") + + assert user_exists(user_id, self.ranger_admin_config, self.base_url, self.headers), "User should exist before deletion" + + response = requests.delete( + f"{self.base_url}/xusers/users/{user_id}", + params={"forceDelete": "true"}, + auth=self.ranger_admin_config, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert response, "User deletion failed" + assert_response(response, 204) + + @pytest.mark.positive + @pytest.mark.delete + def test_delete_external_user_by_username(self, request, client_roles): + if "ROLE_SYS_ADMIN" not in client_roles: + assert False, "Test requires admin privileges" + + user, user_id = request.getfixturevalue("temp_secure_user")(["user"]) + userName = user["name"] + + print(f"\nDeleting external user: {user['name']} | ID: {user_id}") + + assert user_exists(user_id, self.ranger_admin_config, self.base_url, self.headers), "User should exist before deletion" + + response = requests.delete( + f"{self.base_url}/xusers/users/userName/{userName}", + params={"forceDelete": "true"}, + auth=self.ranger_admin_config, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert response, "External user deletion failed" + assert_response(response, 204) + + + # Negative Test Cases + @pytest.mark.negative + @pytest.mark.get + def test_get_users_using_invalid_auth(self): + random_num = random.randint(100000, 999999) + url = self.base_url + f'/xusers/users/{random_num}' + + response = requests.get( + url, + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert_response(response, 400, "Expected status code not returned for invalid user ID") + + + @pytest.mark.negative + @pytest.mark.get + @pytest.mark.parametrize("auth_role, user_role", [ + ("admin", "keyadmin"), + ("auditor", "keyadmin"), + ("keyadmin","admin"), + ("keyadmin","auditor"), + ("user", "admin"), + ("user", "auditor"), + ("user", "user"), + ("user", "keyadmin"), + ]) + def test_get_user_by_id_using_invalid_auth(self, auth_role, user_role, request): + if auth_role == "keyadmin": + auth = self.ranger_key_admin_config + elif auth_role == "admin": + auth = self.ranger_admin_config + elif auth_role == "auditor": + auth = self.ranger_auditor_config + elif auth_role == "user": + auth = self.ranger_user_config + + + if user_role == "keyadmin": + user_id = self.ranger_key_admin_id1 + elif user_role == "admin": + user_id = self.ranger_admin_id1 + elif user_role == "auditor": + user_id = self.ranger_auditor_id1 + elif user_role == "user": + user_id = self.ranger_user_id1 + + + response = requests.get( + f"{self.base_url}/xusers/users/{user_id}", + auth=auth, + headers=self.headers + ) + + assert_response(response, 403, f"{auth_role} should not have permission to access user of role {user_role}") + + + @pytest.mark.negative + @pytest.mark.get + @pytest.mark.parametrize("auth_role, user_role", [ + ("admin", "keyadmin"), + ("keyadmin", "admin"), + ("keyadmin", "auditor"), + ("auditor", "keyadmin"), + ("user", "admin"), + ("user", "auditor"), + ("user", "keyadmin"), + ("user", "user") +]) + def test_get_users_by_username_with_invalid_role(self, request, auth_role, user_role): + if auth_role == "keyadmin": + auth = self.ranger_key_admin_config + elif auth_role == "admin": + auth = self.ranger_admin_config + if self.admin["name"].lower() == "rangerusersync": + assert True, "If any admin's username is rangerusersync it has access to all users irrespective of role" + return + elif auth_role == "auditor": + auth_user, auth_id = self.audit, self.ranger_auditor_id + auth = self.ranger_auditor_config + elif auth_role == "user": + auth_user, auth_id = self.user, self.ranger_user_id + auth = self.ranger_user_config + + if user_role == "keyadmin": + user, user_id = self.ranger_key_admin1, self.ranger_key_admin_id1 + elif user_role == "admin": + user, user_id = self.admin1, self.ranger_admin_id1 + elif user_role == "auditor": + user, user_id = self.audit1, self.ranger_auditor_id1 + elif user_role == "user": + user, user_id = self.user1, self.ranger_user_id1 + + userName = user["name"] + response = requests.get( + f"{self.base_url}/xusers/users/userName/{userName}", + auth=auth, + headers=self.headers + ) + + assert_response(response, 403, f"Unauthorized access should be denied for {auth_role} to access user of role {user_role}") + + @pytest.mark.post + @pytest.mark.negative + def test_create_secure_user_missing_mandatory_field(self): + + payload = { + "name" : "missing_night2d2", + } + + response = requests.post( + f"{self.base_url}/xusers/users", + json=payload, + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert_response(response, 400) + + + @pytest.mark.post + @pytest.mark.negative + def test_create_user_via_invalid_roles(self, request): + + normal_user, n_id = request.getfixturevalue("temp_secure_user")(["user"]) + + auditor_user, a_id = request.getfixturevalue("temp_secure_user")(["auditor"]) + + + users_to_test = [ + (normal_user["name"], "Test@123"), + (auditor_user["name"], "Test@123"), + self.ranger_key_admin_config, + ] + + for username, password in users_to_test: + + random_suffix = ''.join(random.choices(string.ascii_lowercase, k=5)) + blocked_username = f"blocked_{random_suffix}" + + payload = { + "name": blocked_username, + "firstName": "Blocked", + "lastName": "User", + "emailAddress": f"{blocked_username}@test.com", + "password": "Test@123", + "status": 1, + "isVisible": 1, + "userRoleList": ["ROLE_USER"], + "groupIdList": [], + "groupNameList": [] + } + + response = requests.post( + f"{self.base_url}/xusers/users", + json=payload, + auth=(username, password), + headers=self.headers + ) + + print(f"{username} → {response.status_code}") + assert_response(response, 404, f"{username} should not have permission to create users and expected 404 due to spring silent failure, but got {response.status_code}") + + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("auth_role", ["auditor", "user", "keyadmin"]) + def test_create_external_user_using_invalid_roles(self, request, auth_role): + if auth_role == "auditor": + auth = self.ranger_auditor_config + elif auth_role == "user": + auth = self.ranger_user_config + elif auth_role == "keyadmin": + auth = self.ranger_key_admin_config + random_suffix = ''.join(random.choices(string.ascii_lowercase, k=5)) + username = f"external_{random_suffix}" + payload = { + "name": username + } + response = requests.post( + f"{self.base_url}/xusers/users/external", + json=payload, + auth=auth, + headers=self.headers + ) + assert_response(response, 404, f"{auth_role} is expected to return 404 due to spring's silent failure and should not have permission to create external users, but got {response.status_code}") + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("auth_role", ["auditor", "user", "keyadmin"]) + def test_create_user_userinfo_with_invalid_roles(self, request, auth_role): + if auth_role == "auditor": + auth = self.ranger_auditor_config + elif auth_role == "user": + auth = self.ranger_user_config + elif auth_role == "keyadmin": + auth = self.ranger_key_admin_config + + test_user, test_id = self.user, self.ranger_user_id + assert user_exists(test_id, self.ranger_admin_config, self.base_url, self.headers), "User should exist before performing userinfo based creation" + + payload = { + "xuserInfo": { + "name": test_user["name"] + }, + "xgroupInfo": [ + { + "name": random.choice(string.ascii_letters) + ''.join(random.choices(string.ascii_letters + string.digits, k=7)), + "groupType": 1, + "groupSource": 1, + "description": "this is for test" + + } + ] + } + + response = requests.post( + f"{self.base_url}/xusers/users/userinfo", + json=payload, + auth=auth, + headers=self.headers + ) + + assert_response(response, 404, f"{auth_role} expected 404 due to spring's silent failure and should not have permission to create users using userinfo endpoint") + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("missing_field", ["name", "xuserInfo_name"]) + def test_create_user_userinfo_with_invalid_payload(self, request, missing_field): + auth = self.ranger_admin_config + + if missing_field == "name": + username= f"non_exister_userinfo_{''.join(random.choices(string.ascii_lowercase, k=5))}" + x_group_name = "sample" + elif missing_field == "xuserInfo_name": + username = self.user["name"] + x_group_name = None + payload = { + "xuserInfo": { + "name": username + }, + "xgroupInfo": [ + { + "name": x_group_name, + } + ] + } + + response = requests.post( + f"{self.base_url}/xusers/users/userinfo", + json=payload, + auth=auth, + headers=self.headers + ) + + assert_response(response, 404, f"Userinfo creation should not be allowed with missing field: {missing_field}") + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("auth_role", ["auditor", "user", "keyadmin"]) + def test_create_roleassignment_with_invalid_roles(self, request, auth_role): + + if auth_role == "auditor": + auth = self.ranger_auditor_config + elif auth_role == "user": + auth = self.ranger_user_config + elif auth_role == "keyadmin": + auth = self.ranger_key_admin_config + + payload = { + "users": [self.user1["name"]], + "userRoleAssignments": {self.user1["name"]: "ROLE_SYS_ADMIN"}, + "groupRoleAssignments": {}, + "whiteListUserRoleAssignments": {}, + "whiteListGroupRoleAssignments": {}, + "isReset": False, + "isLastPage": False + } + + response = requests.post( + f"{self.base_url}/xusers/users/roleassignments", + json=payload, + auth=auth, + headers=self.headers + ) + + assert_response(response, 404, f"{auth_role} is expected to return 404 due to spring's silent failure and should not have permission to assign roles, but got {response.status_code}") + + + @pytest.mark.negative + @pytest.mark.post + @pytest.mark.parametrize("invalid_role", ["ROLE_NON_EXISTEN", "ROLE_KEY_ADMIN"]) + def test_create_roleassignment_to_invalid_assignment(self, invalid_role): + auth = self.ranger_admin_config + + payload = { + "users": [self.user1["name"]], + "userRoleAssignments": {self.user1["name"]: invalid_role}, + "groupRoleAssignments": {}, + "whiteListUserRoleAssignments": {}, + "whiteListGroupRoleAssignments": {}, + "isReset": False, + "isLastPage": False + } + + response = requests.post( + f"{self.base_url}/xusers/users/roleassignments", + json=payload, + auth=auth, + headers=self.headers + ) + if invalid_role == "ROLE_NON_EXISTEN": + expected_status = 400 + elif invalid_role == "ROLE_KEY_ADMIN": + expected_status = 403 # since ROLE_KEY_ADMIN is not a valid role but it has admin keyword so it will be blocked by permission check before role validation + assert_response(response, expected_status, f"Assigning non-existent role should fail with {expected_status}, but got {response.status_code}") + + + + @pytest.mark.negative + @pytest.mark.put + @pytest.mark.parametrize( + "auth_role,user_role", + [ + ("admin", ["ROLE_KEY_ADMIN"]), + ("auditor", ["ROLE_USER"]), + ("user", ["ROLE_USER"]), + ("keyadmin", ["ROLE_SYS_ADMIN"]), + ("keyadmin", ["ROLE_ADMIN_AUDITOR"]), + ],) + def test_update_user_using_invalid_access(self, temp_secure_user, auth_role, user_role): + + user, user_id = temp_secure_user(["user"]) + + print(f"\nUpdating user: {user['name']} | ID: {user_id}") + + if auth_role == "auditor": + auth_user = self.audit + auth = self.ranger_auditor_config + + assert "ROLE_ADMIN_AUDITOR" in auth_user["userRoleList"], "User does not have auditor role" + + elif auth_role == "user": + auth_user = self.user + auth = self.ranger_user_config + assert "ROLE_USER" in auth_user["userRoleList"], "User does not have user role" + + elif auth_role == "admin": + auth = self.ranger_admin_config + + elif auth_role == "keyadmin": + auth = self.ranger_key_admin_config + + payload = { + "id": user_id, + "name": user["name"], + "firstName": "updatedfirstname", + "lastName": "updatedlastname", + "emailAddress": f"updated_{user['emailAddress']}", + "status": 1, + "isVisible": 1, + "userRoleList": user_role , + "groupIdList": [], + "groupNameList": [] + } + + response = requests.put( + f"{self.base_url}/xusers/users", + json=payload, + auth=auth, + headers=self.headers + ) + if auth_role in ["auditor", "user"]: + assert_response(response, 403, f"{auth_role} should not have permission to update users, but got {response.status_code}") + elif auth_role in ["admin", "keyadmin"]: + assert_response(response, 400, f"{auth_role} should not have permission to update {user_role} role, expected 400 but got {response.status_code}") + + + + @pytest.mark.negative + @pytest.mark.put + def test_update_user_using_invalid_data(self): + rand_id = random.randint(100000, 999999) + payload = { + "id": rand_id, + "name": "name", + "firstName": "updatedfirstname", + "userRoleList": ["RANDOM_ROLE"] + + } + response = requests.put( + f"{self.base_url}/xusers/users", + json=payload, + auth=self.ranger_admin_config, + headers=self.headers + ) + print(response.status_code, response.text) + assert_response(response, 403, "Operation is expected to be denied") + + + @pytest.mark.negative + @pytest.mark.delete + def test_delete_user_using_invalid_id(self): + + #rand_id = random.randint(100000, 999999) + rand_id = 89999 + + response = requests.delete( + f"{self.base_url}/xusers/users/{rand_id}", + params={"forceDelete": "true"}, + auth=self.ranger_admin_config, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert_response(response, [400, 404], f"Expected status code not returned for invalid user ID, got {response.status_code}") + + @pytest.mark.negative + @pytest.mark.delete + @pytest.mark.parametrize("auth_role", ["auditor", "user", "keyadmin"]) + def test_delete_user_using_invalid_roles(self, auth_role, request): + if auth_role == "auditor": + auth_user = self.audit + auth = self.ranger_auditor_config + assert "ROLE_ADMIN_AUDITOR" in auth_user["userRoleList"], "User does not have auditor role" + elif auth_role == "user": + auth_user = self.user + auth = self.ranger_user_config + assert "ROLE_USER" in auth_user["userRoleList"], "User does not have user role" + elif auth_role == "keyadmin": + auth = self.ranger_key_admin_config + + test_user, test_id = request.getfixturevalue("temp_secure_user")(["user"]) + + + response = requests.delete( + f"{self.base_url}/xusers/users/{test_id}", + params={"forceDelete": "true"}, + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert_response(response, 404, f"{auth_role} is expected to return 404 due to spring's silent failure and should not have permission to delete users") + + + @pytest.mark.negative + @pytest.mark.delete + @pytest.mark.parametrize("auth_role", ["auditor", "user", "keyadmin"]) + def test_delete_external_user_with_invalid_role(self,request, auth_role): + if auth_role == "auditor": + auth_user = self.audit + auth = self.ranger_auditor_config + assert "ROLE_ADMIN_AUDITOR" in auth_user["userRoleList"], "User does not have auditor role" + elif auth_role == "user": + auth_user = self.user + auth = self.ranger_user_config + assert "ROLE_USER" in auth_user["userRoleList"], "User does not have user role" + elif auth_role == "keyadmin": + auth = self.ranger_key_admin_config + + test_user, test_id = request.getfixturevalue("temp_secure_user")(["user"]) + + userName = test_user["name"] + response = requests.delete( + f"{self.base_url}/xusers/users/userName/{userName}", + params={"forceDelete": "true"}, + auth=auth, + headers={**self.headers, "X-Requested-By": "ranger"} + ) + assert_response(response, 404, f"{auth_role} is expected to return 404 due to spring's silent failure and should not have permission to delete users") \ No newline at end of file diff --git a/functional-tests/xuserrest/test_utility_fun.py b/functional-tests/xuserrest/test_utility_fun.py new file mode 100644 index 0000000000..60cc362b75 --- /dev/null +++ b/functional-tests/xuserrest/test_utility_fun.py @@ -0,0 +1,905 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +import string +from xuserrest.utility.utils import * +import uuid +import pytest +import requests +from datetime import datetime +import random + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +class TestLookup: + + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_permission_module, + temp_permission_group, + temp_group, + temp_permission_user, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [ + ("admin", "ranger_admin_config"), + ("key_admin", "ranger_key_admin_config"), + ("auditor", "ranger_auditor_config"), + ("user", "ranger_user_config"), + ] + ) + def test_get_lookup_users(self, role, auth): + auth = getattr(self, auth) + response = requests.get( + f"{self.base_url}/xusers/lookup/users", + auth=auth, + headers=self.headers + ) + assert response.status_code == 200 + + data = response.json() + + if data != []: + assert "vXStrings" in data and isinstance(data["vXStrings"], list) + assert isinstance(data["vXStrings"][0]['value'], str) + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [ + ("admin", "ranger_admin_config"), + ("key_admin", "ranger_key_admin_config"), + ("auditor", "ranger_auditor_config"), + ("user", "ranger_user_config"), + ] + ) + + def test_get_lookup_groups(self, role, auth): + auth = getattr(self, auth) + response = requests.get( + f"{self.base_url}/xusers/lookup/groups", + auth=auth, + headers=self.headers + ) + assert response.status_code == 200 + + data = response.json() + + if data != []: + assert "vXStrings" in data and isinstance(data["vXStrings"], list) + assert isinstance(data["vXStrings"][0]['value'], str) + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [ + ("admin", "ranger_admin_config"), + ("key_admin", "ranger_key_admin_config"), + ("auditor", "ranger_auditor_config"), + ("user", "ranger_user_config"), + ] + ) + def test_get_lookup_principals(self, role, auth): + auth = getattr(self, auth) + response = requests.get( + f"{self.base_url}/xusers/lookup/principals", + auth=auth, + headers=self.headers + ) + assert response.status_code == 200 + + data = response.json() + + if data != []: + assert "vXStrings" in data and isinstance(data["vXStrings"], list) + assert isinstance(data["vXStrings"][0]['value'], str) + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +class TestAuthSession: + + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_permission_module, + temp_permission_group, + temp_group, + temp_permission_user, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [ + ("admin", "ranger_admin_config"), + ("key_admin", "ranger_key_admin_config"), + ("auditor", "ranger_auditor_config"), + ] + ) + @pytest.mark.parametrize( + "params, expected_status, test_case", + [ + ({}, 200, "default_request"), + ({"startIndex": -5}, 200, "negative_start_index"), + ({ + "startIndex": 10, + "pageSize": 50, + "getCount": "false", + "ownerId": 123, + "getChildren": "true", + "sortBy": "id", + "sortType": "asc" + }, 200, "valid_sort_all_params"), + + ], + ids=[ + "default", + "negative-start-index", + "valid-sort-all-params", + ], + ) + def test_get_auth_sessions(self, role, auth, params, expected_status, test_case): + if role == "user": + pytest.fail("Users should not have access to auth sessions endpoint, but this test case is marked positive which is incorrect. Please fix the test case or its marker.") + auth = getattr(self, auth) + response = requests.get( + f"{self.base_url}/xusers/authSessions", + params=params, + auth=auth, + headers=self.headers + ) + + assert response.status_code == expected_status, f"Expected {expected_status} but got {response.status_code} for test_case: {test_case}" + + + if expected_status == 200: + json_resp = response.json() + assert "vXAuthSessions" in json_resp or "totalCount" in json_resp + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [ + ("admin", "ranger_admin_config"), + ("key_admin", "ranger_key_admin_config"), + ("auditor", "ranger_auditor_config"), + ] + ) + def test_get_auth_session_info_success(self, role, auth): + if role == "user": + pytest.fail("Users should not have access to auth sessions endpoint, but this test case is marked positive which is incorrect. Please fix the test case or its marker.") + auth = getattr(self, auth) + auth_response = requests.get( + f"{self.base_url}/xusers/authSessions", + auth=auth, + headers=self.headers + ) + + assert auth_response.status_code == 200 + + # Capture the session ID from the cookie jar + session_id = auth_response.cookies.get("RANGERADMINSESSIONID") + assert session_id is not None, "RANGERADMINSESSIONID cookie not found in response" + + # Pass the captured session ID to the /info endpoint + params = {"extSessionId": session_id} + info_response = requests.get( + f"{self.base_url}/xusers/authSessions/info", + params=params, + auth=self.ranger_admin_config, + headers=self.headers + ) + + assert info_response.status_code == 200 + + data = info_response.json() + expected_user = auth[0] + assert data.get("loginId") == expected_user, f"Expected loginId to be {expected_user} but got {data.get('loginId')}" + assert data.get("id") is not None + + + # NEGATIVE TEST CASES + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth", + [ + ("invalid-sort-field", "ranger_admin_config"), + ("invalid-input-data", "ranger_admin_config"), + ("unauthorized-access-user", "ranger_user_config"), + ] + ) + def test_get_auth_sessions_negative(self, test_case, auth): + auth = getattr(self, auth) + params = {} + if test_case == "invalid-sort-field": + params = {"sortBy": "unsupportedFieldXYZ", "sortType": "desc"} # silently ignore sortType when sortBy is invalid to check if it validates sortBy first + elif test_case == "invalid-input-data": + params = {"startIndex": "not_a_number"} + + response = requests.get( + f"{self.base_url}/xusers/authSessions", + params=params, + auth=auth, + headers=self.headers + ) + if test_case.startswith("unauthorized-access"): + assert response.status_code == 403, f"Expected 403 Forbidden for {test_case}, but got {response.status_code}" + elif test_case == "invalid-sort-field": + assert response.status_code == 200, f"Expected 200 silent failure for {test_case}, but got {response.status_code}" + elif test_case == "invalid-input-data": + assert response.status_code == 400, f"Expected 400 Bad Request for {test_case}, but got {response.status_code}" + + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth", + [ + ("missing-session-id", "ranger_admin_config"), + ("invalid-session-id", "ranger_admin_config"), + ("unauthorized-access-user", "ranger_user_config"), + ] + ) + def test_get_auth_session_info_negative(self, test_case, auth): + auth = getattr(self, auth) + params = {} + if test_case == "invalid-session-id": + params = {"extSessionId": "invalidSessionIdXYZ"} + elif test_case == "missing-session-id": + params = {} + + response = requests.get( + f"{self.base_url}/xusers/authSessions/info", + params=params, + auth=auth, + headers=self.headers + ) + + if test_case.startswith("unauthorized-access"): + assert response.status_code == 403, f"Expected 403 Forbidden for {test_case}, but got {response.status_code}" + elif test_case in ["missing-session-id", "invalid-session-id"]: + assert response.status_code == 400, f"Expected 400 Bad Request for {test_case}, but got {response.status_code}" + + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +class TestDownloadServiceName: + + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_permission_module, + temp_permission_group, + temp_group, + temp_permission_user, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + @pytest.mark.skip(reason = "Skipping download tests due to existing code changes in xuserrest") + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [ + ("admin", "ranger_admin_config"), + ("key_admin", "ranger_key_admin_config"), + ("auditor", "ranger_auditor_config"), + ("user", "ranger_user_config"), + ] + ) + @pytest.mark.parametrize( + "test_case, last_known_version, expected_status", + [ + ("valid-service-no-change", -1, 304), # will be updated in future + ("valid-service-with-update", 0, 200), + ("valid-service-old-version", 1, 200), + ]) + def test_get_download_by_serviceName(self, test_case, auth, last_known_version, expected_status, role): + auth = getattr(self, auth) + service_name = "dev_hdfs" # valid existing service + + if test_case == "valid-service-no-change": + seed_response = requests.get( + f"{self.base_url}/xusers/download/{service_name}", + params={"lastKnownUserStoreVersion": -1, "lastActivationTime": 0, + "clusterName": "mycluster", "pluginId": "hdfs@host1"}, + auth=auth, + headers=self.headers, + ) + assert seed_response.status_code == 200, f"Seed request failed: {seed_response.status_code}" + current_version = seed_response.json().get("userStoreVersion") + last_known_version = current_version # now use the actual version → should get 304 + + params = { + "lastKnownUserStoreVersion": last_known_version, + "lastActivationTime": 0, + "clusterName": "mycluster", + "pluginId": f"hdfs@host1" + } + + response = requests.get( + f"{self.base_url}/xusers/download/{service_name}", + params=params, + auth=auth, + headers=self.headers + ) + assert_response(response, expected_status, test_case) + if expected_status == 200: + data = response.json() + assert "userStoreVersion" in data, f"Response missing 'userStoreVersion' for {test_case}" + assert "userGroupMapping" in data or "users" in data, f"Response missing user/group data for {test_case}" + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth, service_name", + [ + ("admin", "ranger_admin_config", "dev_hdfs"), + ("key_admin", "ranger_key_admin_config", "dev_kms"), + ]) + @pytest.mark.parametrize( + "test_case, last_known_version, expected_status", + [ + ("valid-service-no-change", -1, 304), + ("valid-service-with-update", 0, 200), + ("valid-service-old-version", 1, 200), + ]) + def test_get_secure_download_by_serviceName(self, test_case, auth, service_name, last_known_version, expected_status, role): + + auth = getattr(self, auth) + + base_params = { + "lastActivationTime": 0, + "clusterName": "mycluster", + "pluginId": "hdfs@host1" + } + + if test_case == "valid-service-no-change": + seed_response = requests.get( + f"{self.base_url}/xusers/secure/download/{service_name}", + params={ + **base_params, + "lastKnownUserStoreVersion": -1 + }, + auth=auth, + headers=self.headers + ) + + assert seed_response.status_code == 200, \ + f"Seed request failed: {seed_response.status_code}" + + last_known_version = seed_response.json()["userStoreVersion"] + + response = requests.get( + f"{self.base_url}/xusers/secure/download/{service_name}", + params={ + **base_params, + "lastKnownUserStoreVersion": last_known_version + }, + auth=auth, + headers=self.headers + ) + + assert_response(response, expected_status, test_case) + + if expected_status == 200: + data = response.json() + + assert "userStoreVersion" in data + assert "userGroupMapping" in data or "users" in data + + + # NEGATIVE TEST CASES + @pytest.mark.skip(reason = "Skipping download tests due to existing code changes in xuserrest") + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth, expected_status", + [ + ("invalid-service-name", "ranger_admin_config", 404), + ("empty-service-name", "ranger_admin_config", 404), + ("missing-plugin-id", "ranger_admin_config", 200), + ] + ) + def test_get_download_by_serviceName_negative(self, test_case, auth, expected_status): + auth_credentials = getattr(self, auth) + + if test_case == "invalid-service-name": + service_name = "nonExistentService_XYZ" + elif test_case == "empty-service-name": + service_name = "" + else: + service_name = "dev_hdfs" # Assuming this is your valid seed service + + + params = { + "lastKnownUserStoreVersion": -1, + "lastActivationTime": 0, + "clusterName": "mycluster", + } + + if test_case != "missing-plugin-id": + params["pluginId"] = "hdfs@host1" + + response = requests.get( + f"{self.base_url}/xusers/download/{service_name}", + params=params, + auth=auth_credentials, + headers=self.headers, + cookies=self._download_cookies(auth) + ) + + assert_response(response, expected_status, test_case) + + + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth, service_name, expected_status", + [ + ("invalid-service-name", "ranger_admin_config", "nonExistentService_XYZ", 404), + ("empty-service-name", "ranger_admin_config", "", 404), + ("admin-accessing-kms", "ranger_admin_config", "dev_kms", 403), + ("unauthorized-auditor-access", "ranger_auditor_config", "dev_kms", 403), + ("unauthorized-user-access", "ranger_user_config", "dev_kms", 403), + ("keyadmin-nonkms-access", "ranger_key_admin_config", "dev_hdfs", 400), + ] + ) + def test_get_secure_download_by_serviceName_negative(self, test_case, auth, service_name, expected_status): + + auth_credentials = getattr(self, auth) + + params = { + "lastKnownUserStoreVersion": -1, + "lastActivationTime": 0, + "clusterName": "mycluster", + } + + if test_case != "missing-plugin-id": + params["pluginId"] = "hdfs@host1" + + response = requests.get( + f"{self.base_url}/xusers/secure/download/{service_name}", + params=params, + auth=auth_credentials, + headers=self.headers + ) + + assert_response(response, expected_status, test_case) + + +@pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") +@pytest.mark.xuserrest +class TestMiscellaneous: + + SERVICE_NAME = "admin" + + @pytest.fixture(autouse=True, scope="class") + def _setup( + self, + request, + temp_secure_user, + temp_keyadmin_user, + temp_permission_module, + temp_permission_group, + temp_group, + temp_permission_user, + default_headers, + ranger_config, + ): + cls = request.cls + + # ---------- primary users ---------- + cls.admin, _ = temp_secure_user(["admin"]) + cls.ranger_key_admin, _ = temp_keyadmin_user() + cls.audit, _ = temp_secure_user(["auditor"]) + cls.user, _ = temp_secure_user(["user"]) + + cls.ranger_admin_config = (cls.admin["name"], "Test@123") + cls.ranger_key_admin_config = (cls.ranger_key_admin["name"], "Test@123") + cls.ranger_auditor_config = (cls.audit["name"], "Test@123") + cls.ranger_user_config = (cls.user["name"], "Test@123") + + cls.ranger_admin_id = cls.admin["id"] + cls.ranger_key_admin_id = cls.ranger_key_admin["id"] + cls.ranger_auditor_id = cls.audit["id"] + cls.ranger_user_id = cls.user["id"] + + # ---------- group details ---------- + cls.group, cls.group_id = temp_group() + cls.group1, cls.group_id1 = temp_group() + + + cls.headers = default_headers + cls.base_url = ranger_config["base_url"] + + init_configs( + cls.ranger_admin_config, + cls.ranger_key_admin_config, + cls.ranger_auditor_config, + cls.ranger_user_config, + ) + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth, test_case", + [ + ("admin", "ranger_admin_config", "admin-access"), + ("key_admin", "ranger_key_admin_config", "key_admin-access"), + ("auditor", "ranger_auditor_config", "auditor-access"), + ("user", "ranger_user_config", "user-access"), + ("admin", "ranger_admin_config", "admin-without-groups-"), + ]) + def test_get_groups_by_userid(self, role, auth, test_case, request): + auth = getattr(self, auth) + assign_groups_to_user(self.user['name'], [self.group['name'], self.group1['name']], self.ranger_admin_config, self.base_url, self.headers) + + u_id = self.ranger_user_id + if test_case == "admin-without-groups-access": + # create a new user without any groups + new_user, new_user_id = request.getfixturevalue("temp_secure_user")(["user"]) + u_id = new_user_id + response = requests.get( + f"{self.base_url}/xusers/{u_id}/groups", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, test_case) + data = response.json() + assert "vXGroups" in data and isinstance(data["vXGroups"], list) + lis = [self.group_id, self.group_id1] + + if test_case == "admin-without-groups-access": + assert data["vXGroups"] == [], f"Expected no groups for the user in {test_case}, but found some groups in response" + + for i in data["vXGroups"]: + assert i["id"] in lis, f"Unexpected group id {i['id']} found in response for {test_case}" + lis.remove(i["id"]) + + + @pytest.mark.get + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth, test_case", + [ + ("admin", "ranger_admin_config", "admin-access"), + ("key_admin", "ranger_key_admin_config", "key_admin-access"), + ("auditor", "ranger_auditor_config", "auditor-access"), + ("admin", "ranger_admin_config", "admin-without-users"), + ]) + def test_get_users_by_groupid(self, role, auth, test_case, request): + + auth = getattr(self, auth) + temp_group, temp_group_id = request.getfixturevalue("temp_group")() + if test_case != "admin-without-users": + assign_groups_to_user(self.user['name'], [temp_group['name']], self.ranger_admin_config, self.base_url, self.headers) + assign_groups_to_user(self.audit['name'], [temp_group['name']], self.ranger_admin_config, self.base_url, self.headers) + response = requests.get( + f"{self.base_url}/xusers/{temp_group_id}/users", + auth=auth, + headers=self.headers + ) + assert_response(response, 200, test_case) + data = response.json() + assert "vXUsers" in data and isinstance(data["vXUsers"], list) + if test_case == "admin-without-users": + assert data["vXUsers"] == [], f"Expected no users for the group in {test_case}, but found some users in response" + return + user_ids = [self.ranger_user_id, self.ranger_auditor_id] + for i in data["vXUsers"]: + assert i["id"] in user_ids, f"Unexpected user id {i['id']} found in response for {test_case}" + user_ids.remove(i["id"]) + + + + @pytest.mark.delete + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [ + ("admin", "ranger_admin_config"), + ]) + @pytest.mark.parametrize( + "test_case", + ["single external user", "multiple external users"] + ) + def test_forcedelete_external_user(self, role, auth, test_case, request): + if role != "admin": + pytest.fail("Only admin should have access to force delete users, but this test case is marked positive for other roles which is incorrect. Please fix the test case or its marker.") + auth = getattr(self, auth) + ext_user,ext_userid = request.getfixturevalue("temp_secure_user")(["user"]) + + user_list = [ext_user] + + if test_case == "multiple external users": + ext_user1,ext_userid1 = request.getfixturevalue("temp_secure_user")(["user"]) + user_list.append(ext_user1) + + for i in user_list: + assert i["userSource"] == 1, f"User {i['name']} is not an external user as expected for this test case" + assert i["syncSource"] == "EXTERNAL_PYTEST", f"User {i['name']} does not have the expected syncSource for this test case" + assert i["isVisible"] == 1, f"User {i['name']} is not visible as expected for this test case" + + + query_string = "syncSource=EXTERNAL_PYTEST&isVisible=1" + + response = requests.delete( + f"{self.base_url}/xusers/delete/external/users?{query_string}", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, test_case) + + for i in user_list: + # Verify the user is deleted + get_response = requests.get( + f"{self.base_url}/xusers/{i['id']}", + auth=auth, + headers=self.headers + ) + assert get_response.status_code == 404, f"Expected user {i['name']} to be deleted, but it still exists" + + @pytest.mark.delete + @pytest.mark.positive + @pytest.mark.parametrize( + "role, auth", + [ + ("admin", "ranger_admin_config"), + ]) + @pytest.mark.parametrize( + "test_case", + ["single external group", "multiple external group"] + ) + def test_forcedelete_external_group(self, role, auth, test_case, request): + if role != "admin": + pytest.fail("Only admin should have access to force delete groups, but this test case is marked positive for other roles which is incorrect. Please fix the test case or its marker.") + auth = getattr(self, auth) + ext_grp,ext_grpid = request.getfixturevalue("temp_group")() + + group_list = [ext_grp] + + if test_case == "multiple external groups": + ext_grp1,ext_grpid1 = request.getfixturevalue("temp_group")() + group_list.append(ext_grp1) + + for i in group_list: + assert i["groupSource"] == 1, f"Group {i['name']} is not an external group as expected for this test case" + assert i["syncSource"] == "EXTERNAL_PYTEST", f"Group {i['name']} does not have the expected syncSource for this test case" + assert i["isVisible"] == 1, f"Group {i['name']} is not visible as expected for this test case" + + + query_string = "syncSource=EXTERNAL_PYTEST&isVisible=1" + + response = requests.delete( + f"{self.base_url}/xusers/delete/external/groups?{query_string}", + auth=auth, + headers=self.headers + ) + + assert_response(response, 200, test_case) + + for i in group_list: + # Verify the group is deleted + get_response = requests.get( + f"{self.base_url}/xusers/groups/{i['id']}", + auth=auth, + headers=self.headers + ) + assert get_response.status_code == 400, f"Expected group {i['name']} to be deleted, but it still exists" + + + # Negative Tests + + @pytest.mark.get + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth", + [ + ("unauthorized-access-user", "ranger_user_config"), + ] + ) + def test_get_users_by_groupid_negative(self, auth, test_case, request): + + auth = getattr(self, auth) + temp_group, temp_group_id = request.getfixturevalue("temp_group")() + response = requests.get( + f"{self.base_url}/xusers/{temp_group_id}/users", + auth=auth, + headers=self.headers + ) + assert_response(response, 403, f'Expected 403 Forbidden for {test_case}, but got {response.status_code}') + + @pytest.mark.delete + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth", + [ + ("unauthorized-access-keyadmin", "ranger_key_admin_config"), + ("unauthorized-access-auditor", "ranger_auditor_config"), + ("unauthorized-access-user", "ranger_user_config"), + ("no_users_matching_criteria", "ranger_admin_config"), + ]) + def test_forcedelete_external_user_negative(self, test_case, auth): + auth_credentials = getattr(self, auth) + + if test_case.startswith("unauthorized-access"): + expected_status = 404 + + query_string = "syncSource=EXTERNAL_PYTEST&isVisible=1" + elif test_case == "no_users_matching_criteria": + expected_status = 200 + query_string = "syncSource=KKjkbjhvytgc_234" + + response = requests.delete( + f"{self.base_url}/xusers/delete/external/users?{query_string}", + auth=auth_credentials, + headers=self.headers + ) + + assert_response(response, expected_status, test_case) + + if test_case == "no_users_matching_criteria": + assert "No users were deleted!" in response.text + + @pytest.mark.delete + @pytest.mark.negative + @pytest.mark.parametrize( + "test_case, auth", + [ + ("unauthorized-access-keyadmin", "ranger_key_admin_config"), + ("unauthorized-access-auditor", "ranger_auditor_config"), + ("unauthorized-access-user", "ranger_user_config"), + ("no_users_matching_criteria", "ranger_admin_config"), + ]) + def test_forcedelete_external_group_negative(self, test_case, auth): + auth_credentials = getattr(self, auth) + + if test_case.startswith("unauthorized-access"): + expected_status = 404 + + query_string = "syncSource=EXTERNAL_PYTEST&isVisible=1" + elif test_case == "no_users_matching_criteria": + expected_status = 200 + query_string = "syncSource=KKjkbjhvytgc_234" + + response = requests.delete( + f"{self.base_url}/xusers/delete/external/groups?{query_string}", + auth=auth_credentials, + headers=self.headers + ) + + assert_response(response, expected_status, test_case) + + if test_case == "no_users_matching_criteria": + assert "No groups were deleted!" in response.text diff --git a/functional-tests/xuserrest/utility/__init__.py b/functional-tests/xuserrest/utility/__init__.py new file mode 100644 index 0000000000..13878a13a7 --- /dev/null +++ b/functional-tests/xuserrest/utility/__init__.py @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. \ No newline at end of file diff --git a/functional-tests/xuserrest/utility/utils.py b/functional-tests/xuserrest/utility/utils.py new file mode 100644 index 0000000000..30c40e3b9f --- /dev/null +++ b/functional-tests/xuserrest/utility/utils.py @@ -0,0 +1,812 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + + +import json +import pytest +import requests +from datetime import datetime +import inspect +import subprocess + + +RANGER_CONTAINER_NAME = "ranger" +RANGER_LOG_FILE = "/var/log/ranger/ranger-admin-ranger.rangernw-.log" + +BIGINT_MIN = -9223372036854775808 +BIGINT_MAX = 9223372036854775807 + +SERVICE_NAME = "admin" + +RANGER_CONFIG = None +RANGER_KEY_ADMIN_CONFIG = None +RANGER_AUDITOR_CONFIG = None +RANGER_USER_CONFIG = None + + +def init_configs(ranger_config, ranger_key_admin_config, ranger_auditor_config, ranger_user_config): + global RANGER_CONFIG, RANGER_KEY_ADMIN_CONFIG, RANGER_AUDITOR_CONFIG, RANGER_USER_CONFIG + + RANGER_CONFIG = ranger_config + RANGER_KEY_ADMIN_CONFIG = ranger_key_admin_config + RANGER_AUDITOR_CONFIG = ranger_auditor_config + RANGER_USER_CONFIG = ranger_user_config + + +def assert_response(response, expected_status, text = None, service_name=SERVICE_NAME): + + actual_status = response.status_code + + if isinstance(expected_status, int): + valid = actual_status == expected_status + + elif isinstance(expected_status, (list, tuple, set)): + valid = actual_status in expected_status + + else: + raise TypeError( + f"expected_status must be int or list/tuple/set, " + f"got {type(expected_status)}" + ) + + if not valid: + logs = fetch_ranger_logs(actual_status) + + pytest.fail( + + f"\nExpected: {expected_status}" + f"\nActual: {actual_status}" + + (f"\n{text}" if text is not None else "") + + f"\nLogs:\n{logs}" + ) + + +def validate_user_schema(data): + + if "id" in data: + assert "id" in data + assert isinstance(data["id"], int) + assert BIGINT_MIN <= data["id"] <= BIGINT_MAX + + assert isinstance(data.get("name"), str) + assert 1 <= len(data["name"]) <= 767 + + if data.get("firstName"): + assert isinstance(data.get("firstName"), str) + assert len(data["firstName"]) <= 256 + + if data.get("emailAddress"): + assert isinstance(data["emailAddress"], str) + assert len(data["emailAddress"]) <= 512 + + if "status" in data: + assert data.get("status") in [0, 1] + + if "isVisible" in data: + assert data.get("isVisible") in [0, 1] + + if "password" in data: + assert data.get("password") == "*****" + + for field in ["createDate", "updateDate"]: + if field in data: + datetime.fromisoformat( + data[field].replace("Z", "+00:00") + ) + + roles = data.get("userRoleList") + assert isinstance(roles, list) + assert len(roles) > 0 + + for role in roles: + assert isinstance(role, str) + assert len(role) <= 255 + + group_names = data.get("groupNameList", []) + group_ids = data.get("groupIdList", []) + + for name in group_names: + assert isinstance(name, str) + assert len(name) <= 767 + + for gid in group_ids: + assert isinstance(gid, int) + assert BIGINT_MIN <= gid <= BIGINT_MAX + + assert len(group_names) == len(group_ids), \ + "Mismatch between groupNameList and groupIdList" + + sync_source = data.get("syncSource") + if sync_source: + assert isinstance(sync_source, str) + + other_attributes = data.get("otherAttributes") + if other_attributes: + parsed = json.loads(other_attributes) + if "sync_source" in parsed and sync_source: + assert parsed["sync_source"] == sync_source + + for field in ["owner", "updatedBy"]: + if data.get(field): + assert isinstance(data[field], str) + assert len(data[field]) <= 256 + + + +def validate_secure_user_schema(data): + + assert "id" in data + assert isinstance(data["id"], int) + assert BIGINT_MIN <= data["id"] <= BIGINT_MAX + + assert isinstance(data.get("name"), str) + assert 1 <= len(data["name"]) <= 767 + + assert isinstance(data.get("firstName"), str) + assert len(data["firstName"]) <= 256 + + if data.get("emailAddress"): + assert isinstance(data["emailAddress"], str) + assert len(data["emailAddress"]) <= 512 + + assert data.get("status") in [0, 1] + assert data.get("isVisible") in [0, 1] + + assert data.get("password") == "*****" + + for field in ["createDate", "updateDate"]: + if field in data: + datetime.fromisoformat( + data[field].replace("Z", "+00:00") + ) + + roles = data.get("userRoleList") + assert isinstance(roles, list) + assert len(roles) > 0 + + for role in roles: + assert isinstance(role, str) + assert len(role) <= 255 + + group_names = data.get("groupNameList", []) + group_ids = data.get("groupIdList", []) + + for name in group_names: + assert isinstance(name, str) + assert len(name) <= 767 + + for gid in group_ids: + assert isinstance(gid, int) + assert BIGINT_MIN <= gid <= BIGINT_MAX + + assert len(group_names) == len(group_ids), \ + "Mismatch between groupNameList and groupIdList" + + sync_source = data.get("syncSource") + if sync_source: + assert isinstance(sync_source, str) + + other_attributes = data.get("otherAttributes") + if other_attributes: + parsed = json.loads(other_attributes) + if "sync_source" in parsed and sync_source: + assert parsed["sync_source"] == sync_source + + for field in ["owner", "updatedBy"]: + if data.get(field): + assert isinstance(data[field], str) + assert len(data[field]) <= 256 + + + +def user_exists(user_id, ranger_admin_config, base_url, headers, auth=None): + RANGER_CONFIG = {"base_url": base_url, "auth": ranger_admin_config, "headers": headers} + if RANGER_CONFIG is None: + raise RuntimeError("RANGER_CONFIG not initialized") + + if auth is None: + auth = RANGER_CONFIG["auth"] + + response = requests.get( + f"{RANGER_CONFIG['base_url']}/xusers/secure/users/{user_id}", + auth=auth, + headers=RANGER_CONFIG["headers"] + ) + + return response.status_code == 200 + + + +def delete_user(user_id, ranger_admin_config, base_url, headers, force=True): + + RANGER_CONFIG = {"base_url": base_url, "auth": ranger_admin_config, "headers": headers} + if RANGER_CONFIG is None: + raise RuntimeError("RANGER_CONFIG not initialized") + + if force: + url = f"{RANGER_CONFIG['base_url']}/xusers/secure/users/id/{user_id}" + params = {"forceDelete": "true"} + else: + url = f"{RANGER_CONFIG['base_url']}/xusers/secure/users/{user_id}" + params = None + + response = requests.delete( + url, + auth=RANGER_CONFIG["auth"], + headers={ + **RANGER_CONFIG["headers"], + "X-Requested-By": "ranger" + }, + params=params + ) + + print("DELETE:", response.status_code, response.text) + + return response.status_code in (200, 204) + + +def validate_external_user_schema(data): + + assert "startIndex" in data and isinstance(data["startIndex"], int) + assert "pageSize" in data and isinstance(data["pageSize"], int) + assert "totalCount" in data and isinstance(data["totalCount"], int) + assert "resultSize" in data and isinstance(data["resultSize"], int) + + assert "vXStrings" in data + assert isinstance(data["vXStrings"], list) + assert len(data["vXStrings"]) > 0 + + assert "value" in data["vXStrings"][0] + assert isinstance(data["vXStrings"][0]["value"], str) + assert len(data["vXStrings"][0]["value"]) <= 255 + + assert "queryTimeMS" in data and isinstance(data["queryTimeMS"], int) + +def validate_xgroup_schema(data): + + if "id" in data: + assert isinstance(data["id"], int) + assert BIGINT_MIN <= data["id"] <= BIGINT_MAX + + assert isinstance(data.get("name"), str) + assert 0 <= len(data["name"]) <= 767 + + if data.get("description"): + assert isinstance(data["description"], str) + assert len(data["description"]) <= 1024 + + if "groupType" in data: + assert data.get("groupType") in [0, 1, 2] + + if "isVisible" in data: + assert data.get("isVisible") in [0, 1] + + if "groupSource" in data: + assert data.get("groupSource") in [0, 1] + + for field in ["createDate", "updateDate"]: + if field in data: + datetime.fromisoformat( + data[field].replace("Z", "+00:00") + ) + + +def assign_groups_to_user(user_name, group_names, ranger_admin_config, base_url, headers): + RANGER_CONFIG = {"base_url": base_url, "auth": ranger_admin_config, "headers": headers} + if RANGER_CONFIG is None: + raise RuntimeError("RANGER_CONFIG not initialized") + payload = { + "xuserInfo": { + "name": user_name + }, + "xgroupInfo": [ + {"name": str(group)} for group in group_names # fixed: ensure string + ] + } + response = requests.post( + f"{RANGER_CONFIG['base_url']}/xusers/users/userinfo", + json=payload, + auth=RANGER_CONFIG["auth"], + headers=RANGER_CONFIG["headers"] + ) + print(response.json()) + print("Assign Groups Response:", response.status_code) + assert response.status_code == 200, f"Failed to assign groups to user: {response.text}" + + + +def fetch_groups_for_user_id(user_id, ranger_admin_config, base_url, headers): + RANGER_CONFIG = {"base_url": base_url, "auth": ranger_admin_config, "headers": headers} + if RANGER_CONFIG is None: + raise RuntimeError("RANGER_CONFIG not initialized") + + response = requests.get( + f"{RANGER_CONFIG['base_url']}/xusers/{user_id}/groups", # fixed: removed /secure/ + auth=RANGER_CONFIG["auth"], + headers=RANGER_CONFIG["headers"] + ) + + if response.status_code != 200: + print(f"Failed to fetch groups for user id {user_id}: {response.status_code} {response.text}") + return None + + data = response.json() + print(f"Groups for user id {user_id}:", data) + return data.get("vXGroups", []) + +def check_group_in_user_groups(group_id, vXGroups): + for group in vXGroups: + if group.get("id") == group_id: + return True + return False + +def fetch_ranger_logs(resp_code=None, lines: int = 80) -> str: + """ + Fetch useful Ranger debug logs. + + Sections: + 1. Recent Activity + 2. Errors / Warnings (system health) + 3. Recent REST API calls + 4. Logs related to HTTP response meaning + """ + + keyword_map = { + # SUCCESS + 200: (r"createUser|updateUser|deleteUser|added|updated|removed|success|create|update|delete|assigned|completed"), + 201: (r"created|create|success"), + 204: (r"delete|removed|no content|success"), + + # FOR CLIENT ERRORS, focus on common patterns and real log snippets + 400: (r"invalid|validation|bad request|missing"), + + # AUTHENTICATION + + 401: (r"authentication|login failed|Bad credentials|Invalid username|unauthorized"), + # AUTHORIZATION (REAL RANGER STRINGS) + 403: (r"User is not allowed to access the API|Access denied|not authorized|RESTErrorUtil|Request failed |denied|forbidden|permission|not allowed"), + + # NOT FOUND + 404: (r"not found|No record"), + + # METHOD + 405: (r"Request method.*not supported|method not allowed|unsupported method"), + + # CONFLICT + 409: (r"already exists|duplicate|conflict"), + + # SERVER + 500: (r"ERROR|Exception|failed|NullPointerException"), + + 502: (r"gateway|proxy|upstream"), + 503: (r"unavailable|timeout|overloaded") + } + + resp_filter = r"ERROR|WARN|Exception|denied|failed" + + if isinstance(resp_code, int): + resp_filter = keyword_map.get(resp_code, resp_filter) + + elif isinstance(resp_code, (list, tuple, set)): + filters = [ + keyword_map.get(code, resp_filter) + for code in resp_code + ] + resp_filter = "|".join(filters) + + # Docker command + cmd = [ + "docker", "exec", + RANGER_CONTAINER_NAME, + "bash", "-c", + f""" + LOG="{RANGER_LOG_FILE}" + + echo "========== RECENT ACTIVITY ==========" + tail -n 12 $LOG 2>/dev/null + + echo + echo "========== RECENT ERRORS / WARNS ==========" + grep -i -E "ERROR|WARN|Exception|FATAL" $LOG 2>/dev/null | tail -n 25 + + echo + echo "========== RECENT API CALLS ==========" + grep -i -E "REST|XUserREST|ServiceREST|PolicyREST" $LOG 2>/dev/null | tail -n 20 + echo + echo "========== RELATED TO HTTP {resp_code} ==========" + tail -n $(( {lines} * 5 )) $LOG 2>/dev/null | \ + grep -i -E "{resp_filter}" -A3 -B3 | tail -n 30 + """ + ] + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + + output = result.stdout.strip() + + return output if output else "No relevant Ranger logs found." + + except Exception as e: + return f"Failed to fetch Ranger logs: {e}" + +def permission_module_exists(module_name= None, ranger_admin_config = None, base_url=None, headers=None): + RANGER_CONFIG = {"base_url": base_url, "auth": ranger_admin_config, "headers": headers} + if RANGER_CONFIG is None: + raise RuntimeError("RANGER_CONFIG not initialized") + if module_name is None: + raise ValueError("module_name must be provided") + resp = requests.get( + f"{base_url}/xusers/permission", + params={"module": module_name}, + auth=ranger_admin_config, + headers=headers + ) + if resp.status_code != 200: + return False + + data = resp.json() + print("Permission module response data:", data) + module_list = data.get("vXModuleDef", []) + + return len(module_list) > 0 + +def build_user_permission(user_id, module_id, is_allowed): + return { + "userId": user_id, + "moduleId": module_id, + "isAllowed": is_allowed + } + + +def build_group_permission(group_id, module_id, is_allowed): + return { + "groupId": group_id, + "moduleId": module_id, + "isAllowed": is_allowed + } + + +def build_permission_payload(module_id, module_name, user_perm_list=None, group_perm_list=None): + return { + "id": module_id, + "module": module_name, + "userPermList": user_perm_list or [], + "groupPermList": group_perm_list or [] + } + + +def group_permission_schema(data): + assert "startIndex" in data and isinstance(data["startIndex"], int) + assert "pageSize" in data and isinstance(data["pageSize"], int) + assert "totalCount" in data and isinstance(data["totalCount"], int) + assert "resultSize" in data and isinstance(data["resultSize"], int) + assert "queryTimeMS" in data and isinstance(data["queryTimeMS"], int) + assert "vXGroupPermission" in data + print("Group Permission Response Data:", data) + if len(data["vXGroupPermission"]) > 0: + perm = data["vXGroupPermission"][0] + assert "id" in perm and isinstance(perm["id"], int) + assert "groupId" in perm and isinstance(perm["groupId"], int) + assert "isAllowed" in perm and isinstance(perm["isAllowed"], int) and perm["isAllowed"] in [0, 1] + assert "groupName" in perm and isinstance(perm["groupName"], str) and len(perm["groupName"]) <= 767 + +def user_permission_exists(user_id, module_id, auth, base_url, headers, user_perm_id = None): + page_size = 200 + start_index = 0 + + while True: + response = requests.get( + f"{base_url}/xusers/permission/user", + params={"startIndex": start_index, "pageSize": page_size}, + auth=auth, + headers=headers + ) + + if response.status_code != 200: + return False + + data = response.json() + permissions = data.get("vXUserPermission", []) + + for p in permissions: + if p.get("userId") == user_id and p.get("moduleId") == module_id: + if user_perm_id is not None and p.get("id") != user_perm_id: + continue + return True + + total = int(data.get("totalCount", 0)) + start_index += page_size + + if start_index >= total: + break + + return False + +def mandatory_field_check_in_response(response_json, mandatory_fields): + for field in mandatory_fields: + if field not in response_json: + return False, f"Mandatory field '{field}' is missing in the response" + return True, "All mandatory fields are present in the response" + + +def delete_group(group_id, ranger_admin_config, base_url, headers): + RANGER_CONFIG = {"base_url": base_url, "auth": ranger_admin_config, "headers": headers} + if RANGER_CONFIG is None: + raise RuntimeError("RANGER_CONFIG not initialized") + + response = requests.delete( + f"{RANGER_CONFIG['base_url']}/xusers/groups/{group_id}", + auth=RANGER_CONFIG["auth"], + params= {"forceDelete": "true"}, + headers={ + **RANGER_CONFIG["headers"], + "X-Requested-By": "ranger" + } + ) + + print("DELETED Group:", response.status_code, response.text) + + return response.status_code in (200, 204) + +def group_exists(group_id, ranger_admin_config, base_url, headers, auth=None): + RANGER_CONFIG = {"base_url": base_url, "auth": ranger_admin_config, "headers": headers} + if RANGER_CONFIG is None: + raise RuntimeError("RANGER_CONFIG not initialized") + + if auth is None: + auth = RANGER_CONFIG["auth"] + + response = requests.get( + f"{RANGER_CONFIG['base_url']}/xusers/groups/{group_id}", + auth=auth, + headers=RANGER_CONFIG["headers"] + ) + + #assert response.status_code == 200, f"Failed to fetch group with id {group_id}: {response.status_code}" + flag = response.status_code == 200 + return flag,response.json() + +def group_exists_by_name(group_name, ranger_admin_config, base_url, headers, auth=None): + RANGER_CONFIG = {"base_url": base_url, "auth": ranger_admin_config, "headers": headers} + if RANGER_CONFIG is None: + raise RuntimeError("RANGER_CONFIG not initialized") + + if auth is None: + auth = RANGER_CONFIG["auth"] + + resp = requests.get( + f"{base_url}/xusers/groups/groupName/{group_name}", + auth = ranger_admin_config, + headers=headers, + ) + return resp.status_code == 200 + +def user_exists_by_name(user_name, ranger_admin_config, base_url, headers, auth=None): + RANGER_CONFIG = {"base_url": base_url, "auth": ranger_admin_config, "headers": headers} + if RANGER_CONFIG is None: + raise RuntimeError("RANGER_CONFIG not initialized") + + if auth is None: + auth = RANGER_CONFIG["auth"] + + resp = requests.get( + f"{base_url}/xusers/users/userName/{user_name}", + auth = ranger_admin_config, + headers=headers, + ) + return resp.status_code == 200 + +def assign_role_to_group(group_name, role, ranger_admin_config, base_url, headers): + RANGER_CONFIG = {"base_url": base_url, "auth": ranger_admin_config, "headers": headers} + if RANGER_CONFIG is None: + raise RuntimeError("RANGER_CONFIG not initialized") + + role_id = int(role["id"]) + role_name = role["name"] + payload = { + "id": role_id, + "name": role_name, + "description": "My role", + "users": [], + "groups": [ + { + "name": group_name, + "isAdmin": "false" + } + ], + "roles": [] + } + response = requests.put( + f"{RANGER_CONFIG['base_url']}/roles/roles/{role_id}", + json=payload, + auth=RANGER_CONFIG["auth"], + headers=RANGER_CONFIG["headers"] + ) + + print("Assign Role to Group Response:", response.status_code) + assert response.status_code == 200, f"Failed to assign role to group" + + +def create_groupuser(group_name, user_id, ranger_admin_config, base_url, headers): + payload = { + "name": group_name, + "userId": user_id + } + response = requests.post( + f"{base_url}/xusers/groupusers", + json=payload, + auth=ranger_admin_config, + headers=headers + ) + print("Create GroupUser Response:", response.status_code, response.text) + assert response.status_code == 200, f"Failed to create groupuser: {response.text}" + return response.json() + + +def groupuser_exists(gu_id, ranger_admin_config, base_url, headers): + response = requests.get( + f"{base_url}/xusers/groupusers/{gu_id}", + auth=ranger_admin_config, + headers=headers + ) + if response.status_code == 200: + return True + print(f"Failed to fetch groupuser for group id {gu_id}: {response.status_code} ") + return False + + +def delete_groupuser(gu_id, ranger_admin_config, base_url, headers): + groupuser_exists(gu_id, ranger_admin_config, base_url, headers) # Check existence before deletion + response = requests.delete( + f"{base_url}/xusers/groupusers/{gu_id}", + auth=ranger_admin_config, + headers={**headers, "X-Requested-By": "ranger"} + ) + return response.status_code in (200, 204) + + +''' +payload = { + "syncSource": "Unix", + "noOfNewUsers": 10, + "noOfNewGroups": 5, + "noOfModifiedUsers": 3, + "noOfModifiedGroups": 2, + "fileSyncSourceInfo": {} + } + + ''' +def validate_auditinfo_schema(data): + assert "syncSource" in data and isinstance(data["syncSource"], str) and len(data["syncSource"]) <= 128 + assert "noOfNewUsers" in data and isinstance(data["noOfNewUsers"], int) + assert "noOfNewGroups" in data and isinstance(data["noOfNewGroups"], int) + assert "noOfModifiedUsers" in data and isinstance(data["noOfModifiedUsers"], int) + assert "noOfModifiedGroups" in data and isinstance(data["noOfModifiedGroups"], int) + if "fileSyncSourceInfo" in data: + assert isinstance(data["fileSyncSourceInfo"], dict) + if "unixSyncSourceInfo" in data: + assert isinstance(data["unixSyncSourceInfo"], dict) + if "ldapSyncSourceInfo" in data: + assert isinstance(data["ldapSyncSourceInfo"], dict) + +def is_valid_date(value): + try: + datetime.fromisoformat(value.replace("Z", "+00:00")) + return True + except Exception: + return False + +def validate_sync_source_info(payload, response_json): + from datetime import datetime + + priority = [ + "ldapSyncSourceInfo", + "unixSyncSourceInfo", + "fileSyncSourceInfo" + ] + + selected_source = None + for key in priority: + if key in payload: + selected_source = key + break + + if not selected_source: + pytest.fail("No sync source info provided in payload") + + sync_info = response_json.get("syncSourceInfo", {}) + + common_fields = { + "totalUsersSynced": str, + "totalGroupsSynced": str, + "totalUsersDeleted": str, + "totalGroupsDeleted": str, + } + + if selected_source == "unixSyncSourceInfo": + expected_schema = { + "unixBackend": str, + "fileName": str, + "syncTime": "date", + "lastModified": "date", + "minUserId": str, + "minGroupId": str, + **common_fields + } + + elif selected_source == "fileSyncSourceInfo": + expected_schema = { + "fileName": str, + "syncTime": "date", + "lastModified": "date", + **common_fields + } + + elif selected_source == "ldapSyncSourceInfo": + expected_schema = { + "ldapUrl": str, + "isIncrementalSync": str, + "userSearchEnabled": str, + "groupSearchEnabled": str, + "groupSearchFirstEnabled": str, + "userSearchFilter": str, + "groupSearchFilter": str, + "groupHierarchyLevel": str, + **common_fields + } + + else: + pytest.fail(f"Unknown sync source: {selected_source}") + + for key, expected_type in expected_schema.items(): + assert key in sync_info, f"{key} missing in response for {selected_source}" + + value = sync_info[key] + + if expected_type == "date": + assert isinstance(value, str), f"{key} should be string (date)" + #assert is_valid_date(value), f"{key} is not valid ISO date" + else: + assert isinstance(value, expected_type), \ + f"{key} expected {expected_type}, got {type(value)}" + + assert isinstance(response_json.get("id"), int), "id should be int" + assert isinstance(response_json.get("userName"), str), "userName should be string" + + for field in ["createDate", "updateDate", "eventTime"]: + val = response_json.get(field) + assert isinstance(val, str), f"{field} should be string" + assert is_valid_date(val), f"{field} is not valid ISO date" + + for field in [ + "noOfNewUsers", + "noOfNewGroups", + "noOfModifiedUsers", + "noOfModifiedGroups" + ]: + assert isinstance(response_json.get(field), int), f"{field} should be int" + +def return_value_ugsync_groupusers(payload, auth, base_url, headers): + return_value = 0 + for i in payload: + if group_exists_by_name(i["groupName"], auth, base_url, headers): + if len(i["addUsers"]) > 0 or len(i["delUsers"]) > 0: + return_value += 1 + return return_value \ No newline at end of file From e4d2aa557881f7a848cda10c4e0b78ff7eeaf7a8 Mon Sep 17 00:00:00 2001 From: ChinmayHegde24 Date: Wed, 5 Aug 2026 18:25:55 +0530 Subject: [PATCH 2/3] RANGER-4897: Fixed the wrong directory paths and few nit picks --- functional-tests/readme.md | 50 ++++------------------------------- functional-tests/run-tests.sh | 9 ------- 2 files changed, 5 insertions(+), 54 deletions(-) diff --git a/functional-tests/readme.md b/functional-tests/readme.md index 075487f41b..dd6b5e3e0e 100644 --- a/functional-tests/readme.md +++ b/functional-tests/readme.md @@ -13,15 +13,6 @@ 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. - - -This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven - -This workflow uses actions that are not certified by GitHub. -They are provided by a third-party and are governed by -separate terms of service, privacy policy, and support -documentation. --> @@ -46,31 +37,10 @@ This test suite validates REST API endpoints for Apache Ranger services,Admin (r ```text functional-tests/ ├── hdfs/ # Tests on HDFS encryption cycle -│ ├── conftest.py # Fixtures and setup for HDFS tests -│ └── test_file.py # HDFS encryption test cases -│ ├── kms/ # Tests on KMS REST API -│ ├── conftest.py # Fixtures and setup for KMS tests -│ └── test_file.py # KMS API test cases -│ ├── xuserrest/ # Tests on Ranger User/Group/Role REST APIs -│ ├──utility/ # Utility Folder contains helper functions -│ | ├── utils.py -│ ├── conftest.py # Fixtures and setup for user REST tests -│ └── test_file.py # User REST API test cases -│ ├── rolerest/ # Tests on Ranger Role REST APIs -│ ├──utility/ # Utility Folder contains helper functions -│ | ├── utils.py -│ ├── conftest.py # Fixtures and setup for role REST tests -│ └── test_file.py # User REST API test cases -│ ├── servicerest/ # Tests on Ranger Service REST APIs -│ ├──utility/ # Utility Folder contains helper functions -│ | ├── utils.py -│ ├── conftest.py # Fixtures and setup for service REST tests -│ ├── test_file.py # Service REST API test cases -│ └── automation.log # logs related to the tests and the conftest files for service rest │ ├── pytest.ini # Registers custom pytest markers ├── run-tests.sh # Script to automate setup and test execution @@ -85,9 +55,9 @@ functional-tests/ ## Prerequisites 1. Docker & Docker Compose installed and running 2. Python 3.10 or higher -3. Change the working directory to pytest-Tests +3. Change the working directory to functional-tests ```text -cd pytest-Tests/ +cd functional-tests/ ``` 4. Make the shell script executable @@ -102,7 +72,7 @@ Configure container behavior before running the script using the following envir 1. Fresh Setup & Cleanup: -Force a clean environment & helps building binaries with local changes (removes old Ranger containers, prunes Docker space, builds fresh, and cleans up after tests): +Force a clean environment & helps building binaries with local changes: ```text export CLEAN_CONTAINERS=1 ./run-tests.sh @@ -147,7 +117,7 @@ Run the script without arguments to be prompted for inputs: example: ```text -Available DB types: postgres, mysql, oracle, mssql +Available DB types: postgres, mysql, oracle Enter DB type (press Enter to default to postgres): postgres Available test suites: xuserrest servicerest hdfs kms @@ -161,24 +131,14 @@ Pass arguments directly to skip prompts: ```text ./run-tests.sh [db-type] [test-suites...] ``` -db-type — Must be the first argument. Valid values: postgres, mysql, oracle, mssql. +db-type — Must be the first argument. Valid values: postgres, mysql, oracle. test-suites — Space-separated list: hdfs, kms, xuserrest, servicerest. Examples: ```text -# Run all suites with Postgres (default) -./run-tests.sh postgres - -# Run specific suites with Postgres ./run-tests.sh postgres kms hdfs - -# Run only user REST tests with MySQL -./run-tests.sh mysql xuserrest - -# Run service REST tests with Oracle -./run-tests.sh oracle servicerest ``` ## Test Reports diff --git a/functional-tests/run-tests.sh b/functional-tests/run-tests.sh index 28a1d6f63f..652ee5cf14 100755 --- a/functional-tests/run-tests.sh +++ b/functional-tests/run-tests.sh @@ -13,15 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. - -# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - #!/bin/bash # All available test suites (pytest folders) From 84b0f000f0a269793a1fcfa10562cfd1001c4658 Mon Sep 17 00:00:00 2001 From: krishnamuttevi Date: Thu, 6 Aug 2026 12:43:16 +0530 Subject: [PATCH 3/3] RANGER-4897: Added the new validation statements and few nit picks --- functional-tests/rolerest/conftest.py | 5 - functional-tests/rolerest/readme.md | 113 +++++------------- .../rolerest/test_role_management.py | 19 +-- .../rolerest/test_role_utility_fun.py | 30 +---- functional-tests/rolerest/utility/utils.py | 14 +-- functional-tests/xuserrest/readme.md | 2 +- functional-tests/xuserrest/test_groups.py | 2 - functional-tests/xuserrest/test_permission.py | 6 - .../xuserrest/test_secure_user.py | 19 --- functional-tests/xuserrest/test_ugsync.py | 16 +-- functional-tests/xuserrest/test_user.py | 17 +-- .../xuserrest/test_utility_fun.py | 21 +--- 12 files changed, 47 insertions(+), 217 deletions(-) diff --git a/functional-tests/rolerest/conftest.py b/functional-tests/rolerest/conftest.py index 834bd2a3a9..9033468e76 100644 --- a/functional-tests/rolerest/conftest.py +++ b/functional-tests/rolerest/conftest.py @@ -82,9 +82,6 @@ def ranger_key_admin_config(keyadmin_credentials, default_headers): "headers": default_headers } - - - @pytest.fixture(scope="class") def temp_secure_user(ranger_config): @@ -319,8 +316,6 @@ def _create_role(user_list = [], group_list = [], role_list = []): "roles": role_list } - - response = requests.post( f"{ranger_config['base_url']}/roles/roles", json=payload, diff --git a/functional-tests/rolerest/readme.md b/functional-tests/rolerest/readme.md index 26183f0ae6..8ce0c52ca7 100644 --- a/functional-tests/rolerest/readme.md +++ b/functional-tests/rolerest/readme.md @@ -24,24 +24,20 @@ separate terms of service, privacy policy, and support documentation. --> -# This is the main directory for running XUSERREST API functionality tests +# This is the main directory for running ROLEREST API functionality tests -This directory contains automated functional tests for the XUSERREST API, covering user management, group management, permissions, secure user operations, and UGSync functionality. +This directory contains automated functional tests for the ROLEREST API, covering roles functionality. ## Structure ``` -test_xuserrest/ +rolerrest/ ├── utility/ │ ├── __init__.py │ ├── utils.py ├── __init__.py ├── conftest.py -├── test_groups.py -├── test_permission.py -├── test_secure_user.py -├── test_ugsync.py -├── test_user.py -├── test_utility_fun.py +├── test_role_management.py +├── test_role_utility_fun.py ``` @@ -57,81 +53,38 @@ test_xuserrest/ ## `utils.py` -- Shared utility module for all xuser REST test files — provides reusable helpers, schema validators, and API interaction functions +## `utils.py` +- Shared utility module for all role REST test files — provides reusable helpers and API interaction functions - Defines global constants: BIGINT_MIN/BIGINT_MAX for boundary checks, RANGER_CONTAINER_NAME/RANGER_LOG_FILE for Docker log access -init_configs() sets global auth configs for all four roles (admin, keyadmin, auditor, user) -assert_response() validates HTTP status codes (single or list) and on failure fetches Ranger logs via fetch_ranger_logs() -fetch_ranger_logs() runs docker exec to pull categorized log sections — recent activity, errors/warns, API calls, and HTTP-code-specific patterns using keyword_map -- Schema validators: validate_user_schema(), validate_secure_user_schema(), validate_xgroup_schema(), validate_external_user_schema(), group_permission_schema() — enforce field types, lengths, and value constraints -CRUD helpers: user_exists(), delete_user(), group_exists(), delete_group(), groupuser_exists(), delete_groupuser() — used for test setup/teardown -- UGSync helpers: validate_auditinfo_schema(), validate_sync_source_info() — validate sync audit responses across unix, file, and ldap sources with source-specific field schemas -- Permission helpers: build_user_permission(), build_group_permission(), build_permission_payload(), user_permission_exists() — construct and verify module permission payloads return_value_ugsync_groupusers() computes expected count of group-user sync operations for assertion in UGSync tests - - +- init_configs() sets global auth configs for all four roles (admin, keyadmin, auditor, user) +- assert_response() delegates to xuserrest — validates HTTP status codes and fetches Ranger logs on failure +- Service helpers: create_service(), assign_service_admin(), assign_service_admin_group(), delete_service() — provision HDFS services and service-admin access for authorization tests +- Role helpers: get_role_by_name(), delete_role() — lookup and teardown of roles +- ensureRoleAccess() provisions users, groups, roles, and services based on test case (admin, service admin, group admin, role membership) and returns auth, query params, and cleanup items --- - ## `conftest.py` - -- Central pytest configuration file providing shared fixtures and helpers for the entire test suite +- Central pytest configuration file providing shared fixtures and helpers for the test suite - Defines module-level constants: CREDENTIALS, DEFAULT_HEADERS, KEYADMIN_CREDENTIALS -- Includes create_user_with_retry() — retries user POST creation up to 5 times with incremental sleep on failure +- Includes create_user_with_retry() — retries secure user POST creation up to 5 times with incremental sleep on failure - Provides session/function/class scoped fixtures: - 1. Session: credentials, default_headers, keyadmin_credentials, ranger_config, ranger_key_admin_config, client_roles - 2. Function: ranger_session, all_users, all_schema_following_users, get_user_by_id - 3. Class: temp_secure_user, temp_keyadmin_user, temp_permission_module, temp_group, temp_permission_group, temp_permission_user, temp_role — all auto-cleanup after test class - + 1. Session: credentials, default_headers, keyadmin_credentials, ranger_config, ranger_key_admin_config + 2. Function: ranger_session + 3. Class: temp_secure_user, temp_keyadmin_user, temp_group, temp_role — all auto-cleanup after test class --- - -## `test_groups.py` - -- Tests for Group management operations across three test classes: TestSecureGroups, TestGroups, and TestGroupUsers -- Each class uses a _setup fixture (class-scoped) that provisions primary & secondary users across all roles (admin, key admin, auditor, user) and two temporary groups with auto-cleanup -- Default response code for auth failure cases is set to 404 to prevent API endpoint discovery due to Spring silent failures - +## `test_role_management.py` +- Tests Role CRUD and lookup operations within TestRoleCRUD +- Class-scoped _setup provisions primary and secondary users across all roles (admin, keyadmin, auditor, user), groups, and roles with auto-cleanup +- GET: /roles/roles, /roles/roles/{id}, /roles/roles/names, /roles/roles/name/{name}, /roles/roles/user/{userName}, /roles/lookup/roles +- POST/PUT/DELETE: create, update, and delete roles by ID and name +- Covers role-based access control including service admin and service-admin-group paths via ensureRoleAccess() +- Negative tests validate unauthorized access, invalid IDs/names, and malformed payloads --- - -## `test_secure_user.py` - -- Tests Secure User CRUD operations via /xusers/secure/users/* endpoints within TestSecureUserEndpoint -- Class-scoped _setup provisions primary & secondary users across all roles (admin, keyadmin, auditor, user) with auto-cleanup -- Defines BIGINT_MIN / BIGINT_MAX constants for boundary value testing -- Covers role-based access control across all operations — verifying both allowed and restricted role/target combinations -- Validates immutability of fields (name, id, createDate) on PUT and silent 204 behavior on malformed bulk DELETE payloads -Uses forceDelete=true and X-Requested-By: ranger headers where required by the API - ---- - -## `test_ugsync.py` - -- Covers userinfo endpoint for user-group association creation with schema validation -- Tests UGSync (User-Group Sync) operations within TestUgsync for syncing users/groups from external sources (LDAP, UNIX, FILE, MULTI_SOURCES) -- Class-scoped _setup provisions primary & secondary users across all roles (admin, keyadmin, auditor, user) and two temp groups with auto-cleanup -- All endpoints are admin-only; unauthorized roles (keyadmin, auditor, user) consistently return 404 due to Spring's silent failure instead of 403 -- Invalid payloads (bad group/user names, missing fields) often return 200 as silent failures — response body is validated instead of status code -- Validates addUsers/delUsers group membership changes are reflected via follow-up GET calls -- Covers sync source audit logging for all source types with schema validation via validate_auditinfo_schema() and validate_sync_source_info() -- Tests group/user visibility toggling (sets isVisible=0) and verifies persistence via follow-up GET -- Handles semi-failure scenarios (mixed valid/invalid users in one payload) and asserts partial success count in response - ---- - -## `test_user.py` - -- Tests User CRUD operations via /xusers/users/* endpoints within TestUsers -- Class-scoped _setup provisions primary & secondary users across all roles (admin, keyadmin, auditor, user) with auto-cleanup -- Covers role-based access control across all operations — verifying both allowed and restricted role/target combinations -- Tests role assignment logic via /roleassignments endpoint — validating priority order: whiteListUser > whiteListGroup > userMap > groupMap > default -- Validates Spring's silent 404 failure pattern for unauthorized POST/DELETE operations (non-admin roles) -- Tests external user creation flow — 204 on first call (new user), 200 on second call (existing user) -- Covers userinfo endpoint for user-group association creation with schema validation -Validates immutability enforcement and invalid role assignment (ROLE_KEY_ADMIN, ROLE_NON_EXISTEN) returning 400/403 - ---- - -## `test_utility_fun.py` - -- Tests Lookup and AuthSession utility endpoints within TestLookup and AuthSession classes -- Class-scoped _setup provisions users across all roles (admin, keyadmin, auditor, user) with auto-cleanup -- All four roles are permitted for every endpoint — no role-based restriction tests -- TestLookup covers /xusers/lookup/users, /xusers/lookup/groups, and /xusers/lookup/principals — validating vXStrings schema when response is non-empty -- AuthSession covers /xusers/authSessions with query param variations — validates vXAuthSessions or totalCount in response +## `test_role_utility_fun.py` +- Tests role utility and advanced operations within TestRoleUtilityFun +- Class-scoped _setup mirrors TestRoleCRUD — users, groups, roles, and init_configs() +- Export/import: GET /roles/roles/exportJson, POST /roles/roles/importRolesFromFile +- Download: GET /roles/secure/download/{serviceName} with version-based 200/304 responses +- Membership: PUT addUsersAndGroups, removeUsersAndGroups, removeAdminFromUsersAndGroups +- Grant/revoke: PUT /roles/roles/grant/{serviceName} and /roles/roles/revoke/{serviceName} +- Negative and unauthorized variants for all utility endpoints +- Note: /roles/download/{serviceName} tests are currently skipped pending rolerest changes \ No newline at end of file diff --git a/functional-tests/rolerest/test_role_management.py b/functional-tests/rolerest/test_role_management.py index 9f3d20e0e0..46860e8631 100644 --- a/functional-tests/rolerest/test_role_management.py +++ b/functional-tests/rolerest/test_role_management.py @@ -74,7 +74,6 @@ def _setup( cls.ranger_auditor_id1 = cls.audit1["id"] cls.ranger_user_id1 = cls.user1["id"] - # ---------- group details ---------- cls.group, cls.group_id = temp_group() cls.group1, cls.group_id1 = temp_group() @@ -282,7 +281,6 @@ def test_get_role_by_name_same_creds(self, test_case, request): # here always for item in clean_up_items["service_list"]: delete_service(item) - @pytest.mark.get @pytest.mark.positive @pytest.mark.parametrize( @@ -328,7 +326,6 @@ def test_get_role_by_name_with_diff_creds(self, query_param_user, effective_user assign_service_admin(service_id, service, temp_user['name']) auth = (temp_user["name"], "Test@123") - elif test_case == "login user has service admin groups": temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["auditor"]) @@ -336,12 +333,9 @@ def test_get_role_by_name_with_diff_creds(self, query_param_user, effective_user assign_groups_to_user(temp_user["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) service, service_id = create_service() assign_service_admin_group(service_id, service, group["name"]) - - auth = (temp_user["name"], "Test@123") - if query_param_user == "not_exists": role, role_id = request.getfixturevalue("temp_role")() params = {} @@ -358,7 +352,6 @@ def test_get_role_by_name_with_diff_creds(self, query_param_user, effective_user if service: params["serviceName"] = service["name"] - print("\n Auth used: ", auth) print("\n Query params used: ", params) response = requests.get( @@ -370,7 +363,6 @@ def test_get_role_by_name_with_diff_creds(self, query_param_user, effective_user assert_response(response, 200, f"Failed to get role by name with different creds and got response code {response.status_code} with \n response text: {response.text}") - data = response.json() assert data["id"] == role["id"], f"Expected role ID {role['id']} but got {data['id']} for test case {test_case}" print(f"Response data: {data} for test case {test_case}") @@ -396,7 +388,6 @@ def test_get_role_by_name_with_diff_creds(self, query_param_user, effective_user "user assigned to roles via group membership", ]) def test_get_roles_for_user_by_userName(self, test_case, roles, auth, u_role, request): - if u_role == "key_admin": test_user, test_user_id = request.getfixturevalue("temp_keyadmin_user")() @@ -532,8 +523,7 @@ def test_create_role(self, role, auth, test_case, request): auth = getattr(self, auth) else: pytest.fail("Invalid role for this test") - - + param = {} if test_case == "minimal_request": @@ -584,7 +574,6 @@ def test_create_role(self, role, auth, test_case, request): new_user = payload["users"][0]["name"] new_group = payload["groups"][0]["name"] - requests.delete( f"{self.base_url}/xusers/users/userName/{new_user}", params={"forceDelete": "true"}, @@ -593,7 +582,6 @@ def test_create_role(self, role, auth, test_case, request): ) assert_response(response, [200, 204], f"Expected 200 for user deletion but got {response.status_code}") - requests.delete( f"{self.base_url}/xusers/groups/groupName/{new_group}", params={"forceDelete": "true"}, @@ -644,7 +632,6 @@ def test_update_role(self, request, role, test_case): role, r_id = request.getfixturevalue("temp_role")(role_list=role_list) assign_groups_to_user(user["name"], [group["name"]], self.ranger_admin_config, self.base_url, self.headers) - payload = { "id": r_id, "name": role["name"], @@ -672,7 +659,6 @@ def test_update_role(self, request, role, test_case): delete_user(user_id, self.ranger_admin_config, self.base_url, self.headers) - @pytest.mark.delete @pytest.mark.positive @pytest.mark.parametrize( @@ -907,7 +893,6 @@ def test_get_role_by_name_with_diff_creds_negative_login(self, login_test_case, assert_response(response, 400, f"Expected 400 for {login_test_case} but got {response.status_code} with \n response text: {response.text}") - @pytest.mark.get @pytest.mark.negative @pytest.mark.parametrize( @@ -943,7 +928,6 @@ def test_get_role_by_name_with_diff_creds_negative_query_user(self, query_user, assert_response(response, 400, f"Expected 400 for {test_case} but got {response.status_code} with \n response text: {response.text}") - @pytest.mark.get @pytest.mark.negative @pytest.mark.parametrize( @@ -983,7 +967,6 @@ def test_get_lookup_roles_negative(self, test_case, response_code): data = response.json() assert isinstance(data, dict), f"Expected response to be a dictionary of roles but got {type(data)} for test case {test_case}" assert data.get("roles") == [], f"Expected empty roles list for user with no membership but got {data.get('roles')} for test case {test_case}" - @pytest.mark.post @pytest.mark.negative diff --git a/functional-tests/rolerest/test_role_utility_fun.py b/functional-tests/rolerest/test_role_utility_fun.py index e479567c44..d1528d9910 100644 --- a/functional-tests/rolerest/test_role_utility_fun.py +++ b/functional-tests/rolerest/test_role_utility_fun.py @@ -27,7 +27,6 @@ from datetime import datetime import random - @pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") @pytest.mark.rolerest class TestRoleUtilityFun: @@ -98,9 +97,6 @@ def _setup( cls.ranger_user_config, ) - - - @pytest.mark.get @pytest.mark.positive @pytest.mark.parametrize( @@ -136,7 +132,6 @@ def test_get_rokes_exportJson(self, auth): if os.path.exists(export_filename): os.remove(export_filename) - @pytest.mark.get @pytest.mark.positive @pytest.mark.parametrize( @@ -200,7 +195,6 @@ def test_get_secure_roles_download_by_serviceName(self, test_case, auth, service assert data.get("serviceName") == service_name, f"Expected serviceName {service_name}" - @pytest.mark.skip(reason = "Skipping download tests due to existing code changes in rolerest") @pytest.mark.get @pytest.mark.positive @@ -254,8 +248,6 @@ def test_get_download_by_serviceName(self, test_case, auth, last_known_version, data = response.json() assert "userStoreVersion" in data, f"Response missing 'userStoreVersion' for {test_case}" assert "userGroupMapping" in data or "users" in data, f"Response missing user/group data for {test_case}" - - @pytest.mark.post @pytest.mark.positive @@ -386,7 +378,6 @@ def test_post_import_roles_from_file(self, request, auth, test_case): elif "skip existing role identical - updateIfExists=true" in test_case: assert data['Total Role Updated'] == 0, f"Expected 0 roles to be updated but got {data['Total Role Updated']}" - @pytest.mark.put @pytest.mark.positive @pytest.mark.parametrize( @@ -468,8 +459,7 @@ def test_put_role_add_users_and_groups(self, request, auth, isAdmin, test_case): f"Expected 2 groups in the updated role but got {len(returned_groups)}" assert param_group["name"] in returned_groups and group["name"] in returned_groups, \ "Expected both the new and existing group to be present." - - + @pytest.mark.put @pytest.mark.positive @pytest.mark.parametrize( @@ -537,7 +527,6 @@ def test_put_role_remove_users_and_groups(self, request, auth, test_case): else: assert group["name"] not in returned_groups, "Existing group should have been removed from the role." - @pytest.mark.put @pytest.mark.positive @pytest.mark.parametrize( @@ -642,7 +631,6 @@ def test_put_role_grant(self, request, test_case, query_param_user, effective_us assign_service_admin(service_id, service, temp_user['name']) auth = (temp_user["name"], "Test@123") - elif test_case == "login user has service admin groups": temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")(["auditor"]) @@ -651,8 +639,6 @@ def test_put_role_grant(self, request, test_case, query_param_user, effective_us service, service_id = create_service() assign_service_admin_group(service_id, service, group["name"]) auth = (temp_user["name"], "Test@123") - - if query_param_user == "not_exists": role, role_id = request.getfixturevalue("temp_role")() @@ -706,7 +692,6 @@ def test_put_role_grant(self, request, test_case, query_param_user, effective_us assert_response(response, 200, f"Failed to update roles and got response code {response.status_code} with \n response text: {response.text}") - if payload_test_case == "users adition": get_role_by_name_flg, role_data = get_role_by_name(role["name"]) assert get_role_by_name_flg, "Role not found when fetched by name after granting role to user." @@ -723,7 +708,6 @@ def test_put_role_grant(self, request, test_case, query_param_user, effective_us returned_roles = [r["name"] for r in role_data.get("roles", [])] assert payload_role["name"] in returned_roles, "New role added in the grant payload is not present in the updated role." - # # Cleanup if service_id is not None and service_id not in clean_up_items.get("service_list", []): clean_up_items["service_list"].append(service_id) @@ -842,7 +826,6 @@ def test_put_role_revoke(self, request, test_case, query_param_user, effective_u ) assert_response(response, 200, f"Failed to revoke roles and got response code {response.status_code} with \n response text: {response.text}") - get_role_by_name_flg, role_data = get_role_by_name(role["name"]) assert get_role_by_name_flg, "Role not found when fetched by name after revoking role." @@ -883,10 +866,8 @@ def test_put_role_revoke(self, request, test_case, query_param_user, effective_u delete_role(payload_role_id) for item in clean_up_items.get("service_list", []): - delete_service(item) - - - + delete_service(item) + # NEGATIVE TEST CASES @pytest.mark.get @@ -905,7 +886,6 @@ def test_get_rokes_exportJson_unauthorized(self, auth): assert_response(response, 403, f"Expected 403 for unauthorized user but got {response.status_code}") - @pytest.mark.get @pytest.mark.negative @pytest.mark.parametrize( @@ -963,7 +943,6 @@ def test_get_download_by_serviceName_negative(self, test_case, auth, expected_st else: service_name = "dev_hdfs" # Assuming this is your valid seed service - params = { "lastKnownUserStoreVersion": -1, "lastActivationTime": 0, @@ -982,7 +961,6 @@ def test_get_download_by_serviceName_negative(self, test_case, auth, expected_st assert_response(response, expected_status, test_case) - @pytest.mark.post @pytest.mark.negative @pytest.mark.parametrize( @@ -1110,7 +1088,6 @@ def test_put_role_grant_with_diff_creds_negative_login(self, login_test_case, re finally: delete_role(role_id) - @pytest.mark.put @pytest.mark.negative @pytest.mark.parametrize( @@ -1158,7 +1135,6 @@ def test_put_role_grant_with_diff_creds_negative_query_user(self, query_user, te finally: delete_role(role_id) - @pytest.mark.put @pytest.mark.negative @pytest.mark.parametrize( diff --git a/functional-tests/rolerest/utility/utils.py b/functional-tests/rolerest/utility/utils.py index f73f235d60..955cc4fd9b 100644 --- a/functional-tests/rolerest/utility/utils.py +++ b/functional-tests/rolerest/utility/utils.py @@ -22,7 +22,6 @@ import subprocess import xuserrest.utility.utils as xutils - BASE_URL = "http://localhost:6080/service" AUTH = ("admin", "rangerR0cks!") HEADERS = {"Content-Type": "application/json"} @@ -40,7 +39,6 @@ RANGER_AUDITOR_CONFIG = None RANGER_USER_CONFIG = None - def init_configs(ranger_config, ranger_key_admin_config, ranger_auditor_config, ranger_user_config): global RANGER_CONFIG, RANGER_KEY_ADMIN_CONFIG, RANGER_AUDITOR_CONFIG, RANGER_USER_CONFIG @@ -49,9 +47,6 @@ def init_configs(ranger_config, ranger_key_admin_config, ranger_auditor_config, RANGER_AUDITOR_CONFIG = ranger_auditor_config RANGER_USER_CONFIG = ranger_user_config - - - def create_service(user_name = "hdfs", password = "hdfs"): unique_name = f"test_service_{uuid.uuid4().hex[:8]}" @@ -83,10 +78,8 @@ def create_service(user_name = "hdfs", password = "hdfs"): service_id = service["id"] print(f"[+] Service created: name={service['name']}, id={service_id}") - return service, service_id - def assign_service_admin(service_id, service, username): configs = service.get("configs", {}) @@ -154,8 +147,6 @@ def delete_service(service_id): resp.raise_for_status() print(f"[+] Service deleted: id={service_id}") - - def assert_response(response, expected_status, text = None, service_name=SERVICE_NAME): xutils.assert_response(response, expected_status, text, service_name) @@ -179,8 +170,6 @@ def delete_role(role_id): assert resp.status_code in [200, 204], f"Failed to delete role id={role_id}: {resp.text}" print(f" /n /n [+] Role deleted successfully with id={role_id}. Response status: {resp.status_code}. Response text: {resp.text} /n /n") - - def ensureRoleAccess(test_case, request, existing_service=None): service_list = [] @@ -228,7 +217,6 @@ def ensureRoleAccess(test_case, request, existing_service=None): req = requests.get(f"{BASE_URL}/xusers/groups/{group_id}", auth=AUTH, headers=HEADERS) print("\n [+] Group details after assigning service admin group: ", req.json(), "\n") - elif test_case.endswith("role-user"): # must be before endswith("user") temp_user, temp_user_id = request.getfixturevalue("temp_secure_user")("user") c_role, c_id = request.getfixturevalue("temp_role")(user_list=[{"name": temp_user["name"], "isAdmin": True}]) @@ -270,4 +258,4 @@ def ensureRoleAccess(test_case, request, existing_service=None): params = {"serviceName": service["name"], "execUser": temp_user["name"]} clean_up_items = {"role_list": role_list, "service_list": service_list} - return auth, params, role, clean_up_items + return auth, params, role, clean_up_items \ No newline at end of file diff --git a/functional-tests/xuserrest/readme.md b/functional-tests/xuserrest/readme.md index 26183f0ae6..18fad54868 100644 --- a/functional-tests/xuserrest/readme.md +++ b/functional-tests/xuserrest/readme.md @@ -30,7 +30,7 @@ This directory contains automated functional tests for the XUSERREST API, coveri ## Structure ``` -test_xuserrest/ +xuserrest/ ├── utility/ │ ├── __init__.py │ ├── utils.py diff --git a/functional-tests/xuserrest/test_groups.py b/functional-tests/xuserrest/test_groups.py index 6552e18d70..cf8b99e129 100644 --- a/functional-tests/xuserrest/test_groups.py +++ b/functional-tests/xuserrest/test_groups.py @@ -1389,7 +1389,6 @@ def test_create_groupuser_negative(self, test_case, auth): assert_response(response, response_code, f"Group-User association creation should have failed with {response_code} but got {response.status_code} instead for test case: {test_case}") - @pytest.mark.negative @pytest.mark.put @pytest.mark.parametrize( @@ -1453,7 +1452,6 @@ def test_update_groupuser_negative(self, test_case, auth): assert_response(response, response_code, f"Group-User association update should have failed with {response_code} but got {response.status_code} instead for test case: {test_case}") delete_groupuser(grp_user_id, self.ranger_admin_config, self.base_url, self.headers) - @pytest.mark.negative @pytest.mark.delete @pytest.mark.parametrize( diff --git a/functional-tests/xuserrest/test_permission.py b/functional-tests/xuserrest/test_permission.py index 9b51452925..579f2c7c10 100644 --- a/functional-tests/xuserrest/test_permission.py +++ b/functional-tests/xuserrest/test_permission.py @@ -27,8 +27,6 @@ class TestPermissions: SERVICE_NAME = "admin" - - @pytest.fixture(autouse=True, scope="class") def _setup( self, @@ -1101,7 +1099,6 @@ def test_create_permissions_with_negative_testcases(self, role, auth, test_case) ) assert_response(response, expected_status, f"{test_case}: Expected status {expected_status}, got {response.status_code}") - @pytest.mark.put @pytest.mark.negative @pytest.mark.parametrize( @@ -1162,7 +1159,6 @@ def test_create_permissions_group_with_invalid_cases(self, test_case, request): response = requests.post(f"{self.base_url}/xusers/permission/group", json=payload, auth=auth, headers=self.headers) assert_response(response, expected_status, f"Case {test_case} failed: {response.status_code}") - @pytest.mark.post @pytest.mark.negative @pytest.mark.parametrize( @@ -1288,7 +1284,6 @@ def test_update_permissions_group(self, request, test_case, auth_name, expected_ ) assert_response(del_response, 204, f"Cleanup failed for permission group ID {perm_id}: {del_response.text}") - @pytest.mark.put @pytest.mark.negative @pytest.mark.parametrize( @@ -1361,7 +1356,6 @@ def test_update_permissions_user_with_invalid_testcases(self, request, role, aut def test_delete_permissions_with_invalid_roles(self, role, auth): auth_config = getattr(self, auth) - perm_id = self.permission_module_id diff --git a/functional-tests/xuserrest/test_secure_user.py b/functional-tests/xuserrest/test_secure_user.py index 2723972df7..250d08327e 100644 --- a/functional-tests/xuserrest/test_secure_user.py +++ b/functional-tests/xuserrest/test_secure_user.py @@ -84,7 +84,6 @@ def _setup( cls.ranger_auditor_config, cls.ranger_user_config, ) - # POSITIVE TESTS @pytest.mark.get @@ -122,13 +121,10 @@ def test_get_secure_user_schema_validation(self, client_roles, all_schema_follow assert_response(response, 200) - data = response.json() validate_secure_user_schema(data) - - @pytest.mark.get @pytest.mark.positive @pytest.mark.parametrize("auth_role, target_role", [ @@ -154,7 +150,6 @@ def test_get_secure_external_user(self, client_roles, auth_role, target_role, re target_id = self.ranger_auditor_id elif target_role == "user": target_id = self.ranger_user_id - if auth_role == "admin": authorization = self.ranger_admin_config @@ -167,7 +162,6 @@ def test_get_secure_external_user(self, client_roles, auth_role, target_role, re authorization = self.ranger_auditor_config elif auth_role == "keyadmin": authorization = self.ranger_key_admin_config - response = requests.get( f"{self.base_url}/xusers/secure/users/external/{target_id}", @@ -190,7 +184,6 @@ def test_get_secure_external_user(self, client_roles, auth_role, target_role, re assert data["vXStrings"] == [], \ "Auditor should not see other users' external roles" - @pytest.mark.get @pytest.mark.positive @pytest.mark.parametrize("auth_role, target_role", [ @@ -455,7 +448,6 @@ def test_update_secure_role_using_username(self, auth_role, target_role, client_ authorization = self.ranger_auditor_config elif auth_role == "user": authorization = self.ranger_user_config - print(f"\nTesting role update for user: {target_user['name']}") @@ -481,7 +473,6 @@ def test_update_secure_role_using_username(self, auth_role, target_role, client_ validate_external_user_schema(updated_data) assert "ROLE_USER" in updated_data["vXStrings"][0]["value"] - @pytest.mark.parametrize("auth_role, target_role", [ ("admin", "admin"), ("admin", "auditor"), @@ -557,7 +548,6 @@ def test_delete_secure_user_by_id(self, request, client_roles): assert_response(resp, 204, f"Failed to delete user: {target_user['name']}") - @pytest.mark.delete @pytest.mark.positive def test_delete_secure_bulk_users(self, request, client_roles): @@ -611,7 +601,6 @@ def test_get_deleted_user_by_name(self, request, client_roles): auth=self.ranger_admin_config ) assert_response(response, 404) - # NEGATIVE TESTS @@ -706,7 +695,6 @@ def test_get_secure_external_user_invalid_role(self, request, target_role): assert_response(response, 403, f"{target_role} should not access external user endpoint") - @pytest.mark.get @pytest.mark.negative @pytest.mark.parametrize("auth_role, target_role", [ @@ -762,7 +750,6 @@ def test_get_secure_user_roles_by_username_by_invalid_role(self, request, client else: assert_response(response, 403) - @pytest.mark.post @pytest.mark.negative def test_create_secure_user_missing_name(self): @@ -789,7 +776,6 @@ def test_create_secure_user_missing_name(self): assert_response(response, 400) - @pytest.mark.post @pytest.mark.negative def test_create_secure_user_duplicate_username(self): @@ -883,7 +869,6 @@ def test_create_secure_user_via_invalid_roles(self, request): print(f"{username} → {response.status_code}") assert_response(response, [403,404] , f"{username} should not have permission to create secure users, but got {response.status_code}") - @pytest.mark.put @pytest.mark.negative def test_edit_secure_user_using_invalid_id(self): @@ -982,7 +967,6 @@ def test_edit_secure_mandatory_fields(self, request, field): assert_response(update_response, 400, f"Expected status code 400 when trying to modify mandatory field: {field}") - @pytest.mark.put @pytest.mark.negative def test_update_secure_user_active_status_with_invalid_roles(self, request): @@ -1120,8 +1104,6 @@ def test_update_secure_role_using_id_with_invalid_roles(self, auth_role, target_ assert_response(response, 403) - - @pytest.mark.parametrize("auth_role, target_role", [ ("admin", "keyadmin"), ("keyadmin", "admin"), @@ -1175,7 +1157,6 @@ def test_update_secure_role_using_username_with_invalid_roles(self, auth_role, t assert_response(response, 403) - @pytest.mark.delete @pytest.mark.negative def test_delete_secure_users_invalid_payload(self, client_roles): diff --git a/functional-tests/xuserrest/test_ugsync.py b/functional-tests/xuserrest/test_ugsync.py index 52cf30d27c..9e02439515 100644 --- a/functional-tests/xuserrest/test_ugsync.py +++ b/functional-tests/xuserrest/test_ugsync.py @@ -117,7 +117,6 @@ def test_get_ugsync_groupusers(self, role, auth): if first_value: assert isinstance(first_value[0], str) and len(first_value[0]) <= 767, f"Expected elements of the list to be strings, got {type(first_value[0])}" - @pytest.mark.positive @pytest.mark.post @pytest.mark.parametrize("role, auth", [ @@ -164,8 +163,6 @@ def test_create_ugsync_groupusers(self, role, auth): data = fetch_groups_for_user_id(self.user1["id"], self.ranger_admin_config, self.base_url, self.headers) assert not check_group_in_user_groups(self.group1['id'], data), \ f"Group with id {self.group1['id']} is still assigned to user1 with id {self.user1['id']} which is not expected for test case: {role}" - - @pytest.mark.positive @pytest.mark.post @@ -219,7 +216,6 @@ def test_create_ugsync_auditinfo(self, role, auth, test_case): validate_auditinfo_schema(data) validate_sync_source_info(payload, data) - @pytest.mark.positive @pytest.mark.post @pytest.mark.parametrize("role, auth, test_case", [ @@ -291,8 +287,6 @@ def test_create_ugsync_group(self, role, auth, test_case): if resp.status_code == 200: delete_group(resp.json().get("id"), self.ranger_admin_config, self.base_url, self.headers) - - @pytest.mark.positive @pytest.mark.post @pytest.mark.parametrize("role, auth", [ @@ -322,7 +316,6 @@ def test_create_ugsync_group_visibility(self, role, auth): flg, resp = group_exists(self.group1["id"], auth, self.base_url, self.headers) assert flg and resp.get("isVisible") == 0, f"Group visibility is not updated to 0 as expected for test case: {role}" - @pytest.mark.positive @pytest.mark.post @pytest.mark.parametrize("role, auth, testcase", [ @@ -387,7 +380,6 @@ def test_create_ugsync_users(self, role, auth, testcase): delete_user(resp.json().get("id"), self.ranger_admin_config, self.base_url, self.headers) deleted_users.add(username) - @pytest.mark.positive @pytest.mark.post @pytest.mark.parametrize("role, auth", [ @@ -422,8 +414,6 @@ def test_create_ugsync_users_visibility(self, role, auth): data = resp.json() assert resp.status_code == 200 and data.get("isVisible") == 0, f"User visibility is not updated to 0 as expected for test case: {role}" - - # NEGATIVE TESTS @pytest.mark.negative @@ -441,7 +431,6 @@ def test_get_ugsync_groupusers_unauthorized(self, role, auth): headers = self.headers ) assert_response(response, 404, f"Expected status code 404 for role {role} due to spring's silent failure, got {response.status_code}") - @pytest.mark.negative @pytest.mark.post @@ -496,8 +485,7 @@ def test_create_ugsync_groupusers_negative(self, auth, testcase): response_data = return_value_ugsync_groupusers(payload, self.ranger_admin_config, self.base_url, self.headers) data = response.json() - assert data == response_data, f"Expected response to be {response_data} for test case: {testcase}, got {data}" - + assert data == response_data, f"Expected response to be {response_data} for test case: {testcase}, got {data}" @pytest.mark.negative @pytest.mark.post @@ -544,7 +532,6 @@ def test_create_ugsync_auditinfo_unauthorized(self, auth, testcase): assert_response(response, expected_status, f"Expected status code {expected_status} for test case: {testcase}, got {response.status_code}") - @pytest.mark.negative @pytest.mark.post @pytest.mark.parametrize("auth, testcase", [ @@ -584,7 +571,6 @@ def test_create_ugsync_group_negative(self, auth, testcase): assert_response(response, expected_status, f"Expected status code {expected_status} for test case: {testcase}, got {response.status_code}") - @pytest.mark.negative @pytest.mark.post @pytest.mark.parametrize("auth, testcase", [ diff --git a/functional-tests/xuserrest/test_user.py b/functional-tests/xuserrest/test_user.py index 81589947ab..c7352bed72 100644 --- a/functional-tests/xuserrest/test_user.py +++ b/functional-tests/xuserrest/test_user.py @@ -169,7 +169,6 @@ def test_get_user_by_id(self, auth_role, user_role, auth): data = response.json() validate_user_schema(data) assert data["id"] == user_id, "Fetched user ID does not match requested ID" - @pytest.mark.positive @pytest.mark.get @@ -349,14 +348,12 @@ def test_create_user_userinfo(self, request, auth_role, auth): headers=self.headers ) - assert_response(response, 200) data = response.json() validate_xgroup_schema(data["xgroupInfo"][0]) - assert data["xgroupInfo"][0]["name"] == payload["xgroupInfo"][0]["name"], "Response group name should match the test user name" grp_id = data["xgroupInfo"][0]["id"] @@ -370,7 +367,6 @@ def test_create_user_userinfo(self, request, auth_role, auth): ) print(f"Deleted group {grp_name} created during test, status code: {del_req.status_code}") - @pytest.mark.positive @pytest.mark.post @pytest.mark.parametrize("auth_role, auth", [("admin", "ranger_admin_config")]) @@ -461,7 +457,6 @@ def test_create_roleassignment(self, auth_role, auth, request): assert expected_role in actual_roles, \ f"{user}: expected {expected_role}, got {actual_roles}" - # cleanup of created groups for group in [group_admin, group_auditor, group_user]: resp = requests.get( @@ -517,7 +512,6 @@ def test_update_user(self, auth_role, request): assert "ROLE_ADMIN_AUDITOR" not in payload["userRoleList"], "Key Admin can not promote any one to auditor role" assert "ROLE_SYS_ADMIN" not in user["userRoleList"], "Key Admin can not update admin role" assert "ROLE_ADMIN_AUDITOR" not in user["userRoleList"], "Key Admin can not update auditor role" - resp = requests.get( f"{self.base_url}/xusers/users/{user_id}", @@ -586,9 +580,9 @@ def test_delete_external_user_by_username(self, request, client_roles): ) assert response, "External user deletion failed" assert_response(response, 204) - # Negative Test Cases + @pytest.mark.negative @pytest.mark.get def test_get_users_using_invalid_auth(self): @@ -603,7 +597,6 @@ def test_get_users_using_invalid_auth(self): assert_response(response, 400, "Expected status code not returned for invalid user ID") - @pytest.mark.negative @pytest.mark.get @pytest.mark.parametrize("auth_role, user_role", [ @@ -645,7 +638,6 @@ def test_get_user_by_id_using_invalid_auth(self, auth_role, user_role, request): assert_response(response, 403, f"{auth_role} should not have permission to access user of role {user_role}") - @pytest.mark.negative @pytest.mark.get @pytest.mark.parametrize("auth_role, user_role", [ @@ -708,7 +700,6 @@ def test_create_secure_user_missing_mandatory_field(self): assert_response(response, 400) - @pytest.mark.post @pytest.mark.negative def test_create_user_via_invalid_roles(self, request): @@ -751,7 +742,6 @@ def test_create_user_via_invalid_roles(self, request): print(f"{username} → {response.status_code}") assert_response(response, 404, f"{username} should not have permission to create users and expected 404 due to spring silent failure, but got {response.status_code}") - @pytest.mark.negative @pytest.mark.post @@ -877,7 +867,6 @@ def test_create_roleassignment_with_invalid_roles(self, request, auth_role): assert_response(response, 404, f"{auth_role} is expected to return 404 due to spring's silent failure and should not have permission to assign roles, but got {response.status_code}") - @pytest.mark.negative @pytest.mark.post @pytest.mark.parametrize("invalid_role", ["ROLE_NON_EXISTEN", "ROLE_KEY_ADMIN"]) @@ -905,8 +894,6 @@ def test_create_roleassignment_to_invalid_assignment(self, invalid_role): elif invalid_role == "ROLE_KEY_ADMIN": expected_status = 403 # since ROLE_KEY_ADMIN is not a valid role but it has admin keyword so it will be blocked by permission check before role validation assert_response(response, expected_status, f"Assigning non-existent role should fail with {expected_status}, but got {response.status_code}") - - @pytest.mark.negative @pytest.mark.put @@ -965,8 +952,6 @@ def test_update_user_using_invalid_access(self, temp_secure_user, auth_role, use assert_response(response, 403, f"{auth_role} should not have permission to update users, but got {response.status_code}") elif auth_role in ["admin", "keyadmin"]: assert_response(response, 400, f"{auth_role} should not have permission to update {user_role} role, expected 400 but got {response.status_code}") - - @pytest.mark.negative @pytest.mark.put diff --git a/functional-tests/xuserrest/test_utility_fun.py b/functional-tests/xuserrest/test_utility_fun.py index 60cc362b75..ae09440d97 100644 --- a/functional-tests/xuserrest/test_utility_fun.py +++ b/functional-tests/xuserrest/test_utility_fun.py @@ -244,7 +244,6 @@ def test_get_auth_sessions(self, role, auth, params, expected_status, test_case) json_resp = response.json() assert "vXAuthSessions" in json_resp or "totalCount" in json_resp - @pytest.mark.get @pytest.mark.positive @pytest.mark.parametrize( @@ -286,7 +285,6 @@ def test_get_auth_session_info_success(self, role, auth): expected_user = auth[0] assert data.get("loginId") == expected_user, f"Expected loginId to be {expected_user} but got {data.get('loginId')}" assert data.get("id") is not None - # NEGATIVE TEST CASES @@ -321,7 +319,6 @@ def test_get_auth_sessions_negative(self, test_case, auth): elif test_case == "invalid-input-data": assert response.status_code == 400, f"Expected 400 Bad Request for {test_case}, but got {response.status_code}" - @pytest.mark.get @pytest.mark.negative @pytest.mark.parametrize( @@ -352,7 +349,6 @@ def test_get_auth_session_info_negative(self, test_case, auth): elif test_case in ["missing-session-id", "invalid-session-id"]: assert response.status_code == 400, f"Expected 400 Bad Request for {test_case}, but got {response.status_code}" - @pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") @pytest.mark.xuserrest class TestDownloadServiceName: @@ -453,7 +449,6 @@ def test_get_download_by_serviceName(self, test_case, auth, last_known_version, data = response.json() assert "userStoreVersion" in data, f"Response missing 'userStoreVersion' for {test_case}" assert "userGroupMapping" in data or "users" in data, f"Response missing user/group data for {test_case}" - @pytest.mark.get @pytest.mark.positive @@ -512,10 +507,14 @@ def test_get_secure_download_by_serviceName(self, test_case, auth, service_name, data = response.json() assert "userStoreVersion" in data - assert "userGroupMapping" in data or "users" in data - + assert ( + "userAttrMapping" in data + or "groupAttrMapping" in data + or "userGroupMapping" in data + ), "Response missing user/group data" # NEGATIVE TEST CASES + @pytest.mark.skip(reason = "Skipping download tests due to existing code changes in xuserrest") @pytest.mark.get @pytest.mark.negative @@ -557,8 +556,6 @@ def test_get_download_by_serviceName_negative(self, test_case, auth, expected_st assert_response(response, expected_status, test_case) - - @pytest.mark.get @pytest.mark.negative @pytest.mark.parametrize( @@ -594,7 +591,6 @@ def test_get_secure_download_by_serviceName_negative(self, test_case, auth, serv assert_response(response, expected_status, test_case) - @pytest.mark.usefixtures("ranger_config", "ranger_key_admin_config") @pytest.mark.xuserrest class TestMiscellaneous: @@ -647,7 +643,6 @@ def _setup( cls.ranger_user_config, ) - @pytest.mark.get @pytest.mark.positive @pytest.mark.parametrize( @@ -686,7 +681,6 @@ def test_get_groups_by_userid(self, role, auth, test_case, request): assert i["id"] in lis, f"Unexpected group id {i['id']} found in response for {test_case}" lis.remove(i["id"]) - @pytest.mark.get @pytest.mark.positive @pytest.mark.parametrize( @@ -720,8 +714,6 @@ def test_get_users_by_groupid(self, role, auth, test_case, request): assert i["id"] in user_ids, f"Unexpected user id {i['id']} found in response for {test_case}" user_ids.remove(i["id"]) - - @pytest.mark.delete @pytest.mark.positive @pytest.mark.parametrize( @@ -818,7 +810,6 @@ def test_forcedelete_external_group(self, role, auth, test_case, request): ) assert get_response.status_code == 400, f"Expected group {i['name']} to be deleted, but it still exists" - # Negative Tests @pytest.mark.get