diff --git a/CHANGELOG.md b/CHANGELOG.md index 784d99c..be4ced6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [Version 1.5.0](https://github.com/dataiku/dss-plugin-pi-server/releases/tag/v1.5.0) - Bugfix release - 2026-02-02 + +- Add blank OAuth SSO preset + ## [Version 1.4.1](https://github.com/dataiku/dss-plugin-pi-server/releases/tag/v1.4.1) - Bugfix release - 2026-01-27 - Fix issue with writing diff --git a/custom-recipes/pi-system-retrieve-event-frames/recipe.json b/custom-recipes/pi-system-retrieve-event-frames/recipe.json index 79c7b37..b4fe8ef 100644 --- a/custom-recipes/pi-system-retrieve-event-frames/recipe.json +++ b/custom-recipes/pi-system-retrieve-event-frames/recipe.json @@ -32,11 +32,29 @@ "type": "SEPARATOR", "label": "Authentication" }, + { + "name": "credentials_type", + "label": "Credentials type", + "type": "SELECT", + "selectChoices":[ + {"value": "basic_auth", "label": "User / Password"}, + {"value": "oauth_sso", "label": "Single Sign On"} + ], + "defaultValue": "basic_auth" + }, { "name": "credentials", "label": "User preset", "type": "PRESET", - "parameterSetId": "basic-auth" + "parameterSetId": "basic-auth", + "visibilityCondition": "model.credentials_type=='basic_auth'" + }, + { + "name": "oauth_credentials", + "label": "User preset", + "type": "PRESET", + "parameterSetId": "oauth-sso", + "visibilityCondition": "model.credentials_type=='oauth_sso'" }, { "name": "show_advanced_parameters", diff --git a/custom-recipes/pi-system-retrieve-list/recipe.json b/custom-recipes/pi-system-retrieve-list/recipe.json index a742514..4e8a545 100644 --- a/custom-recipes/pi-system-retrieve-list/recipe.json +++ b/custom-recipes/pi-system-retrieve-list/recipe.json @@ -32,11 +32,29 @@ "type": "SEPARATOR", "label": "Authentication" }, + { + "name": "credentials_type", + "label": "Credentials type", + "type": "SELECT", + "selectChoices":[ + {"value": "basic_auth", "label": "User / Password"}, + {"value": "oauth_sso", "label": "Single Sign On"} + ], + "defaultValue": "basic_auth" + }, { "name": "credentials", "label": "User preset", "type": "PRESET", - "parameterSetId": "basic-auth" + "parameterSetId": "basic-auth", + "visibilityCondition": "model.credentials_type=='basic_auth'" + }, + { + "name": "oauth_credentials", + "label": "User preset", + "type": "PRESET", + "parameterSetId": "oauth-sso", + "visibilityCondition": "model.credentials_type=='oauth_sso'" }, { "name": "show_advanced_parameters", diff --git a/custom-recipes/pi-system-write/recipe.json b/custom-recipes/pi-system-write/recipe.json index e4fa265..fd8c9a2 100644 --- a/custom-recipes/pi-system-write/recipe.json +++ b/custom-recipes/pi-system-write/recipe.json @@ -32,11 +32,29 @@ "type": "SEPARATOR", "label": "Authentication" }, + { + "name": "credentials_type", + "label": "Credentials type", + "type": "SELECT", + "selectChoices":[ + {"value": "basic_auth", "label": "User / Password"}, + {"value": "oauth_sso", "label": "Single Sign On"} + ], + "defaultValue": "basic_auth" + }, { "name": "credentials", "label": "User preset", "type": "PRESET", - "parameterSetId": "basic-auth" + "parameterSetId": "basic-auth", + "visibilityCondition": "model.credentials_type=='basic_auth'" + }, + { + "name": "oauth_credentials", + "label": "User preset", + "type": "PRESET", + "parameterSetId": "oauth-sso", + "visibilityCondition": "model.credentials_type=='oauth_sso'" }, { "name": "show_advanced_parameters", diff --git a/parameter-sets/oauth-sso/parameter-set.json b/parameter-sets/oauth-sso/parameter-set.json new file mode 100644 index 0000000..47f36ec --- /dev/null +++ b/parameter-sets/oauth-sso/parameter-set.json @@ -0,0 +1,70 @@ +{ + "meta" : { + "label": "OAuth SSO", + "description": "", + "icon": "icon-pi-system icon-cogs" + }, + "defaultDefinableInline": false, + "defaultDefinableAtProjectLevel": false, + "params": [ + { + "name": "default_server", + "label": "Server URL", + "type": "STRING", + "description": "my.pi-system.server.com" + }, + { + "name": "can_override_server_url", + "label": "Users can override server URL", + "type": "BOOLEAN", + "description": "Unsafe !", + "defaultValue": false + }, + { + "name": "can_disable_ssl_check", + "label": "Users can disable SSL checks", + "type": "BOOLEAN", + "description": "Unsafe !", + "defaultValue": false + }, + { + "name": "ssl_cert_path", + "label": "Path to SSL certificate", + "type": "STRING", + "description": "(optional)", + "defaultValue": "" + }, + { + "name": "secure_token", + "type": "CREDENTIAL_REQUEST", + "label": "Single Sign On", + "credentialRequestSettings": { + "type": "OAUTH2", + "authorizationEndpoint": " ", + "tokenEndpoint": " ", + "scope": " " + } + }, + { + "name": "authorizationEndpoint", + "label": "Authorization endpoint", + "type": "STRING", + "description": "", + "mandatory": false + }, + { + "name": "tokenEndpoint", + "label": "Token endpoint", + "type": "STRING", + "description": "", + "mandatory": false + }, + { + "name": "scope", + "label": "Scope", + "type": "STRING", + "description": "", + "mandatory": false + } + ] +} diff --git a/plugin.json b/plugin.json index 1b199f6..30972ec 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "id": "pi-system", - "version": "1.4.1", + "version": "1.5.0", "meta": { "label": "PI System", "description": "Retrieve data from your OSIsoft PI System servers", diff --git a/python-connectors/pi-system_attribute-search/connector.json b/python-connectors/pi-system_attribute-search/connector.json index 13fa9f2..4bc1da0 100644 --- a/python-connectors/pi-system_attribute-search/connector.json +++ b/python-connectors/pi-system_attribute-search/connector.json @@ -9,11 +9,29 @@ "kind": "PYTHON", "paramsPythonSetup": "browse_attributes.py", "params": [ + { + "name": "credentials_type", + "label": "Credentials type", + "type": "SELECT", + "selectChoices":[ + {"value": "basic_auth", "label": "User / Password"}, + {"value": "oauth_sso", "label": "Single Sign On"} + ], + "defaultValue": "basic_auth" + }, { "name": "credentials", "label": "User preset", "type": "PRESET", - "parameterSetId": "basic-auth" + "parameterSetId": "basic-auth", + "visibilityCondition": "model.credentials_type=='basic_auth'" + }, + { + "name": "oauth_credentials", + "label": "User preset", + "type": "PRESET", + "parameterSetId": "oauth-sso", + "visibilityCondition": "model.credentials_type=='oauth_sso'" }, { "name": "show_advanced_parameters", diff --git a/python-connectors/pi-system_event-frames-search/connector.json b/python-connectors/pi-system_event-frames-search/connector.json index 11decc0..6ebd5f8 100644 --- a/python-connectors/pi-system_event-frames-search/connector.json +++ b/python-connectors/pi-system_event-frames-search/connector.json @@ -9,11 +9,29 @@ "kind": "PYTHON", "paramsPythonSetup": "browse_event_frames.py", "params": [ + { + "name": "credentials_type", + "label": "Credentials type", + "type": "SELECT", + "selectChoices":[ + {"value": "basic_auth", "label": "User / Password"}, + {"value": "oauth_sso", "label": "Single Sign On"} + ], + "defaultValue": "basic_auth" + }, { "name": "credentials", "label": "User preset", "type": "PRESET", - "parameterSetId": "basic-auth" + "parameterSetId": "basic-auth", + "visibilityCondition": "model.credentials_type=='basic_auth'" + }, + { + "name": "oauth_credentials", + "label": "User preset", + "type": "PRESET", + "parameterSetId": "oauth-sso", + "visibilityCondition": "model.credentials_type=='oauth_sso'" }, { "name": "show_advanced_parameters", diff --git a/python-connectors/pi-system_pi-explorer/connector.json b/python-connectors/pi-system_pi-explorer/connector.json index 6194bb1..41aba4b 100644 --- a/python-connectors/pi-system_pi-explorer/connector.json +++ b/python-connectors/pi-system_pi-explorer/connector.json @@ -9,11 +9,29 @@ "kind": "PYTHON", "paramsPythonSetup": "browse_tags.py", "params": [ + { + "name": "credentials_type", + "label": "Credentials type", + "type": "SELECT", + "selectChoices":[ + {"value": "basic_auth", "label": "User / Password"}, + {"value": "oauth_sso", "label": "Single Sign On"} + ], + "defaultValue": "basic_auth" + }, { "name": "credentials", "label": "User preset", "type": "PRESET", - "parameterSetId": "basic-auth" + "parameterSetId": "basic-auth", + "visibilityCondition": "model.credentials_type=='basic_auth'" + }, + { + "name": "oauth_credentials", + "label": "User preset", + "type": "PRESET", + "parameterSetId": "oauth-sso", + "visibilityCondition": "model.credentials_type=='oauth_sso'" }, { "name": "show_advanced_parameters", diff --git a/python-connectors/pi-system_piwebapi-toolbox/connector.json b/python-connectors/pi-system_piwebapi-toolbox/connector.json index 4ffcab0..e53c5f4 100644 --- a/python-connectors/pi-system_piwebapi-toolbox/connector.json +++ b/python-connectors/pi-system_piwebapi-toolbox/connector.json @@ -7,11 +7,29 @@ "readable": true, "writable": true, "params": [ + { + "name": "credentials_type", + "label": "Credentials type", + "type": "SELECT", + "selectChoices":[ + {"value": "basic_auth", "label": "User / Password"}, + {"value": "oauth_sso", "label": "Single Sign On"} + ], + "defaultValue": "basic_auth" + }, { "name": "credentials", "label": "User preset", "type": "PRESET", - "parameterSetId": "basic-auth" + "parameterSetId": "basic-auth", + "visibilityCondition": "model.credentials_type=='basic_auth'" + }, + { + "name": "oauth_credentials", + "label": "User preset", + "type": "PRESET", + "parameterSetId": "oauth-sso", + "visibilityCondition": "model.credentials_type=='oauth_sso'" }, { "name": "show_advanced_parameters", diff --git a/python-lib/osisoft_client.py b/python-lib/osisoft_client.py index 15c96b8..83ea208 100644 --- a/python-lib/osisoft_client.py +++ b/python-lib/osisoft_client.py @@ -4,7 +4,6 @@ import json import simplejson from datetime import datetime -from requests_ntlm import HttpNtlmAuth from osisoft_constants import OSIsoftConstants from osisoft_endpoints import OSIsoftEndpoints from osisoft_plugin_common import ( @@ -12,6 +11,7 @@ is_filtered_out, is_server_throttling, escape, epoch_to_iso, iso_to_epoch, RecordsLimit, is_iso8601, get_next_page_url, change_key_in_dict ) +from osisoft_plugin_auth import get_auth from osisoft_pagination import OffsetPagination from safe_logger import SafeLogger @@ -29,7 +29,7 @@ def __init__(self, server_url, auth_type, username, password, is_ssl_check_disab if can_raise: assert_server_url_ok(server_url) self.session = requests.Session() - self.session.auth = self.get_auth(auth_type, username, password) + self.session.auth = get_auth(auth_type, username, password) self.session.verify = (not is_ssl_check_disabled) logger.info("Initialization server_url={}, is_ssl_check_disabled={}".format(server_url, is_ssl_check_disabled)) self.endpoint = OSIsoftEndpoints(server_url) @@ -39,14 +39,6 @@ def __init__(self, server_url, auth_type, username, password, is_ssl_check_disab self.debug_level = None self.network_timer = network_timer - def get_auth(self, auth_type, username, password): - if auth_type == "basic": - return (username, password) - elif auth_type == "ntlm": - return HttpNtlmAuth(username, password) - else: - return None - def recursive_get_rows_from_webid(self, webid, data_type, **kwargs): # Split the time range until no more HTTP 400 kwargs["endpoint_type"] = kwargs.get("endpoint_type", "event_frames") @@ -993,7 +985,7 @@ def close(self): def validate_timestamp(timestamp): - valid_formats=["%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"] + valid_formats = ["%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"] for valid_format in valid_formats: try: datetime.strptime(timestamp, valid_format) diff --git a/python-lib/osisoft_constants.py b/python-lib/osisoft_constants.py index 252094c..2ec3a18 100644 --- a/python-lib/osisoft_constants.py +++ b/python-lib/osisoft_constants.py @@ -405,7 +405,7 @@ class OSIsoftConstants(object): "Security": "{base_url}/eventframes/{webid}/security", "SecurityEntries": "{base_url}/eventframes/{webid}/securityentries" } - PLUGIN_VERSION = "1.4.1" + PLUGIN_VERSION = "1.5.0-beta.2" VALUE_COLUMN_SUFFIX = "_val" WEB_API_PATH = "piwebapi" WRITE_HEADERS = {'X-Requested-With': 'XmlHttpRequest'} diff --git a/python-lib/osisoft_plugin_auth.py b/python-lib/osisoft_plugin_auth.py new file mode 100644 index 0000000..9726448 --- /dev/null +++ b/python-lib/osisoft_plugin_auth.py @@ -0,0 +1,22 @@ +import requests +from requests_ntlm import HttpNtlmAuth + + +class BearerAuth(requests.auth.AuthBase): + def __init__(self, token): + self.token = token + + def __call__(self, request): + request.headers["Authorization"] = "Bearer {}".format(self.token) + return request + + +def get_auth(auth_type, username, password): + if auth_type == "basic": + return (username, password) + elif auth_type == "ntlm": + return HttpNtlmAuth(username, password) + elif auth_type == "bearer_token": + return BearerAuth(password) + else: + return None diff --git a/python-lib/osisoft_plugin_common.py b/python-lib/osisoft_plugin_common.py index 2a160dd..79d4d8a 100644 --- a/python-lib/osisoft_plugin_common.py +++ b/python-lib/osisoft_plugin_common.py @@ -18,16 +18,47 @@ class PISystemConnectorError(ValueError): pass +class ErrorMessage(): + def __init__(self): + self.first_error_message = None + + def add(self, error_message): + if not self.first_error_message: + self.first_error_message = error_message + + def get(self): + return self.first_error_message + + def exists(self): + return self.first_error_message is not None + + def get_credentials(config, can_raise=True): - error_message = None - credentials = config.get('credentials', {}) - auth_type = credentials.get("auth_type", "basic") - osisoft_basic = credentials.get("osisoft_basic", {}) + error_message = ErrorMessage() + credentials_type = config.get("credentials_type", "basic_auth") + if credentials_type == "oauth_sso": + auth_type = "bearer_token" + credentials = config.get("oauth_credentials", {}) + if not credentials: + error_message.add("Pick a credential") + password = credentials.get("secure_token") + if not password: + error_message.add("Incorrect credential. Go to you profile page > Credentials > Your preset, click the connect button and processed to Single Sign On.") + username = None + else: + credentials = config.get('credentials', {}) + if not credentials: + error_message.add("Pick a credential") + auth_type = credentials.get("auth_type", "basic") + osisoft_basic = credentials.get("osisoft_basic", {}) + username = osisoft_basic.get("user") + password = osisoft_basic.get("password") + if not username or not password: + error_message.add("Incorrect credential. Go to you profile page > Credentials > Your preset, click the edit button and fill in you username and password details.") + ssl_cert_path = credentials.get("ssl_cert_path") if ssl_cert_path: setup_ssl_certificate(ssl_cert_path) - username = osisoft_basic.get("user") - password = osisoft_basic.get("password") show_advanced_parameters = config.get('show_advanced_parameters', False) server_url = credentials.get("default_server") is_ssl_check_disabled = False @@ -46,14 +77,16 @@ def get_credentials(config, can_raise=True): else: server_url = overwrite_server_url if (not can_disable_ssl_check) and is_ssl_check_disabled: - error_message = "You cannot disable SSL check on this preset. Please refer to your Dataiku admin" + error_message.add("You cannot disable SSL check on this preset. Please refer to your Dataiku admin") is_ssl_check_disabled = can_disable_ssl_check and is_ssl_check_disabled - if can_raise and error_message: - raise PISystemConnectorError(error_message) + if not server_url: + error_message.add("Fill in the server address") + if can_raise and error_message.exists(): + raise PISystemConnectorError(error_message.get()) if can_raise: return auth_type, username, password, server_url, is_ssl_check_disabled else: - return auth_type, username, password, server_url, is_ssl_check_disabled, error_message + return auth_type, username, password, server_url, is_ssl_check_disabled, error_message.get() def get_advanced_parameters(config): @@ -510,6 +543,13 @@ def get_element_name_from_path(path): return element_name +def assert_correct_config(config): + if "server_name" not in config: + raise PISystemConnectorError("There is no server selected") + if "database_name" not in config: + raise PISystemConnectorError("There is no database selected") + + class RecordsLimit(): def __init__(self, records_limit=-1): self.has_no_limit = (records_limit == -1) diff --git a/resource/browse_attributes.py b/resource/browse_attributes.py index cd39fd1..3d43188 100644 --- a/resource/browse_attributes.py +++ b/resource/browse_attributes.py @@ -9,26 +9,12 @@ def do(payload, config, plugin_config, inputs): config = config.get("config") if "credentials" not in config: return {"choices": [{"label": "Requires DSS v10.0.4 or above. Please use the OSIsoft Search custom dataset instead"}]} - elif config.get("credentials") == {}: - return {"choices": [{"label": "Pick a credential"}]} auth_type, username, password, server_url, is_ssl_check_disabled, credential_error = get_credentials(config, can_raise=False) if credential_error: return build_select_choices(credential_error) - if not (auth_type and username and password): - return build_select_choices("Pick a credential") - - if not username or not password: - return build_select_choices( - "Incorrect credential. " - + "Go to you profile page > Credentials > Your preset, click the edit button and fill in you username and password details." - ) - - if not server_url: - return build_select_choices("Fill in the server address") - is_debug_mode = check_debug_mode(config) client = OSIsoftClient(server_url, auth_type, username, password, is_ssl_check_disabled=is_ssl_check_disabled, is_debug_mode=is_debug_mode)