def check_required_parameters(self: "JwtCredentialsProvider") -> None: if self.role_arn is None or self.role_arn == "": raise InterfaceError("Missing required property: {}".format( JwtCredentialsProvider.KEY_ROLE_ARN)) elif self.jwt is None or self.jwt == "": raise InterfaceError("Missing required property: {}".format( JwtCredentialsProvider.KEY_WEB_IDENTITY_TOKEN))
def get_credentials( self: "AWSCredentialsProvider", ) -> typing.Union[AWSDirectCredentialsHolder, AWSProfileCredentialsHolder]: """ Retrieves a :class`ABCCredentialsHolder` from cache or builds one. Returns ------- An `AWSCredentialsHolder` object containing end-user specified AWS credential information: :class`ABCAWSCredentialsHolder` """ key: int = self.get_cache_key() if key not in self.cache: try: self.refresh() except Exception as e: _logger.error("refresh failed: {}".format(str(e))) raise InterfaceError(e) credentials: typing.Union[ AWSDirectCredentialsHolder, AWSProfileCredentialsHolder] = self.cache[key] if credentials is None: raise InterfaceError("Unable to load AWS credentials") return credentials
def check_required_parameters(self: "SamlCredentialsProvider") -> None: if self.user_name == "" or self.user_name is None: raise InterfaceError("Missing required property: user_name") if self.password == "" or self.password is None: raise InterfaceError("Missing required property: password") if self.idp_host == "" or self.idp_host is None: raise InterfaceError("Missing required property: idp_host")
def run_server(self: "BrowserAzureCredentialsProvider", listen_socket: socket.socket, idp_response_timeout: int, state: str) -> str: conn, addr = listen_socket.accept() conn.settimeout(float(idp_response_timeout)) size: int = 102400 with conn: while True: part: bytes = conn.recv(size) decoded_part = part.decode() state_idx: int = decoded_part.find("state=") if state_idx > -1: received_state: str = decoded_part[state_idx + 6:decoded_part. find("&", state_idx)] if received_state != state: raise InterfaceError( "Incoming state {received} does not match the outgoing state {expected}" .format(received=received_state, expected=state)) code_idx: int = decoded_part.find("code=") if code_idx < 0: raise InterfaceError("No code found") received_code: str = decoded_part[code_idx + 5:decoded_part. find("&", code_idx)] if received_code == "": raise InterfaceError("No valid code found") conn.send(self.close_window_http_resp()) return received_code
def get_saml_assertion(self: "BrowserAzureCredentialsProvider") -> str: if self.idp_tenant == "" or self.idp_tenant is None: raise InterfaceError("Missing required property: idp_tenant") if self.client_id == "" or self.client_id is None: raise InterfaceError("Missing required property: client_id") if self.idp_response_timeout < 10: raise InterfaceError( "idp_response_timeout should be 10 seconds or greater.") listen_socket: socket.socket = self.get_listen_socket() self.redirectUri = "http://localhost:{port}/redshift/".format( port=self.listen_port) _logger.debug("Listening for connection on port {}".format( self.listen_port)) try: token: str = self.fetch_authorization_token(listen_socket) saml_assertion: str = self.fetch_saml_response(token) except Exception as e: raise e finally: listen_socket.close() _logger.debug("Got SAML assertion") return self.wrap_and_encode_assertion(saml_assertion)
def run_server( self: "BrowserAzureOAuth2CredentialsProvider", listen_socket: socket.socket, idp_response_timeout: int, state: int, ): """ Runs a server on localhost to listen for the IdP's response to our HTTP POST request for JWT assertion. Parameters ---------- :param listen_socket: socket.socket The socket on which the method listens for a response :param idp_response_timeout: int The maximum time to listen on the socket, specified in seconds :param state: str The state generated by the client. This must match the state received from the IdP server Returns ------- The IdP's response, including JWT assertion """ conn, addr = listen_socket.accept() conn.settimeout(float(idp_response_timeout)) size: int = 102400 with conn: while True: part: bytes = conn.recv(size) decoded_part = part.decode() state_idx: int = decoded_part.find( "{}=".format(BrowserAzureOAuth2CredentialsProvider. OAuthParamNames.STATE.value)) if state_idx > -1: received_state: str = decoded_part[state_idx + 6:decoded_part. find("&", state_idx)] if received_state != state: raise InterfaceError( "Incoming state {received} does not match the outgoing state {expected}" .format(received=received_state, expected=state)) code_idx: int = decoded_part.find( "{}=".format(BrowserAzureOAuth2CredentialsProvider. OAuthParamNames.IDP_CODE.value)) if code_idx < 0: raise InterfaceError("No code found") received_code: str = decoded_part[code_idx + 5:state_idx - 1] if received_code == "": raise InterfaceError("No valid code found") conn.send(self.close_window_http_resp()) return received_code
def get_saml_assertion(self: "BrowserSamlCredentialsProvider") -> str: if self.login_url == "" or self.login_url is None: raise InterfaceError("Missing required property: login_url") if self.idp_response_timeout < 10: raise InterfaceError("idp_response_timeout should be 10 seconds or greater.") if self.listen_port < 1 or self.listen_port > 65535: raise InterfaceError("Invalid property value: listen_port") return self.authenticate()
def get_credentials(self: "SamlCredentialsProvider") -> CredentialsHolder: key: str = self.get_cache_key() if key not in self.cache or self.cache[key].is_expired(): try: self.refresh() except Exception as e: _logger.error("refresh failed: {}".format(str(e))) raise InterfaceError(e) if key not in self.cache or self.cache[key] is None: raise InterfaceError("Unable to load AWS credentials from IDP") return self.cache[key]
def check_required_parameters( self: "BrowserAzureOAuth2CredentialsProvider") -> None: super().check_required_parameters() if not self.idp_tenant: raise InterfaceError( "BrowserAzureOauth2CredentialsProvider requires idp_tenant") if not self.client_id: raise InterfaceError( "BrowserAzureOauth2CredentialsProvider requires client_id") if not self.idp_response_timeout or self.idp_response_timeout < 10: raise InterfaceError( "BrowserAzureOauth2CredentialsProvider requires idp_response_timeout to be 10 seconds or greater" )
def fetch_saml_response(self: "BrowserAzureCredentialsProvider", token): url: str = "https://login.microsoftonline.com/{tenant}/oauth2/token".format( tenant=self.idp_tenant) # headers to pass with POST request headers: typing.Dict[str, str] = azure_headers # required parameters to pass in POST body payload: typing.Dict[str, typing.Optional[str]] = { "code": token, "requested_token_type": "urn:ietf:params:oauth:token-type:saml2", "grant_type": "authorization_code", "scope": "openid", "resource": self.client_id, "client_id": self.client_id, "client_secret": self.client_secret, "redirect_uri": self.redirectUri, } try: response = requests.post(url, data=payload, headers=headers) response.raise_for_status() except requests.exceptions.HTTPError as e: logger.error( "Request for authentication from Microsoft was unsuccessful. {}" .format(str(e))) raise InterfaceError(e) except requests.exceptions.Timeout as e: logger.error( "A timeout occurred when requesting authentication from Azure") raise InterfaceError(e) except requests.exceptions.TooManyRedirects as e: logger.error( "A error occurred when requesting authentication from Azure. Verify RedshiftProperties are correct" ) raise InterfaceError(e) except requests.exceptions.RequestException as e: logger.error( "A unknown error occurred when requesting authentication from Azure" ) raise InterfaceError(e) try: saml_assertion: str = response.json()["access_token"] except TypeError as e: logger.error("Failed to decode saml assertion returned from Azure") raise InterfaceError(e) except KeyError as e: logger.error("Azure access_token was not found in saml assertion") raise InterfaceError(e) except Exception as e: raise InterfaceError(e) if saml_assertion == "": raise InterfaceError("Azure access_token is empty") missing_padding: int = 4 - len(saml_assertion) % 4 if missing_padding: saml_assertion += "=" * missing_padding return str(base64.urlsafe_b64decode(saml_assertion))
def set_iam_properties(info: RedshiftProperty) -> RedshiftProperty: """ Helper function to handle connection properties and ensure required parameters are specified. Parameters """ # set properties present for both IAM, Native authentication IamHelper.set_auth_properties(info) if info.is_serverless_host and info.iam: raise ProgrammingError("This feature is not yet available") # if Version(pkg_resources.get_distribution("boto3").version) <= Version("1.20.22"): # raise pkg_resources.VersionConflict( # "boto3 >= XXX required for authentication with Amazon Redshift serverless. " # "Please upgrade the installed version of boto3 to use this functionality." # ) if info.is_serverless_host: info.set_account_id_from_host() info.set_region_from_host() if info.iam is True: if info.cluster_identifier is None and not info.is_serverless_host: raise InterfaceError( "Invalid connection property setting. cluster_identifier must be provided when IAM is enabled" ) IamHelper.set_iam_credentials(info) # Check for Browser based OAuth Native authentication NativeAuthPluginHelper.set_native_auth_plugin_properties(info) return info
def open_browser(self: "BrowserSamlCredentialsProvider") -> None: import webbrowser url: typing.Optional[str] = self.login_url if url is None: raise InterfaceError("the login_url could not be empty") webbrowser.open(url)
def test_redshiftconnector_get_status_with_error_port(mock_port, redshift_connector): mock_port.side_effect = InterfaceError('error mock') result = redshift_connector.get_status() assert type(result.error) == str assert result.status is False assert str(result.error) == 'error mock'
def get_saml_assertion(self: "OktaCredentialsProvider") -> str: self.check_required_parameters() if self.app_id == "" or self.app_id is None: raise InterfaceError("Missing required property: app_id") okta_session_token: str = self.okta_authentication() return self.handle_saml_assertion(okta_session_token)
def get_saml_assertion(self: "AzureCredentialsProvider") -> str: # idp_tenant, client_secret, and client_id are # all required parameters to be able to authenticate with Microsoft Azure. # user and password are also required and need to be set to the username and password of the # Microsoft Azure account that is logging in. if self.user_name == "" or self.user_name is None: raise InterfaceError("Missing required property: user_name") if self.password == "" or self.password is None: raise InterfaceError("Missing required property: password") if self.idp_tenant == "" or self.idp_tenant is None: raise InterfaceError("Missing required property: idp_tenant") if self.client_secret == "" or self.client_secret is None: raise InterfaceError("Missing required property: client_secret") if self.client_id == "" or self.client_id is None: raise InterfaceError("Missing required property: client_id") return self.azure_oauth_based_authentication()
def handle_saml_assertion(self: "OktaCredentialsProvider", okta_session_token: str) -> str: import bs4 # type: ignore import requests url: str = "https://{host}/home/{app_name}/{app_id}?onetimetoken={session_token}".format( host=self.idp_host, app_name=self.app_name, app_id=self.app_id, session_token=okta_session_token) try: response: "requests.Response" = requests.get( url, verify=self.do_verify_ssl_cert()) response.raise_for_status() except requests.exceptions.HTTPError as e: _logger.error( "Request for SAML assertion from Okta was unsuccessful. {}". format(str(e))) raise InterfaceError(e) except requests.exceptions.Timeout as e: _logger.error( "A timeout occurred when requesting SAML assertion from Okta") raise InterfaceError(e) except requests.exceptions.TooManyRedirects as e: _logger.error( "A error occurred when requesting SAML assertion from Okta. Verify RedshiftProperties are correct" ) raise InterfaceError(e) except requests.exceptions.RequestException as e: _logger.error( "A unknown error occurred when requesting SAML assertion from Okta" ) raise InterfaceError(e) text: str = response.text try: soup = bs4.BeautifulSoup(text, "html.parser") saml_response: str = soup.find("input", {"name": "SAMLResponse"})["value"] return saml_response except Exception as e: _logger.error( "An error occurred while parsing SAML response: {}".format( str(e))) raise InterfaceError(e)
def get_saml_assertion( self: "AdfsCredentialsProvider") -> typing.Optional[str]: if self.idp_host == "" or self.idp_host is None: raise InterfaceError("Missing required property: idp_host") if self.user_name == "" or self.user_name is None or self.password == "" or self.password is None: return self.windows_integrated_authentication() return self.form_based_authentication()
def extract_jwt_assertion(self: "BrowserAzureOAuth2CredentialsProvider", content: str) -> str: """ Returns encoded JWT assertion extracted from IdP response content """ import json response_content: typing.Dict[str, str] = json.loads(content) if "access_token" not in response_content: raise InterfaceError("Failed to find access_token") encoded_jwt_assertion: str = response_content["access_token"] if not encoded_jwt_assertion: raise InterfaceError("Invalid access_token value") return encoded_jwt_assertion
def get_credentials(self: "SamlCredentialsProvider") -> CredentialsHolder: key: str = self.get_cache_key() if key not in self.cache or self.cache[key].is_expired(): try: self.refresh() except Exception as e: logger.error("refresh failed: {}".format(str(e))) raise InterfaceError(e) # if the SAML response has db_user argument, it will be picked up at this point. credentials: CredentialsHolder = self.cache[key] if credentials is None: raise InterfaceError("Unable to load AWS credentials from IDP") # if db_user argument has been passed in the connection string, add it to metadata. if self.db_user: credentials.metadata.set_db_user(self.db_user) return credentials
def read_auth_profile( auth_profile: str, iam_access_key_id: str, iam_secret_key: str, iam_session_token: typing.Optional[str], info: RedshiftProperty, ) -> RedshiftProperty: import json import boto3 from botocore.exceptions import ClientError # 1st phase - authenticate with boto3 client for Amazon Redshift via IAM # credentials provided by end user creds: typing.Dict[str, str] = { "aws_access_key_id": iam_access_key_id, "aws_secret_access_key": iam_secret_key, "region_name": typing.cast(str, info.region), } for opt_key, opt_val in ( ("aws_session_token", iam_session_token), ("endpoint_url", info.endpoint_url), ): if opt_val is not None and opt_val != "": creds[opt_key] = opt_val try: _logger.debug("Initial authentication with boto3...") client = boto3.client(service_name="redshift", **creds) _logger.debug("Requesting authentication profiles") # 2nd phase - request Amazon Redshift authentication profiles and record contents for retrieving # temporary credentials for the Amazon Redshift cluster specified by end user response = client.describe_authentication_profiles( AuthenticationProfileName=auth_profile) except ClientError: raise InterfaceError( "Unable to retrieve contents of Redshift authentication profile from server" ) _logger.debug("Received {} authentication profiles".format( len(response["AuthenticationProfiles"]))) # the first matching authentication profile will be used profile_content: typing.Union[str] = response[ "AuthenticationProfiles"][0]["AuthenticationProfileContent"] try: profile_content_dict: typing.Dict = json.loads(profile_content) return RedshiftProperty(**profile_content_dict) except ValueError: raise ProgrammingError( "Unable to decode the JSON content of the Redshift authentication profile: {}" .format(auth_profile))
def refresh(self: "JwtCredentialsProvider") -> None: import boto3 # type: ignore client = boto3.client("sts") try: _logger.debug("JWT: {}".format(self.jwt)) if self.jwt is None: raise InterfaceError("Unable to refresh, no jwt provided") jwt: str = self.process_jwt(self.jwt) decoded_jwt: typing.Optional[typing.List[typing.Union[ str, bytes]]] = self.decode_jwt(self.jwt) response = client.assume_role_with_web_identity( RoleArn=self.role_arn, RoleSessionName=self.role_session_name, WebIdentityToken=jwt, DurationSeconds=self.duration if (self.duration is not None) and (self.duration > 0) else None, ) stscred: typing.Dict[str, typing.Any] = response["Credentials"] credentials: CredentialsHolder = CredentialsHolder(stscred) key: str = self.get_cache_key() self.cache[key] = credentials except client.exceptions.MalformedPolicyDocumentException as e: _logger.error("MalformedPolicyDocumentException: %s", e) raise e except client.exceptions.PackedPolicyTooLargeException as e: _logger.error("PackedPolicyTooLargeException: %s", e) raise e except client.exceptions.IDPRejectedClaimException as e: _logger.error("IDPRejectedClaimException: %s", e) raise e except client.exceptions.InvalidIdentityTokenException as e: _logger.error("InvalidIdentityTokenException: %s", e) raise e except client.exceptions.ExpiredTokenException as e: _logger.error("ExpiredTokenException: %s", e) raise e except client.exceptions.RegionDisabledException as e: _logger.error("RegionDisabledException: %s", e) raise e except Exception as e: _logger.error("other Exception: %s", e) raise e
def okta_authentication(self: "OktaCredentialsProvider") -> str: import requests # HTTP Post request to Okta API for session token url: str = "https://{host}/api/v1/authn".format(host=self.idp_host) headers: typing.Dict[str, str] = okta_headers payload: typing.Dict[str, typing.Optional[str]] = { "username": self.user_name, "password": self.password } try: response: "requests.Response" = requests.post( url, data=json.dumps(payload), headers=headers, verify=self.do_verify_ssl_cert()) response.raise_for_status() except requests.exceptions.HTTPError as e: _logger.error( "Request for authentication from Okta was unsuccessful. {}". format(str(e))) raise InterfaceError(e) except requests.exceptions.Timeout as e: _logger.error( "A timeout occurred when requesting authentication from Okta") raise InterfaceError(e) except requests.exceptions.TooManyRedirects as e: _logger.error( "A error occurred when requesting authentication from Okta. Verify RedshiftProperties are correct" ) raise InterfaceError(e) except requests.exceptions.RequestException as e: _logger.error( "A unknown error occurred when requesting authentication from Okta" ) raise InterfaceError(e) # Retrieve and parse the Okta response for session token if response is None: raise InterfaceError( "Request for authentication returned empty payload") response_payload: typing.Dict[str, typing.Any] = response.json() if "status" not in response_payload: raise InterfaceError( "Request for authentication retrieved malformed payload.") elif response_payload["status"] != "SUCCESS": raise InterfaceError( "Request for authentication received non success response.") else: return str(response_payload["sessionToken"])
def refresh(self: "JwtCredentialsProvider") -> None: jwt: str = self.get_jwt_assertion() _logger.debug("JWT: {}".format(jwt)) if jwt is None: raise InterfaceError("Unable to refresh, no jwt provided") credentials: NativeTokenHolder = NativeTokenHolder(access_token=jwt, expiration=None) credentials.refresh = True _logger.debug("disable_cache={}".format(str(self.disable_cache))) if not self.disable_cache: self.cache[self.get_cache_key()] = credentials else: self.last_refreshed_credentials = credentials
def open_browser(self: "BrowserAzureOAuth2CredentialsProvider", state: str) -> None: """ Opens the default browser to allow user authentication with the IdP Parameters ---------- :param state: str The state generated by the client Returns ------- None """ import webbrowser url: str = self.get_authorization_token_url(state=state) _logger.debug("SSO URI: {}".format(url)) if url is None: raise InterfaceError("the login_url could not be empty") webbrowser.open(url)
def refresh(self: "SamlCredentialsProvider") -> None: import boto3 # type: ignore import bs4 # type: ignore try: # get SAML assertion from specific identity provider saml_assertion = self.get_saml_assertion() except Exception as e: _logger.error("Get saml assertion failed: {}".format(str(e))) raise InterfaceError(e) # decode SAML assertion into xml format doc: bytes = base64.b64decode(saml_assertion) soup = bs4.BeautifulSoup(doc, "xml") attrs = soup.findAll("Attribute") # extract RoleArn adn PrincipleArn from SAML assertion role_pattern = re.compile(r"arn:aws:iam::\d*:role/\S+") provider_pattern = re.compile(r"arn:aws:iam::\d*:saml-provider/\S+") roles: typing.Dict[str, str] = {} for attr in attrs: name: str = attr.attrs["Name"] values: typing.Any = attr.findAll("AttributeValue") if name == "https://aws.amazon.com/SAML/Attributes/Role": for value in values: arns = value.contents[0].split(",") role: str = "" provider: str = "" for arn in arns: arn = arn.strip( ) # remove trailing or leading whitespace if role_pattern.match(arn): role = arn if provider_pattern.match(arn): provider = arn if role != "" and provider != "": roles[role] = provider if len(roles) == 0: raise InterfaceError("No role found in SamlAssertion") role_arn: str = "" principle: str = "" if self.preferred_role: role_arn = self.preferred_role if role_arn not in roles: raise InterfaceError( "Preferred role not found in SamlAssertion") principle = roles[role_arn] else: role_arn = random.choice(list(roles)) principle = roles[role_arn] client = boto3.client("sts") try: response = client.assume_role_with_saml( RoleArn=role_arn, # self.preferred_role, PrincipalArn=principle, # self.principal, SAMLAssertion=saml_assertion, ) stscred: typing.Dict[str, typing.Any] = response["Credentials"] credentials: CredentialsHolder = CredentialsHolder(stscred) # get metadata from SAML assertion credentials.set_metadata(self.read_metadata(doc)) key: str = self.get_cache_key() self.cache[key] = credentials except AttributeError as e: _logger.error("AttributeError: %s", e) raise e except KeyError as e: _logger.error("KeyError: %s", e) raise e except client.exceptions.MalformedPolicyDocumentException as e: _logger.error("MalformedPolicyDocumentException: %s", e) raise e except client.exceptions.PackedPolicyTooLargeException as e: _logger.error("PackedPolicyTooLargeException: %s", e) raise e except client.exceptions.IDPRejectedClaimException as e: _logger.error("IDPRejectedClaimException: %s", e) raise e except client.exceptions.InvalidIdentityTokenException as e: _logger.error("InvalidIdentityTokenException: %s", e) raise e except client.exceptions.ExpiredTokenException as e: _logger.error("ExpiredTokenException: %s", e) raise e except client.exceptions.RegionDisabledException as e: _logger.error("RegionDisabledException: %s", e) raise e except Exception as e: _logger.error("Other Exception: %s", e) raise e
def set_iam_properties( info: RedshiftProperty, user: str, host: str, database: str, port: int, password: str, source_address: typing.Optional[str], unix_sock: typing.Optional[str], ssl: bool, sslmode: str, timeout: typing.Optional[int], max_prepared_statements: int, tcp_keepalive: bool, application_name: typing.Optional[str], replication: typing.Optional[str], idp_host: typing.Optional[str], db_user: typing.Optional[str], iam: bool, app_id: typing.Optional[str], app_name: str, preferred_role: typing.Optional[str], principal_arn: typing.Optional[str], credentials_provider: typing.Optional[str], region: typing.Optional[str], cluster_identifier: typing.Optional[str], client_id: typing.Optional[str], idp_tenant: typing.Optional[str], client_secret: typing.Optional[str], partner_sp_id: typing.Optional[str], idp_response_timeout: int, listen_port: int, login_url: typing.Optional[str], auto_create: bool, db_groups: typing.Optional[typing.List[str]], force_lowercase: bool, allow_db_user_override: bool, ) -> None: if info is None: raise InterfaceError( "Invalid connection property setting. info must be specified") # IAM requires an SSL connection to work. # Make sure that is set to SSL level VERIFY_CA or higher. info.ssl = ssl if info.ssl is True: if sslmode == SSLMode.VERIFY_CA.value: info.sslmode = SSLMode.VERIFY_CA.value elif sslmode == SSLMode.VERIFY_FULL.value: info.sslmode = SSLMode.VERIFY_FULL.value else: info.sslmode = SSLMode.VERIFY_CA.value else: info.sslmode = "" if (info.ssl is False) and (iam is True): raise InterfaceError( "Invalid connection property setting. SSL must be enabled when using IAM" ) else: info.iam = iam if (info.iam is False) and (credentials_provider is not None): raise InterfaceError( "Invalid connection property setting. IAM must be enabled when using credentials " "via identity provider") elif (info.iam is True) and (credentials_provider is None): raise InterfaceError( "Invalid connection property setting. " "Credentials provider cannot be None when IAM is enabled") else: info.credentials_provider = credentials_provider if user is None: raise InterfaceError( "Invalid connection property setting. user must be specified") if host is None: raise InterfaceError( "Invalid connection property setting. host must be specified") if database is None: raise InterfaceError( "Invalid connection property setting. database must be specified") if port is None: raise InterfaceError( "Invalid connection property setting. port must be specified") if password is None: raise InterfaceError( "Invalid connection property setting. password must be specified") # basic driver parameters info.user_name = user info.host = host info.db_name = database info.port = port info.password = password info.source_address = source_address info.unix_sock = unix_sock info.timeout = timeout info.max_prepared_statements = max_prepared_statements info.tcp_keepalive = tcp_keepalive info.application_name = application_name info.replication = replication # Idp parameters info.idp_host = idp_host info.db_user = db_user info.app_id = app_id info.app_name = app_name info.preferred_role = preferred_role info.principal = principal_arn # Regions.fromName(string) requires the string to be lower case and in this format: # E.g. "us-west-2" info.region = region # cluster_identifier parameter is required info.cluster_identifier = cluster_identifier info.auto_create = auto_create info.db_groups = db_groups info.force_lowercase = force_lowercase info.allow_db_user_override = allow_db_user_override # Azure specified parameters info.client_id = client_id info.idp_tenant = idp_tenant info.client_secret = client_secret # Browser idp parameters info.idp_response_timeout = idp_response_timeout info.listen_port = listen_port info.login_url = login_url info.partner_sp_id = partner_sp_id if info.iam is True: set_iam_credentials(info) else: return
def azure_oauth_based_authentication( self: "AzureCredentialsProvider") -> str: import requests # endpoint to connect with Microsoft Azure to get SAML Assertion token url: str = "https://login.microsoftonline.com/{tenant}/oauth2/token".format( tenant=self.idp_tenant) # headers to pass with POST request headers: typing.Dict[str, str] = azure_headers # required parameters to pass in POST body payload: typing.Dict[str, typing.Optional[str]] = { "grant_type": "password", "requested_token_type": "urn:ietf:params:oauth:token-type:saml2", "username": self.user_name, "password": self.password, "client_secret": self.client_secret, "client_id": self.client_id, "resource": self.client_id, } try: response: "requests.Response" = requests.post( url, data=payload, headers=headers, verify=self.do_verify_ssl_cert()) response.raise_for_status() except requests.exceptions.HTTPError as e: if "response" in vars(): _logger.debug( "azure_oauth_based_authentication https response: {}". format(response.text) # type: ignore ) else: _logger.debug( "azure_oauth_based_authentication could not receive https response due to an error" ) _logger.error( "Request for authentication from Azure was unsuccessful. {}". format(str(e))) raise InterfaceError(e) except requests.exceptions.Timeout as e: _logger.error( "A timeout occurred when requesting authentication from Azure") raise InterfaceError(e) except requests.exceptions.TooManyRedirects as e: _logger.error( "A error occurred when requesting authentication from Azure. Verify RedshiftProperties are correct" ) raise InterfaceError(e) except requests.exceptions.RequestException as e: _logger.error( "A unknown error occurred when requesting authentication from Azure." ) raise InterfaceError(e) # parse the JSON response to grab access_token field which contains Base64 encoded SAML # Assertion and decode it saml_assertion: str = "" try: saml_assertion = response.json()["access_token"] except Exception as e: _logger.error( "Failed to authenticate with Azure. Response from Azure did not include access_token." ) raise InterfaceError(e) if saml_assertion == "": raise InterfaceError("Azure access_token is empty") missing_padding: int = 4 - len(saml_assertion) % 4 if missing_padding: saml_assertion += "=" * missing_padding # decode the SAML Assertion to a String to add XML tags to form a SAML Response decoded_saml_assertion: str = "" try: decoded_saml_assertion = str( base64.urlsafe_b64decode(saml_assertion)) except TypeError as e: _logger.error( "Failed to decode saml assertion returned from Azure") raise InterfaceError(e) # SAML Response is required to be sent to base class. We need to provide a minimum of: # 1) samlp:Response XML tag with xmlns:samlp protocol value # 2) samlp:Status XML tag and samlpStatusCode XML tag with Value indicating Success # 3) followed by Signed SAML Assertion saml_response: str = ( '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">' "<samlp:Status>" '<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>' "</samlp:Status>" "{decoded_saml_assertion}" "</samlp:Response>".format( decoded_saml_assertion=decoded_saml_assertion[2:-1])) # re-encode the SAML Response in Base64 and return this to the base class saml_response = str(base64.b64encode( saml_response.encode("utf-8")))[2:-1] return saml_response
def connect( user: typing.Optional[str] = None, database: typing.Optional[str] = None, password: typing.Optional[str] = None, port: typing.Optional[int] = None, host: typing.Optional[str] = None, source_address: typing.Optional[str] = None, unix_sock: typing.Optional[str] = None, ssl: typing.Optional[bool] = None, sslmode: typing.Optional[str] = None, timeout: typing.Optional[int] = None, max_prepared_statements: typing.Optional[int] = None, tcp_keepalive: typing.Optional[bool] = None, application_name: typing.Optional[str] = None, replication: typing.Optional[str] = None, idp_host: typing.Optional[str] = None, db_user: typing.Optional[str] = None, app_id: typing.Optional[str] = None, app_name: typing.Optional[str] = None, preferred_role: typing.Optional[str] = None, principal_arn: typing.Optional[str] = None, access_key_id: typing.Optional[str] = None, secret_access_key: typing.Optional[str] = None, session_token: typing.Optional[str] = None, profile: typing.Optional[str] = None, credentials_provider: typing.Optional[str] = None, region: typing.Optional[str] = None, cluster_identifier: typing.Optional[str] = None, iam: typing.Optional[bool] = None, client_id: typing.Optional[str] = None, idp_tenant: typing.Optional[str] = None, client_secret: typing.Optional[str] = None, partner_sp_id: typing.Optional[str] = None, idp_response_timeout: typing.Optional[int] = None, listen_port: typing.Optional[int] = None, login_url: typing.Optional[str] = None, auto_create: typing.Optional[bool] = None, db_groups: typing.Optional[typing.List[str]] = None, force_lowercase: typing.Optional[bool] = None, allow_db_user_override: typing.Optional[bool] = None, client_protocol_version: typing.Optional[int] = None, database_metadata_current_db_only: typing.Optional[bool] = None, ssl_insecure: typing.Optional[bool] = None, web_identity_token: typing.Optional[str] = None, role_session_name: typing.Optional[str] = None, role_arn: typing.Optional[str] = None, iam_disable_cache: typing.Optional[bool] = None, auth_profile: typing.Optional[str] = None, endpoint_url: typing.Optional[str] = None, provider_name: typing.Optional[str] = None, scope: typing.Optional[str] = None, ) -> Connection: """ Establishes a :class:`Connection` to an Amazon Redshift cluster. This function validates user input, optionally authenticates using an identity provider plugin, then constructs a :class:`Connection` object. Parameters ---------- user : Optional[str] The username to use for authentication with the Amazon Redshift cluster. password : Optional[str] The password to use for authentication with the Amazon Redshift cluster. database : Optional[str] The name of the database instance to connect to. host : Optional[str] The hostname of the Amazon Redshift cluster. port : Optional[int] The port number of the Amazon Redshift cluster. Default value is 5439. source_address : typing.Optional[str] unix_sock : Optional[str] ssl : Optional[bool] Is SSL enabled. Default value is ``True``. SSL must be enabled when authenticating using IAM. sslmode : Optional[str] The security of the connection to the Amazon Redshift cluster. 'verify-ca' and 'verify-full' are supported. timeout : Optional[int] The number of seconds before the connection to the server will timeout. By default there is no timeout. max_prepared_statements : Optional[int] tcp_keepalive : Optional[bool] Is `TCP keepalive <https://en.wikipedia.org/wiki/Keepalive#TCP_keepalive>`_ used. The default value is ``True``. application_name : Optional[str] Sets the application name. The default value is None. replication : Optional[str] Used to run in `streaming replication mode <https://www.postgresql.org/docs/12/protocol-replication.html>`_. idp_host : Optional[str] The hostname of the IdP. db_user : Optional[str] The user ID to use with Amazon Redshift app_id : Optional[str] app_name : Optional[str] The name of the identity provider (IdP) application used for authentication. preferred_role : Optional[str] The IAM role preferred for the current connection. principal_arn : Optional[str] The ARN of the IAM entity (user or role) for which you are generating a policy. credentials_provider : Optional[str] The class name of the IdP that will be used for authenticating with the Amazon Redshift cluster. region : Optional[str] The AWS region where the Amazon Redshift cluster is located. cluster_identifier : Optional[str] The cluster identifier of the Amazon Redshift cluster. iam : Optional[bool] If IAM authentication is enabled. Default value is False. IAM must be True when authenticating using an IdP. client_id : Optional[str] The client id from Azure IdP. idp_tenant : Optional[str] The IdP tenant. client_secret : Optional[str] The client secret from Azure IdP. partner_sp_id : Optional[str] The Partner SP Id used for authentication with Ping. idp_response_timeout : Optional[int] The timeout for retrieving SAML assertion from IdP. Default value is `120`. listen_port : Optional[int] The listen port the IdP will send the SAML assertion to. Default value is `7890`. login_url : Optional[str] The SSO url for the IdP. auto_create : Optional[bool] Indicates whether the user should be created if they do not exist. Default value is `False`. db_groups : Optional[str] A comma-separated list of existing database group names that the `db_user` joins for the current session. force_lowercase : Optional[bool] allow_db_user_override : Optional[bool] Specifies if the driver uses the `db_user` value from the SAML assertion. TDefault value is `False`. client_protocol_version : Optional[int] The requested server protocol version. The default value is 2 representing `BINARY`. If the requested server protocol cannot be satisfied a warning will be displayed to the user and the driver will default to the highest supported protocol. See `ClientProtocolVersion` for more details. database_metadata_current_db_only : Optional[bool] Is `datashare <https://docs.aws.amazon.com/redshift/latest/dg/datashare-overview.html>`_ disabled. Default value is True, implying datasharing will not be used. ssl_insecure : Optional[bool] Specifies if IdP host's server certificate will be verified. Default value is True web_identity_token: Optional[str] A web identity token used for authentication with JWT. role_session_name: Optional[str] An identifier for the assumed role session used for authentication with JWT. role_arn: Optional[str] The role ARN used for authentication with JWT. This parameter is required when using a JWTCredentialsProvider. iam_disable_cache: Optional[bool] This option specifies whether the IAM credentials are cached. By default caching is enabled. auth_profile: Optional[str] The name of an Amazon Redshift Authentication profile having connection properties as JSON. See :class:RedshiftProperty to learn how connection properties should be named. endpoint_url: Optional[str] The Amazon Redshift endpoint url. This option is only used by AWS internal teams. provider_name: Optional[str] The name of the Redshift Native Auth Provider. scope: Optional[str] Scope for BrowserAzureOauth2CredentialsProvider authentication. Returns ------- A Connection object associated with the specified Amazon Redshift cluster: :class:`Connection` """ info: RedshiftProperty = RedshiftProperty() info.put("access_key_id", access_key_id) info.put("allow_db_user_override", allow_db_user_override) info.put("app_id", app_id) info.put("app_name", app_name) info.put("application_name", application_name) info.put("auth_profile", auth_profile) info.put("auto_create", auto_create) info.put("client_id", client_id) info.put("client_protocol_version", client_protocol_version) info.put("client_secret", client_secret) info.put("cluster_identifier", cluster_identifier) info.put("credentials_provider", credentials_provider) info.put("database_metadata_current_db_only", database_metadata_current_db_only) info.put("db_groups", db_groups) info.put("db_name", database) info.put("db_user", db_user) info.put("endpoint_url", endpoint_url) info.put("force_lowercase", force_lowercase) info.put("host", host) info.put("iam", iam) info.put("iam_disable_cache", iam_disable_cache) info.put("idp_host", idp_host) info.put("idp_response_timeout", idp_response_timeout) info.put("idp_tenant", idp_tenant) info.put("listen_port", listen_port) info.put("login_url", login_url) info.put("max_prepared_statements", max_prepared_statements) info.put("partner_sp_id", partner_sp_id) info.put("password", password) info.put("port", port) info.put("preferred_role", preferred_role) info.put("principal", principal_arn) info.put("profile", profile) info.put("provider_name", provider_name) info.put("region", region) info.put("replication", replication) info.put("role_arn", role_arn) info.put("role_session_name", role_session_name) info.put("scope", scope) info.put("secret_access_key", secret_access_key) info.put("session_token", session_token) info.put("source_address", source_address) info.put("ssl", ssl) info.put("ssl_insecure", ssl_insecure) info.put("sslmode", sslmode) info.put("tcp_keepalive", tcp_keepalive) info.put("timeout", timeout) info.put("unix_sock", unix_sock) info.put("user_name", user) info.put("web_identity_token", web_identity_token) _logger.debug(make_divider_block()) _logger.debug("User provided connection arguments") _logger.debug(make_divider_block()) _logger.debug(mask_secure_info_in_props(info).__str__()) _logger.debug(make_divider_block()) if (info.ssl is False) and (info.iam is True): raise InterfaceError( "Invalid connection property setting. SSL must be enabled when using IAM" ) if (info.iam is False) and (info.ssl_insecure is False): raise InterfaceError( "Invalid connection property setting. IAM must be enabled when using ssl_insecure" ) if info.client_protocol_version not in ClientProtocolVersion.list(): raise InterfaceError( "Invalid connection property setting. client_protocol_version must be in: {}" .format(ClientProtocolVersion.list())) redshift_native_auth: bool = False if info.iam: if info.credentials_provider == "BasicJwtCredentialsProvider": redshift_native_auth = True _logger.debug("redshift_native_auth enabled") if not redshift_native_auth: IamHelper.set_iam_properties(info) _logger.debug(make_divider_block()) _logger.debug( "Connection arguments following validation and IAM auth (if applicable)" ) _logger.debug(make_divider_block()) _logger.debug(mask_secure_info_in_props(info).__str__()) _logger.debug(make_divider_block()) return Connection( user=info.user_name, host=info.host, database=info.db_name, port=info.port, password=info.password, source_address=info.source_address, unix_sock=info.unix_sock, ssl=info.ssl, sslmode=info.sslmode, timeout=info.timeout, max_prepared_statements=info.max_prepared_statements, tcp_keepalive=info.tcp_keepalive, application_name=info.application_name, replication=info.replication, client_protocol_version=info.client_protocol_version, database_metadata_current_db_only=info. database_metadata_current_db_only, credentials_provider=info.credentials_provider, provider_name=info.provider_name, web_identity_token=info.web_identity_token, )
def form_based_authentication(self: "AdfsCredentialsProvider") -> str: import bs4 # type: ignore import requests url: str = "https://{host}:{port}/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=urn:amazon:webservices".format( host=self.idp_host, port=str(self.idpPort)) try: response: "requests.Response" = requests.get( url, verify=self.do_verify_ssl_cert()) response.raise_for_status() except requests.exceptions.HTTPError as e: if "response" in vars(): _logger.debug( "form_based_authentication https response: {}".format( response.text)) # type: ignore else: _logger.debug( "form_based_authentication could not receive https response due to an error" ) _logger.error( "Request for SAML assertion when refreshing credentials was unsuccessful. {}" .format(str(e))) raise InterfaceError(e) except requests.exceptions.Timeout as e: _logger.error("A timeout occurred when requesting SAML assertion") raise InterfaceError(e) except requests.exceptions.TooManyRedirects as e: _logger.error( "A error occurred when requesting SAML assertion to refresh credentials. " "Verify RedshiftProperties are correct") raise InterfaceError(e) except requests.exceptions.RequestException as e: _logger.error( "A unknown error occurred when requesting SAML assertion to refresh credentials" ) raise InterfaceError(e) try: soup = bs4.BeautifulSoup(response.text, features="lxml") except Exception as e: _logger.error( "An error occurred while parsing response: {}".format(str(e))) raise InterfaceError(e) payload: typing.Dict[str, typing.Optional[str]] = {} for inputtag in soup.find_all(re.compile("(INPUT|input)")): name: str = inputtag.get("name", "") value: str = inputtag.get("value", "") if "username" in name.lower(): payload[name] = self.user_name elif "authmethod" in name.lower(): payload[name] = value elif "password" in name.lower(): payload[name] = self.password elif name != "": payload[name] = value action: typing.Optional[str] = self.get_form_action(soup) if action and action.startswith("/"): url = "https://{host}:{port}{action}".format(host=self.idp_host, port=str( self.idpPort), action=action) try: response = requests.post(url, data=payload, verify=self.do_verify_ssl_cert()) response.raise_for_status() except requests.exceptions.HTTPError as e: _logger.error( "Request to refresh credentials was unsuccessful. {}".format( str(e))) raise InterfaceError(e) except requests.exceptions.Timeout as e: _logger.error( "A timeout occurred when attempting to refresh credentials") raise InterfaceError(e) except requests.exceptions.TooManyRedirects as e: _logger.error( "A error occurred when refreshing credentials. Verify RedshiftProperties are correct" ) raise InterfaceError(e) except requests.exceptions.RequestException as e: _logger.error( "A unknown error occurred when refreshing credentials") raise InterfaceError(e) try: soup = bs4.BeautifulSoup(response.text, features="lxml") except Exception as e: _logger.error( "An error occurred while parsing response: {}".format(str(e))) raise InterfaceError(e) assertion: str = "" for inputtag in soup.find_all("input"): if inputtag.get("name") == "SAMLResponse": assertion = inputtag.get("value") if assertion == "": raise InterfaceError("Failed to find Adfs access_token") return assertion
def set_auth_properties(info: RedshiftProperty): """ Helper function to handle IAM and Native Auth connection properties and ensure required parameters are specified. Parameters """ import pkg_resources from packaging.version import Version if info is None: raise InterfaceError( "Invalid connection property setting. info must be specified") # IAM requires an SSL connection to work. # Make sure that is set to SSL level VERIFY_CA or higher. if info.ssl is True: if info.sslmode not in SupportedSSLMode.list(): info.put("sslmode", SupportedSSLMode.default()) _logger.debug( "A non-supported value: {} was provides for sslmode. Falling back to default value: {}" .format(info.sslmode, SupportedSSLMode.default())) else: info.put("sslmode", "") # elif (info.iam is False) and any( # (info.credentials_provider, info.access_key_id, info.secret_access_key, info.session_token, info.profile) # ): # raise InterfaceError( # "Invalid connection property setting. IAM must be enabled when using credential_provider, " # "AWS credentials, Amazon Redshift authentication profile, or AWS profile" # ) if info.iam is True: _logger.debug("boto3 version: {}".format( Version(pkg_resources.get_distribution("boto3").version))) _logger.debug("botocore version: {}".format( Version(pkg_resources.get_distribution("botocore").version))) if info.cluster_identifier is None and not info.is_serverless_host: raise InterfaceError( "Invalid connection property setting. cluster_identifier must be provided when IAM is enabled" ) if info.credentials_provider is not None: if info.auth_profile is None and any( (info.access_key_id, info.secret_access_key, info.session_token, info.profile)): raise InterfaceError( "Invalid connection property setting. It is not valid to provide both Credentials provider and " "AWS credentials or AWS profile") elif not isinstance(info.credentials_provider, str): raise InterfaceError( "Invalid connection property setting. It is not valid to provide a non-string value to " "credentials_provider.") elif info.profile is not None: if info.auth_profile is None and any( (info.access_key_id, info.secret_access_key, info.session_token)): raise InterfaceError( "Invalid connection property setting. It is not valid to provide any of access_key_id, " "secret_access_key, or session_token when profile is provided" ) elif info.access_key_id is not None: if info.secret_access_key is not None: pass elif info.password != "": info.put("secret_access_key", info.password) _logger.debug( "Value of password will be used for secret_access_key") else: raise InterfaceError( "Invalid connection property setting. " "secret access key must be provided in either secret_access_key or password field" ) _logger.debug( "AWS Credentials access_key_id: {} secret_access_key: {} session_token: {}" .format(bool(info.access_key_id), bool(info.secret_access_key), bool(info.session_token))) elif info.secret_access_key is not None: raise InterfaceError( "Invalid connection property setting. access_key_id is required when secret_access_key is " "provided") elif info.session_token is not None: raise InterfaceError( "Invalid connection property setting. access_key_id and secret_access_key are required when " "session_token is provided") if info.db_groups and info.force_lowercase: info.put("db_groups", [group.lower() for group in info.db_groups]) # Check for IAM keys and AuthProfile first if info.auth_profile is not None: if Version(pkg_resources.get_distribution( "boto3").version) < Version("1.17.111"): raise pkg_resources.VersionConflict( "boto3 >= 1.17.111 required for authentication via Amazon Redshift authentication profile. " "Please upgrade the installed version of boto3 to use this functionality." ) if not all( (info.access_key_id, info.secret_access_key, info.region)): raise InterfaceError( "Invalid connection property setting. access_key_id, secret_access_key, and region are required " "for authentication via Redshift auth_profile") else: # info.put("region", info.region) # info.put("endpoint_url", info.endpoint_url) resp = IdpAuthHelper.read_auth_profile( auth_profile=typing.cast(str, info.auth_profile), iam_access_key_id=typing.cast(str, info.access_key_id), iam_secret_key=typing.cast(str, info.secret_access_key), iam_session_token=info.session_token, info=info, ) info.put_all(resp)