This is an automated email from the ASF dual-hosted git repository.
madhan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new d0c6bdb6e RANGER-3982: updated Python client to support Ranger KMS
REST APIs
d0c6bdb6e is described below
commit d0c6bdb6eb193d849bbf0cea8d7c425c0901981d
Author: Madhan Neethiraj <[email protected]>
AuthorDate: Wed Nov 23 17:17:50 2022 -0800
RANGER-3982: updated Python client to support Ranger KMS REST APIs
---
intg/src/main/python/README.md | 110 ++++++++++++++-
.../python/apache_ranger/client/ranger_client.py | 29 +++-
.../apache_ranger/client/ranger_kms_client.py | 157 +++++++++++++++++++++
intg/src/main/python/apache_ranger/exceptions.py | 11 +-
.../main/python/apache_ranger/model/ranger_base.py | 17 ++-
.../main/python/apache_ranger/model/ranger_kms.py | 77 ++++++++++
intg/src/main/python/apache_ranger/utils.py | 26 +++-
intg/src/main/python/setup.py | 2 +-
8 files changed, 411 insertions(+), 18 deletions(-)
diff --git a/intg/src/main/python/README.md b/intg/src/main/python/README.md
index f21628fb0..46de769fc 100644
--- a/intg/src/main/python/README.md
+++ b/intg/src/main/python/README.md
@@ -35,7 +35,7 @@ Verify if apache-ranger client is installed:
Package Version
------------ ---------
-apache-ranger 0.0.7
+apache-ranger 0.0.8
```
## Usage
@@ -120,4 +120,112 @@ ranger.delete_service_by_id(created_service.id)
print(' deleted service: id=' + str(created_service.id))
```
+
+```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 + ')')
+```
+
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.
diff --git a/intg/src/main/python/apache_ranger/client/ranger_client.py
b/intg/src/main/python/apache_ranger/client/ranger_client.py
index 7bb6493a1..527380de7 100644
--- a/intg/src/main/python/apache_ranger/client/ranger_client.py
+++ b/intg/src/main/python/apache_ranger/client/ranger_client.py
@@ -30,10 +30,14 @@ from apache_ranger.model.ranger_service_tags import
RangerServiceTags
from apache_ranger.utils import *
from requests import Session
from requests import Response
+from requests.auth import AuthBase
+from urllib.parse import urlencode
from urllib.parse import urljoin
LOG = logging.getLogger(__name__)
+QUERY_PARAM_USER_DOT_NAME = 'user.name'.encode("utf-8")
+
class RangerClient:
def __init__(self, url, auth):
@@ -368,6 +372,21 @@ class RangerClient:
DELETE_POLICY_DELTAS = API(URI_POLICY_DELTAS, HttpMethod.DELETE,
HTTPStatus.NO_CONTENT)
+
+class HadoopSimpleAuth(AuthBase):
+ def __init__(self, user_name):
+ self.user_name = user_name.encode("utf-8")
+
+ def __call__(self, req):
+ sep_char = '?'
+
+ if req.url.find('?') != -1:
+ sep_char = '&'
+
+ req.url = req.url + sep_char + urlencode({ QUERY_PARAM_USER_DOT_NAME:
self.user_name })
+
+ return req
+
class Message(RangerBase):
def __init__(self, attrs=None):
if attrs is None:
@@ -449,9 +468,13 @@ class RangerClientHttp:
if LOG.isEnabledFor(logging.DEBUG):
LOG.debug("<== __call_api(%s, %s, %s), result=%s",
vars(api), params, request_data, response)
- LOG.debug(response.json())
+ LOG.debug(response.content)
- ret = response.json()
+ if response.content:
+ try:
+ ret = response.json()
+ except Exception:
+ ret = response.content
except Exception as e:
print(e)
@@ -459,7 +482,7 @@ class RangerClientHttp:
raise RangerServiceException(api, response)
elif response.status_code == HTTPStatus.SERVICE_UNAVAILABLE:
- LOG.error("Ranger admin unavailable. HTTP Status: %s",
HTTPStatus.SERVICE_UNAVAILABLE)
+ LOG.error("Ranger server at %s unavailable. HTTP Status: %s",
self.url, HTTPStatus.SERVICE_UNAVAILABLE)
ret = None
elif response.status_code == HTTPStatus.NOT_FOUND:
diff --git a/intg/src/main/python/apache_ranger/client/ranger_kms_client.py
b/intg/src/main/python/apache_ranger/client/ranger_kms_client.py
new file mode 100644
index 000000000..38b652e6f
--- /dev/null
+++ b/intg/src/main/python/apache_ranger/client/ranger_kms_client.py
@@ -0,0 +1,157 @@
+#!/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.
+
+
+import json
+import logging
+from apache_ranger.exceptions import RangerServiceException
+from apache_ranger.client.ranger_client import RangerClientHttp
+from apache_ranger.model.ranger_kms import RangerKey
+from apache_ranger.model.ranger_kms import RangerKeyVersion
+from apache_ranger.model.ranger_kms import RangerKeyMetadata
+from apache_ranger.model.ranger_kms import RangerEncryptedKeyVersion
+from apache_ranger.utils import *
+
+LOG = logging.getLogger(__name__)
+
+#
+# Python client for KMS REST APIs
+# More details in
https://hadoop.apache.org/docs/current/hadoop-kms/index.html#KMS_HTTP_REST_API
+#
+class RangerKMSClient:
+ def __init__(self, url, auth):
+ self.client_http = RangerClientHttp(url, auth)
+
+ logging.getLogger("requests").setLevel(logging.WARNING)
+
+
+ def create_key(self, key):
+ resp = self.client_http.call_api(RangerKMSClient.CREATE_KEY,
request_data=key)
+
+ return type_coerce(resp, RangerKeyVersion)
+
+ def rollover_key(self, key_name, material=None):
+ resp =
self.client_http.call_api(RangerKMSClient.ROLLOVER_KEY.format_path({ 'name':
key_name }), request_data={ 'material': material})
+
+ return type_coerce(resp, RangerKeyVersion)
+
+ def invalidate_cache_for_key(self, key_name):
+
self.client_http.call_api(RangerKMSClient.INVALIDATE_CACHE_FOR_KEY.format_path({
'name': key_name }))
+
+ def delete_key(self, key_name):
+ self.client_http.call_api(RangerKMSClient.DELETE_KEY.format_path({
'name': key_name }))
+
+ def get_key_metadata(self, key_name):
+ resp =
self.client_http.call_api(RangerKMSClient.GET_KEY_METADATA.format_path({
'name': key_name }))
+
+ return type_coerce(resp, RangerKeyMetadata)
+
+ def get_current_key(self, key_name):
+ resp =
self.client_http.call_api(RangerKMSClient.GET_CURRENT_KEY.format_path({ 'name':
key_name }))
+
+ return type_coerce(resp, RangerKeyVersion)
+
+ def generate_encrypted_key(self, key_name, num_keys):
+ resp =
self.client_http.call_api(RangerKMSClient.GENERATE_ENCRYPTED_KEY.format_path({'name':
key_name}), query_params={'eek_op': 'generate', 'num_keys': num_keys})
+
+ return type_coerce_list(resp, RangerEncryptedKeyVersion)
+
+ def decrypt_encrypted_key(self, key_name, version_name, iv, material):
+ 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'})
+
+ return type_coerce(resp, RangerKeyVersion)
+
+ def reencrypt_encrypted_key(self, key_name, version_name, iv, material):
+ 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'})
+
+ return type_coerce(resp, RangerEncryptedKeyVersion)
+
+ def batch_reencrypt_encrypted_keys(self, key_name, encrypted_key_versions):
+ resp =
self.client_http.call_api(RangerKMSClient.BATCH_REENCRYPT_ENCRYPTED_KEYS.format_path({'name':
key_name}), request_data=encrypted_key_versions)
+
+ return type_coerce_list(resp, RangerEncryptedKeyVersion)
+
+ def get_key_version(self, version_name):
+ resp =
self.client_http.call_api(RangerKMSClient.GET_KEY_VERSION.format_path({
'version_name': version_name }))
+
+ return type_coerce(resp, RangerKeyVersion)
+
+ def get_key_versions(self, key_name):
+ resp =
self.client_http.call_api(RangerKMSClient.GET_KEY_VERSIONS.format_path({
'name': key_name}))
+
+ return type_coerce_list(resp, RangerKeyVersion)
+
+ def get_key_names(self):
+ resp = self.client_http.call_api(RangerKMSClient.GET_KEYS_NAMES)
+
+ return resp
+
+ def get_keys_metadata(self, key_names):
+ resp = self.client_http.call_api(RangerKMSClient.GET_KEYS_METADATA,
query_params={'key': key_names})
+
+ return type_coerce_list(resp, RangerKeyMetadata)
+
+ # Ranger KMS
+ def get_key(self, key_name):
+ resp = self.client_http.call_api(RangerKMSClient.GET_KEY.format_path({
'name': key_name }))
+
+ return type_coerce(resp, RangerKeyMetadata)
+
+ # Ranger KMS
+ def kms_status(self):
+ resp = self.client_http.call_api(RangerKMSClient.KMS_STATUS)
+
+ return resp
+
+ # URIs
+ URI_KEYS = "kms/v1/keys"
+ URI_KEY_BY_NAME = "kms/v1/key/{name}"
+ URI_KEY_INVALIDATE_CACHE = URI_KEY_BY_NAME + "/_invalidatecache"
+ URI_KEY_METADATA = URI_KEY_BY_NAME + "/_metadata"
+ URI_CURRENT_KEY = URI_KEY_BY_NAME + "/_currentversion"
+ URI_KEY_EEK = URI_KEY_BY_NAME + "/_eek"
+ URI_BATCH_REENCRYPT_KEYS = URI_KEY_BY_NAME + "/_reencryptbatch"
+ URI_KEY_VERSIONS = URI_KEY_BY_NAME + "/_versions"
+ URI_KEY_VERSION_BY_NAME = "kms/v1/keyversion/{version_name}"
+ URI_KEY_VERSION_BY_NAME_EEK = URI_KEY_VERSION_BY_NAME + "/_eek"
+ URI_KEYS_NAMES = URI_KEYS + "/names"
+ URI_KEYS_METADATA = URI_KEYS + "/metadata"
+
+ # Ranger KMS
+ URI_KMS_STATUS = "kms/api/status"
+
+
+ # APIs
+ CREATE_KEY = API(URI_KEYS, HttpMethod.POST,
HTTPStatus.CREATED)
+ ROLLOVER_KEY = API(URI_KEY_BY_NAME, HttpMethod.POST,
HTTPStatus.OK)
+ INVALIDATE_CACHE_FOR_KEY = API(URI_KEY_INVALIDATE_CACHE,
HttpMethod.POST, HTTPStatus.OK)
+ DELETE_KEY = API(URI_KEY_BY_NAME, HttpMethod.DELETE,
HTTPStatus.OK)
+ GET_KEY_METADATA = API(URI_KEY_METADATA, HttpMethod.GET,
HTTPStatus.OK)
+ GET_CURRENT_KEY = API(URI_CURRENT_KEY, HttpMethod.GET,
HTTPStatus.OK)
+ GENERATE_ENCRYPTED_KEY = API(URI_KEY_EEK, HttpMethod.GET,
HTTPStatus.OK)
+ DECRYPT_ENCRYPTED_KEY = API(URI_KEY_VERSION_BY_NAME_EEK,
HttpMethod.POST, HTTPStatus.OK)
+ REENCRYPT_ENCRYPTED_KEY = API(URI_KEY_VERSION_BY_NAME_EEK,
HttpMethod.POST, HTTPStatus.OK)
+ BATCH_REENCRYPT_ENCRYPTED_KEYS = API(URI_BATCH_REENCRYPT_KEYS,
HttpMethod.POST, HTTPStatus.OK)
+ GET_KEY_VERSION = API(URI_KEY_VERSION_BY_NAME,
HttpMethod.GET, HTTPStatus.OK)
+ GET_KEY_VERSIONS = API(URI_KEY_VERSIONS, HttpMethod.GET,
HTTPStatus.OK)
+ GET_KEYS_NAMES = API(URI_KEYS_NAMES, HttpMethod.GET,
HTTPStatus.OK)
+ GET_KEYS_METADATA = API(URI_KEYS_METADATA, HttpMethod.GET,
HTTPStatus.OK)
+
+ # Ranger KMS
+ GET_KEY = API(URI_KEY_BY_NAME, HttpMethod.GET, HTTPStatus.OK)
+ KMS_STATUS = API(URI_KMS_STATUS, HttpMethod.GET, HTTPStatus.OK)
diff --git a/intg/src/main/python/apache_ranger/exceptions.py
b/intg/src/main/python/apache_ranger/exceptions.py
index a2299479c..9f19a59ac 100644
--- a/intg/src/main/python/apache_ranger/exceptions.py
+++ b/intg/src/main/python/apache_ranger/exceptions.py
@@ -36,10 +36,15 @@ class RangerServiceException(Exception):
print(response)
if api is not None and response is not None:
- respJson = response.json()
+ if response.content:
+ try:
+ respJson = response.json()
+ self.msgDesc = respJson['msgDesc'] if respJson is not
None and 'msgDesc' in respJson else None
+ self.messageList = respJson['messageList'] if respJson is not
None and 'messageList' in respJson else None
+ except Exception:
+ self.msgDesc = response.content
+ self.messageList = [ response.content ]
self.statusCode = response.status_code
- self.msgDesc = respJson['msgDesc'] if respJson is not None
and 'msgDesc' in respJson else None
- self.messageList = respJson['messageList'] if respJson is not None
and 'messageList' in respJson else None
Exception.__init__(self, "{} {} failed: expected_status={}, status={},
message={}".format(self.method, self.path, self.expected_status,
self.statusCode, self.msgDesc))
diff --git a/intg/src/main/python/apache_ranger/model/ranger_base.py
b/intg/src/main/python/apache_ranger/model/ranger_base.py
index 83ec44dd4..2111534d0 100644
--- a/intg/src/main/python/apache_ranger/model/ranger_base.py
+++ b/intg/src/main/python/apache_ranger/model/ranger_base.py
@@ -29,18 +29,25 @@ class RangerBase(dict):
return self.get(attr)
def __setattr__(self, key, value):
- self.__setitem__(key, value)
+ if value is None:
+ self.__delitem__(key)
+ else:
+ self.__setitem__(key, value)
def __setitem__(self, key, value):
- super(RangerBase, self).__setitem__(key, value)
- self.__dict__.update({key: value})
+ if value is None:
+ self.__delitem__(key)
+ else:
+ super(RangerBase, self).__setitem__(key, value)
+ self.__dict__.update({key: value})
def __delattr__(self, item):
self.__delitem__(item)
def __delitem__(self, key):
- super(RangerBase, self).__delitem__(key)
- del self.__dict__[key]
+ if key in self.__dict__:
+ super(RangerBase, self).__delitem__(key)
+ del self.__dict__[key]
def __repr__(self):
return json.dumps(self)
diff --git a/intg/src/main/python/apache_ranger/model/ranger_kms.py
b/intg/src/main/python/apache_ranger/model/ranger_kms.py
new file mode 100644
index 000000000..c1a0973e1
--- /dev/null
+++ b/intg/src/main/python/apache_ranger/model/ranger_kms.py
@@ -0,0 +1,77 @@
+#!/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 apache_ranger.model.ranger_base import *
+from apache_ranger.model.ranger_policy import RangerValiditySchedule
+from apache_ranger.utils import *
+
+class RangerKey(RangerBase):
+ def __init__(self, attrs=None):
+ if attrs is None:
+ attrs = {}
+
+ RangerBase.__init__(self, attrs)
+
+ self.name = attrs.get('name')
+ self.cipher = attrs.get('cipher')
+ self.material = attrs.get('material')
+ self.length = attrs.get("length")
+ self.description = attrs.get("description")
+ self.attributes = attrs.get("attributes")
+
+class RangerKeyVersion(RangerBase):
+ def __init__(self, attrs=None):
+ if attrs is None:
+ attrs = {}
+
+ RangerBase.__init__(self, attrs)
+
+ self.name = attrs.get('name')
+ self.versionName = attrs.get('versionName')
+ self.material = attrs.get('material')
+
+class RangerKeyMetadata(RangerBase):
+ def __init__(self, attrs=None):
+ if attrs is None:
+ attrs = {}
+
+ RangerBase.__init__(self, attrs)
+
+ self.cipher = attrs.get('cipher')
+ self.bitLength = attrs.get('bitLength')
+ self.description = attrs.get('description')
+ self.attributes = attrs.get('attributes')
+ self.created = attrs.get('created')
+ self.versions = attrs.get('versions')
+
+class RangerEncryptedKeyVersion(RangerBase):
+ def __init__(self, attrs=None):
+ if attrs is None:
+ attrs = {}
+
+ RangerBase.__init__(self, attrs)
+
+ self.versionName = attrs.get('versionName')
+ self.iv = attrs.get('iv')
+ self.encryptedKeyVersion = attrs.get('encryptedKeyVersion')
+
+ def type_coerce_attrs(self):
+ super(RangerEncryptedKeyVersion, self).type_coerce_attrs()
+
+ self.encryptedKeyVersion = type_coerce(self.encryptedKeyVersion,
RangerKeyVersion)
diff --git a/intg/src/main/python/apache_ranger/utils.py
b/intg/src/main/python/apache_ranger/utils.py
index b0ceb5c59..b3c568f1f 100644
--- a/intg/src/main/python/apache_ranger/utils.py
+++ b/intg/src/main/python/apache_ranger/utils.py
@@ -87,8 +87,24 @@ class HttpMethod(enum.Enum):
class HTTPStatus:
- OK = 200
- NO_CONTENT = 204
- NOT_FOUND = 404
- INTERNAL_SERVER_ERROR = 500
- SERVICE_UNAVAILABLE = 503
+ OK = 200
+ CREATED = 201
+ ACCEPTED = 202
+ NO_CONTENT = 204
+ MOVED_PERMANENTLY = 301
+ SEE_OTHER = 303
+ NOT_MODIFIED = 304
+ TEMPORARY_REDIRECT = 307
+ BAD_REQUEST = 400
+ UNAUTHORIZED = 401
+ FORBIDDEN = 403
+ NOT_FOUND = 404
+ NOT_ACCEPTABLE = 406
+ CONFLICT = 409
+ GONE = 410
+ PRECONDITION_FAILED = 412
+ UNSUPPORTED_MEDIA_TYPE = 415
+ INTERNAL_SERVER_ERROR = 500
+ SERVICE_UNAVAILABLE = 503
+
+
diff --git a/intg/src/main/python/setup.py b/intg/src/main/python/setup.py
index 8ea476320..5222d7153 100644
--- a/intg/src/main/python/setup.py
+++ b/intg/src/main/python/setup.py
@@ -27,7 +27,7 @@ with open("README.md", "r") as fh:
setup(
name="apache-ranger",
- version="0.0.7",
+ version="0.0.8",
author="Apache Ranger",
author_email="[email protected]",
description="Apache Ranger Python client",