cloudant / python-cloudant

A Python library for Cloudant and CouchDB
Apache License 2.0
163 stars 55 forks source link

Error 403 when trying to access couchDB server that uses SSL certificate. #499

Closed othmanechentouf closed 3 years ago

othmanechentouf commented 3 years ago

Hello, I'm trying to connect to a remote couchDB server, this server uses a SSL certificate which I installed correctly on my mac and I can access the server via my browser. When I try to connect to the server via python-cloudant, I get an error, here's the code :

from cloudant.client import Cloudant
from cloudant.client import CouchDB

#client = CouchDB('myuser', 'mypassword', url='https://xxx.xx.xxx.xx', connect=True)

session = client.session()
print('Username: {0}'.format(session['userCtx']['name']))
print('Databases: {0}'.format(client.all_dbs()))

# Disconnect from the server
client.disconnect()

and here's the error I get :

HTTPError                                 Traceback (most recent call last)
<ipython-input-57-e11f4f61f4aa> in <module>
      5 
      6 
----> 7 client = CouchDB('myuser', 'mypassword', url='https://xxx.xx.xxx.x', connect = True)
      8 
      9 

~/miniconda3/lib/python3.8/site-packages/cloudant/client.py in __init__(self, user, auth_token, admin_party, **kwargs)
    120         connect_to_couch = kwargs.get('connect', False)
    121         if connect_to_couch and self._DATABASE_CLASS == CouchDatabase:
--> 122             self.connect()
    123 
    124     @property

~/miniconda3/lib/python3.8/site-packages/cloudant/client.py in connect(self)
    188             self.r_session.headers.update(self._client_user_header)
    189 
--> 190         self.session_login()
    191 
    192         # Utilize an event hook to append to the response message

~/miniconda3/lib/python3.8/site-packages/cloudant/client.py in session_login(self, user, passwd)
    229         :param str auth_token: Authentication token used to connect to server.
    230         """
--> 231         self.change_credentials(user=user, auth_token=passwd)
    232 
    233     def change_credentials(self, user=None, auth_token=None):

~/miniconda3/lib/python3.8/site-packages/cloudant/client.py in change_credentials(self, user, auth_token)
    239         """
    240         self.r_session.set_credentials(user, auth_token)
--> 241         self.r_session.login()
    242 
    243     def session_logout(self):

~/miniconda3/lib/python3.8/site-packages/cloudant/_client_session.py in login(self)
    153             data={'name': self._username, 'password': self._password},
    154         )
--> 155         resp.raise_for_status()
    156 
    157     def logout(self):

~/miniconda3/lib/python3.8/site-packages/requests/models.py in raise_for_status(self)
    891 
    892         if http_error_msg:
--> 893             raise HTTPError(http_error_msg, response=self)
    894 
    895     def close(self):

HTTPError: 403 Client Error: Forbidden for url:

I can't figure out a way around it, another thing is when I point requests directly to the certificate using the code below, I get a response 200, which means that the connection is established. Here's the code :

r = requests.get('https://xxx.xx.xxx.x', verify='catrust/',cert=('client01.crt', 'client01.key'))

I'm using macOS Big Sur, Python 3.9.

Thank you in advance.

emlaver commented 3 years ago

Hello @othmanechentouf, there's some helpful info mentioned in another issue:

Since all the HTTP requests python-cloudant makes go through Requests you should also be able to use the REQUESTS_CA_BUNDLE environment variable to point to a CA certificate file that will allow Requests to validate your certificate without needing to pass the verify argument to each request.

I believe you'll want to set the REQUESTS_CA_BUNDLE env variable and set the verify attribute on the client's r_session:

client = CouchDB('myuser', 'mypassword', url='https://xxx.xx.xxx.xx')
client.r_session.verify = '/path/to/certfile'
client.connect()
...
othmanechentouf commented 3 years ago

Hello @emlaver, thank you for your response. the code you pointed out is giving me this error :

AttributeError: 'NoneType' object has no attribute 'verify'

This is cause this line of code :

CouchDB('myuser', 'mypassword', url='https://xxx.xx.xxx.xx')

is returning an empty object. I've set the REQUESTS_CA_BUNDLE variable to point here /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/certifi/cacert.pem, I'm not sure where the problem is coming from.

othmanechentouf commented 3 years ago

I have to point out that with this code :

import requests

r = requests.get('https://xxx.xx.xxx.xx', verify='catrust/ca.crt',cert=('client01.crt', 'client01.key'))

data = r.content  # Content of response

print(r.status_code)  # Status code of response
print(data)

I get no error and a connection is established. My guess is that I have to find a way to point couchDB to these same files to get the connection through, do you have any suggestions ?

emlaver commented 3 years ago

@othmanechentouf I did a bit more digging and the connect call initializes the underlying session. I think what you'll want to do is:

from cloudant import CouchDB
from cloudant._client_session import ClientSession

client = CouchDB('myuser', 'mypassword', url='https://xxx.xx.xxx.xx')
client.r_session = ClientSession()
client.r_session.verify = ''
client.connect()
othmanechentouf commented 3 years ago

@emlaver It didn't work, but I found another solution.

You have to install a package called requests_pkcs12, which allows you to create an adapter to use within the Cloudant/CouchDB function. This adapter takes the certificate file and its password as arguments, and then you pass the adapter as an argument when calling Cloudant/CouchDB. Here's the code :

from requests import Session 
from requests_pkcs12 import Pkcs12Adapter 
from requests_pkcs12 import get
from cloudant.client import Cloudant
from cloudant.client import CouchDB

 # Loading the server's certificate file and its password 
my_adapter=Pkcs12Adapter(pkcs12_filename='client01.p12', pkcs12_password='certificate_password')

# calling CouchDB with our new adapter as one of its arguments 
client=CouchDB('myusername', 'mypassword', url='https://xxx.xx.xxx.xx',connect=True,adapter=my_adapter)
session = client.session()

# Accessing the Server and printing all the databases withtin
print('Username: {0}'.format(session['userCtx']['name']))
print('Databases: {0}'.format(client.all_dbs()))
client.disconnect()

In case the server you are trying to access is using other kind of certificates (.pem/.cer/.crt..), You just have to point an adapter to that and use your adapter variable as argument for your Cloudant/CouchDB function call.