Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Tdl 14803 check api access in discovery mode #74

Open
wants to merge 49 commits into
base: master
Choose a base branch
from

Conversation

prijendev
Copy link
Contributor

@prijendev prijendev commented Sep 27, 2021

Description of change

TDL-14803 Check API access in Discovery mode

  • In Discovery Mode, the tap makes an API call to the each stream.
  • Write unittest cases for the same.
  • Updated error handling. (TDL-15392)
  • Replaced get call in get_incremental_export(http.py) by call_api because it doing the same as call_api. So, now error handling will work properly with backoff as earlier backoff was not available.

Note - Added code of PR79 in this PR because it was require. So, first PR79 need to merge and once that PR merged changes will not be reflected here.

Manual QA steps

  • Check that tap shows error in discovery code for streams which does not have access.
  • Check that ticket_metrics, ticket_comments, ticket_audit stream skip 404 error.

Risks

Rollback steps

  • revert this branch

elif json.loads(e.args[0]).get('description') == "You are missing the following required scopes: read":
error_list.append(s.name)
else:
raise e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Raise an exception from None.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raised an exception from None.

except Exception: # pylint: disable=broad-except
response_json = {}
if response.status_code != 200:
message = "HTTP-error-code: {}, Error: {}".format(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use f-strings instead of format.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced format with f-string.

Comment on lines 124 to 128
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer {}'.format(self.config['access_token'])
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer {}'.format(self.config['access_token'])
}
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': f'Bearer {self.config['access_token']}'
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced format with f-string.

Comment on lines 142 to 143
LOGGER.warning("The account credentials supplied do not have access to `%s` custom fields.",
stream)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LOGGER.warning("The account credentials supplied do not have access to `%s` custom fields.",
stream)
LOGGER.warning("The account credentials supplied do not have access to `%s` custom fields.",
stream)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

},
403: {
"raise_exception": ZendeskForbiddenError,
"message": "You are missing the following required scopes: read"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it be each time read only? If No then please put a generalized message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Because we are calling just GET API for each stream. So, it will return the same message of read scopes.

},
404: {
"raise_exception": ZendeskNotFoundError,
"message": "There is no help desk configured at this address. This means that the address is available and that you can claim it at http://www.zendesk.com/signup"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above if it is not the fixed message for 404 each time then please put a generalized message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to generalized message.

@@ -114,13 +115,33 @@ def load_metadata(self):
def is_selected(self):
return self.stream is not None

def check_access(self):
'''
Check whether permission given to access stream resource or not.
Copy link
Contributor

@dbshah1212 dbshah1212 Sep 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Check whether permission given to access stream resource or not.
Check whether the permission was given to access stream resources or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

def raise_or_log_zenpy_apiexception(schema, stream, e):
# There are multiple tiers of Zendesk accounts. Some of them have
# access to `custom_fields` and some do not. This is the specific
# error that appears to be return from the API call in the event that
# it doesn't have access.
if not isinstance(e, zenpy.lib.exception.APIException):
raise ValueError("Called with a bad exception type") from e
#If read permission not available in oauth access_token, then it returns below error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#If read permission not available in oauth access_token, then it returns below error.
#If read permission is not available in OAuth access_token, then it returns the below error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

@dbshah1212 dbshah1212 self-requested a review October 27, 2021 08:36

return 400 <=status_code < 500

def raise_for_error(response):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comments for this function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added comments.

else:
raise e

except http.ZendeskNotFoundError:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment for this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added comments.

@prijendev prijendev requested a review from dbshah1212 October 28, 2021 09:19
try:
# Here it call the check_access method to check whether stream have read permission or not.
# If stream does not have read permission then append that stream name to list and at the end of all streams
# raise forbidden error with proper message containinn stream names.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Typo in 'containing' word in the comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

for s in STREAMS.values():
s = s(client)
s = s(client, config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Can you please give understandable variable name instead of 's'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In existing code they have used 's'. Change in name will reflect lot off changes in code as it is used in many place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Please do the variable name changes. If it means change will reflect lot off changes that's fine

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Add Comments to the code

Copy link
Contributor

@namrata270998 namrata270998 Nov 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Add Comments to the code

Added comments to the code. And you can find detailed comments in the try block as well. Also updated the variable name s to stream

# raise forbidden error with proper message containinn stream names.
s.check_access()
except ZendeskForbiddenError as e:
error_list.append(s.name) # Append stream name to the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Incomplete comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

except ZendeskForbiddenError as e:
error_list.append(s.name) # Append stream name to the
except zenpy.lib.exception.APIException as e:
err = json.loads(e.args[0]).get('error')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Since e.args[0] is being used multiple times. Please make it a variable and get it converted in the json.loads. and re-use it further everywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

if error_list:
streams_name = ", ".join(error_list)
message = "HTTP-error-code: 403, Error: You are missing the following required scopes: read. "\
"The account credentials supplied do not have read access for the following stream(s): {}".format(streams_name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev As per the best practices, we should not use format(). Please update this code

Copy link
Contributor Author

@prijendev prijendev Nov 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They don't updated python to the latest version. That's why format() has been kept.

response_json = {}
if response.status_code != 200:
if response_json.get('error'):
message = "HTTP-error-code: {}, Error: {}".format(response.status_code, response_json.get('error'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Don't use format() in the code

Copy link
Contributor Author

@prijendev prijendev Nov 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They don't updated python to the latest version. That's why format() has been kept.

except http.ZendeskNotFoundError:
# Skip stream if ticket_audit does not found for particular ticekt_id. Earlier it throwing HTTPError
# but now as error handling updated, it throws ZendeskNotFoundError.
message = "Unable to retrieve audits for ticket (ID: {}), record not found".format(ticket['id'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Don't use format()

Copy link
Contributor Author

@prijendev prijendev Nov 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They don't updated python to the latest version. That's why format() has been kept.

if metrics_stream.is_selected():
try:
for metric in metrics_stream.sync(ticket["id"]):
self._buffer_record(metric)
except HTTPError as e:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Why did we remove the HTTPError exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because earlier it throwing HTTPError but now as error handling updated, it throws ZendeskNotFoundError. So we replaced HTTPError with ZendeskNotFoundError. Added comment in code also.

url = self.endpoint.format(self.config['subdomain'])
HEADERS['Authorization'] = 'Bearer {}'.format(self.config["access_token"])

http.call_api(url, self.config.get('request_timeout', DEFAULT_TIMEOUT), params={'start_time': 1610368140, 'per_page': 1}, headers=HEADERS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Why is there a static time in the code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we calls api to just check api permission and we need to pass required start_time parameter. It can be anything. Replaced it with constant

url = self.endpoint.format(self.config['subdomain'])
HEADERS['Authorization'] = 'Bearer {}'.format(self.config["access_token"])

http.call_api(url, self.config.get('request_timeout', DEFAULT_TIMEOUT), params={'per_page': 1}, headers=HEADERS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Wouldn't there be a data-type issue here if config['request_timeout'] would be a number in string format?
Eg: config[request_timeout]: '200'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated it. Actually I had updated it in PR 79 as part of request timeout but remained that changes this PR.


for page in http.get_cursor_based(url, self.config['access_token'], **kwargs):
# Pass `request_timeout` parameter
for page in http.get_cursor_based(url, self.config['access_token'], self.config.get('request_timeout', DEFAULT_TIMEOUT), **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here as well
Please initialize this value at a common place in the file and use it everywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated it.

'''
Check whether the permission was given to access stream resources or not.
'''
self.client.search("", updated_after=datetime.datetime.utcnow(), updated_before='2000-01-02T00:00:00Z', type="user")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev What are we searching here?
What does "" mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are using this API call in sync. This API call is used to retrieve users records in sync. We just checking whether that stream has read permission or not by same API call which used in sync. "" parameter is just any name of query. We followed the same call as in sync.

args0 = json.loads(e.args[0])
err = args0.get('error')

if isinstance(err, dict):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

@@ -16,6 +15,12 @@
LOGGER = singer.get_logger()
KEY_PROPERTIES = ['id']

REQUEST_TIMEOUT = 300
TICKET_START_TIME = 1610368140
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Why to keep a static value. You can use start time provided in the config.json file. Correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, updated and removed static time

@@ -199,7 +200,7 @@ def main():
LOGGER.error("""No suitable authentication keys provided.""")

if parsed_args.discover:
do_discover(client)
do_discover(client, parsed_args.config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comments to the code changes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comments to the code changes

Added comments

for s in STREAMS.values():
s = s(client)
s = s(client, config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Please do the variable name changes. If it means change will reflect lot off changes that's fine

for s in STREAMS.values():
s = s(client)
s = s(client, config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prijendev Add Comments to the code

Copy link
Contributor

@KrisPersonal KrisPersonal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please look at my comments

@dbshah1212 dbshah1212 mentioned this pull request Nov 11, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants