Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Changelog

## [Version 1.3.1](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.3.1) - Security release - 2026-04-08
## [Version 1.4.0](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.4.0) - Security and feature release - 2026-04-08

- Increase the version of the package cryptography to 46.0.7 in response to CVE-2026-34073 and CVE-2026-39892
- Add access token refreshing for *Certificates* and *App username password*
- Add access token refreshing for *Azure Single Sign On* preset on DSS 14.5+

## [Version 1.3.0](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.3.0) - Security release - 2026-02-26

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.3.1",
"version": "1.4.0",
"meta": {
"label": "SharePoint Online",
"description": "Read and write data from/to your SharePoint Online account",
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 @@ -38,7 +38,7 @@ class DSSConstants(object):
"sharepoint_oauth": "The access token is missing"
}
PATH = 'path'
PLUGIN_VERSION = "1.3.1"
PLUGIN_VERSION = "1.4.0"
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
33 changes: 19 additions & 14 deletions python-lib/sharepoint_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
format_private_key, format_certificate_thumbprint, url_encode
)
from safe_logger import SafeLogger
from sharepoint_fresh_token import FreshToken


logger = SafeLogger("sharepoint-online plugin", DSSConstants.SECRET_PARAMETERS_KEYS)
Expand Down Expand Up @@ -59,12 +60,19 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False):
self.apply_paths_overwrite(config)
self.setup_sharepoint_online_url(login_details)
self.sharepoint_access_token = login_details['sharepoint_oauth']
if "__credentials" in login_details:
logger.info("Refreshable access token")
from dataiku.core import plugin
access_token = plugin.OAuthCredentials(login_details.get("__credentials", {}).get("sharepoint_oauth"))
else:
logger.info("One time access token")
access_token = FreshToken(self.sharepoint_access_token)
self.session.update_settings(session=SharePointSession(
None,
None,
self.sharepoint_url,
self.sharepoint_site,
sharepoint_access_token=self.sharepoint_access_token
access_token_getter=access_token
),
max_retries=SharePointConstants.MAX_RETRIES,
base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC
Expand Down Expand Up @@ -103,13 +111,12 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False):
self.tenant_id = login_details.get("tenant_id")
self.client_secret = login_details.get("client_secret")
self.client_id = login_details.get("client_id")
self.sharepoint_access_token = self.get_site_app_access_token()
self.session.update_settings(session=SharePointSession(
None,
None,
self.sharepoint_url,
self.sharepoint_site,
sharepoint_access_token=self.sharepoint_access_token
access_token_getter=FreshToken(self.get_site_app_access_token())
),
max_retries=SharePointConstants.MAX_RETRIES,
base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC
Expand All @@ -126,13 +133,12 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False):
self.client_certificate_thumbprint = format_certificate_thumbprint(login_details.get("client_certificate_thumbprint"))
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.session.update_settings(session=SharePointSession(
None,
None,
self.sharepoint_url,
self.sharepoint_site,
sharepoint_access_token=self.sharepoint_access_token
access_token_getter=FreshToken(self.get_certificate_app_access_token)
),
max_retries=SharePointConstants.MAX_RETRIES,
base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC
Expand All @@ -148,13 +154,12 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False):
self.sharepoint_tenant = login_details.get("sharepoint_tenant")
username = login_details.get("username")
password = login_details.get("password")
self.sharepoint_access_token = self.get_username_password_access_token(username, password)
self.session.update_settings(session=SharePointSession(
None,
None,
self.sharepoint_url,
self.sharepoint_site,
sharepoint_access_token=self.sharepoint_access_token
access_token_getter=FreshToken(self.get_username_password_access_token(username, password))
),
max_retries=SharePointConstants.MAX_RETRIES,
base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC
Expand Down Expand Up @@ -1098,12 +1103,12 @@ def is_column_displayable(self, column, display_metadata=False, metadata_to_retr

class SharePointSession():

def __init__(self, sharepoint_user_name, sharepoint_password, sharepoint_url, sharepoint_site, sharepoint_access_token=None, max_retry=10):
def __init__(self, sharepoint_user_name, sharepoint_password, sharepoint_url, sharepoint_site, access_token_getter=None, max_retry=10):
self.sharepoint_url = sharepoint_url
self.sharepoint_site = sharepoint_site
self.sharepoint_access_token = sharepoint_access_token
self.access_token_getter = access_token_getter
requests.adapters.DEFAULT_RETRIES = max_retry
self.form_digest_value = get_form_digest_value(sharepoint_url, sharepoint_site, sharepoint_access_token=self.sharepoint_access_token)
self.form_digest_value = get_form_digest_value(sharepoint_url, sharepoint_site, access_token_getter=self.access_token_getter)

def get(self, url, headers=None, params=None):
retries_limit = ItemsLimit(SharePointConstants.MAX_RETRIES)
Expand Down Expand Up @@ -1152,10 +1157,10 @@ def close():
logger.info("Closing SharePointSession.")

def get_authorization_bearer(self):
return "Bearer {}".format(self.sharepoint_access_token)
return "Bearer {}".format(self.access_token_getter.access_token)


def get_form_digest_value(sharepoint_url, sharepoint_site, session=None, sharepoint_access_token=None):
def get_form_digest_value(sharepoint_url, sharepoint_site, session=None, access_token_getter=None):
def get_contextinfo_url():
return "https://{}/{}/_api/contextinfo".format(
sharepoint_url, sharepoint_site
Expand All @@ -1170,8 +1175,8 @@ def get_contextinfo_url():
)
form_digest_value = None
try:
if sharepoint_access_token:
headers = {**DSSConstants.JSON_HEADERS, **{"Authorization": "Bearer {}".format(sharepoint_access_token)}}
if access_token_getter:
headers = {**DSSConstants.JSON_HEADERS, **{"Authorization": "Bearer {}".format(access_token_getter.access_token)}}
response = session.post(
url=get_contextinfo_url(),
headers=headers,
Expand Down
64 changes: 64 additions & 0 deletions python-lib/sharepoint_fresh_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from safe_logger import SafeLogger
from dss_constants import DSSConstants
import time

logger = SafeLogger("sharepoint-online plugin FreshToken", DSSConstants.SECRET_PARAMETERS_KEYS)
TOKEN_VALIDITY_SAFETY_MARGIN_SECONDS = 60


class FreshToken():
def __init__(self, token_refresh_method):
logger.info("FreshToken init")
if isinstance(token_refresh_method, str):
logger.info("No refresh method available")
self.current_token = token_refresh_method
self.token_refresh_method = self._default_refresh_method
self.token_validity = None
else:
logger.info("Using refresh method")
self.token_refresh_method = token_refresh_method
self.refresh_token()

def _default_refresh_method(self):
return self.current_token

def is_token_still_valid(self):
if self.token_validity is None:
return True
epoch_time_now = int(time.time())
if (epoch_time_now > self.token_validity):
return False
return True

def refresh_token(self):
self.current_token = self.token_refresh_method()
decoded_jwt = decode_jwt(self.current_token)
self.token_validity = decoded_jwt.get("exp", None)
if isinstance(self.token_validity, int):
self.token_validity = self.token_validity - TOKEN_VALIDITY_SAFETY_MARGIN_SECONDS
logger.info("The token is valid until {}".format(self.token_validity))

@property
def access_token(self):
if not self.is_token_still_valid():
logger.info("Token reaching its time limit, refreshing it...")
self.refresh_token()
return self.current_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 {}