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

Feature/diagnostic for oauth tokens #59

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [Version 1.1.4](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.1.4) - Feature release - 2024-06-10

- Add JWT token diag in case of authorization error

## [Version 1.1.3](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.1.3) - Feature release - 2024-06-04

- Add login with Azure AD app certificate
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "sharepoint-online",
"version": "1.1.3",
"version": "1.1.4",
"meta": {
"label": "SharePoint Online",
"description": "Read and write data from/to your SharePoint Online account",
Expand Down
61 changes: 61 additions & 0 deletions python-lib/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,67 @@ def update_dict_in_kwargs(kwargs, key_to_update, update):
return kwargs


def run_oauth_diagnostic(jwt_token):
censored_token = diagnose_jwt(jwt_token)
ip_in_jwt = censored_token.get("ipaddr", "")
if not ip_in_jwt:
logger.info("No IP address in the JWT token")
else:
# Retrieve the plugin's external IP to check it matches the one
# stored in the JWT token
kernel_external_ip = get_kernel_external_ip()
if not kernel_external_ip:
return
if ip_in_jwt != kernel_external_ip:
logger.error("The plugin external IP address does not match the IP allowed in the JWT token")
else:
logger.info("IP addresses in the OAuth token and the plugin kernel match")


def diagnose_jwt(jwt_token):
# Display ebough details about the JWT token to allow debugging
# without making it possible to reuse it
keys_to_report = ["aud", "exp", "app_displayname", "appid", "ipaddr", "name", "scp", "unique_name", "upn", "roles"]
decoded_token = decode_jwt(jwt_token)
censored_token = {}
for key_to_report in keys_to_report:
censored_token[key_to_report] = decoded_token.get(key_to_report)
logger.info("Decoded token: {}".format(censored_token))
return censored_token


def decode_jwt(jwt_token):
try:
import base64
import json
sub_tokens = jwt_token.split('.')
if len(sub_tokens)<2:
logger.error("JWT format is wrong")
return {}
token_of_interest = sub_tokens[1]
padded_token = token_of_interest + "="*divmod(len(token_of_interest),4)[1]
decoded_token = base64.urlsafe_b64decode(padded_token.encode('utf-8'))
json_token = json.loads(decoded_token)
return json_token
except Exception as error:
logger.error("Could not decode JWT token ({})".format(error))
return {}


def get_kernel_external_ip():
try:
import requests
response = requests.get("https://api64.ipify.org?format=json")
if response.status_code >= 400:
logger.error("Error {} trying to check kernel's external ip:{}".format(response.status_code, response.content))
json_response = response.json()
kernel_external_ip = json_response.get("ip", "")
return kernel_external_ip
except Exception as error:
logger.error("Could not fetch kernel's remote ip ({})".format(error))
return ""


class ItemsLimit():
def __init__(self, records_limit=-1):
self.has_no_limit = (records_limit == -1)
Expand Down
2 changes: 1 addition & 1 deletion python-lib/dss_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class DSSConstants(object):
"sharepoint_oauth": "The access token is missing"
}
PATH = 'path'
PLUGIN_VERSION = "1.1.3"
PLUGIN_VERSION = "1.1.4"
SECRET_PARAMETERS_KEYS = ["Authorization", "sharepoint_username", "sharepoint_password", "client_secret", "client_certificate", "passphrase"]
SITE_APP_DETAILS = {
"sharepoint_tenant": "The tenant name is missing",
Expand Down
17 changes: 14 additions & 3 deletions python-lib/sharepoint_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from common import (
is_email_address, get_value_from_path, parse_url,
get_value_from_paths, is_request_performed, ItemsLimit,
is_empty_path, merge_paths, get_lnt_path,
is_empty_path, merge_paths, run_oauth_diagnostic, get_lnt_path,
format_private_key, format_certificate_thumbprint
)
from safe_logger import SafeLogger
Expand All @@ -41,6 +41,7 @@ def __init__(self, config):
self.session = RobustSession(status_codes_to_retry=[429, 503], attempt_session_reset_on_403=attempt_session_reset_on_403)
self.number_dumped_logs = 0
self.username_for_namespace_diag = None
self.jwt_diag_done = False

self.dss_column_name = {}
self.column_ids = {}
Expand All @@ -57,6 +58,7 @@ def __init__(self, config):
self.apply_paths_overwrite(config)
self.setup_sharepoint_online_url(login_details)
self.sharepoint_access_token = login_details['sharepoint_oauth']
self.auth_token_for_diag =self.sharepoint_access_token
self.session.update_settings(session=SharePointSession(
None,
None,
Expand Down Expand Up @@ -125,6 +127,7 @@ def __init__(self, config):
self.passphrase = login_details.get("passphrase")
self.client_id = login_details.get("client_id")
self.sharepoint_access_token = self.get_certificate_app_access_token()
self.auth_token_for_diag =self.sharepoint_access_token
self.session.update_settings(session=SharePointSession(
None,
None,
Expand Down Expand Up @@ -779,9 +782,8 @@ def assert_response_ok(self, response, no_json=False, calling_method=""):
if status_code == 404:
raise SharePointClientError("Not found. Please check tenant, site type or site name. ({})".format(calling_method))
if status_code == 403:
logger.error("403 error. Checking for federated namespace.")
self.assert_non_federated_namespace()
logger.error("User does not belong to federated namespace.")
self.run_jwt_validity_test()
raise SharePointClientError("403 Forbidden. Please check your account credentials. ({})".format(calling_method))
raise SharePointClientError("Error {} ({})".format(status_code, calling_method))
if not no_json:
Expand All @@ -790,6 +792,7 @@ def assert_response_ok(self, response, no_json=False, calling_method=""):
def assert_non_federated_namespace(self):
# Called following 403 error
if self.username_for_namespace_diag:
logger.error("403 error. Checking for federated namespace.")
# username / password login was used to login
# we check if the email used as username belongs to a federated namespace
json_response = ""
Expand All @@ -810,6 +813,14 @@ def assert_non_federated_namespace(self):
+ "Dataiku might not be able to use it to access SharePoint-Online. "
+ "Please contact your administrator to configure a Single Sign On or an App token access."
)
logger.error("User does not belong to federated namespace.")

def run_jwt_validity_test(self):
# Called following 403 error
if self.auth_token_for_diag and not self.jwt_diag_done:
self.jwt_diag_done = True
logger.info("403 Error. Running diag on auth token")
run_oauth_diagnostic(self.auth_token_for_diag)

@staticmethod
def get_enriched_error_message(response):
Expand Down