Example #1
0
    def _fetch_saml_assertion_using_http_spegno_auth(self,
                                                     saml_config: Dict[str,
                                                                       Any]):
        import requests

        # requests_gssapi will need paramiko > 2.6 since you'll need
        # 'gssapi' not 'python-gssapi' from PyPi.
        # https://github.com/paramiko/paramiko/pull/1311
        import requests_gssapi
        from lxml import etree

        idp_url = saml_config["idp_url"]
        self.log.info("idp_url= %s", idp_url)
        idp_request_kwargs = saml_config["idp_request_kwargs"]
        auth = requests_gssapi.HTTPSPNEGOAuth()
        if 'mutual_authentication' in saml_config:
            mutual_auth = saml_config['mutual_authentication']
            if mutual_auth == 'REQUIRED':
                auth = requests_gssapi.HTTPSPNEGOAuth(requests_gssapi.REQUIRED)
            elif mutual_auth == 'OPTIONAL':
                auth = requests_gssapi.HTTPSPNEGOAuth(requests_gssapi.OPTIONAL)
            elif mutual_auth == 'DISABLED':
                auth = requests_gssapi.HTTPSPNEGOAuth(requests_gssapi.DISABLED)
            else:
                raise NotImplementedError(
                    f'mutual_authentication={mutual_auth} in Connection {self.conn.conn_id} Extra.'
                    'Currently "REQUIRED", "OPTIONAL" and "DISABLED" are supported.'
                    '(Exclude this setting will default to HTTPSPNEGOAuth() ).'
                )
        # Query the IDP
        idp_reponse = requests.get(idp_url, auth=auth, **idp_request_kwargs)
        idp_reponse.raise_for_status()
        # Assist with debugging. Note: contains sensitive info!
        xpath = saml_config['saml_response_xpath']
        log_idp_response = 'log_idp_response' in saml_config and saml_config[
            'log_idp_response']
        if log_idp_response:
            self.log.warning(
                'The IDP response contains sensitive information,'
                ' but log_idp_response is ON (%s).',
                log_idp_response,
            )
            self.log.info('idp_reponse.content= %s', idp_reponse.content)
            self.log.info('xpath= %s', xpath)
        # Extract SAML Assertion from the returned HTML / XML
        xml = etree.fromstring(idp_reponse.content)
        saml_assertion = xml.xpath(xpath)
        if isinstance(saml_assertion, list):
            if len(saml_assertion) == 1:
                saml_assertion = saml_assertion[0]
        if not saml_assertion:
            raise ValueError('Invalid SAML Assertion')
        return saml_assertion
 def _post(url, data, headers):
     try:
         # try to authenticate using opportunistic auth first
         return requests.post(url,
                              data=data,
                              headers=headers,
                              auth=requests_gssapi.HTTPSPNEGOAuth(
                                  opportunistic_auth=True))
     except requests_gssapi.exceptions.SPNEGOExchangeError:
         return requests.post(url,
                              data=data,
                              headers=headers,
                              auth=requests_gssapi.HTTPSPNEGOAuth())
Example #3
0
    def _login_kerberos(self):
        """
        private function, use login_kerberos instead
        """
        if isinstance(requests_gssapi, ImportError):
            raise requests_gssapi

        login_url = 'https://{0}/ipa/session/login_kerberos'.format(
            self._current_host)
        headers = {'Referer': 'https://{0}/ipa'.format(self._current_host)}
        response = self._session.post(
            login_url,
            headers=headers,
            verify=self._verify_ssl,
            auth=requests_gssapi.HTTPSPNEGOAuth(),
        )

        if not response.ok:
            raise Unauthorized(response.text)

        self.log.info(
            'Successfully logged to {0} using Kerberos credentials.'.format(
                self._current_host))

        return AuthenticatedSession(self, logged_in=True)
    def _upload_source(cls,
                       url,
                       package,
                       filename,
                       hashtype,
                       hsh,
                       auth=requests_gssapi.HTTPSPNEGOAuth()):
        class ChunkedData(object):
            def __init__(self, check_only, chunksize=8192):
                self.check_only = check_only
                self.chunksize = chunksize
                self.start = time.time()
                self.uploaded = False
                fields = [
                    ('name', package),
                    ('{}sum'.format(hashtype), hsh),
                ]
                if check_only:
                    fields.append(('filename', filename))
                else:
                    with open(filename, 'rb') as f:
                        rf = RequestField('file', f.read(), filename)
                        rf.make_multipart()
                        fields.append(rf)
                self.data, content_type = encode_multipart_formdata(fields)
                self.headers = {'Content-Type': content_type}

            def __iter__(self):
                if self.uploaded:
                    # ensure the progressbar is shown only once (HTTPSPNEGOAuth causes second request)
                    yield self.data
                else:
                    totalsize = len(self.data)
                    for offset in range(0, totalsize, self.chunksize):
                        transferred = min(offset + self.chunksize, totalsize)
                        if not self.check_only:
                            DownloadHelper.progress(totalsize, transferred,
                                                    self.start)
                        yield self.data[offset:transferred]
                    self.uploaded = True

        def post(check_only=False):
            cd = ChunkedData(check_only)
            r = requests.post(url, data=cd, headers=cd.headers, auth=auth)
            if not 200 <= r.status_code < 300:
                raise LookasideCacheError(r.reason)
            return r.content

        state = post(check_only=True)
        if state.strip() == b'Available':
            # already uploaded
            return

        logger.info('Uploading %s to lookaside cache', filename)
        try:
            post()
        finally:
            sys.stdout.write('\n')
            sys.stdout.flush()
Example #5
0
    def _login_gssapi(self):
        """Login using kerberos credentials (uses gssapi)."""

        login_url = urlparse.urljoin(self._hub_url, "auth/krb5login/")

        # read default values from settings
        principal = self._conf.get("KRB_PRINCIPAL")
        keytab = self._conf.get("KRB_KEYTAB")
        service = self._conf.get("KRB_SERVICE")
        realm = self._conf.get("KRB_REALM")
        ccache = self._conf.get("KRB_CCACHE")

        import requests
        import gssapi
        import requests_gssapi

        request_args = {}

        # NOTE behavior difference from hub proxy overall:
        # HubProxy by default DOES NOT verify https connections :(
        # See the constructor. It could be repeated here by defaulting verify to False,
        # but let's not do that, instead you must have an unbroken SSL setup to
        # use this auth method.
        if self._conf.get("CA_CERT"):
            request_args["verify"] = self._conf["CA_CERT"]

        server_name = self.get_server_principal(service=service, realm=realm)
        server_name = gssapi.Name(server_name,
                                  gssapi.NameType.kerberos_principal)

        auth_args = {
            "target_name": server_name,
        }
        if principal is not None:
            if keytab is None:
                raise ImproperlyConfigured(
                    "Cannot specify a principal without a keytab")
            name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
            store = {"client_keytab": keytab}
            if ccache is not None:
                store["ccache"] = "FILE:" + ccache

            auth_args["creds"] = gssapi.Credentials(name=name,
                                                    store=store,
                                                    usage="initiate")

        # We only do one request, but a Session is used to allow requests to write
        # the new session ID into the cookiejar.
        with requests.Session() as s:
            s.cookies = self._transport.cookiejar
            response = s.get(login_url,
                             auth=requests_gssapi.HTTPSPNEGOAuth(**auth_args),
                             allow_redirects=False,
                             **request_args)

        self._logger and self._logger.debug("Login response: %s %s", response,
                                            response.headers)
        response.raise_for_status()
    def test_opportunistic_auth(self):
        with patch.multiple("gssapi.SecurityContext", __init__=fake_init,
                            step=fake_resp):
            auth = requests_gssapi.HTTPSPNEGOAuth(opportunistic_auth=True)

            request = requests.Request(url="http://www.example.org")

            auth.__call__(request)

            self.assertTrue('Authorization' in request.headers)
            self.assertEqual(request.headers.get('Authorization'),
                             b64_negotiate_response)
 def test_target_name(self):
     with patch.multiple("gssapi.SecurityContext", __init__=fake_init,
                         step=fake_resp):
         response = requests.Response()
         response.url = "http://www.example.org/"
         response.headers = {'www-authenticate': b64_negotiate_token}
         host = urlparse(response.url).hostname
         auth = requests_gssapi.HTTPSPNEGOAuth(
             target_name="*****@*****.**")
         auth.generate_request_header(response, host)
         fake_init.assert_called_with(
             name=gssapi_name("*****@*****.**"),
             usage="initiate", flags=gssflags, creds=None)
         fake_resp.assert_called_with(b"token")
 def test_explicit_creds(self):
     with patch.multiple("gssapi.Credentials", __new__=fake_creds), \
          patch.multiple("gssapi.SecurityContext", __init__=fake_init,
                         step=fake_resp):
         response = requests.Response()
         response.url = "http://www.example.org/"
         response.headers = {'www-authenticate': b64_negotiate_token}
         host = urlparse(response.url).hostname
         creds = gssapi.Credentials()
         auth = requests_gssapi.HTTPSPNEGOAuth(creds=creds)
         auth.generate_request_header(response, host)
         fake_init.assert_called_with(
             name=gssapi_name("*****@*****.**"),
             usage="initiate", flags=gssflags, creds=b"fake creds")
         fake_resp.assert_called_with(b"token")
Example #9
0
 def set_gssapi_auth(self, **kwargs):
     if requests_gssapi is None:
         raise ImportError('requests_gssapi')
     self.session.auth = requests_gssapi.HTTPSPNEGOAuth(**kwargs)
Example #10
0
    def _assume_role_with_saml(self, sts_client: boto3.client,
                               extra_config: dict, role_arn: str,
                               assume_role_kwargs: dict):

        saml_config = extra_config['assume_role_with_saml']
        principal_arn = saml_config['principal_arn']

        idp_url = saml_config["idp_url"]
        self.log.info("idp_url= %s", idp_url)

        idp_request_kwargs = saml_config["idp_request_kwargs"]

        idp_auth_method = saml_config['idp_auth_method']
        if idp_auth_method == 'http_spegno_auth':
            # requests_gssapi will need paramiko > 2.6 since you'll need
            # 'gssapi' not 'python-gssapi' from PyPi.
            # https://github.com/paramiko/paramiko/pull/1311
            import requests_gssapi
            auth = requests_gssapi.HTTPSPNEGOAuth()
            if 'mutual_authentication' in saml_config:
                mutual_auth = saml_config['mutual_authentication']
                if mutual_auth == 'REQUIRED':
                    auth = requests_gssapi.HTTPSPNEGOAuth(
                        requests_gssapi.REQUIRED)
                elif mutual_auth == 'OPTIONAL':
                    auth = requests_gssapi.HTTPSPNEGOAuth(
                        requests_gssapi.OPTIONAL)
                elif mutual_auth == 'DISABLED':
                    auth = requests_gssapi.HTTPSPNEGOAuth(
                        requests_gssapi.DISABLED)
                else:
                    raise NotImplementedError(
                        f'mutual_authentication={mutual_auth} in Connection {self.aws_conn_id} Extra.'
                        'Currently "REQUIRED", "OPTIONAL" and "DISABLED" are supported.'
                        '(Exclude this setting will default to HTTPSPNEGOAuth() ).'
                    )

            # Query the IDP
            import requests
            idp_reponse = requests.get(idp_url,
                                       auth=auth,
                                       **idp_request_kwargs)
            idp_reponse.raise_for_status()

            # Assist with debugging. Note: contains sensitive info!
            xpath = saml_config['saml_response_xpath']
            log_idp_response = 'log_idp_response' in saml_config and saml_config[
                'log_idp_response']
            if log_idp_response:
                self.log.warning(
                    'The IDP response contains sensitive information,'
                    ' but log_idp_response is ON (%s).', log_idp_response)
                self.log.info('idp_reponse.content= %s', idp_reponse.content)
                self.log.info('xpath= %s', xpath)

            # Extract SAML Assertion from the returned HTML / XML
            from lxml import etree
            xml = etree.fromstring(idp_reponse.content)
            saml_assertion = xml.xpath(xpath)
            if isinstance(saml_assertion, list):
                if len(saml_assertion) == 1:
                    saml_assertion = saml_assertion[0]
            if not saml_assertion:
                raise ValueError('Invalid SAML Assertion')
        else:
            raise NotImplementedError(
                f'idp_auth_method={idp_auth_method} in Connection {self.aws_conn_id} Extra.'
                'Currently only "http_spegno_auth" is supported, and must be specified.'
            )

        self.log.info("Doing sts_client.assume_role_with_saml to role_arn=%s",
                      role_arn)
        return sts_client.assume_role_with_saml(RoleArn=role_arn,
                                                PrincipalArn=principal_arn,
                                                SAMLAssertion=saml_assertion,
                                                **assume_role_kwargs)
    def _upload_source(
        cls,
        url,
        package,
        source_dir,
        filename,
        hashtype,
        hsh,
        auth=requests_gssapi.HTTPSPNEGOAuth(opportunistic_auth=True)):
        class ChunkedData:
            def __init__(self, check_only, chunksize=8192):
                self.check_only = check_only
                self.chunksize = chunksize
                self.start = time.time()
                fields = [
                    ('name', package),
                    ('{}sum'.format(hashtype), hsh),
                ]
                if check_only:
                    fields.append(('filename', filename))
                else:
                    fields.append(
                        ('mtime', str(int(os.stat(filename).st_mtime))))
                    with open(path, 'rb') as f:
                        rf = RequestField('file', f.read(), filename)
                        rf.make_multipart()
                        fields.append(rf)
                self.data, content_type = encode_multipart_formdata(fields)
                self.headers = {'Content-Type': content_type}

            def __iter__(self):
                totalsize = len(self.data)
                for offset in range(0, totalsize, self.chunksize):
                    transferred = min(offset + self.chunksize, totalsize)
                    if not self.check_only:
                        DownloadHelper.progress(totalsize, transferred,
                                                self.start)
                    yield self.data[offset:transferred]

        class FakeProgress(threading.Thread):
            def __init__(self, check_only, interval=0.2):
                self.check_only = check_only
                self.interval = interval
                self.stop_event = threading.Event()
                super().__init__()

            def run(self):
                if self.check_only:
                    return
                n = 0
                start = time.time()
                while not self.stop_event.is_set():
                    DownloadHelper.progress(-1,
                                            n * 256 * 1024,
                                            start,
                                            show_size=False)
                    n += 1
                    self.stop_event.wait(self.interval)

            def stop(self):
                self.stop_event.set()
                super().join()

        def post(check_only=False):
            cd = ChunkedData(check_only)
            if 'src.fedoraproject.org' in url:
                # src.fedoraproject.org seems to have trouble with chunked requests, don't even try
                fp = FakeProgress(check_only)
                fp.start()
                try:
                    r = requests.post(url,
                                      data=cd.data,
                                      headers=cd.headers,
                                      auth=auth)
                finally:
                    fp.stop()
            else:
                r = requests.post(url, data=cd, headers=cd.headers, auth=auth)
            if not 200 <= r.status_code < 300:
                raise LookasideCacheError('{0}: {1}'.format(
                    r.reason, r.text.strip()))
            return r.content

        path = os.path.join(source_dir, filename)

        try:
            state = post(check_only=True)
        except (requests.exceptions.ConnectionError, LookasideCacheError) as e:
            # just log the error and bail out
            logger.error('Attempt to upload to lookaside cache failed: %s',
                         str(e))
            return

        if state.strip() == b'Available':
            # already uploaded
            logger.info(
                '%s is already present in lookaside cache, not uploading',
                path)
            return

        logger.info('Uploading %s to lookaside cache', path)
        try:
            try:
                post()
            finally:
                sys.stdout.write('\n')
                sys.stdout.flush()
        except (requests.exceptions.ConnectionError, LookasideCacheError) as e:
            # Skip error, the rebase can continue even after a failed upload
            logger.error("Upload to lookaside cache failed: %s", str(e))
Example #12
0
    def _login_gssapi_common(self, login_url, force_service, allow_redirects):
        """Common authentication logic for auth methods using gssapi.
        - if force_service is True, client always calculates a service principal up-front
          even if no KRB_SERVICE was set in config. Breaks OIDC since we are not actually
          doing gssapi with the kobo hub.
        - if allow_redirects is True, authentication is allowed to follow redirects. Required
          in the OIDC case.
        """

        # read default values from settings
        principal = self._conf.get("KRB_PRINCIPAL")
        keytab = self._conf.get("KRB_KEYTAB")
        service = self._conf.get("KRB_SERVICE")
        realm = self._conf.get("KRB_REALM")
        ccache = self._conf.get("KRB_CCACHE")

        import requests
        import gssapi
        import requests_gssapi

        request_args = {}

        # NOTE behavior difference from hub proxy overall:
        # HubProxy by default DOES NOT verify https connections :(
        # See the constructor. It could be repeated here by defaulting verify to False,
        # but let's not do that, instead you must have an unbroken SSL setup to
        # use this auth method.
        if self._conf.get("CA_CERT"):
            request_args["verify"] = self._conf["CA_CERT"]

        auth_args = {"mutual_authentication": requests_gssapi.OPTIONAL}

        if service or force_service:
            if realm is None:
                # let gssapi select the correct realm (according to system configuration)
                if service is None:
                    service = "HTTP"
                server_name = '%s@%s' % (service, self.get_hub_hostname())
                server_name = gssapi.Name(server_name,
                                          gssapi.NameType.hostbased_service)
            else:
                server_name = self.get_server_principal(service=service,
                                                        realm=realm)
                server_name = gssapi.Name(server_name,
                                          gssapi.NameType.kerberos_principal)
            auth_args["target_name"] = server_name

        if principal is not None:
            if keytab is None:
                raise ImproperlyConfigured(
                    "Cannot specify a principal without a keytab")
            name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
            store = {"client_keytab": keytab}
            if ccache is not None:
                store["ccache"] = "FILE:" + ccache

            auth_args["creds"] = gssapi.Credentials(name=name,
                                                    store=store,
                                                    usage="initiate")

        # We only do one request, but a Session is used to allow requests to write
        # the new session ID into the cookiejar.
        with requests.Session() as s:
            s.cookies = self._transport.cookiejar
            response = s.get(login_url,
                             auth=requests_gssapi.HTTPSPNEGOAuth(**auth_args),
                             allow_redirects=allow_redirects,
                             **request_args)

        self._logger and self._logger.debug("Login response: %s %s", response,
                                            response.headers)
        response.raise_for_status()