Skip to content

Commit

Permalink
Merge pull request #50 from dataiku/feature/sc-147685-review-external…
Browse files Browse the repository at this point in the history
…-pr-site-app-registration

feat: [sc-147685] Review external PR to add Site App registration
  • Loading branch information
alexbourret authored Jun 10, 2024
2 parents bbc6169 + 143393f commit 95bf4c9
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 5 deletions.
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.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

## [Version 1.1.2](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.1.2) - Bugfix release - 2024-05-28

- Fix path creation inside read-only parent directory
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Richard Kroegel
3 changes: 2 additions & 1 deletion code-env/python/spec/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
sharepy==1.3.0
sharepy==1.3.0
msal==1.23.0
11 changes: 11 additions & 0 deletions custom-recipes/sharepoint-online-append-list/recipe.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
{
"value": "site-app-permissions",
"label": "Site App Permissions"
},
{
"value": "app-certificate",
"label": "Certificates"
}
]
},
Expand All @@ -68,6 +72,13 @@
"parameterSetId": "site-app-permissions",
"visibilityCondition": "model.auth_type == 'site-app-permissions'"
},
{
"name": "app_certificate",
"label": "Certificates",
"type": "PRESET",
"parameterSetId": "app-certificate",
"visibilityCondition": "model.auth_type == 'app-certificate'"
},
{
"name": "sharepoint_list_title",
"label": "List title",
Expand Down
68 changes: 68 additions & 0 deletions parameter-sets/app-certificate/parameter-set.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"meta": {
"label": "Certificates",
"description": "",
"icon": "icon-cloud"
},
"defaultDefinableInline": true,
"defaultDefinableAtProjectLevel": true,
"pluginParams": [],
"params": [
{
"name": "sharepoint_tenant",
"label": "Tenant",
"type": "STRING",
"description": "As in <tenant>.sharepoint.com. Please refer to plugin doc.",
"mandatory": true
},
{
"name": "sharepoint_site",
"label": "Site path",
"type": "STRING",
"description": "sites/site_name/subsite...",
"mandatory": true
},
{
"name": "sharepoint_root",
"label": "Root directory",
"type": "STRING",
"description": "",
"defaultValue": "Shared Documents"
},
{
"name": "tenant_id",
"label": "Tenant ID",
"type": "STRING",
"description": "",
"mandatory": true
},
{
"name": "client_id",
"label": "Client ID",
"type": "STRING",
"description": "",
"mandatory": true
},
{
"name": "client_certificate_thumbprint",
"label": "Client certificate thumbprint",
"type": "STRING",
"description": "",
"mandatory": true
},
{
"name": "client_certificate",
"label": "Client certificate (private key)",
"type": "PASSWORD",
"description": "Base64-encoded, starting with '-----BEGIN PRIVATE KEY-----'",
"mandatory": true
},
{
"name": "passphrase",
"label": "Certificate passphrase",
"type": "PASSWORD",
"description": "If required by private key",
"mandatory": false
}
]
}
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.2",
"version": "1.1.3",
"meta": {
"label": "SharePoint Online",
"description": "Read and write data from/to your SharePoint Online account",
Expand Down
11 changes: 11 additions & 0 deletions python-connectors/sharepoint-online_lists/connector.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
{
"value": "site-app-permissions",
"label": "Site App Permissions"
},
{
"value": "app-certificate",
"label": "Certificates"
}
]
},
Expand All @@ -47,6 +51,13 @@
"parameterSetId": "site-app-permissions",
"visibilityCondition": "model.auth_type == 'site-app-permissions'"
},
{
"name": "app_certificate",
"label": "Certificates",
"type": "PRESET",
"parameterSetId": "app-certificate",
"visibilityCondition": "model.auth_type == 'app-certificate'"
},
{
"name": "sharepoint_list_title",
"label": "List title",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
{
"value": "site-app-permissions",
"label": "Site App Permissions"
},
{
"value": "app-certificate",
"label": "Certificates"
}
]
},
Expand All @@ -45,6 +49,13 @@
"parameterSetId": "site-app-permissions",
"visibilityCondition": "model.auth_type == 'site-app-permissions'"
},
{
"name": "app_certificate",
"label": "Certificates",
"type": "PRESET",
"parameterSetId": "app-certificate",
"visibilityCondition": "model.auth_type == 'app-certificate'"
},
{
"name": "advanced_parameters",
"label": "Show advanced parameters",
Expand Down
23 changes: 23 additions & 0 deletions python-lib/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,29 @@ def merge_paths(first_path, second_path):
return joined_path.strip("/")


def format_private_key(private_key):
"""Formats the private key as the secret parameter replaces newlines with spaces."""
private_key = private_key.strip(" ")
if private_key.startswith(SharePointConstants.CLEAR_KEY_START):
start_marker = SharePointConstants.CLEAR_KEY_START
end_marker = SharePointConstants.CLEAR_KEY_END
else:
start_marker = SharePointConstants.ENCRYPTED_KEY_START
end_marker = SharePointConstants.ENCRYPTED_KEY_END
private_key = private_key.replace(start_marker, "")
private_key = private_key.replace(end_marker, "")
private_key = "\n".join([start_marker, *private_key.split(), end_marker])
return private_key


def format_certificate_thumbprint(certificate_thumbprint):
if ":" in certificate_thumbprint:
certificate_thumbprint = certificate_thumbprint.replace(":", "")
elif " " in certificate_thumbprint:
certificate_thumbprint = certificate_thumbprint.replace(" ", "")
return certificate_thumbprint


def update_dict_in_kwargs(kwargs, key_to_update, update):
if not update:
return kwargs
Expand Down
13 changes: 11 additions & 2 deletions python-lib/dss_constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
class DSSConstants(object):
APP_CERTIFICATE_DETAILS = {
"sharepoint_tenant": "The tenant name is missing",
"sharepoint_site": "The site name is missing",
"tenant_id": "The tenant ID is missing. See documentation on how to obtain this information",
"client_id": "The client ID is missing",
"client_certificate_thumbprint": "The client certificate thumbprint is missing",
"client_certificate": "The client certificate is missing"
}
APPLICATION_JSON = "application/json;odata=verbose"
APPLICATION_JSON_NOMETADATA = "application/json;odata=nometadata"
AUTH_APP_CERTIFICATE = "app-certificate"
AUTH_LOGIN = "login"
AUTH_OAUTH = "oauth"
AUTH_SITE_APP = "site-app-permissions"
Expand Down Expand Up @@ -28,8 +37,8 @@ class DSSConstants(object):
"sharepoint_oauth": "The access token is missing"
}
PATH = 'path'
PLUGIN_VERSION = "1.1.2"
SECRET_PARAMETERS_KEYS = ["Authorization", "sharepoint_username", "sharepoint_password", "client_secret"]
PLUGIN_VERSION = "1.1.3"
SECRET_PARAMETERS_KEYS = ["Authorization", "sharepoint_username", "sharepoint_password", "client_secret", "client_certificate", "passphrase"]
SITE_APP_DETAILS = {
"sharepoint_tenant": "The tenant name is missing",
"sharepoint_site": "The site name is missing",
Expand Down
40 changes: 39 additions & 1 deletion python-lib/sharepoint_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
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, get_lnt_path,
format_private_key, format_certificate_thumbprint
)
from safe_logger import SafeLogger

Expand Down Expand Up @@ -111,6 +112,29 @@ def __init__(self, config):
max_retries=SharePointConstants.MAX_RETRIES,
base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC
)
elif config.get('auth_type') == DSSConstants.AUTH_APP_CERTIFICATE:
logger.info("SharePointClient:app-certificate")
login_details = config.get('app_certificate')
self.assert_login_details(DSSConstants.APP_CERTIFICATE_DETAILS, login_details)
self.setup_sharepoint_online_url(login_details)
self.setup_login_details(login_details)
self.apply_paths_overwrite(config)
self.tenant_id = login_details.get("tenant_id")
self.client_certificate = format_private_key(login_details.get("client_certificate"))
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
),
max_retries=SharePointConstants.MAX_RETRIES,
base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC
)
else:
raise SharePointClientError("The type of authentication is not selected")
self.sharepoint_list_title = config.get("sharepoint_list_title")
Expand Down Expand Up @@ -840,6 +864,20 @@ def get_site_app_access_token(self):
json_response = response.json()
return json_response.get("access_token")

def get_certificate_app_access_token(self):
import msal
app = msal.ConfidentialClientApplication(
self.client_id,
authority=f"https://login.microsoftonline.com/{self.tenant_id}",
client_credential={
"thumbprint": self.client_certificate_thumbprint,
"private_key": self.client_certificate,
"passphrase": self.passphrase,
},
)
json_response = app.acquire_token_for_client(scopes=[f"{self.sharepoint_origin}/.default"])
return json_response.get("access_token")

def get_view_id(self, list_title, view_title):
if not list_title:
return None
Expand Down
4 changes: 4 additions & 0 deletions python-lib/sharepoint_constants.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
class SharePointConstants(object):
CLEAR_KEY_END = "-----END PRIVATE KEY-----"
CLEAR_KEY_START = "-----BEGIN PRIVATE KEY-----"
COLUMNS = 'columns'
COMMENT_COLUMN = 'comment'
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
DEFAULT_VIEW_ENDPOINT = "DefaultView/ViewFields"
DEFAULT_WAIT_BEFORE_RETRY = 60
ENCRYPTED_KEY_END = "-----END ENCRYPTED PRIVATE KEY-----"
ENCRYPTED_KEY_START = "-----BEGIN ENCRYPTED PRIVATE KEY-----"
ENTITY_PROPERTY_NAME = 'EntityPropertyName'
ERROR_CONTAINER = 'error'
EXPENDABLES_FIELDS = {"Author": "Title", "Editor": "Title"}
Expand Down
8 changes: 8 additions & 0 deletions tests/python/integration/test_scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,11 @@ def test_run_sharepoint_online_append_to_list_recipe(user_dss_clients):

def test_run_sharepoint_online_write_file_in_path_w_ro_parent(user_dss_clients):
dss_scenario.run(user_dss_clients, project_key=TEST_PROJECT_KEY, scenario_id="SC169288_WRITE_FILE_WITH_RO_PARENT_FOLDER")


def test_run_sharepoint_online_certificate_auth(user_dss_clients):
dss_scenario.run(user_dss_clients, project_key=TEST_PROJECT_KEY, scenario_id="CERTIFICATEAUTH")


def test_run_sharepoint_online_encrypted_certificate_auth(user_dss_clients):
dss_scenario.run(user_dss_clients, project_key=TEST_PROJECT_KEY, scenario_id="ENCRYPTEDCERTIFICATEAUTH")

0 comments on commit 95bf4c9

Please sign in to comment.