diff --git a/.cursor/rules/ranger-python.mdc b/.cursor/rules/ranger-python.mdc new file mode 100644 index 00000000000..9b1e1680704 --- /dev/null +++ b/.cursor/rules/ranger-python.mdc @@ -0,0 +1,88 @@ +--- +description: Apache Ranger Python client and test conventions (apache-ranger on PyPI) +globs: intg/**/*.py,ranger-examples/sample-client/**/*.py +alwaysApply: false +--- + + + +# Ranger Python client (`apache-ranger`) + +**Authoritative package root:** `intg/src/main/python/` — packaging via `setup.py` there only. Do not add duplicate `setup.py` scripts elsewhere. + +**Python version:** `apache-ranger` targets **Python 3.13+** (`python_requires` in `setup.py`). + +## Style and structure + +- Preserve the ASF license header on new files (match neighboring modules). +- Match existing layout: `apache_ranger/client/` for HTTP clients, `apache_ranger/model/` for typed models, `utils.py` for coercion helpers. +- Reuse `type_coerce` / `type_coerce_attrs()` — accept dict-like input and coerce to model types; do not duplicate parsing logic. +- Keep changes minimal; follow naming, imports, and spacing of the file you edit. + +## Vertical alignment + +Within a **consecutive block** of related lines, align `=` (and dict values after `:` where keys share one dict): + +```python +self.status_code = status_code +self.response = response +self.content = content + +from apache_ranger.exceptions import RangerServiceException +from apache_ranger.model.ranger_service import RangerService + +RangerAuthzRequest({ + "requestId": "req-1", + "user": RangerUserInfo({"name": "alice"}), + "access": RangerAccessInfo({...}), + "context": RangerAccessContext({...}), +}) +``` + +Apply alignment only within the block; do not pad unrelated single assignments. Reference points: `exceptions.py`, `sample_pdp_client.py`, `test_ranger_client.py`. + +## Model and sample data + +Use **compact dict literals** — one line per top-level field when still readable (`sample_pdp_client.py`). Avoid verbose nesting when a flat line is clear. + +## Unit tests + +Primary test file: `intg/src/test/python/test_ranger_client.py` + +Run from `intg/`: + +```bash +PYTHONPATH=src/main/python python -B src/test/python/test_ranger_client.py +``` + +Split tests **by client** — one `unittest.TestCase` per area (`TestRangerClient`, `TestPDPClient`). + +Fixtures: shared data as **class-level constants** (`URL`, `AUTH`, `AUTHZ_REQUEST`); single-use data inline in the test method. + +Assertions and mocking: + +- Prefer `assertRaises`, `assertIsNone`, `assertIsInstance` over bare `try/except`. +- Assert **current** client behavior — not obsolete error text or exceptions. +- Mock at the boundary: `Session` for Admin HTTP, `client_http.call_api` for PDP. +- Only add tests for real coercion, exports, or API wiring. + +## References + +- `intg/src/main/python/README.md` — published API surface and quickstarts +- `ranger-examples/sample-client/src/main/python/` — Admin, PDP, KMS usage examples diff --git a/intg/scripts/setup.py b/intg/scripts/setup.py deleted file mode 100644 index 9ce9773017f..00000000000 --- a/intg/scripts/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python - -# -# 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 setuptools import setup, find_packages - -# External dependencies -requirements = ['requests>=2.24'] - -setup( - name='pyclient', - packages=['pylib', 'pylib/ranger/client', 'pylib/ranger/model'], - data_files=[('../../lib/resources', ['./../src/main/resources/logging.conf'])], - install_requires=requirements, - zip_safe=False -) diff --git a/intg/src/main/python/README.md b/intg/src/main/python/README.md index fd6b2e2c04d..babe29db18b 100644 --- a/intg/src/main/python/README.md +++ b/intg/src/main/python/README.md @@ -17,69 +17,60 @@ specific language governing permissions and limitations under the License. --> -# Apache Ranger - Python client +# Apache Ranger Python Client -Python library for Apache Ranger. +`apache-ranger` is the official Python client package for Apache Ranger. +It provides typed helpers for Ranger Admin, user/group management, KMS, +and Policy Decision Point (PDP) APIs. + +## Requirements + +- Python 3.13 or later +- Apache Ranger Admin, KMS, or PDP service reachable from your Python process ## Installation -Use the package manager [pip](https://pip.pypa.io/en/stable/) to install python client for Apache Ranger. +Install the client from PyPI: ```bash -> pip install apache-ranger -> pip install requests_kerberos (If using kerberos for authentication) +pip install apache-ranger ``` -Verify if apache-ranger client is installed: -```bash -> pip list +For Kerberos/SPNEGO authentication, install the optional Kerberos dependency: -Package Version ------------- --------- -apache-ranger 0.0.12 +```bash +pip install requests-kerberos ``` -## Python API clients - -### Ranger Admin (`RangerClient`) +Verify the installed package: -Base URL: `http(s)://:` - -`RangerClient` wraps APIs under `service/public/v2/api`, including: +```bash +python -m pip show apache-ranger +``` -- service-def CRUD -- service CRUD -- policy CRUD/apply/search -- roles -- security-zones -- plugin-info and policy-delta maintenance +## Supported Clients -Authentication options: +- `RangerClient`: Ranger Admin APIs for service definitions, services, policies, + roles, security zones, plugin information, and policy delta maintenance. +- `RangerUserMgmtClient`: user, group, and user-group management APIs. +- `RangerKMSClient`: Ranger KMS key management APIs. +- `RangerPDPClient`: PDP authorization APIs for single authorization, batch + authorization, and effective resource permissions. -- Basic auth tuple, for example `("admin", "password")` -- Kerberos/SPNEGO (`requests-kerberos`) -- custom headers/query params via constructor +## Quick Start -Example: +### Ranger Admin ```python from apache_ranger.client.ranger_client import RangerClient ranger = RangerClient("http://localhost:6080", ("admin", "rangerR0cks!")) + services = ranger.find_services() -print(len(services.list)) +print(f"{len(services.list)} services found") ``` -### Ranger User Management (`RangerUserMgmtClient`) - -`RangerUserMgmtClient` builds on `RangerClient` and covers user/group/group-user operations, including: - -- create/get/update/delete user -- create/get/update/delete group -- add/remove/list group-user mappings -- list groups for user, list users in group - -Example: +### User And Group Management ```python from apache_ranger.client.ranger_client import RangerClient @@ -89,534 +80,181 @@ ranger = RangerClient("http://localhost:6080", ("admin", "rangerR0cks!")) user_mgmt = RangerUserMgmtClient(ranger) users = user_mgmt.find_users() -print(len(users.list)) -``` - -### Ranger KMS (`RangerKMSClient`) - -Base URL: `http(s)://:` - -`RangerKMSClient` wraps KMS APIs such as: - -- create/delete key -- rollover/get metadata/current version -- generate/decrypt/reencrypt encrypted keys -- status and key-name listing - -Authentication options: +groups = user_mgmt.find_groups() -- Hadoop simple auth (`HadoopSimpleAuth("user")`) -- Kerberos/SPNEGO (`requests-kerberos`) -- Basic auth tuple (when enabled) +print(f"{len(users.list)} users found") +print(f"{len(groups.list)} groups found") +``` -Example: +### Ranger KMS ```python -from apache_ranger.client.ranger_kms_client import RangerKMSClient from apache_ranger.client.ranger_client import HadoopSimpleAuth +from apache_ranger.client.ranger_kms_client import RangerKMSClient kms = RangerKMSClient("http://localhost:9292", HadoopSimpleAuth("keyadmin")) + print(kms.kms_status()) ``` -### Ranger PDP (`RangerPDPClient`) - -`RangerPDPClient` is a thin Python wrapper for the PDP REST APIs exposed at `http(s)://:/authz/v1`. - -Endpoints: - -- `POST /authz/v1/authorize` -> single access evaluation -- `POST /authz/v1/authorizeMulti` -> batch access evaluation -- `POST /authz/v1/permissions` -> effective permissions for a resource - -Request context requirements: - -- `context.serviceType` (for example: `hive`, `hdfs`, `kafka`) -- `context.serviceName` (Ranger service name, for example: `dev_hive`) -- for `authorize` and `authorizeMulti`, `user.name` must be present - -Authentication options: - -- **Kerberos/SPNEGO** - - install dependency: `pip install requests-kerberos` - - use `HTTPKerberosAuth()` as `auth` in `RangerPDPClient` -- **Trusted header** - - pass caller header (must be configured using `ranger.pdp.authn.header.username`) - - only behind a trusted proxy -- **JWT bearer** - - pass `Authorization: Bearer ` in request headers - -Delegation behavior: - -- if caller differs from `request.user.name`, delegation permission is required -- delegation users are configured with `ranger.pdp.service..delegation.users` - (or wildcard `ranger.pdp.service.*.delegation.users`) -- without delegation permission, PDP returns `403 FORBIDDEN` - -`RangerPDPClient` example (Kerberos): +### Policy Decision Point ```python -from requests_kerberos import HTTPKerberosAuth from apache_ranger.client.ranger_pdp_client import RangerPDPClient from apache_ranger.model.ranger_authz import ( - RangerAccessContext, RangerAccessInfo, RangerAuthzRequest, - RangerResourceInfo, RangerUserInfo + RangerAccessContext, + RangerAccessInfo, + RangerAuthzRequest, + RangerResourceInfo, + RangerUserInfo, ) -pdp = RangerPDPClient("http://localhost:6500", HTTPKerberosAuth()) +pdp = RangerPDPClient( + "http://localhost:6500", + auth=None, + headers={"X-Forwarded-User": "hive"}, +) -req = RangerAuthzRequest({ - 'requestId': 'req-1', - 'user': RangerUserInfo({'name': 'alice'}), - 'access': RangerAccessInfo({ - 'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email']}), - 'action': 'QUERY', - 'permissions': ['select'] +request = RangerAuthzRequest({ + "requestId": "req-1", + "user": RangerUserInfo({"name": "alice"}), + "access": RangerAccessInfo({ + "resource": RangerResourceInfo({"name": "table:default/test_tbl1"}), + "permissions": ["select"], + }), + "context": RangerAccessContext({ + "serviceType": "hive", + "serviceName": "dev_hive", }), - 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) }) -res = pdp.authorize(req) -print(res.decision) +result = pdp.authorize(request) +print(result.decision) ``` -Raw REST example using `requests` (JWT bearer): - -```python -import requests - -url = "http://localhost:6500/authz/v1/authorize" -headers = { - "Authorization": "Bearer ", - "Content-Type": "application/json" -} -payload = { - "requestId": "req-1", - "user": {"name": "alice"}, - "access": { - "resource": {"name": "table:default/test_tbl1", "subResources": ["column:id", "column:name", "column:email"]}, - "action": "QUERY", - "permissions": ["select"] - }, - "context": {"serviceType": "hive", "serviceName": "dev_hive"} -} - -resp = requests.post(url, headers=headers, json=payload, timeout=30) -resp.raise_for_status() -print(resp.json()) -``` - -## Usage - -```python test_ranger.py``` -```python -# test_ranger.py - -from apache_ranger.model.ranger_service import * -from apache_ranger.client.ranger_client import * -from apache_ranger.model.ranger_policy import * - - -## Step 1: create a client to connect to Apache Ranger admin -ranger_url = 'http://localhost:6080' -ranger_auth = ('admin', 'rangerR0cks!') - -# For Kerberos authentication -# -# from requests_kerberos import HTTPKerberosAuth -# -# ranger_auth = HTTPKerberosAuth() - -ranger = RangerClient(ranger_url, ranger_auth) - -# to disable SSL certificate validation (not recommended for production use!) -# -# ranger.session.verify = False - +## Authentication -## Step 2: Let's create a service -service = RangerService() -service.name = 'test_hive' -service.type = 'hive' -service.configs = {'username':'hive', 'password':'hive', 'jdbc.driverClassName': 'org.apache.hive.jdbc.HiveDriver', 'jdbc.url': 'jdbc:hive2://ranger-hadoop:10000', 'hadoop.security.authorization': 'true'} +Use the authentication mechanism configured for the Ranger service you are +calling: -print('Creating service: name=' + service.name) +- Basic auth: pass a `(username, password)` tuple to `RangerClient`. +- Kerberos/SPNEGO: pass `requests_kerberos.HTTPKerberosAuth()` after installing + `requests-kerberos`. +- KMS Hadoop simple auth: use `HadoopSimpleAuth("user")` when simple auth is + enabled for Ranger KMS. +- Custom headers or query parameters: pass `headers` or `query_params` to the + client constructor when the deployment requires them. +- PDP trusted-header or JWT auth: pass the configured trusted caller header or + `Authorization: Bearer ` header to `RangerPDPClient`. -created_service = ranger.create_service(service) +Example Kerberos setup: -print(' created service: name=' + created_service.name + ', id=' + str(created_service.id)) - - -## Step 3: Let's create a policy -policy = RangerPolicy() -policy.service = service.name -policy.name = 'test policy' -policy.resources = { 'database': RangerPolicyResource({ 'values': ['test_db'] }), - 'table': RangerPolicyResource({ 'values': ['test_tbl'] }), - 'column': RangerPolicyResource({ 'values': ['*'] }) } - -allowItem1 = RangerPolicyItem() -allowItem1.users = [ 'admin' ] -allowItem1.accesses = [ RangerPolicyItemAccess({ 'type': 'create' }), - RangerPolicyItemAccess({ 'type': 'alter' }) ] - -denyItem1 = RangerPolicyItem() -denyItem1.users = [ 'admin' ] -denyItem1.accesses = [ RangerPolicyItemAccess({ 'type': 'drop' }) ] - -policy.policyItems = [ allowItem1 ] -policy.denyPolicyItems = [ denyItem1 ] - -print('Creating policy: name=' + policy.name) - -created_policy = ranger.create_policy(policy) - -print(' created policy: name=' + created_policy.name + ', id=' + str(created_policy.id)) - - -## Step 4: Delete policy and service created above -print('Deleting policy: id=' + str(created_policy.id)) - -ranger.delete_policy_by_id(created_policy.id) - -print(' deleted policy: id=' + str(created_policy.id)) - -print('Deleting service: id=' + str(created_service.id)) - -ranger.delete_service_by_id(created_service.id) - -print(' deleted service: id=' + str(created_service.id)) - -``` - -```python test_ranger_user_mgmt.py``` ```python -# test_ranger_user_mgmt.py -from apache_ranger.client.ranger_client import * -from apache_ranger.utils import * -from apache_ranger.model.ranger_user_mgmt import * -from apache_ranger.client.ranger_user_mgmt_client import * -from datetime import datetime - -## -## Step 1: create a client to connect to Apache Ranger -## -ranger_url = 'http://localhost:6080' -ranger_auth = ('admin', 'rangerR0cks!') - -# For Kerberos authentication -# -# from requests_kerberos import HTTPKerberosAuth -# -# ranger_auth = HTTPKerberosAuth() -# -# For HTTP Basic authentication -# -# ranger_auth = ('admin', 'rangerR0cks!') - -ranger = RangerClient(ranger_url, ranger_auth) -user_mgmt = RangerUserMgmtClient(ranger) - - - -## -## Step 2: Let's call User Management APIs -## - -print('\nListing users') - -users = user_mgmt.find_users() - -print(f' {len(users.list)} users found') - -for user in users.list: - print(f' id: {user.id}, name: {user.name}') - - -print('\nListing groups') - -groups = user_mgmt.find_groups() - -print(f' {len(groups.list)} groups found') - -for group in groups.list: - print(f' id: {group.id}, name: {group.name}') - -print('\nListing group-users') - -group_users = user_mgmt.find_group_users() - -print(f' {len(group_users.list)} group-users found') - -for group_user in group_users.list: - print(f' id: {group_user.id}, groupId: {group_user.parentGroupId}, userId: {group_user.userId}') - - -now = datetime.now() - -name_suffix = '-' + now.strftime('%Y%m%d-%H%M%S-%f') -user_name = 'test-user' + name_suffix -group_name = 'test-group' + name_suffix - - -user = RangerUser({ 'name': user_name, 'firstName': user_name, 'lastName': 'user', 'emailAddress': user_name + '@test.org', 'password': 'Welcome1', 'userRoleList': [ 'ROLE_USER' ], 'otherAttributes': '{ "dept": "test" }' }) - -print(f'\nCreating user: name={user.name}') - -created_user = user_mgmt.create_user(user) - -print(f' created user: {created_user}') - - -group = RangerGroup({ 'name': group_name, 'otherAttributes': '{ "dept": "test" }' }) - -print(f'\nCreating group: name={group.name}') - -created_group = user_mgmt.create_group(group) - -print(f' created group: {created_group}') - - -group_user = RangerGroupUser({ 'name': created_group.name, 'parentGroupId': created_group.id, 'userId': created_user.id }) - -print(f'\nAdding user {created_user.name} to group {created_group.name}') - -created_group_user = user_mgmt.create_group_user(group_user) - -print(f' created group-user: {created_group_user}') - - -print('\nListing group-users') - -group_users = user_mgmt.find_group_users() - -print(f' {len(group_users.list)} group-users found') - -for group_user in group_users.list: - print(f' id: {group_user.id}, groupId: {group_user.parentGroupId}, userId: {group_user.userId}') - - -print(f'\nListing users for group {group.name}') - -users = user_mgmt.get_users_in_group(group.name) - -print(f' users: {users}') - - -print(f'\nListing groups for user {user.name}') +from requests_kerberos import HTTPKerberosAuth +from apache_ranger.client.ranger_client import RangerClient -groups = user_mgmt.get_groups_for_user(user.name) +ranger = RangerClient("https://ranger.example.com:6182", HTTPKerberosAuth()) +``` -print(f' groups: {groups}') +## PDP Request Notes +`RangerPDPClient` sends requests to the PDP REST APIs exposed under +`/authz/v1`. -print(f'\nDeleting group-user {created_group_user.id}') +- `authorize(request)` calls `POST /authz/v1/authorize`. +- `authorize_multi(request)` calls `POST /authz/v1/authorizeMulti`. +- `get_resource_permissions(request)` calls `POST /authz/v1/permissions`. -user_mgmt.delete_group_user_by_id(created_group_user.id) +PDP authorization requests should include: +- `context.serviceType`, for example `hive`, `hdfs`, or `kafka`. +- `context.serviceName`, the Ranger service name. +- `user.name` for `authorize` and `authorize_multi` calls. -print(f'\nDeleting group {group.name}') +If the authenticated caller is different from `request.user.name`, the caller +must be allowed to delegate for that service. Without delegation permission, PDP +returns `403 FORBIDDEN`. -user_mgmt.delete_group_by_id(created_group.id, True) +## Examples and Code References +The quick starts above cover the basics. For day-to-day API usage — creating +services and policies, managing users, or calling KMS or PDP — use the runnable +samples and client sources below. -print(f'\nDeleting user {user.name}') +### Running sample scripts -user_mgmt.delete_user_by_id(created_user.id, True) -``` +After `pip install apache-ranger`, run a sample directly: -```python test_ranger_kms.py``` -```python -# test_ranger_kms.py -from apache_ranger.client.ranger_kms_client import RangerKMSClient -from apache_ranger.client.ranger_client import HadoopSimpleAuth -from apache_ranger.model.ranger_kms import RangerKey -import time - - -## -## Step 1: create a client to connect to Apache Ranger KMS -## -kms_url = 'http://localhost:9292' -kms_auth = HadoopSimpleAuth('keyadmin') - -# For Kerberos authentication -# -# from requests_kerberos import HTTPKerberosAuth -# -# kms_auth = HTTPKerberosAuth() -# -# For HTTP Basic authentication -# -# kms_auth = ('keyadmin', 'rangerR0cks!') - -kms_client = RangerKMSClient(kms_url, kms_auth) - - - -## -## Step 2: Let's call KMS APIs -## - -kms_status = kms_client.kms_status() -print('kms_status():', kms_status) -print() - -key_name = 'test_' + str(int(time.time() * 1000)) - -key = kms_client.create_key(RangerKey({'name':key_name})) -print('create_key(' + key_name + '):', key) -print() - -rollover_key = kms_client.rollover_key(key_name, key.material) -print('rollover_key(' + key_name + '):', rollover_key) -print() - -kms_client.invalidate_cache_for_key(key_name) -print('invalidate_cache_for_key(' + key_name + ')') -print() - -key_metadata = kms_client.get_key_metadata(key_name) -print('get_key_metadata(' + key_name + '):', key_metadata) -print() - -current_key = kms_client.get_current_key(key_name) -print('get_current_key(' + key_name + '):', current_key) -print() - -encrypted_keys = kms_client.generate_encrypted_key(key_name, 6) -print('generate_encrypted_key(' + key_name + ', ' + str(6) + '):') -for i in range(len(encrypted_keys)): - encrypted_key = encrypted_keys[i] - decrypted_key = kms_client.decrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material) - reencrypted_key = kms_client.reencrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material) - print(' encrypted_keys[' + str(i) + ']: ', encrypted_key) - print(' decrypted_key[' + str(i) + ']: ', decrypted_key) - print(' reencrypted_key[' + str(i) + ']:', reencrypted_key) -print() - -reencrypted_keys = kms_client.batch_reencrypt_encrypted_keys(key_name, encrypted_keys) -print('batch_reencrypt_encrypted_keys(' + key_name + ', ' + str(len(encrypted_keys)) + '):') -for i in range(len(reencrypted_keys)): - print(' batch_reencrypt_encrypted_key[' + str(i) + ']:', reencrypted_keys[i]) -print() - -key_versions = kms_client.get_key_versions(key_name) -print('get_key_versions(' + key_name + '):', len(key_versions)) -for i in range(len(key_versions)): - print(' key_versions[' + str(i) + ']:', key_versions[i]) -print() - -for i in range(len(key_versions)): - key_version = kms_client.get_key_version(key_versions[i].versionName) - print('get_key_version(' + str(i) + '):', key_version) -print() - -key_names = kms_client.get_key_names() -print('get_key_names():', len(key_names)) -for i in range(len(key_names)): - print(' key_name[' + str(i) + ']:', key_names[i]) -print() - -keys_metadata = kms_client.get_keys_metadata(key_names) -print('get_keys_metadata(' + str(key_names) + '):', len(keys_metadata)) -for i in range(len(keys_metadata)): - print(' key_metadata[' + str(i) + ']:', keys_metadata[i]) -print() - -key = kms_client.get_key(key_name) -print('get_key(' + key_name + '):', key) -print() - -kms_client.delete_key(key_name) -print('delete_key(' + key_name + ')') +```bash +python ranger-examples/sample-client/src/main/python/sample_client.py ``` -```python test_ranger_pdp.py``` -```python -from apache_ranger.client.ranger_pdp_client import RangerPDPClient -from apache_ranger.model.ranger_authz import RangerAccessContext, RangerAccessInfo -from apache_ranger.model.ranger_authz import RangerAuthzRequest, RangerMultiAuthzRequest -from apache_ranger.model.ranger_authz import RangerResourceInfo, RangerResourcePermissionsRequest, RangerUserInfo - -## -## Step 1: create a client to connect to Ranger PDP -## -pdp_url = 'http://localhost:6500' - -# For Kerberos authentication -# -# from requests_kerberos import HTTPKerberosAuth -# -# pdp = RangerPDPClient(pdp_url, HTTPKerberosAuth()) - -# For trusted-header authN with PDP (example only): -# -pdp = RangerPDPClient(pdp_url, auth=None, headers={ 'X-Forwarded-User': 'hive' }) - -## -## Step 2: call PDP authorization APIs -## -req = RangerAuthzRequest({ - 'requestId': 'req-1', - 'user': RangerUserInfo({'name': 'alice'}), - 'access': RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1'}), 'permissions': ['create']}), - 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) -}) - -res = pdp.authorize(req) - -print('authorize():') -print(f' {req}') -print(f' {res}') -print('\n') +When working from a Ranger source checkout without installing the package, set +`PYTHONPATH` to the client sources first: +```bash +# from the repository root +PYTHONPATH=intg/src/main/python \ + python ranger-examples/sample-client/src/main/python/sample_client.py +``` -req = RangerAuthzRequest({ - 'requestId': 'req-2', - 'user': RangerUserInfo({'name': 'alice'}), - 'access': RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email']}), 'permissions': ['select']}), - 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) -}) +Edit the `ranger_url`, `ranger_auth`, and other connection settings at the top +of each sample before running it against your environment. -res = pdp.authorize(req) +### Runnable examples by task -print('authorize():') -print(f' {req}') -print(f' {res}') -print('\n') +| Task | Sample script | What it demonstrates | +|-----------------------------------------------------| --- |---------------------------------------------------------------------------------------| +| Ranger Admin — services, policies, roles, tags CRUD | [`sample_client.py`](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python/sample_client.py) | service-def and service, access/data-masking/row-filter policies, roles, service tags | +| User and group management | [`user_mgmt.py`](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python/user_mgmt.py) | List/create/delete users and groups, group-user mappings | +| Ranger KMS | [`sample_kms_client.py`](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python/sample_kms_client.py) | Key lifecycle, encrypt/decrypt/reencrypt, metadata and status | +| PDP AuthZ | [`sample_pdp_client.py`](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python/sample_pdp_client.py) | Authz/MultiAuthz calls to PDP Server | +| Security zones (v2) | [`security_zone_v2.py`](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python/security_zone_v2.py) | Create/update security zones and zone resources | +All samples live under +[`ranger-examples/sample-client/src/main/python`](https://github.com/apache/ranger/tree/master/ranger-examples/sample-client/src/main/python). -req = RangerMultiAuthzRequest({ - 'requestId': 'req-3', - 'user': RangerUserInfo({'name': 'alice'}), - 'accesses': [ - RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email'], 'attributes': {'OWNER': 'alice'}}), 'permissions': ['select']}), - RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_vw1'}), 'permissions': ['create']}) - ], - 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) -}) +### Client API source code -res = pdp.authorize_multi(req) +Each public client is implemented under +[`intg/src/main/python/apache_ranger/client/`](https://github.com/apache/ranger/tree/master/intg/src/main/python/apache_ranger/client): -print('authorize_multi():') -print(f' {req}') -print(f' {res}') -print('\n') +| Client | Source file | REST base path | +| --- | --- | --- | +| `RangerClient` | [`ranger_client.py`](https://github.com/apache/ranger/blob/master/intg/src/main/python/apache_ranger/client/ranger_client.py) | `service/public/v2/api` | +| `RangerUserMgmtClient` | [`ranger_user_mgmt_client.py`](https://github.com/apache/ranger/blob/master/intg/src/main/python/apache_ranger/client/ranger_user_mgmt_client.py) | user/group APIs on Ranger Admin | +| `RangerKMSClient` | [`ranger_kms_client.py`](https://github.com/apache/ranger/blob/master/intg/src/main/python/apache_ranger/client/ranger_kms_client.py) | KMS REST APIs | +| `RangerPDPClient` | [`ranger_pdp_client.py`](https://github.com/apache/ranger/blob/master/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py) | `/authz/v1` | +Request and response models are under +[`intg/src/main/python/apache_ranger/model/`](https://github.com/apache/ranger/tree/master/intg/src/main/python/apache_ranger/model). -req = RangerResourcePermissionsRequest({ - 'requestId': 'req-4', - 'resource': RangerResourceInfo({'name': 'table:default/test_tbl1'}), - 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) -}) +### Unit tests -res = pdp.get_resource_permissions(req) +[`intg/src/test/python/test_ranger_client.py`](https://github.com/apache/ranger/blob/master/intg/src/test/python/test_ranger_client.py) +contains mocked examples for Admin and PDP client wiring. Run from +`intg/`: -print('get_resource_permissions():') -print(f' {req}') -print(f' {res}') -print('\n') +```bash +PYTHONPATH=src/main/python python -B src/test/python/test_ranger_client.py ``` -For more examples, checkout `sample-client` python project in [ranger-examples](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python) module (including `sample_client.py`, `user_mgmt.py`, `sample_kms_client.py`, and `sample_pdp_client.py`). +## Troubleshooting + +- `ModuleNotFoundError: requests_kerberos`: install `requests-kerberos`. +- `401 Unauthorized`: verify the credentials, Kerberos ticket, or auth headers + used by the target Ranger service. +- `403 Forbidden`: verify Ranger permissions and PDP delegation configuration + when authorizing on behalf of another user. +- SSL certificate errors: configure a valid trust store for production. For + local testing only, `ranger.session.verify = False` can disable certificate + verification. +- Connection timeouts: verify the Ranger Admin, KMS, or PDP URL and confirm the + service is reachable from the client host. + +## Release 0.0.13 Highlights + +- Python 3.13+ support. +- New PDP client coverage for Ranger authorization APIs. +- New authorization request and response models. +- User/group management, KMS, Ranger Admin, and model updates. diff --git a/intg/src/main/python/setup.py b/intg/src/main/python/setup.py index 89db40e6431..f57f08536fc 100644 --- a/intg/src/main/python/setup.py +++ b/intg/src/main/python/setup.py @@ -19,25 +19,27 @@ from setuptools import setup, find_packages # External dependencies -requirements = ['requests>=2.24','strenum>=0.4.15'] +requirements = ['requests>=2.34.2', 'strenum>=0.4.15'] long_description = '' -with open("README.md", "r") as fh: +with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() setup( name="apache-ranger", - version="0.0.12", + version="0.0.13", author="Apache Ranger", author_email="dev@ranger.apache.org", description="Apache Ranger Python client", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/apache/ranger/tree/master/intg/src/main/python", - license='Apache LICENSE 2.0', + license="Apache-2.0", + license_files=("LICENSE",), classifiers=[ - "Programming Language :: Python :: 2.7", - "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", ], packages=find_packages(), @@ -45,5 +47,5 @@ include_package_data=True, zip_safe=False, keywords='ranger client, apache ranger', - python_requires='>=2.7', + python_requires='>=3.13', ) diff --git a/intg/src/test/python/test_ranger_client.py b/intg/src/test/python/test_ranger_client.py index cbedcbf7631..03961630429 100644 --- a/intg/src/test/python/test_ranger_client.py +++ b/intg/src/test/python/test_ranger_client.py @@ -18,14 +18,23 @@ import unittest -from unittest.mock import patch +from unittest.mock import Mock, patch + from apache_ranger.exceptions import RangerServiceException from apache_ranger.model.ranger_service import RangerService try: - from apache_ranger.client.ranger_client import API, HttpMethod, HTTPStatus, RangerClient -except ModuleNotFoundError: # requests not installed - exit() # skipping unit tests + import apache_ranger.client as ranger_client_package + import apache_ranger.model as ranger_model_package + + from apache_ranger.client.ranger_client import API, HttpMethod, HTTPStatus, RangerClient + from apache_ranger.client.ranger_pdp_client import RangerPDPClient + from apache_ranger.model.ranger_authz import ( + RangerAccessContext, RangerAccessInfo, RangerAuthzRequest, RangerAuthzResult, + RangerPermissionResult, RangerResourceInfo, RangerResultInfo, RangerUserInfo, + ) +except ModuleNotFoundError: # requests not installed + exit() # skipping unit tests class MockResponse: @@ -33,7 +42,6 @@ def __init__(self, status_code, response=None, content=None): self.status_code = status_code self.response = response self.content = content - return def json(self): return self.response @@ -49,67 +57,118 @@ class TestRangerClient(unittest.TestCase): @patch('apache_ranger.client.ranger_client.Session') def test_get_service_unavailable(self, mock_session): mock_session.return_value.get.return_value = MockResponse(HTTPStatus.SERVICE_UNAVAILABLE) - result = RangerClient(TestRangerClient.URL, TestRangerClient.AUTH).find_services() - - self.assertTrue(result is None) + result = RangerClient(self.URL, self.AUTH).find_services() + self.assertIsNone(result) @patch('apache_ranger.client.ranger_client.Session') def test_get_success(self, mock_session): - response = [ RangerService() ] - mock_session.return_value.get.return_value = MockResponse(HTTPStatus.OK, response=response, content='Success') - result = RangerClient(TestRangerClient.URL, TestRangerClient.AUTH).find_services() + response = [RangerService()] + mock_session.return_value.get.return_value = MockResponse( + HTTPStatus.OK, response=response, content='Success') + result = RangerClient(self.URL, self.AUTH).find_services() self.assertEqual(response, result) - @patch('apache_ranger.client.ranger_client.Session') @patch('apache_ranger.client.ranger_client.Response') def test_get_unexpected_status_code(self, mock_response, mock_session): - content = 'Internal Server Error' - mock_response.text = content - mock_response.content = content - mock_response.status_code = HTTPStatus.INTERNAL_SERVER_ERROR + content = 'Internal Server Error' + mock_response.text = content + mock_response.content = content + mock_response.status_code = HTTPStatus.INTERNAL_SERVER_ERROR mock_session.return_value.get.return_value = mock_response - try: - RangerClient(TestRangerClient.URL, TestRangerClient.AUTH).find_services() - except RangerServiceException as e: - self.assertTrue(HTTPStatus.INTERNAL_SERVER_ERROR, e.statusCode) + with self.assertRaises(RangerServiceException) as context: + RangerClient(self.URL, self.AUTH).find_services() + self.assertEqual(HTTPStatus.INTERNAL_SERVER_ERROR, context.exception.statusCode) @patch('apache_ranger.client.ranger_client.RangerClient.FIND_SERVICES') def test_unexpected_http_method(self, mock_api): - mock_api.method.return_value = "PATCH" - mock_api.url = TestRangerClient.URL - mock_api.path = RangerClient.URI_SERVICE + mock_api.method = "PATCH" + mock_api.path = RangerClient.URI_SERVICE - try: - RangerClient(TestRangerClient.URL, TestRangerClient.AUTH).find_services() - except RangerServiceException as e: - self.assertTrue('Unsupported HTTP Method' in repr(e)) + result = RangerClient(self.URL, self.AUTH).find_services() + self.assertIsNone(result) def test_url_missing_format(self): - params = {'arg1': 1, 'arg2': 2} - - try: - API("{arg1}test{arg2}path{arg3}", HttpMethod.GET, HTTPStatus.OK).format_path(params) - - self.fail("Supposed to fail") - except KeyError as e: - self.assertTrue('KeyError' in repr(e)) - + with self.assertRaises(KeyError): + API("{arg1}test{arg2}path{arg3}", HttpMethod.GET, HTTPStatus.OK).format_path( + {'arg1': 1, 'arg2': 2}) def test_url_invalid_format(self): - params = {'1', '2'} - - try: - API("{}test{}path{}", HttpMethod.GET, HTTPStatus.OK).format_path(params) - - self.fail("Supposed to fail") - except TypeError as e: - self.assertTrue('TypeError' in repr(e)) + with self.assertRaises(TypeError): + API("{}test{}path{}", HttpMethod.GET, HTTPStatus.OK).format_path({'1', '2'}) + + +class TestPDPClient(unittest.TestCase): + PDP_URL = "http://localhost:6500" + AUTHZ_REQUEST = { + "requestId": "req-1", + "user": {"name": "alice", "groups": ["analytics"]}, + "access": {"resource": {"name": "table:default/sales", "subResources": ["column:id"]}, "permissions": ["select"]}, + "context": {"serviceType": "hive", "serviceName": "dev_hive"}, + } + AUTHZ_RESULT = { + "requestId": "req-1", + "decision": RangerAuthzResult.DECISION_ALLOW, + "permissions": {"select": { + "access": {"decision": RangerAuthzResult.DECISION_ALLOW, "policy": {"id": 7, "version": 3}}, + "subResources": {"column:id": {"access": {"decision": RangerAuthzResult.DECISION_DENY}}}, + }}, + } + + def test_package_export(self): + self.assertIs(ranger_client_package.RangerPDPClient, RangerPDPClient) + self.assertIn("RangerPDPClient", ranger_client_package.__all__) + + def test_authz_model_exports(self): + self.assertIs(ranger_model_package.RangerAuthzRequest, RangerAuthzRequest) + self.assertIs(ranger_model_package.RangerAuthzResult, RangerAuthzResult) + self.assertIn("RangerAuthzRequest", ranger_model_package.__all__) + self.assertIn("RangerResourcePermissions", ranger_model_package.__all__) + + def test_authz_request_type_coercion(self): + request = RangerAuthzRequest(self.AUTHZ_REQUEST) + request.type_coerce_attrs() + + self.assertIsInstance(request.user, RangerUserInfo) + self.assertIsInstance(request.access, RangerAccessInfo) + self.assertIsInstance(request.access.resource, RangerResourceInfo) + self.assertIsInstance(request.context, RangerAccessContext) + self.assertEqual("alice", request.user.name) + self.assertEqual(["select"], request.access.permissions) + + def test_authz_result_type_coercion(self): + result = RangerAuthzResult(self.AUTHZ_RESULT) + result.type_coerce_attrs() + + permission = result.permissions["select"] + self.assertIsInstance(permission, RangerPermissionResult) + self.assertEqual(7, permission.access.policy.id) + self.assertIsInstance(permission.subResources["column:id"], RangerResultInfo) + + def test_authorize_calls_expected_api(self): + client = RangerPDPClient(self.PDP_URL, auth=None) + client.client_http.call_api = Mock(return_value={ + "requestId": "req-1", + "decision": RangerAuthzResult.DECISION_ALLOW, + }) + + result = client.authorize(self.AUTHZ_REQUEST) + + client.client_http.call_api.assert_called_once() + api = client.client_http.call_api.call_args.args[0] + + self.assertEqual(RangerPDPClient.URI_AUTHORIZE, api.path) + self.assertEqual(HttpMethod.POST, api.method) + self.assertIsInstance( + client.client_http.call_api.call_args.kwargs["request_data"], + RangerAuthzRequest, + ) + self.assertIsInstance(result, RangerAuthzResult) if __name__ == '__main__': diff --git a/ranger-examples/sample-client/scripts/run-pyclient.sh b/ranger-examples/sample-client/scripts/run-pyclient.sh deleted file mode 100755 index 8649f0bfb63..00000000000 --- a/ranger-examples/sample-client/scripts/run-pyclient.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# 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. - -usage() { - echo "usage: run-pyclient.sh - -n url to connect to - -h show help." - exit 1 -} - -cd venv || exit -source bin/activate -INSTALL="python3 ./../setup.py install" -$INSTALL >> build.log -PYTHON_CMD="python3 build/lib/pylib/sample_client.py" -while getopts "n:h" opt; do - case $opt in - n) URL=$OPTARG - PYTHON_CMD="$PYTHON_CMD --url $URL" - ;; - h) usage - ;; - \?) echo -e \\n"Option -$OPTARG not allowed." - usage - ;; - esac -done - -prompt="Sample Authentication User Name:" -read -p "$prompt" userName -prompt="Sample Authentication User Password:" -read -p "$prompt" -s password -printf "\n" -PYTHON_CMD="$PYTHON_CMD --username $userName --password $password" -printf "Python command : %s\n" "$PYTHON_CMD" -$PYTHON_CMD \ No newline at end of file diff --git a/ranger-examples/sample-client/scripts/setup.py b/ranger-examples/sample-client/scripts/setup.py deleted file mode 100644 index c4e740769d7..00000000000 --- a/ranger-examples/sample-client/scripts/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python - -# -# 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 setuptools import setup, find_packages - -# External dependencies -requirements = ['requests>=2.24'] - -setup( - name='sample_pyclient', - packages=['pylib', 'pylib/ranger/client', 'pylib/ranger/model'], - data_files=[('../../lib/resources', ['resources/logging.conf'])], - install_requires=requirements, - zip_safe=False -)