Skip to content

Commit 28f3c09

Browse files
committed
RANGER-3982: updated Python client to support Ranger KMS REST APIs
(cherry picked from commit d0c6bdb)
1 parent 1f7e5a0 commit 28f3c09

File tree

8 files changed

+411
-18
lines changed

8 files changed

+411
-18
lines changed

intg/src/main/python/README.md

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Verify if apache-ranger client is installed:
3535

3636
Package Version
3737
------------ ---------
38-
apache-ranger 0.0.7
38+
apache-ranger 0.0.8
3939
```
4040

4141
## Usage
@@ -120,4 +120,112 @@ ranger.delete_service_by_id(created_service.id)
120120
print(' deleted service: id=' + str(created_service.id))
121121

122122
```
123+
124+
```python test_ranger_kms.py```
125+
```python
126+
# test_ranger_kms.py
127+
from apache_ranger.client.ranger_kms_client import RangerKMSClient
128+
from apache_ranger.client.ranger_client import HadoopSimpleAuth
129+
from apache_ranger.model.ranger_kms import RangerKey
130+
import time
131+
132+
133+
##
134+
## Step 1: create a client to connect to Apache Ranger KMS
135+
##
136+
kms_url = 'http://localhost:9292'
137+
kms_auth = HadoopSimpleAuth('keyadmin')
138+
139+
# For Kerberos authentication
140+
#
141+
# from requests_kerberos import HTTPKerberosAuth
142+
#
143+
# kms_auth = HTTPKerberosAuth()
144+
#
145+
# For HTTP Basic authentication
146+
#
147+
# kms_auth = ('keyadmin', 'rangerR0cks!')
148+
149+
kms_client = RangerKMSClient(kms_url, kms_auth)
150+
151+
152+
153+
##
154+
## Step 2: Let's call KMS APIs
155+
##
156+
157+
kms_status = kms_client.kms_status()
158+
print('kms_status():', kms_status)
159+
print()
160+
161+
key_name = 'test_' + str(int(time.time() * 1000))
162+
163+
key = kms_client.create_key(RangerKey({'name':key_name}))
164+
print('create_key(' + key_name + '):', key)
165+
print()
166+
167+
rollover_key = kms_client.rollover_key(key_name, key.material)
168+
print('rollover_key(' + key_name + '):', rollover_key)
169+
print()
170+
171+
kms_client.invalidate_cache_for_key(key_name)
172+
print('invalidate_cache_for_key(' + key_name + ')')
173+
print()
174+
175+
key_metadata = kms_client.get_key_metadata(key_name)
176+
print('get_key_metadata(' + key_name + '):', key_metadata)
177+
print()
178+
179+
current_key = kms_client.get_current_key(key_name)
180+
print('get_current_key(' + key_name + '):', current_key)
181+
print()
182+
183+
encrypted_keys = kms_client.generate_encrypted_key(key_name, 6)
184+
print('generate_encrypted_key(' + key_name + ', ' + str(6) + '):')
185+
for i in range(len(encrypted_keys)):
186+
encrypted_key = encrypted_keys[i]
187+
decrypted_key = kms_client.decrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
188+
reencrypted_key = kms_client.reencrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
189+
print(' encrypted_keys[' + str(i) + ']: ', encrypted_key)
190+
print(' decrypted_key[' + str(i) + ']: ', decrypted_key)
191+
print(' reencrypted_key[' + str(i) + ']:', reencrypted_key)
192+
print()
193+
194+
reencrypted_keys = kms_client.batch_reencrypt_encrypted_keys(key_name, encrypted_keys)
195+
print('batch_reencrypt_encrypted_keys(' + key_name + ', ' + str(len(encrypted_keys)) + '):')
196+
for i in range(len(reencrypted_keys)):
197+
print(' batch_reencrypt_encrypted_key[' + str(i) + ']:', reencrypted_keys[i])
198+
print()
199+
200+
key_versions = kms_client.get_key_versions(key_name)
201+
print('get_key_versions(' + key_name + '):', len(key_versions))
202+
for i in range(len(key_versions)):
203+
print(' key_versions[' + str(i) + ']:', key_versions[i])
204+
print()
205+
206+
for i in range(len(key_versions)):
207+
key_version = kms_client.get_key_version(key_versions[i].versionName)
208+
print('get_key_version(' + str(i) + '):', key_version)
209+
print()
210+
211+
key_names = kms_client.get_key_names()
212+
print('get_key_names():', len(key_names))
213+
for i in range(len(key_names)):
214+
print(' key_name[' + str(i) + ']:', key_names[i])
215+
print()
216+
217+
keys_metadata = kms_client.get_keys_metadata(key_names)
218+
print('get_keys_metadata(' + str(key_names) + '):', len(keys_metadata))
219+
for i in range(len(keys_metadata)):
220+
print(' key_metadata[' + str(i) + ']:', keys_metadata[i])
221+
print()
222+
223+
key = kms_client.get_key(key_name)
224+
print('get_key(' + key_name + '):', key)
225+
print()
226+
227+
kms_client.delete_key(key_name)
228+
print('delete_key(' + key_name + ')')
229+
```
230+
123231
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/sample_client.py) module.

intg/src/main/python/apache_ranger/client/ranger_client.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@
3030
from apache_ranger.utils import *
3131
from requests import Session
3232
from requests import Response
33+
from requests.auth import AuthBase
34+
from urllib.parse import urlencode
3335
from urllib.parse import urljoin
3436

3537
LOG = logging.getLogger(__name__)
3638

39+
QUERY_PARAM_USER_DOT_NAME = 'user.name'.encode("utf-8")
40+
3741

3842
class RangerClient:
3943
def __init__(self, url, auth):
@@ -368,6 +372,21 @@ def delete_policy_deltas(self, days, reloadServicePoliciesCache):
368372
DELETE_POLICY_DELTAS = API(URI_POLICY_DELTAS, HttpMethod.DELETE, HTTPStatus.NO_CONTENT)
369373

370374

375+
376+
class HadoopSimpleAuth(AuthBase):
377+
def __init__(self, user_name):
378+
self.user_name = user_name.encode("utf-8")
379+
380+
def __call__(self, req):
381+
sep_char = '?'
382+
383+
if req.url.find('?') != -1:
384+
sep_char = '&'
385+
386+
req.url = req.url + sep_char + urlencode({ QUERY_PARAM_USER_DOT_NAME: self.user_name })
387+
388+
return req
389+
371390
class Message(RangerBase):
372391
def __init__(self, attrs=None):
373392
if attrs is None:
@@ -449,17 +468,21 @@ def call_api(self, api, query_params=None, request_data=None):
449468
if LOG.isEnabledFor(logging.DEBUG):
450469
LOG.debug("<== __call_api(%s, %s, %s), result=%s", vars(api), params, request_data, response)
451470

452-
LOG.debug(response.json())
471+
LOG.debug(response.content)
453472

454-
ret = response.json()
473+
if response.content:
474+
try:
475+
ret = response.json()
476+
except Exception:
477+
ret = response.content
455478
except Exception as e:
456479
print(e)
457480

458481
LOG.exception("Exception occurred while parsing response with msg: %s", e)
459482

460483
raise RangerServiceException(api, response)
461484
elif response.status_code == HTTPStatus.SERVICE_UNAVAILABLE:
462-
LOG.error("Ranger admin unavailable. HTTP Status: %s", HTTPStatus.SERVICE_UNAVAILABLE)
485+
LOG.error("Ranger server at %s unavailable. HTTP Status: %s", self.url, HTTPStatus.SERVICE_UNAVAILABLE)
463486

464487
ret = None
465488
elif response.status_code == HTTPStatus.NOT_FOUND:
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env python
2+
3+
#
4+
# Licensed to the Apache Software Foundation (ASF) under one or more
5+
# contributor license agreements. See the NOTICE file distributed with
6+
# this work for additional information regarding copyright ownership.
7+
# The ASF licenses this file to You under the Apache License, Version 2.0
8+
# (the "License"); you may not use this file except in compliance with
9+
# the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
19+
20+
import json
21+
import logging
22+
from apache_ranger.exceptions import RangerServiceException
23+
from apache_ranger.client.ranger_client import RangerClientHttp
24+
from apache_ranger.model.ranger_kms import RangerKey
25+
from apache_ranger.model.ranger_kms import RangerKeyVersion
26+
from apache_ranger.model.ranger_kms import RangerKeyMetadata
27+
from apache_ranger.model.ranger_kms import RangerEncryptedKeyVersion
28+
from apache_ranger.utils import *
29+
30+
LOG = logging.getLogger(__name__)
31+
32+
#
33+
# Python client for KMS REST APIs
34+
# More details in https://hadoop.apache.org/docs/current/hadoop-kms/index.html#KMS_HTTP_REST_API
35+
#
36+
class RangerKMSClient:
37+
def __init__(self, url, auth):
38+
self.client_http = RangerClientHttp(url, auth)
39+
40+
logging.getLogger("requests").setLevel(logging.WARNING)
41+
42+
43+
def create_key(self, key):
44+
resp = self.client_http.call_api(RangerKMSClient.CREATE_KEY, request_data=key)
45+
46+
return type_coerce(resp, RangerKeyVersion)
47+
48+
def rollover_key(self, key_name, material=None):
49+
resp = self.client_http.call_api(RangerKMSClient.ROLLOVER_KEY.format_path({ 'name': key_name }), request_data={ 'material': material})
50+
51+
return type_coerce(resp, RangerKeyVersion)
52+
53+
def invalidate_cache_for_key(self, key_name):
54+
self.client_http.call_api(RangerKMSClient.INVALIDATE_CACHE_FOR_KEY.format_path({ 'name': key_name }))
55+
56+
def delete_key(self, key_name):
57+
self.client_http.call_api(RangerKMSClient.DELETE_KEY.format_path({ 'name': key_name }))
58+
59+
def get_key_metadata(self, key_name):
60+
resp = self.client_http.call_api(RangerKMSClient.GET_KEY_METADATA.format_path({ 'name': key_name }))
61+
62+
return type_coerce(resp, RangerKeyMetadata)
63+
64+
def get_current_key(self, key_name):
65+
resp = self.client_http.call_api(RangerKMSClient.GET_CURRENT_KEY.format_path({ 'name': key_name }))
66+
67+
return type_coerce(resp, RangerKeyVersion)
68+
69+
def generate_encrypted_key(self, key_name, num_keys):
70+
resp = self.client_http.call_api(RangerKMSClient.GENERATE_ENCRYPTED_KEY.format_path({'name': key_name}), query_params={'eek_op': 'generate', 'num_keys': num_keys})
71+
72+
return type_coerce_list(resp, RangerEncryptedKeyVersion)
73+
74+
def decrypt_encrypted_key(self, key_name, version_name, iv, material):
75+
resp = self.client_http.call_api(RangerKMSClient.DECRYPT_ENCRYPTED_KEY.format_path({'version_name': version_name}), request_data={'name': key_name, 'iv': iv, 'material': material}, query_params={'eek_op': 'decrypt'})
76+
77+
return type_coerce(resp, RangerKeyVersion)
78+
79+
def reencrypt_encrypted_key(self, key_name, version_name, iv, material):
80+
resp = self.client_http.call_api(RangerKMSClient.REENCRYPT_ENCRYPTED_KEY.format_path({'version_name': version_name}), request_data={'name': key_name, 'iv': iv, 'material': material}, query_params={'eek_op': 'reencrypt'})
81+
82+
return type_coerce(resp, RangerEncryptedKeyVersion)
83+
84+
def batch_reencrypt_encrypted_keys(self, key_name, encrypted_key_versions):
85+
resp = self.client_http.call_api(RangerKMSClient.BATCH_REENCRYPT_ENCRYPTED_KEYS.format_path({'name': key_name}), request_data=encrypted_key_versions)
86+
87+
return type_coerce_list(resp, RangerEncryptedKeyVersion)
88+
89+
def get_key_version(self, version_name):
90+
resp = self.client_http.call_api(RangerKMSClient.GET_KEY_VERSION.format_path({ 'version_name': version_name }))
91+
92+
return type_coerce(resp, RangerKeyVersion)
93+
94+
def get_key_versions(self, key_name):
95+
resp = self.client_http.call_api(RangerKMSClient.GET_KEY_VERSIONS.format_path({ 'name': key_name}))
96+
97+
return type_coerce_list(resp, RangerKeyVersion)
98+
99+
def get_key_names(self):
100+
resp = self.client_http.call_api(RangerKMSClient.GET_KEYS_NAMES)
101+
102+
return resp
103+
104+
def get_keys_metadata(self, key_names):
105+
resp = self.client_http.call_api(RangerKMSClient.GET_KEYS_METADATA, query_params={'key': key_names})
106+
107+
return type_coerce_list(resp, RangerKeyMetadata)
108+
109+
# Ranger KMS
110+
def get_key(self, key_name):
111+
resp = self.client_http.call_api(RangerKMSClient.GET_KEY.format_path({ 'name': key_name }))
112+
113+
return type_coerce(resp, RangerKeyMetadata)
114+
115+
# Ranger KMS
116+
def kms_status(self):
117+
resp = self.client_http.call_api(RangerKMSClient.KMS_STATUS)
118+
119+
return resp
120+
121+
# URIs
122+
URI_KEYS = "kms/v1/keys"
123+
URI_KEY_BY_NAME = "kms/v1/key/{name}"
124+
URI_KEY_INVALIDATE_CACHE = URI_KEY_BY_NAME + "/_invalidatecache"
125+
URI_KEY_METADATA = URI_KEY_BY_NAME + "/_metadata"
126+
URI_CURRENT_KEY = URI_KEY_BY_NAME + "/_currentversion"
127+
URI_KEY_EEK = URI_KEY_BY_NAME + "/_eek"
128+
URI_BATCH_REENCRYPT_KEYS = URI_KEY_BY_NAME + "/_reencryptbatch"
129+
URI_KEY_VERSIONS = URI_KEY_BY_NAME + "/_versions"
130+
URI_KEY_VERSION_BY_NAME = "kms/v1/keyversion/{version_name}"
131+
URI_KEY_VERSION_BY_NAME_EEK = URI_KEY_VERSION_BY_NAME + "/_eek"
132+
URI_KEYS_NAMES = URI_KEYS + "/names"
133+
URI_KEYS_METADATA = URI_KEYS + "/metadata"
134+
135+
# Ranger KMS
136+
URI_KMS_STATUS = "kms/api/status"
137+
138+
139+
# APIs
140+
CREATE_KEY = API(URI_KEYS, HttpMethod.POST, HTTPStatus.CREATED)
141+
ROLLOVER_KEY = API(URI_KEY_BY_NAME, HttpMethod.POST, HTTPStatus.OK)
142+
INVALIDATE_CACHE_FOR_KEY = API(URI_KEY_INVALIDATE_CACHE, HttpMethod.POST, HTTPStatus.OK)
143+
DELETE_KEY = API(URI_KEY_BY_NAME, HttpMethod.DELETE, HTTPStatus.OK)
144+
GET_KEY_METADATA = API(URI_KEY_METADATA, HttpMethod.GET, HTTPStatus.OK)
145+
GET_CURRENT_KEY = API(URI_CURRENT_KEY, HttpMethod.GET, HTTPStatus.OK)
146+
GENERATE_ENCRYPTED_KEY = API(URI_KEY_EEK, HttpMethod.GET, HTTPStatus.OK)
147+
DECRYPT_ENCRYPTED_KEY = API(URI_KEY_VERSION_BY_NAME_EEK, HttpMethod.POST, HTTPStatus.OK)
148+
REENCRYPT_ENCRYPTED_KEY = API(URI_KEY_VERSION_BY_NAME_EEK, HttpMethod.POST, HTTPStatus.OK)
149+
BATCH_REENCRYPT_ENCRYPTED_KEYS = API(URI_BATCH_REENCRYPT_KEYS, HttpMethod.POST, HTTPStatus.OK)
150+
GET_KEY_VERSION = API(URI_KEY_VERSION_BY_NAME, HttpMethod.GET, HTTPStatus.OK)
151+
GET_KEY_VERSIONS = API(URI_KEY_VERSIONS, HttpMethod.GET, HTTPStatus.OK)
152+
GET_KEYS_NAMES = API(URI_KEYS_NAMES, HttpMethod.GET, HTTPStatus.OK)
153+
GET_KEYS_METADATA = API(URI_KEYS_METADATA, HttpMethod.GET, HTTPStatus.OK)
154+
155+
# Ranger KMS
156+
GET_KEY = API(URI_KEY_BY_NAME, HttpMethod.GET, HTTPStatus.OK)
157+
KMS_STATUS = API(URI_KMS_STATUS, HttpMethod.GET, HTTPStatus.OK)

intg/src/main/python/apache_ranger/exceptions.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@ def __init__(self, api, response):
3636
print(response)
3737

3838
if api is not None and response is not None:
39-
respJson = response.json()
39+
if response.content:
40+
try:
41+
respJson = response.json()
42+
self.msgDesc = respJson['msgDesc'] if respJson is not None and 'msgDesc' in respJson else None
43+
self.messageList = respJson['messageList'] if respJson is not None and 'messageList' in respJson else None
44+
except Exception:
45+
self.msgDesc = response.content
46+
self.messageList = [ response.content ]
4047

4148
self.statusCode = response.status_code
42-
self.msgDesc = respJson['msgDesc'] if respJson is not None and 'msgDesc' in respJson else None
43-
self.messageList = respJson['messageList'] if respJson is not None and 'messageList' in respJson else None
4449

4550
Exception.__init__(self, "{} {} failed: expected_status={}, status={}, message={}".format(self.method, self.path, self.expected_status, self.statusCode, self.msgDesc))

intg/src/main/python/apache_ranger/model/ranger_base.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,25 @@ def __getattr__(self, attr):
2929
return self.get(attr)
3030

3131
def __setattr__(self, key, value):
32-
self.__setitem__(key, value)
32+
if value is None:
33+
self.__delitem__(key)
34+
else:
35+
self.__setitem__(key, value)
3336

3437
def __setitem__(self, key, value):
35-
super(RangerBase, self).__setitem__(key, value)
36-
self.__dict__.update({key: value})
38+
if value is None:
39+
self.__delitem__(key)
40+
else:
41+
super(RangerBase, self).__setitem__(key, value)
42+
self.__dict__.update({key: value})
3743

3844
def __delattr__(self, item):
3945
self.__delitem__(item)
4046

4147
def __delitem__(self, key):
42-
super(RangerBase, self).__delitem__(key)
43-
del self.__dict__[key]
48+
if key in self.__dict__:
49+
super(RangerBase, self).__delitem__(key)
50+
del self.__dict__[key]
4451

4552
def __repr__(self):
4653
return json.dumps(self)

0 commit comments

Comments
 (0)