Beispiel #1
0
 def set_logger(self, logger):
     CheckValue.check_logger(logger, 'logger')
     self._logger = logger
     self._logutils = LogUtils(logger)
     self._request_utils = borneo.http.RequestUtils(self._sess,
                                                    self._logutils)
     return self
Beispiel #2
0
    def __init__(self, user_name=None, password=None):
        self._endpoint = None
        self._url = None
        self._auth_string = None
        self._auto_renew = True
        self._disable_ssl_hook = False
        self._is_closed = False
        # The base path for security related services.
        self._base_path = HttpConstants.KV_SECURITY_PATH
        # The login token expiration time.
        self._expiration_time = 0
        self._logger = None
        self._logutils = LogUtils(self._logger)
        self._sess = Session()
        self._request_utils = borneo.http.RequestUtils(self._sess,
                                                       self._logutils)
        self._lock = Lock()
        self._timer = None
        self.lock = Lock()

        if user_name is None and password is None:
            # Used to access to a store without security enabled.
            self._is_secure = False
        else:
            if user_name is None or password is None:
                raise IllegalArgumentException('Invalid input arguments.')
            CheckValue.check_str(user_name, 'user_name')
            CheckValue.check_str(password, 'password')
            self._is_secure = True
            self._user_name = user_name
            self._password = password
Beispiel #3
0
    def set_logger(self, logger):
        """
        Sets a logger instance for this provider. If not set, the logger
        associated with the driver is used.

        :param logger: the logger.
        :returns: self.
        :raises IllegalArgumentException: raises the exception if logger is not
            an instance of Logger.
        """
        CheckValue.check_logger(logger, 'logger')
        self.__logger = logger
        self.__logutils = LogUtils(logger)
        self.__request_utils = RequestUtils(self.__sess, self.__logutils)
        return self
Beispiel #4
0
 def __init__(
         self,
         idcs_props_file=_DEFAULT_PROPS_FILE,
         idcs_url=None,
         entitlement_id=None,
         creds_provider=None,
         timeout_ms=Utils.DEFAULT_TIMEOUT_MS,
         cache_duration_seconds=AccessTokenProvider.MAX_ENTRY_LIFE_TIME,
         refresh_ahead=AccessTokenProvider.DEFAULT_REFRESH_AHEAD):
     # Constructs a default access token provider.
     if idcs_url is None:
         CheckValue.check_str(idcs_props_file, 'idcs_props_file')
         super(DefaultAccessTokenProvider, self).__init__(
             DefaultAccessTokenProvider.MAX_ENTRY_LIFE_TIME,
             DefaultAccessTokenProvider.DEFAULT_REFRESH_AHEAD)
         self.__idcs_url = self.__get_idcs_url(idcs_props_file)
         entitlement = self.__get_entitlement_id(idcs_props_file)
         self.__creds_provider = (
             PropertiesCredentialsProvider().set_properties_file(
                 self.__get_credential_file(idcs_props_file)))
         self.__timeout_ms = Utils.DEFAULT_TIMEOUT_MS
     else:
         CheckValue.check_str(idcs_url, 'idcs_url')
         self.__is_credentials_provider(creds_provider)
         CheckValue.check_int_gt_zero(timeout_ms, 'timeout_ms')
         CheckValue.check_int_gt_zero(cache_duration_seconds,
                                      'cache_duration_seconds')
         CheckValue.check_int_gt_zero(refresh_ahead, 'refresh_ahead')
         super(DefaultAccessTokenProvider,
               self).__init__(cache_duration_seconds, refresh_ahead)
         self.__idcs_url = idcs_url
         entitlement = entitlement_id
         self.__creds_provider = (PropertiesCredentialsProvider()
                                  if creds_provider is None else
                                  creds_provider)
         self.__timeout_ms = timeout_ms
     url = urlparse(self.__idcs_url)
     self.__host = url.hostname
     self.__andc_fqs = None
     if entitlement is not None:
         CheckValue.check_str(entitlement, 'entitlement_id')
         self.__andc_fqs = (AccessTokenProvider.ANDC_AUD_PREFIX +
                            entitlement + AccessTokenProvider.SCOPE)
     self.__psm_fqs = None
     self.__logger = None
     self.__logutils = LogUtils()
     self.__sess = Session()
     self.__request_utils = RequestUtils(self.__sess, self.__logutils)
Beispiel #5
0
 def __init__(self):
     self.__parse_args()
     url = urlparse(self.__idcs_url)
     self.__host = url.hostname
     # logger used for HTTP request logging
     self.__logger = self.__get_logger()
     self.__logutils = LogUtils(self.__logger)
     self.__sess = Session()
     self.__request_utils = RequestUtils(self.__sess, self.__logutils)
Beispiel #6
0
class StoreAccessTokenProvider(AuthorizationProvider):
    """
    On-premise only.

    StoreAccessTokenProvider is an :py:class:`borneo.AuthorizationProvider` that
    performs the following functions:

        Initial (bootstrap) login to store, using credentials provided.\n
        Storage of bootstrap login token for re-use.\n
        Optionally renews the login token before it expires.\n
        Logs out of the store when closed.

    If accessing an insecure instance of Oracle NoSQL Database the default
    constructor is used, with no arguments.

    If accessing a secure instance of Oracle NoSQL Database a user name and
    password must be provided. That user must already exist in the NoSQL
    Database and have sufficient permission to perform table operations. That
    user's identity is used to authorize all database operations.

    To access to a store without security enabled, no parameter need to be set
    to the constructor.

    To access to a secure store, the constructor requires a valid user name and
    password to access the target store. The user must exist and have sufficient
    permission to perform table operations required by the application. The user
    identity is used to authorize all operations performed by the application.

    :param user_name: the user name to use for the store. This user must exist
        in the NoSQL Database and is the identity that is used for authorizing
        all database operations.
    :type user_name: str
    :param password: the password for the user.
    :type password: str
    :raises IllegalArgumentException: raises the exception if one or more of the
        parameters is malformed or a required parameter is missing.
    """
    # Used when we send user:password pair.
    _BASIC_PREFIX = 'Basic '
    # The general prefix for the login token.
    _BEARER_PREFIX = 'Bearer '
    # Login service end point name.
    _LOGIN_SERVICE = '/login'
    # Login token renew service end point name.
    _RENEW_SERVICE = '/renew'
    # Logout service end point name.
    _LOGOUT_SERVICE = '/logout'
    # Default timeout when sending http request to server
    _HTTP_TIMEOUT_MS = 30000

    def __init__(self, user_name=None, password=None):
        self._endpoint = None
        self._url = None
        self._auth_string = None
        self._auto_renew = True
        self._disable_ssl_hook = False
        self._is_closed = False
        # The base path for security related services.
        self._base_path = HttpConstants.KV_SECURITY_PATH
        # The login token expiration time.
        self._expiration_time = 0
        self._logger = None
        self._logutils = LogUtils(self._logger)
        self._sess = Session()
        self._request_utils = borneo.http.RequestUtils(self._sess,
                                                       self._logutils)
        self._lock = Lock()
        self._timer = None
        self.lock = Lock()

        if user_name is None and password is None:
            # Used to access to a store without security enabled.
            self._is_secure = False
        else:
            if user_name is None or password is None:
                raise IllegalArgumentException('Invalid input arguments.')
            CheckValue.check_str(user_name, 'user_name')
            CheckValue.check_str(password, 'password')
            self._is_secure = True
            self._user_name = user_name
            self._password = password

    @synchronized
    def bootstrap_login(self):
        # Bootstrap login using the provided credentials.
        if not self._is_secure or self._is_closed:
            return
        # Convert the username:password pair in base 64 format.
        pair = self._user_name + ':' + self._password
        try:
            encoded_pair = b64encode(pair)
        except TypeError:
            encoded_pair = b64encode(pair.encode()).decode()
        try:
            # Send request to server for login token.
            response = self._send_request(
                StoreAccessTokenProvider._BASIC_PREFIX + encoded_pair,
                StoreAccessTokenProvider._LOGIN_SERVICE)
            content = response.get_content()
            # Login fail
            if response.get_status_code() != codes.ok:
                raise InvalidAuthorizationException(
                    'Fail to login to service: ' + content)
            if self._is_closed:
                return
            # Generate the authentication string using login token.
            self._auth_string = (StoreAccessTokenProvider._BEARER_PREFIX +
                                 self._parse_json_result(content))
            # Schedule login token renew thread.
            self._schedule_refresh()
        except (ConnectionError, InvalidAuthorizationException) as e:
            self._logutils.log_debug(format_exc())
            raise e
        except Exception as e:
            self._logutils.log_debug(format_exc())
            raise NoSQLException('Bootstrap login fail.', e)

    @synchronized
    def close(self):
        """
        Close the provider, releasing resources such as a stored login token.
        """
        # Don't do anything for non-secure case.
        if not self._is_secure or self._is_closed:
            return
        # Send request for logout.
        try:
            response = self._send_request(
                self._auth_string, StoreAccessTokenProvider._LOGOUT_SERVICE)
            if response.get_status_code() != codes.ok:
                self._logutils.log_info('Failed to logout user ' +
                                        self._user_name + ': ' +
                                        response.get_content())
        except Exception as e:
            self._logutils.log_info('Failed to logout user ' +
                                    self._user_name + ': ' + str(e))

        # Clean up.
        self._is_closed = True
        self._auth_string = None
        self._expiration_time = 0
        self._password = None
        if self._timer is not None:
            self._timer.cancel()
            self._timer = None
        if self._sess is not None:
            self._sess.close()

    def get_authorization_string(self, request=None):
        if request is not None and not isinstance(request, Request):
            raise IllegalArgumentException(
                'get_authorization_string requires an instance of Request or '
                + 'None as parameter.')
        if not self._is_secure or self._is_closed:
            return None
        # If there is no cached auth string, re-authentication to retrieve the
        # login token and generate the auth string.
        if self._auth_string is None:
            self.bootstrap_login()
        return self._auth_string

    def is_secure(self):
        """
        Returns whether the provider is accessing a secured store.

        :returns: True if accessing a secure store, otherwise False.
        :rtype: bool
        """
        return self._is_secure

    def set_auto_renew(self, auto_renew):
        """
        Sets the auto-renew state. If True, automatic renewal of the login token
        is enabled.

        :param auto_renew: set to True to enable auto-renew.
        :type auto_renew: bool
        :returns: self.
        :raises IllegalArgumentException: raises the exception if auto_renew is
            not True or False.
        """
        CheckValue.check_boolean(auto_renew, 'auto_renew')
        self._auto_renew = auto_renew
        return self

    def is_auto_renew(self):
        """
        Returns whether the login token is to be automatically renewed.

        :returns: True if auto-renew is set, otherwise False.
        :rtype: bool
        """
        return self._auto_renew

    def set_endpoint(self, endpoint):
        """
        Sets the endpoint of the on-prem proxy.

        :param endpoint: the endpoint.
        :type endpoint: str
        :returns: self.
        :raises IllegalArgumentException: raises the exception if endpoint is
            not a string.
        """
        CheckValue.check_str(endpoint, 'endpoint')
        self._endpoint = endpoint
        self._url = NoSQLHandleConfig.create_url(endpoint, '')
        if self._is_secure and self._url.scheme.lower() != 'https':
            raise IllegalArgumentException(
                'StoreAccessTokenProvider requires use of https.')
        return self

    def get_endpoint(self):
        """
        Returns the endpoint of the on-prem proxy.

        :returns: the endpoint.
        :rtype: str
        """
        return self._endpoint

    def set_logger(self, logger):
        CheckValue.check_logger(logger, 'logger')
        self._logger = logger
        self._logutils = LogUtils(logger)
        self._request_utils = borneo.http.RequestUtils(self._sess,
                                                       self._logutils)
        return self

    def get_logger(self):
        return self._logger

    def set_url_for_test(self):
        self._url = urlparse(self._url.geturl().replace('https', 'http'))
        return self

    def validate_auth_string(self, auth_string):
        if self._is_secure and auth_string is None:
            raise IllegalArgumentException(
                'Secured StoreAccessProvider requires a non-none string.')

    def _parse_json_result(self, json_result):
        # Retrieve login token from JSON string.
        result = loads(json_result)
        # Extract expiration time from JSON result.
        self._expiration_time = result['expireAt']
        # Extract login token from JSON result.
        return result['token']

    def _refresh_task(self):
        """
        This task sends a request to the server for login session extension.
        Depending on the server policy, a new login token with new expiration
        time may or may not be granted.
        """
        if not self._is_secure or not self._auto_renew or self._is_closed:
            return
        try:
            old_auth = self._auth_string
            response = self._send_request(
                old_auth, StoreAccessTokenProvider._RENEW_SERVICE)
            token = self._parse_json_result(response.get_content())
            if response.get_status_code() != codes.ok:
                raise InvalidAuthorizationException(token)
            if self._is_closed:
                return
            with self._lock:
                if self._auth_string == old_auth:
                    self._auth_string = (
                        StoreAccessTokenProvider._BEARER_PREFIX + token)
            self._schedule_refresh()
        except Exception as e:
            self._logutils.log_info('Failed to renew login token: ' + str(e))
            if self._timer is not None:
                self._timer.cancel()
                self._timer = None

    def _schedule_refresh(self):
        # Schedule a login token renew when half of the token life time is
        # reached.
        if not self._is_secure or not self._auto_renew:
            return
        # Clean up any existing timer
        if self._timer is not None:
            self._timer.cancel()
            self._timer = None
        acquire_time = int(round(time() * 1000))
        if self._expiration_time <= 0:
            return
        # If it is 10 seconds before expiration, don't do further renew to avoid
        # to many renew request in the last few seconds.
        if self._expiration_time > acquire_time + 10000:
            renew_time = (acquire_time +
                          (self._expiration_time - acquire_time) // 2)
            self._timer = Timer(
                float(renew_time - acquire_time) / 1000, self._refresh_task)
            self._timer.start()

    def _send_request(self, auth_header, service_name):
        # Send HTTPS request to login/renew/logout service location with proper
        # authentication information.
        headers = {'Authorization': auth_header}
        return self._request_utils.do_get_request(
            self._url.geturl() + self._base_path + service_name, headers,
            StoreAccessTokenProvider._HTTP_TIMEOUT_MS)
Beispiel #7
0
class DefaultAccessTokenProvider(AccessTokenProvider):
    """
    An instance of :py:class:`AccessTokenProvider` that acquires access tokens
    from Oracle Identity Cloud Service (IDCS) using information provided by
    :py:class:`CredentialsProvider`. By default the
    :py:class:`CredentialsProvider` is used.

    This class requires tenant-specific information in order to properly
    communicate with IDCS using the credential information:

        A tenant-specific URL used to communicate with IDCS.\n
        An entitlement id. This is generated by Oracle during account creation.

    The tenant-specific IDCS URL is the IDCS host assigned to the tenant. After
    logging into the IDCS admin console, copy the host of the IDCS admin console
    URL. For example, the format of the admin console URL is
    "https\://{tenantId}.identity.oraclecloud.com/ui/v1/adminconsole". The
    "https\://{tenantId}.identity.oraclecloud.com" portion is the required
    parameter.

    The entitlement id can be found using the IDCS admin console. After logging
    into the IDCS admin console, choose *Applications* from the button on the
    top left. Find the Application named ANDC, enter the Resources tab in the
    Configuration. There is a field called primary audience, the entitlement id
    parameter is the value of "urn\:opc\:andc\:entitlementid", which is treated
    as a string. For example if your primary audience is
    "urn\:opc\:andc\:entitlementid=123456789" then the parameter is "123456789"

    NOTE: above is simple python doc. This information is on the implementation.

    ATs are acquiring using OAuth client ID and secret pair along with user name
    and password using `Resource Owner Grant Type <https://docs.oracle.com/en/\
    cloud/paas/identity-cloud/rest-api/ROPCGT.html>`_

    This provider uses :py:class:`PropertiesCredentialsProvider` by default to
    obtain credentials. These credentials are used to build IDCS access token
    payloads to acquire the required access tokens. The expiry window is a
    constant 120,000 milliseconds.

    :param idcs_props_file: the path of an IDCS properties file.
    :param idcs_url: an IDCS URL, provided by IDCS.
    :param entitlement_id: service entitlement ID, which can be found from the
        primary audience of the application named ANDC from IDCS.
    :param creds_provider: a credentials provider.
    :param timeout_ms: the access token acquisition request timeout in
        milliseconds.
    :param cache_duration_seconds: the amount of time the access tokens are
        cached in the provider, in seconds.
    :param refresh_ahead: the refresh time before AT expired from cache.
    :raises IllegalArgumentException: raises the exception if parameters are not
        expected type.
    """
    # Payload used to acquire access token with resource owner grant flow.
    _RO_GRANT_FORMAT = 'grant_type=password&username={0}&scope={1}&password='******'grant_type=client_credentials&scope=urn:opc:idm:__myscopes__')

    # IDCS fully qualified scope to acquire IDCS AT.
    _IDCS_SCOPE = 'urn:opc:idm:__myscopes__'

    # Filter using service type URN to get PSMApp metadata from IDCS when
    # provider attempts to acquire PSM AT using PSMApp client id and secret. The
    # PSMApp metadata contains PSMApp client id, secret and primary audience of
    # PSM for this cloud account.
    _PSM_APP_FILTER = '?filter=serviceTypeURN+eq+%22PSMResourceTenatApp%22'

    # Filter using OAuth client id to find client metadata from IDCS.
    _CLIENT_FILTER = '?filter=name+eq+'

    # Field name in the IDCS access token response.
    _AT_FIELD = 'access_token'

    # Properties in the IDCS properties file.
    _IDCS_URL_PROP = 'idcs_url'
    _CREDS_FILE_PROP = 'creds_file'
    _ENTITLEMENT_ID_PROP = 'entitlement_id'

    # Default properties file at ~/.andc/idcs.props
    _DEFAULT_PROPS_FILE = environ['HOME'] + sep + '.andc' + sep + 'idcs.props'

    def __init__(
            self,
            idcs_props_file=_DEFAULT_PROPS_FILE,
            idcs_url=None,
            entitlement_id=None,
            creds_provider=None,
            timeout_ms=Utils.DEFAULT_TIMEOUT_MS,
            cache_duration_seconds=AccessTokenProvider.MAX_ENTRY_LIFE_TIME,
            refresh_ahead=AccessTokenProvider.DEFAULT_REFRESH_AHEAD):
        # Constructs a default access token provider.
        if idcs_url is None:
            CheckValue.check_str(idcs_props_file, 'idcs_props_file')
            super(DefaultAccessTokenProvider, self).__init__(
                DefaultAccessTokenProvider.MAX_ENTRY_LIFE_TIME,
                DefaultAccessTokenProvider.DEFAULT_REFRESH_AHEAD)
            self.__idcs_url = self.__get_idcs_url(idcs_props_file)
            entitlement = self.__get_entitlement_id(idcs_props_file)
            self.__creds_provider = (
                PropertiesCredentialsProvider().set_properties_file(
                    self.__get_credential_file(idcs_props_file)))
            self.__timeout_ms = Utils.DEFAULT_TIMEOUT_MS
        else:
            CheckValue.check_str(idcs_url, 'idcs_url')
            self.__is_credentials_provider(creds_provider)
            CheckValue.check_int_gt_zero(timeout_ms, 'timeout_ms')
            CheckValue.check_int_gt_zero(cache_duration_seconds,
                                         'cache_duration_seconds')
            CheckValue.check_int_gt_zero(refresh_ahead, 'refresh_ahead')
            super(DefaultAccessTokenProvider,
                  self).__init__(cache_duration_seconds, refresh_ahead)
            self.__idcs_url = idcs_url
            entitlement = entitlement_id
            self.__creds_provider = (PropertiesCredentialsProvider()
                                     if creds_provider is None else
                                     creds_provider)
            self.__timeout_ms = timeout_ms
        url = urlparse(self.__idcs_url)
        self.__host = url.hostname
        self.__andc_fqs = None
        if entitlement is not None:
            CheckValue.check_str(entitlement, 'entitlement_id')
            self.__andc_fqs = (AccessTokenProvider.ANDC_AUD_PREFIX +
                               entitlement + AccessTokenProvider.SCOPE)
        self.__psm_fqs = None
        self.__logger = None
        self.__logutils = LogUtils()
        self.__sess = Session()
        self.__request_utils = RequestUtils(self.__sess, self.__logutils)

    def set_credentials_provider(self, provider):
        """
        Sets :py:class:`CredentialsProvider`.

        :param provider: the credentials provider.
        :returns: self.
        :raises IllegalArgumentException: raises the exception if provider is
            not an instance of CredentialsProvider.
        """
        self.__is_credentials_provider(provider)
        self.__creds_provider = provider
        return self

    def set_logger(self, logger):
        """
        Sets a logger instance for this provider. If not set, the logger
        associated with the driver is used.

        :param logger: the logger.
        :returns: self.
        :raises IllegalArgumentException: raises the exception if logger is not
            an instance of Logger.
        """
        CheckValue.check_logger(logger, 'logger')
        self.__logger = logger
        self.__logutils = LogUtils(logger)
        self.__request_utils = RequestUtils(self.__sess, self.__logutils)
        return self

    def get_logger(self):
        """
        Returns the logger of this provider if set, None if not.

        :returns: the logger.
        """
        return self.__logger

    def get_account_access_token(self):
        self.__ensure_creds_provider()
        self.__find_oauth_scopes()
        if self.__psm_fqs is not None:
            return self.__get_at_by_password(self.__psm_fqs)
        return self.__get_at_by_psm_app()

    def get_service_access_token(self):
        self.__ensure_creds_provider()
        if self.__andc_fqs is None:
            self.__find_oauth_scopes()
        if self.__andc_fqs is None:
            raise IllegalStateException(
                'Unable to find service scope, OAuth client isn\'t ' +
                'configured properly, run OAuthClient utility to verify and ' +
                'recreate.')
        return self.__get_at_by_password(self.__andc_fqs)

    def close(self):
        super(DefaultAccessTokenProvider, self).close()
        if self.__sess is not None:
            self.__sess.close()

    def __ensure_creds_provider(self):
        if self.__creds_provider is None:
            raise IllegalArgumentException('CredentialsProvider unavailable.')

    def __find_oauth_scopes(self):
        # Find PSM and ANDC FQS from allowed scopes of OAuth client.
        if self.__andc_fqs is not None and self.__psm_fqs is not None:
            return
        creds = self.__get_client_creds()
        oauth_id = creds.get_credential_alias()
        auth = self.__get_auth_header(oauth_id, creds.get_secret())
        try:
            auth = 'Bearer ' + self.__get_access_token(
                auth, DefaultAccessTokenProvider._CLIENT_GRANT_PAYLOAD,
                DefaultAccessTokenProvider._IDCS_SCOPE)
        except InvalidAuthorizationException:
            self.__logutils.log_debug(
                'Cannot get access token with IDCS scope using client grant.')
            return
        response = self.__request_utils.do_get_request(
            self.__idcs_url + Utils.APP_ENDPOINT +
            DefaultAccessTokenProvider._CLIENT_FILTER + '%22' + oauth_id +
            '%22', Utils.scim_headers(self.__host, auth), self.__timeout_ms)
        if response is None:
            raise IllegalStateException(
                'Error getting client metadata from Identity Cloud Service, ' +
                'no response.')
        if response.get_status_code() >= codes.multiple_choices:
            Utils.handle_idcs_errors(
                response, 'Getting client metadata',
                'Please verify if the OAuth client is configured properly.')
        fqs_list = Utils.get_field(response.get_content(), 'allowedScopes',
                                   'fqs')
        if fqs_list is None:
            return
        for fqs in fqs_list:
            if fqs.startswith(AccessTokenProvider.ANDC_AUD_PREFIX):
                self.__andc_fqs = fqs
            elif fqs.endswith(Utils.PSM_SCOPE):
                self.__psm_fqs = fqs

    def __get_access_token(self, auth_header, payload, fqs):
        response = self.__request_utils.do_post_request(
            self.__idcs_url + Utils.TOKEN_ENDPOINT,
            Utils.token_headers(self.__host, auth_header), payload,
            self.__timeout_ms)
        if response is None:
            raise IllegalStateException('Error acquiring access token with '
                                        'scope ' + fqs + ', no response')
        response_code = response.get_status_code()
        content = response.get_content()
        if response_code >= codes.multiple_choices:
            self.__handle_token_error_response(response_code, content)
        return self.__parse_access_token_response(content)

    def __get_at_by_password(self, fqs):
        user_creds = self.__get_user_creds()
        client_creds = self.__get_client_creds()
        # URL encode fqs.
        encoded_fqs = quote(fqs.encode())
        auth_header = self.__get_auth_header(
            client_creds.get_credential_alias(), client_creds.get_secret())
        replaced = str.format(DefaultAccessTokenProvider._RO_GRANT_FORMAT,
                              user_creds.get_credential_alias(), encoded_fqs)
        # Build the actual payload to acquire access token.
        payload = replaced + user_creds.get_secret()
        return self.__get_access_token(auth_header, payload, fqs)

    def __get_at_by_psm_app(self):
        """
        Acquiring account access token using PSMApp provisioned by Oracle for
        each tenant. Keeping this path to remain the backward compatibility if
        users are using the client id and secret from Application ANDC. This
        path shouldn't work after IDCS hides client secret of Oracle-created
        Applications. This will be deprecated eventually.
        """
        # 1. acquire IDCS AT
        result = self.__get_at_by_password(
            DefaultAccessTokenProvider._IDCS_SCOPE)
        if result is None:
            raise IllegalStateException(
                'Error acquiring Identity Cloud Service access token, unable '
                + 'to get metadata to proceed acquiring account access token.')
        # 2. look up audience, client id and secret of PSMApp
        auth_header = 'Bearer ' + result
        psm_info = self.__get_psm_app(auth_header)
        if psm_info is None:
            raise IllegalStateException(
                'Error finding required metadata from Identity Cloud Service,'
                + ' unable to proceed acquiring account access token.')
        # 3. acquire PSM AT
        auth_header = self.__get_auth_header(psm_info.client_id,
                                             psm_info.client_secret)
        psm_fqs = psm_info.audience + Utils.PSM_SCOPE
        user_creds = self.__get_user_creds()
        replaced = str.format(DefaultAccessTokenProvider._RO_GRANT_FORMAT,
                              user_creds.get_credential_alias(), psm_fqs)
        payload = replaced + user_creds.get_secret()
        return self.__get_access_token(auth_header, payload, psm_fqs)

    def __get_auth_header(self, client_id, secret):
        # Return authorization header in form of 'Basic <clientId:secret>'.
        pair = client_id + ':' + secret
        try:
            return 'Basic ' + b64encode(pair)
        except TypeError:
            return 'Basic ' + b64encode(pair.encode()).decode()

    def __get_client_creds(self):
        creds = self.__creds_provider.get_oauth_client_credentials()
        if creds is None:
            raise IllegalArgumentException(
                'OAuth client credentials unavailable.')
        return creds

    def __get_credential_file(self, properties_file):
        creds_file = PropertiesCredentialsProvider.get_property_from_file(
            properties_file, DefaultAccessTokenProvider._CREDS_FILE_PROP)
        if creds_file is None:
            return PropertiesCredentialsProvider._DEFAULT_CREDS_FILE
        return creds_file

    def __get_entitlement_id(self, properties_file):
        return PropertiesCredentialsProvider.get_property_from_file(
            properties_file, DefaultAccessTokenProvider._ENTITLEMENT_ID_PROP)

    def __get_idcs_url(self, properties_file):
        # Methods used to fetch IDCS-related properties from given file.
        idcs_url = PropertiesCredentialsProvider.get_property_from_file(
            properties_file, DefaultAccessTokenProvider._IDCS_URL_PROP)
        if idcs_url is None:
            raise IllegalArgumentException(
                'Must specify IDCS URL in IDCS properties file.')
        return idcs_url

    def __get_psm_app(self, auth_header):
        """
        Get PSMApp metadata from IDCS. The secret of PSMApp will be hidden by
        IDCS, if no secret, return an error to ask users create custom client.
        This will be deprecated eventually.
        """
        # Get PSMApp metadata from IDCS.
        response = self.__request_utils.do_get_request(
            self.__idcs_url + Utils.APP_ENDPOINT +
            DefaultAccessTokenProvider._PSM_APP_FILTER,
            Utils.token_headers(self.__host, auth_header), self.__timeout_ms)
        if response is None:
            raise IllegalStateException(
                'Error getting required metadata from Identity Cloud Service,'
                + ' unable to acquire account access token, no response')
        response_code = response.get_status_code()
        content = response.get_content()
        if response_code >= codes.multiple_choices:
            Utils.handle_idcs_errors(
                response, 'Getting account metadata',
                'Please grant user Identity Domain Administrator or ' +
                'Application Administrator role')
        oauth_id = 'name'
        audience = 'audience'
        secret = 'clientSecret'
        try:
            oauth_id_value = Utils.get_field(content, oauth_id)
            audience_value = Utils.get_field(content, audience)
            secret_value = Utils.get_field(content, secret)
        except IllegalStateException as ise:
            raise UnauthorizedException(
                'Please grant user Identity Domain Administrator or ' +
                'Application Administrator role. ' + str(ise))
        if oauth_id_value is None or audience_value is None:
            raise IllegalStateException(
                'Account metadata response contains invalid value: ' + content)
        if secret_value is None:
            raise IllegalStateException(
                'Account metadata doesn\'t have a secret, unable to acquire ' +
                'account access token. Must create the custom OAuth Client ' +
                'first. Account metadata: ' + content)
        return DefaultAccessTokenProvider.PSMAppInfo(oauth_id_value,
                                                     secret_value,
                                                     audience_value)

    def __get_user_creds(self):
        user_creds = self.__creds_provider.get_user_credentials()
        if user_creds is None:
            raise IllegalArgumentException('User credentials unavailable.')
        return user_creds

    def __handle_token_error_response(self, response_code, content):
        if response_code >= codes.server_error:
            self.__logutils.log_info(
                'Error acquiring access token, expected to retry, error ' +
                'response: ' + content + ', status code: ' +
                str(response_code))
            raise RequestTimeoutException(
                'Error acquiring access token, expected to retry, error ' +
                'response: ' + content + ', status code: ' +
                str(response_code))
        elif response_code == codes.bad and content is None:
            # IDCS doesn't return error message in case of credentials has
            # invalid URL encoded characters.
            raise IllegalArgumentException(
                'Error acquiring access token, status code: ' +
                str(response_code) +
                ', CredentialsProvider supplies invalid credentials')
        else:
            raise InvalidAuthorizationException(
                'Error acquiring access token from Identity Cloud Service. ' +
                'IDCS error response: ' + content + ', status code: ' +
                str(response_code))

    def __is_credentials_provider(self, provider):
        if (provider is not None
                and not isinstance(provider, CredentialsProvider)):
            raise IllegalArgumentException('provider must be an instance of ' +
                                           'CredentialsProvider.')

    def __parse_access_token_response(self, response):
        """
        A valid response from IDCS is in JSON format and must contains the field
        "access_token" and "expires_in".
        """
        response = loads(response)
        access_token = response.get(DefaultAccessTokenProvider._AT_FIELD)
        if access_token is None:
            raise IllegalStateException(
                'Access token response contains invalid value, response: ' +
                str(response))
        self.__logutils.log_debug('Acquired access token ' + access_token)
        return access_token

    class PSMAppInfo:
        def __init__(self, client_id, client_secret, audience):
            self.client_id = client_id
            self.client_secret = client_secret
            self.audience = audience
class SignatureProvider(AuthorizationProvider):
    """
    Cloud service only.

    An instance of :py:class:`borneo.AuthorizationProvider` that generates and
    caches signature for each request as authorization string. A number of
    pieces of information are required for configuration. See `Required Keys and
    OCIDs <https://docs.cloud.oracle.com/iaas/Content/API/Concepts/
    apisigningkey.htm>`_ for information and instructions on how to create the
    required keys and OCIDs for configuration. The required information
    includes:

        * A signing key, used to sign requests.
        * A pass phrase for the key, if it is encrypted.
        * The fingerprint of the key pair used for signing.
        * The OCID of the tenancy.
        * The OCID of a user in the tenancy.

    All of this information is required to authenticate and authorize access to
    the service. See :ref:`creds-label` for information on how to acquire this
    information.

    There are three different ways to authorize an application:

    1. Using a specific user's identity.
    2. Using an Instance Principal, which can be done when running on a compute
       instance in the Oracle Cloud Infrastructure (OCI). See
       :py:meth:`create_with_instance_principal` and `Calling Services from
       Instances <https://docs.cloud.oracle.com/iaas/Content/Identity/Tasks/
       callingservicesfrominstances.htm>`_.
    3. Using a Resource Principal, which can be done when running within a
       Function within the Oracle Cloud Infrastructure (OCI). See
       :py:meth:`create_with_resource_principal` and `Accessing Other Oracle
       Cloud Infrastructure Resources from Running Functions <https://docs.
       cloud.oracle.com/en-us/iaas/Content/Functions/Tasks/
       functionsaccessingociresources.htm>`_.

    The latter 2 limit the ability to use a compartment name vs OCID when naming
    compartments and tables in :py:class:`Request` classes and when naming
    tables in queries. A specific user identity is best for naming flexibility,
    allowing both compartment names and OCIDs.

    When using a specific user's identity there are 3 options for providing the
    required information:

    1. Using a instance of oci.signer.Signer or
       oci.auth.signers.SecurityTokenSigner
    2. Directly providing the credentials via parameters
    3. Using a configuration file

    Only one method of providing credentials can be used, and if they are mixed
    the priority from high to low is:

    * Signer or SecurityTokenSigner(provider is used)
    * Credentials as arguments (tenant_id, etc used)
    * Configuration file (config_file is used)

    :param provider: an instance of oci.signer.Signer or
        oci.auth.signers.SecurityTokenSigner.
    :type provider: Signer or SecurityTokenSigner
    :param config_file: path of configuration file.
    :type config_file: str
    :param profile_name: user profile name. Only valid with config_file.
    :type profile_name: str
    :param tenant_id: id of the tenancy
    :type tenant_id: str
    :param user_id: id of a specific user
    :type user_id: str
    :param private_key: path to private key or private key content
    :type private_key: str
    :param fingerprint: fingerprint for the private key
    :type fingerprint: str
    :param pass_phrase: pass_phrase for the private key if created
    :type pass_phrase: str
    :param region: identifies the region will be accessed by the NoSQLHandle
    :type region: Region
    :param duration_seconds: the signature cache duration in seconds.
    :type duration_seconds: int
    :param refresh_ahead: the refresh time before signature cache expiry
       in seconds.
    :type refresh_ahead: int
    :raises IllegalArgumentException: raises the exception if the parameters
        are not valid.
    """

    CACHE_KEY = 'signature'
    """Cache key name."""
    MAX_ENTRY_LIFE_TIME = 300
    """Maximum lifetime of signature 300 seconds."""
    DEFAULT_REFRESH_AHEAD = 10
    """Default refresh time before signature expiry, 10 seconds."""
    def __init__(self,
                 provider=None,
                 config_file=None,
                 profile_name=None,
                 tenant_id=None,
                 user_id=None,
                 fingerprint=None,
                 private_key=None,
                 pass_phrase=None,
                 region=None,
                 duration_seconds=MAX_ENTRY_LIFE_TIME,
                 refresh_ahead=DEFAULT_REFRESH_AHEAD):
        """
        The SignatureProvider that generates and caches request signature.
        """
        #
        # This class depends on the oci package
        #
        if oci is None:
            raise ImportError('Package "oci" is required; please install.')

        CheckValue.check_int_gt_zero(duration_seconds, 'duration_seconds')
        CheckValue.check_int_gt_zero(refresh_ahead, 'refresh_ahead')
        if duration_seconds > SignatureProvider.MAX_ENTRY_LIFE_TIME:
            raise IllegalArgumentException(
                'Access token cannot be cached longer than ' +
                str(SignatureProvider.MAX_ENTRY_LIFE_TIME) + ' seconds.')

        self._region = None
        if provider is not None:
            if not isinstance(
                    provider,
                (oci.signer.Signer, oci.auth.signers.SecurityTokenSigner)):
                raise IllegalArgumentException(
                    'provider should be an instance of oci.signer.Signer or ' +
                    'oci.auth.signers.SecurityTokenSigner.')
            self._provider = provider
            try:
                region_id = provider.region
            except AttributeError:
                region_id = None
            if region_id is not None:
                self._region = Regions.from_region_id(region_id)
        elif (tenant_id is None or user_id is None or fingerprint is None
              or private_key is None):
            CheckValue.check_str(config_file, 'config_file', True)
            CheckValue.check_str(profile_name, 'profile_name', True)
            if config_file is None and profile_name is None:
                # Use default user profile and private key from default path of
                # configuration file ~/.oci/config.
                config = oci.config.from_file()
            elif config_file is None and profile_name is not None:
                # Use user profile with given profile name and private key from
                # default path of configuration file ~/.oci/config.
                config = oci.config.from_file(profile_name=profile_name)
            elif config_file is not None and profile_name is None:
                # Use user profile with default profile name and private key
                # from specified configuration file.
                config = oci.config.from_file(file_location=config_file)
            else:  # config_file is not None and profile_name is not None
                # Use user profile with given profile name and private key from
                # specified configuration file.
                config = oci.config.from_file(file_location=config_file,
                                              profile_name=profile_name)
            self._provider = oci.signer.Signer(config['tenancy'],
                                               config['user'],
                                               config['fingerprint'],
                                               config['key_file'],
                                               config.get('pass_phrase'),
                                               config.get('key_content'))
            region_id = config.get('region')
            if region_id is not None:
                self._provider.region = region_id
                self._region = Regions.from_region_id(region_id)
        else:
            CheckValue.check_str(tenant_id, 'tenant_id')
            CheckValue.check_str(user_id, 'user_id')
            CheckValue.check_str(fingerprint, 'fingerprint')
            CheckValue.check_str(private_key, 'private_key')
            CheckValue.check_str(pass_phrase, 'pass_phrase', True)
            if path.isfile(private_key):
                key_file = private_key
                key_content = None
            else:
                key_file = None
                key_content = private_key
            self._provider = oci.signer.Signer(tenant_id, user_id, fingerprint,
                                               key_file, pass_phrase,
                                               key_content)
            if region is not None:
                if not isinstance(region, Region):
                    raise IllegalArgumentException(
                        'region must be an instance of an instance of Region.')
                self._provider.region = region.get_region_id()
                self._region = region

        self._signature_cache = Memoize(duration_seconds)
        self._refresh_interval_s = (duration_seconds - refresh_ahead
                                    if duration_seconds > refresh_ahead else 0)

        # Refresh timer.
        self._timer = None
        self._service_url = None
        self._logger = None
        self._logutils = LogUtils()
        self._sess = Session()
        self._request_utils = RequestUtils(self._sess, self._logutils)

    def close(self):
        """
        Closes the signature provider.
        """
        if self._timer is not None:
            self._timer.cancel()
            self._timer = None

    def get_authorization_string(self, request=None):
        if self._service_url is None:
            raise IllegalArgumentException(
                'Unable to find service url, use set_service_url to load ' +
                'from NoSQLHandleConfig')
        sig_details = self._get_signature_details()
        if sig_details is not None:
            return sig_details['authorization']

    def get_logger(self):
        return self._logger

    def get_region(self):
        # Internal use only.
        return self._region

    def set_logger(self, logger):
        CheckValue.check_logger(logger, 'logger')
        self._logger = logger
        self._logutils = LogUtils(logger)
        self._request_utils = RequestUtils(self._sess, self._logutils)
        return self

    def set_required_headers(self, request, auth_string, headers):
        sig_details = self._get_signature_details()
        if sig_details is None:
            return
        headers[HttpConstants.AUTHORIZATION] = sig_details['authorization']
        headers[HttpConstants.DATE] = sig_details['date']
        compartment = request.get_compartment()
        if compartment is None:
            # If request doesn't has compartment, set the tenant id as the
            # default compartment, which is the root compartment in IAM if using
            # user principal. If using an instance principal this value is
            # None.
            compartment = self._get_tenant_ocid()
        if compartment is not None:
            headers[HttpConstants.REQUEST_COMPARTMENT_ID] = compartment
        else:
            raise IllegalArgumentException(
                'Compartment is None. When authenticating using an Instance ' +
                'Principal the compartment for the operation must be specified.'
            )

    def set_service_url(self, config):
        service_url = config.get_service_url()
        if service_url is None:
            raise IllegalArgumentException('Must set service URL first.')
        self._service_url = (service_url.scheme + '://' +
                             service_url.hostname + '/' +
                             HttpConstants.NOSQL_DATA_PATH)
        return self

    @staticmethod
    def create_with_instance_principal(iam_auth_uri=None,
                                       region=None,
                                       logger=None):
        """
        Creates a SignatureProvider using an instance principal. This method may
        be used when calling the Oracle NoSQL Database Cloud Service from an
        Oracle Cloud compute instance. It authenticates with the instance
        principal and uses a security token issued by IAM to do the actual
        request signing.

        When using an instance principal the compartment id (OCID) must be
        specified on each request or defaulted by using
        :py:meth:`borneo.NoSQLHandleConfig.set_default_compartment`. If the
        compartment is not specified for an operation an exception will be
        thrown.

        See `Calling Services from Instances <https://docs.cloud.oracle.com/
        iaas/Content/Identity/Tasks/callingservicesfrominstances.htm>`_

        :param iam_auth_uri: the URI is usually detected automatically, specify
            the URI if you need to overwrite the default, or encounter the
            *Invalid IAM URI* error, it is optional.
        :type iam_auth_uri: str
        :param region: identifies the region will be accessed by the
            NoSQLHandle.
        :type region: Region
        :param logger: the logger used by the SignatureProvider.
        :type logger: Logger
        :returns: a SignatureProvider.
        :rtype: SignatureProvider
        """
        if iam_auth_uri is None:
            provider = oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
        else:
            provider = oci.auth.signers.InstancePrincipalsSecurityTokenSigner(
                federation_endpoint=iam_auth_uri)
        if region is not None:
            provider.region = region.get_region_id()
        signature_provider = SignatureProvider(provider)
        return (signature_provider
                if logger is None else signature_provider.set_logger(logger))

    @staticmethod
    def create_with_resource_principal():
        """
        Creates a SignatureProvider using a resource principal. This method may
        be used when calling the Oracle NoSQL Database Cloud Service within an
        Oracle Cloud Function. It authenticates with the resource principal and
        uses a security token issued by IAM to do the actual request signing.

        When using a resource principal the compartment (OCID) must be specified
        on each request or defaulted by using
        :py:meth:`borneo.NoSQLHandleConfig.set_default_compartment`. If the
        compartment is not specified for an operation an exception will be
        thrown.

        See `Accessing Other Oracle Cloud Infrastructure Resources from Running
        Functions <https://docs.cloud.oracle.com/en-us/iaas/Content/Functions/
        Tasks/functionsaccessingociresources.htm>`_.

        :returns: a SignatureProvider.
        :rtype: SignatureProvider
        """
        return SignatureProvider(
            oci.auth.signers.get_resource_principals_signer())

    def _get_signature_details(self):
        sig_details = self._signature_cache.get(SignatureProvider.CACHE_KEY)
        if sig_details is not None:
            return sig_details
        sig_details = self._get_signature_details_internal()
        self._signature_cache.set(SignatureProvider.CACHE_KEY, sig_details)
        self._schedule_refresh()
        return sig_details

    def _get_signature_details_internal(self):
        request = Request(method='post', url=self._service_url)
        request = self._provider.without_content_headers(request.prepare())
        return request.headers

    def _get_tenant_ocid(self):
        """
        Get tenant OCID if using user principal.

        :returns: tenant OCID of user.
        :rtype: str
        """
        if isinstance(self._provider, oci.signer.Signer):
            return self._provider.api_key.split('/')[0]

    def _refresh_task(self):
        try:
            sig_details = self._get_signature_details_internal()
            if sig_details is not None:
                self._signature_cache.set(SignatureProvider.CACHE_KEY,
                                          sig_details)
                self._schedule_refresh()
        except Exception as e:
            # Ignore the failure of refresh. The driver would try to generate
            # signature in the next request if signature is not available, the
            # failure would be reported at that moment.
            self._logutils.log_warning(
                'Unable to refresh cached request signature, ' + str(e))
            self._timer.cancel()
            self._timer = None

    def _schedule_refresh(self):
        # If refresh interval is 0, don't schedule a refresh.
        if self._refresh_interval_s == 0:
            return
        if self._timer is not None:
            self._timer.cancel()
            self._timer = None
        self._timer = Timer(self._refresh_interval_s, self._refresh_task)
        self._timer.start()
    def __init__(self,
                 provider=None,
                 config_file=None,
                 profile_name=None,
                 tenant_id=None,
                 user_id=None,
                 fingerprint=None,
                 private_key=None,
                 pass_phrase=None,
                 region=None,
                 duration_seconds=MAX_ENTRY_LIFE_TIME,
                 refresh_ahead=DEFAULT_REFRESH_AHEAD):
        """
        The SignatureProvider that generates and caches request signature.
        """
        #
        # This class depends on the oci package
        #
        if oci is None:
            raise ImportError('Package "oci" is required; please install.')

        CheckValue.check_int_gt_zero(duration_seconds, 'duration_seconds')
        CheckValue.check_int_gt_zero(refresh_ahead, 'refresh_ahead')
        if duration_seconds > SignatureProvider.MAX_ENTRY_LIFE_TIME:
            raise IllegalArgumentException(
                'Access token cannot be cached longer than ' +
                str(SignatureProvider.MAX_ENTRY_LIFE_TIME) + ' seconds.')

        self._region = None
        if provider is not None:
            if not isinstance(
                    provider,
                (oci.signer.Signer, oci.auth.signers.SecurityTokenSigner)):
                raise IllegalArgumentException(
                    'provider should be an instance of oci.signer.Signer or ' +
                    'oci.auth.signers.SecurityTokenSigner.')
            self._provider = provider
            try:
                region_id = provider.region
            except AttributeError:
                region_id = None
            if region_id is not None:
                self._region = Regions.from_region_id(region_id)
        elif (tenant_id is None or user_id is None or fingerprint is None
              or private_key is None):
            CheckValue.check_str(config_file, 'config_file', True)
            CheckValue.check_str(profile_name, 'profile_name', True)
            if config_file is None and profile_name is None:
                # Use default user profile and private key from default path of
                # configuration file ~/.oci/config.
                config = oci.config.from_file()
            elif config_file is None and profile_name is not None:
                # Use user profile with given profile name and private key from
                # default path of configuration file ~/.oci/config.
                config = oci.config.from_file(profile_name=profile_name)
            elif config_file is not None and profile_name is None:
                # Use user profile with default profile name and private key
                # from specified configuration file.
                config = oci.config.from_file(file_location=config_file)
            else:  # config_file is not None and profile_name is not None
                # Use user profile with given profile name and private key from
                # specified configuration file.
                config = oci.config.from_file(file_location=config_file,
                                              profile_name=profile_name)
            self._provider = oci.signer.Signer(config['tenancy'],
                                               config['user'],
                                               config['fingerprint'],
                                               config['key_file'],
                                               config.get('pass_phrase'),
                                               config.get('key_content'))
            region_id = config.get('region')
            if region_id is not None:
                self._provider.region = region_id
                self._region = Regions.from_region_id(region_id)
        else:
            CheckValue.check_str(tenant_id, 'tenant_id')
            CheckValue.check_str(user_id, 'user_id')
            CheckValue.check_str(fingerprint, 'fingerprint')
            CheckValue.check_str(private_key, 'private_key')
            CheckValue.check_str(pass_phrase, 'pass_phrase', True)
            if path.isfile(private_key):
                key_file = private_key
                key_content = None
            else:
                key_file = None
                key_content = private_key
            self._provider = oci.signer.Signer(tenant_id, user_id, fingerprint,
                                               key_file, pass_phrase,
                                               key_content)
            if region is not None:
                if not isinstance(region, Region):
                    raise IllegalArgumentException(
                        'region must be an instance of an instance of Region.')
                self._provider.region = region.get_region_id()
                self._region = region

        self._signature_cache = Memoize(duration_seconds)
        self._refresh_interval_s = (duration_seconds - refresh_ahead
                                    if duration_seconds > refresh_ahead else 0)

        # Refresh timer.
        self._timer = None
        self._service_url = None
        self._logger = None
        self._logutils = LogUtils()
        self._sess = Session()
        self._request_utils = RequestUtils(self._sess, self._logutils)
Beispiel #10
0
 def set_logger(self, logger):
     CheckValue.check_logger(logger, 'logger')
     self._logger = logger
     self._logutils = LogUtils(logger)
     return self
Beispiel #11
0
class SignatureProvider(AuthorizationProvider):
    """
    Cloud service only.

    An instance of :py:class:`borneo.AuthorizationProvider` that generates and
    caches signature for each request as authorization string. A number of
    pieces of information are required for configuration. See `Required Keys and
    OCIDs <https://docs.cloud.oracle.com/iaas/Content/API/Concepts/
    apisigningkey.htm>`_ for information and instructions on how to create the
    required keys and OCIDs for configuration. The required information
    includes:

        * A signing key, used to sign requests.
        * A pass phrase for the key, if it is encrypted.
        * The fingerprint of the key pair used for signing.
        * The OCID of the tenancy.
        * The OCID of a user in the tenancy.

    All of this information is required to authenticate and authorize access to
    the service. See :ref:`creds-label` for information on how to acquire this
    information.

    There are two different ways to authorize an application:

    1. Using a specific user's identity.
    2. Using an Instance Principal, which can be done when running on a compute
       instance in the Oracle Cloud Infrastructure (OCI). See
       :py:meth:`create_with_instance_principal` and `Calling Services from
       Instances <https://docs.cloud.oracle.com/iaas/Content/Identity/Tasks/
       callingservicesfrominstances.htm>`_.

    The latter can be simpler to use when running on an OCI compute instance,
    but limits the ability to use a compartment name vs OCID when naming
    compartments and tables in :py:class:`Request` classes and when naming
    tables in queries. A specific user identity is best for naming flexibility,
    allowing both compartment names and OCIDs.

    When using a specific user's identity there are 3 options for providing the
    required information:

    1. Using a instance of oci.signer.Signer or
       oci.auth.signers.InstancePrincipalsSecurityTokenSigner
    2. Directly providing the credentials via parameters
    3. Using a configuration file

    Only one method of providing credentials can be used, and if they are mixed
    the priority from high to low is:

    * Signer or InstancePrincipalsSecurityTokenSigner(provider is used)
    * Credentials as arguments (tenant_id, etc used)
    * Configuration file (config_file is used)

    :param provider: an instance of oci.signer.Signer or
        oci.auth.signers.InstancePrincipalsSecurityTokenSigner.
    :type provider: Signer or InstancePrincipalsSecurityTokenSigner
    :param config_file: path of configuration file.
    :type config_file: str
    :param profile_name: user profile name. Only valid with config_file.
    :type profile_name: str
    :param tenant_id: id of the tenancy
    :type tenant_id: str
    :param user_id: id of a specific user
    :type user_id: str
    :param private_key: path to private key or private key content
    :type private_key: str
    :param fingerprint: fingerprint for the private key
    :type fingerprint: str
    :param pass_phrase: pass_phrase for the private key if created
    :type pass_phrase: str
    :param duration_seconds: the signature cache duration in seconds.
    :type duration_seconds: int
    :param refresh_ahead: the refresh time before signature cache expiry
       in seconds.
    :type refresh_ahead: int
    :raises IllegalArgumentException: raises the exception if the parameters
        are not valid.
    """

    SIGNING_HEADERS = '(request-target) host date'
    CACHE_KEY = 'signature'
    """Cache key name."""
    MAX_ENTRY_LIFE_TIME = 300
    """Maximum lifetime of signature 300 seconds."""
    DEFAULT_REFRESH_AHEAD = 10
    """Default refresh time before signature expiry, 10 seconds."""
    SIGNATURE_HEADER_FORMAT = (
        'Signature headers="{0}",keyId="{1}",algorithm="{2}",signature="{3}",'
        + 'version="{4}"')
    SIGNATURE_VERSION = 1

    def __init__(self,
                 provider=None,
                 config_file=None,
                 profile_name=None,
                 tenant_id=None,
                 user_id=None,
                 fingerprint=None,
                 private_key=None,
                 pass_phrase=None,
                 duration_seconds=MAX_ENTRY_LIFE_TIME,
                 refresh_ahead=DEFAULT_REFRESH_AHEAD):
        """
        The SignatureProvider that generates and caches request signature.
        """
        CheckValue.check_int_gt_zero(duration_seconds, 'duration_seconds')
        CheckValue.check_int_gt_zero(refresh_ahead, 'refresh_ahead')
        if duration_seconds > SignatureProvider.MAX_ENTRY_LIFE_TIME:
            raise IllegalArgumentException(
                'Access token cannot be cached longer than ' +
                str(SignatureProvider.MAX_ENTRY_LIFE_TIME) + ' seconds.')

        #
        # This class depends on the oci package
        #
        if oci is None:
            raise ImportError('Package \'oci\' is required; please install')

        try:
            if provider is not None:
                if not isinstance(
                        provider,
                    (oci.signer.Signer,
                     oci.auth.signers.InstancePrincipalsSecurityTokenSigner)):
                    raise IllegalArgumentException(
                        'provider should be an instance of oci.signer.Signer' +
                        'or oci.auth.signers.' +
                        'InstancePrincipalsSecurityTokenSigner.')
                self._provider = provider
            elif (tenant_id is None or user_id is None or fingerprint is None
                  or private_key is None):
                CheckValue.check_str(config_file, 'config_file', True)
                CheckValue.check_str(profile_name, 'profile_name', True)
                if config_file is None and profile_name is None:
                    # Use default user profile and private key from default path
                    # of configuration file ~/.oci/config.
                    config = oci.config.from_file()
                elif config_file is None and profile_name is not None:
                    # Use user profile with given profile name and private key
                    # from default path of configuration file ~/.oci/config.
                    config = oci.config.from_file(profile_name=profile_name)
                elif config_file is not None and profile_name is None:
                    # Use user profile with default profile name and private key
                    # from specified configuration file.
                    config = oci.config.from_file(file_location=config_file)
                else:  # config_file is not None and profile_name is not None
                    # Use user profile with given profile name and private key
                    # from specified configuration file.
                    config = oci.config.from_file(file_location=config_file,
                                                  profile_name=profile_name)
                self._provider = oci.signer.Signer(config['tenancy'],
                                                   config['user'],
                                                   config['fingerprint'],
                                                   config['key_file'],
                                                   config.get('pass_phrase'),
                                                   config.get('key_content'))
            else:
                CheckValue.check_str(tenant_id, 'tenant_id')
                CheckValue.check_str(user_id, 'user_id')
                CheckValue.check_str(fingerprint, 'fingerprint')
                CheckValue.check_str(private_key, 'private_key')
                CheckValue.check_str(pass_phrase, 'pass_phrase', True)
                if path.isfile(private_key):
                    key_file = private_key
                    key_content = None
                else:
                    key_file = None
                    key_content = private_key
                self._provider = oci.signer.Signer(tenant_id, user_id,
                                                   fingerprint, key_file,
                                                   pass_phrase, key_content)
        except AttributeError:
            raise ImportError('Package \'oci\' is required; please install')
        self._signature_cache = Memoize(duration_seconds)
        self._refresh_interval_s = (duration_seconds - refresh_ahead
                                    if duration_seconds > refresh_ahead else 0)

        # Refresh timer.
        self._timer = None
        self._service_host = None
        self._logger = None
        self._logutils = LogUtils()
        self._sess = Session()
        self._request_utils = RequestUtils(self._sess, self._logutils)

    def close(self):
        """
        Closes the signature provider.
        """
        if self._timer is not None:
            self._timer.cancel()
            self._timer = None

    def get_authorization_string(self, request=None):
        if self._service_host is None:
            raise IllegalArgumentException(
                'Unable to find service host, use set_service_host to load ' +
                'from NoSQLHandleConfig')
        sig_details = self._get_signature_details()
        if sig_details is not None:
            return sig_details.get_signature_header()

    def get_logger(self):
        return self._logger

    def set_logger(self, logger):
        CheckValue.check_logger(logger, 'logger')
        self._logger = logger
        self._logutils = LogUtils(logger)
        self._request_utils = RequestUtils(self._sess, self._logutils)
        return self

    def set_required_headers(self, request, auth_string, headers):
        sig_details = self._get_signature_details()
        if sig_details is None:
            return
        headers[HttpConstants.AUTHORIZATION] = (
            sig_details.get_signature_header())
        headers[HttpConstants.DATE] = sig_details.get_date()
        compartment = request.get_compartment()
        if compartment is None:
            # If request doesn't has compartment, set the tenant id as the
            # default compartment, which is the root compartment in IAM if using
            # user principal. If using an instance principal this value is
            # None.
            compartment = self._get_tenant_ocid()
        if compartment is not None:
            headers[HttpConstants.REQUEST_COMPARTMENT_ID] = compartment
        else:
            raise IllegalArgumentException(
                'Compartment is None. When authenticating using an Instance ' +
                'Principal the compartment for the operation must be specified.'
            )

    def set_service_host(self, config):
        service_url = config.get_service_url()
        if service_url is None:
            raise IllegalArgumentException('Must set service URL first.')
        self._service_host = service_url.hostname
        return self

    @staticmethod
    def create_with_instance_principal(iam_auth_uri=None):
        """
        Creates a SignatureProvider using an instance principal. This method may
        be used when calling the Oracle NoSQL Database Cloud Service from an
        Oracle Cloud compute instance. It authenticates with the instance
        principal and uses a security token issued by IAM to do the actual
        request signing.

        When using an instance principal the compartment (OCID) must be
        specified on each request or defaulted by using
        :py:meth:`borneo.NoSQLHandleConfig.set_default_compartment`. If the
        compartment is not specified for an operation an exception will be
        thrown.

        See `Calling Services from Instances <https://docs.cloud.oracle.com/
        iaas/Content/Identity/Tasks/callingservicesfrominstances.htm>`_

        :param iam_auth_uri: the URI is usually detected automatically, specify
            the URI if you need to overwrite the default, or encounter the
            *Invalid IAM URI* error, it is optional.
        :type iam_auth_uri: str
        :returns: a SignatureProvider.
        :rtype: SignatureProvider
        """
        if iam_auth_uri is None:
            return SignatureProvider(
                oci.auth.signers.InstancePrincipalsSecurityTokenSigner())
        else:
            return SignatureProvider(
                oci.auth.signers.InstancePrincipalsSecurityTokenSigner(
                    federation_endpoint=iam_auth_uri))

    def _get_signature_details(self):
        sig_details = self._signature_cache.get(SignatureProvider.CACHE_KEY)
        if sig_details is not None:
            return sig_details
        sig_details = self._get_signature_details_internal()
        self._signature_cache.set(SignatureProvider.CACHE_KEY, sig_details)
        self._schedule_refresh()
        return sig_details

    def _get_signature_details_internal(self):
        setlocale(LC_ALL, 'en_US')
        date_str = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
        if isinstance(self._provider,
                      oci.auth.signers.InstancePrincipalsSecurityTokenSigner):
            self._provider.refresh_security_token()
        private_key = self._provider.private_key
        key_id = self._provider.api_key

        try:
            signature = private_key.sign(self._signing_content(date_str),
                                         PKCS1v15(), SHA256())
            signature = b64encode(signature)
        except TypeError:
            signature = private_key.sign(
                self._signing_content(date_str).encode(), PKCS1v15(), SHA256())
            signature = b64encode(signature).decode()
        sig_header = str.format(SignatureProvider.SIGNATURE_HEADER_FORMAT,
                                SignatureProvider.SIGNING_HEADERS, key_id,
                                'rsa-sha256', signature,
                                SignatureProvider.SIGNATURE_VERSION)
        return SignatureProvider.SignatureDetails(sig_header, date_str)

    def _get_tenant_ocid(self):
        """
        Get tenant OCID if using user principal.

        :returns: tenant OCID of user.
        :rtype: str
        """
        if isinstance(self._provider, oci.signer.Signer):
            return self._provider.api_key.split('/')[0]

    def _refresh_task(self):
        try:
            sig_details = self._get_signature_details_internal()
            if sig_details is not None:
                self._signature_cache.set(SignatureProvider.CACHE_KEY,
                                          sig_details)
                self._schedule_refresh()
        except Exception as e:
            # Ignore the failure of refresh. The driver would try to generate
            # signature in the next request if signature is not available, the
            # failure would be reported at that moment.
            self._logutils.log_warning(
                'Unable to refresh cached request signature, ' + str(e))
            self._timer.cancel()
            self._timer = None

    def _schedule_refresh(self):
        # If refresh interval is 0, don't schedule a refresh.
        if self._refresh_interval_s == 0:
            return
        if self._timer is not None:
            self._timer.cancel()
            self._timer = None
        self._timer = Timer(self._refresh_interval_s, self._refresh_task)
        self._timer.start()

    def _signing_content(self, date_str):
        return (HttpConstants.REQUEST_TARGET + ': post /' +
                HttpConstants.NOSQL_DATA_PATH + '\nhost: ' +
                self._service_host + '\ndate: ' + date_str)

    class SignatureDetails(object):
        def __init__(self, signature_header, date_str):
            # Signing date, keep it and pass along with each request, so
            # requests can reuse the signature within the 5-mins time window.
            self._date = date_str
            # Signature header string.
            self._signature_header = signature_header

        def get_date(self):
            return self._date

        def get_signature_header(self):
            return self._signature_header

        def is_valid(self, header):
            if header is None:
                return False
            return header == self._signature_header