Skip to content
This repository has been archived by the owner on Aug 30, 2024. It is now read-only.

Can you connect using an auth token/API key instead of user/password? #49

Closed
bradwbonn opened this issue Dec 11, 2015 · 24 comments
Closed
Labels

Comments

@bradwbonn
Copy link

Is there a way to pass a base64 auth token to Cloudant() for creating a new client? Right now I only see passing it username and password. It calls the attribute "auth_token" but it doesn't seem to work unless I use the explicit password.

Code I'm trying:

    with Cloudant(config['cloudant_user'],config['cloudant_auth'], account=config['cloudant_user']) as client:

Reponse:

Traceback (most recent call last):
  File "./dirscan.py", line 818, in <module>
    main(sys.argv[1:])
  File "./dirscan.py", line 152, in main
    load_config(config['passed_config_file'])
  File "./dirscan.py", line 390, in load_config
    with Cloudant(config['cloudant_user'], config['cloudant_auth'], account=config['cloudant_user']) as client:
AttributeError: __exit__

Related question:
I see that I can generate an API key pair, but I don't see any calls in the API to set what permissions those keys have. (Such as setting their permissions on a specific database once they're created.) Am I just missing it?

@alfinkel
Copy link
Contributor

Are you looking for share_database, unshare_database?

@bradwbonn
Copy link
Author

No, that's sharing databases with another Cloudant user. The idea is to create an API key using generate_api_key() and then assign permissions to a specific database, or give the API key permission to create a new database itself. Particularly helpful for temporal, per-user, or per-device database use cases.

@alfinkel
Copy link
Contributor

I may be misunderstanding what you are looking for but you can use share_database from the database module (It uses the /_api/v2/db/$DB/_security endpoint) and pass it the API Key as the username and then set permissions for reader, writer, admin and this would grant permissions to a specific database for that API key. As for giving permission to create databases, what endpoint would that be?

@alfinkel
Copy link
Contributor

Also a little confused on your first question re: the base64 auth token. Are you looking to do basic authentication or are you looking to base64.urlsafe_b64encode your passcode and authenticate using the encoded value? Can you provide a curl statement that gives an example of what you mean here? That might help me uderstand a bit better.

@bradwbonn
Copy link
Author

Basically, there are two things I'm trying to here, yes, so I'll separate them out and describe with examples.
Number one is to increase the security profile for a script which is meant to run via cron and utilize Cloudant as its database. Having it run as a script without a security gateway means that (like any local program that utilizes a cloud resource) it will have to store its authentication data somewhere. I'd prefer to store the configuration file using hashed credentials so they can't be human-read.

An example using curl would be: curl -s --proto '=https' -g -H 'Authorization: Basic <BASE64HASHOFUSERPASSWORDCREDS>'�
This is easy to do with the python requests library with code such as:

import requests
response = requests.get(
    url="https://bradwbonn.cloudant.com/_all_dbs",
    headers = {"Content-Type": "application/json", "Authorization": "Basic <BASE64HASHOFUSERPASSWORDCREDS>"}
)
print r.json()

Number two is to use API keys as the stored credentials instead of a Cloudant username, but now I'm thinking this may be moot. I'm planning to use a rolling database model, which means whatever stored credentials are used will need to be able to create and delete databases, and I'm fairly certain API keys only exist on a per-database basis. It's good to know that share_database() can interact with the _security endpoint though.

@alfinkel
Copy link
Contributor

Thanks for the clarification. Currently we are set up for cookie authentication only when using the connect(...) method on a client. But fortunately with Python there is usually a workaround. For this case the work around to use Basic authentication you basically would need to "roll your own" connection routine as in the following:

from cloudant import Cloudant
import requests

# Construct a client object but don't provide it with your password.
# However, the password (auth_token) argument must be set to something.
client = Cloudant('bradwbonn', 'not-giving-you-my-password', account='bradwbonn')
# Roll your own connect() routine...
client.r_session = requests.Session()
client.r_session.headers.update({'Authorization': 'Basic <BASE64HASHOFUSERPASSWORDCREDS>'})
client._cloudant_session = client.session()
# Done ...

Unfortunately, you cannot use the Cloudant with context manager using this work around as in your original example. Also, if you were using the url kwarg along with the x_cloudant_user kwarg you would need to account for an additional User-Agent header but you get the gist...

I hope that works out for you.

I'll open a new "Enhancement" Issue for adding Basic authentication and reference this one.

@alfinkel
Copy link
Contributor

For added context: take a look at client.connect() to see what exactly you would be overriding. Hopefully seeing that would add a bit more context around the proposed the work around.

@alfinkel
Copy link
Contributor

Now that I've had more time to think about it, there's no reason why you could not create your own context manager to replicate what the cloudant context manager does and then you could do all this via with as well. ref: https://github.com/cloudant/python-cloudant/blob/master/src/cloudant/__init__.py#L59

I hope some of this helps you along... I'll stop beating this horse now. :)

@bradwbonn
Copy link
Author

This is really helpful, thank you! I've actually never extended a library in Python before, so this will end up being yet another learning experience for me. ;)

@bradwbonn
Copy link
Author

In your example above: client._cloudant_session = c.session() I'm not sure I understand what the c refers to. Is it supposed to be client.session()?

@vinomaster
Copy link

not much luck using

from cloudant import Cloudant
import requests

# Construct a client object but don't provide it with your password.
# However, the password (auth_token) argument must be set to something.
client = Cloudant(USERNAME, PASSWORD, account=USERNAME)

Error using Python 3 notebook is after a !pip install --pre cloudant


NameError Traceback (most recent call last)
in ()
----> 1 from cloudant import Cloudant
2 import requests
3
4 # Construct a client object but don't provide it with your password.
5 # However, the password (auth_token) argument must be set to something.

/opt/conda/lib/python3.4/site-packages/cloudant/init.py in ()
20 import contextlib
21
---> 22 from .account import Cloudant, CouchDB
23
24 @contextlib.contextmanager

/opt/conda/lib/python3.4/site-packages/cloudant/account.py in ()
23 import sys
24
---> 25 from .database import CloudantDatabase, CouchDatabase
26 from .changes import Feed
27 from .errors import CloudantException

/opt/conda/lib/python3.4/site-packages/cloudant/database.py in ()
23
24 from .document import Document
---> 25 from .design_document import DesignDocument
26 from .views import View
27 from .errors import CloudantException

/opt/conda/lib/python3.4/site-packages/cloudant/design_document.py in ()
17 """
18 from .document import Document
---> 19 from .views import View
20 from .errors import CloudantArgumentError
21

/opt/conda/lib/python3.4/site-packages/cloudant/views.py in ()
19 import posixpath
20
---> 21 from .result import Result, python_to_couch
22
23 class Code(str):

/opt/conda/lib/python3.4/site-packages/cloudant/result.py in ()
24 ARG_TYPES = {
25 "descending": bool,
---> 26 "endkey": (basestring, Sequence),
27 "endkey_docid": basestring,
28 "group": bool,

NameError: name 'basestring' is not defined

@vinomaster
Copy link

Ideally, I would like to avoid UID/PW credentials and leverage this type of approach for Cloudant:

import couchdb
couch = couchdb.Server("https://%s.cloudant.com" % USERNAME)
couch.resource.credentials = (USERNAME, auth=(API_KEY, API_SECRET))

Any plans for enabling this approach?

@mikerhodes
Copy link
Member

There appear to be a few questions and misconceptions here around API keys and security.

base64

Firstly, @bradwbonn remember base64 doesn't make any difference to security. I suggest storing the plain credentials, because it's easier for others and your future self to understand what the security properties of the script are (if the server's insecure, you've bigger issues anyway :) )

I think this negates the need for accessing the authorization header in your case; but clearly it could be useful for other scenarios, such as authenticating against a proxy server rather than Cloudant itself. I think our workaround works for now, but I do quite like @vinomaster's suggestion -- though it appears requests-specific (though I can't imagine changing!).

Setting permissions

To set the security on a database from an API key and password, you'll need to set up the _security document. However, looks like you can't do that yet from issue #52.

Using API keys with python-cloudant

@vinomaster I think your question is around using API keys; let me know if I misunderstood, there's a bunch of issues here :)

To use an API key and password, it's important to note that they are just username and password pairs, so you use them as follows (feel free to try it as-is, the key is a valid read key for that db at least for the next few days):

USERNAME = "bouninamendouldnimendepa"
PASSWORD = "e6fda548ce40d21ae675d03068bb0f913f2d99f1"
ACCOUNT_NAME = "mikerhodes"
DATABASE_NAME = "animaldb"
DOC_NAME = "badger"

from cloudant.account import Cloudant

client = Cloudant(USERNAME, PASSWORD, account=ACCOUNT_NAME)

# Connect to the account and establish a session cookie
client.connect()
session = client.session()

database = client[DATABASE_NAME]

document = database[DOC_NAME]
print "Got document: ", document

client.disconnect()

As @bradwbonn said, this appears to not be working with the context manager, that is, this fails:

# With context manager
with Cloudant(USERNAME, PASSWORD, account=ACCOUNT_NAME) as client:
    session = client.session()
    database = client[DATABASE_NAME]
    document = database[DOC_NAME]
    print "Got document from context manager: ", document

I filed this as issue #53.

@alfinkel
Copy link
Contributor

The call needs to be with cloudant not with Cloudant. cloudant is the name of the context manager defined in the init.py. Cloudant is the name of the Cloudant client in the account module. See https://github.com/cloudant/python-cloudant/blob/master/src/cloudant/__init__.py#L24 and http://python-cloudant.readthedocs.org/en/latest/cloudant.html#cloudant.cloudant

@alfinkel
Copy link
Contributor

@bradwbonn, I'm not sure where we are at with this conversation. Apparently a lot has happened while I was sleeping. I'll be sure never to sleep again :). But yes you found a typo in #49 (comment). The line in question should read client._cloudant_session = client.session(). I apologize for that, in my testing I was using c as the client but wanted to make it more readible in my comments to you so I changed it to client. Unfortunately I did not change it everywhere.

@alfinkel
Copy link
Contributor

@vinomaster, this library is not yet supported in Python3. See http://python-cloudant.readthedocs.org/en/latest/compatibility.html. The problem you are seeing is because of the use of basestring which was removed in Python3. We have plans to make the library Python3 compatible in the near future. See #23.

@alfinkel
Copy link
Contributor

Below is a summary of "where we are" with this issue, since it seems that we have diverged in a few directions.

@bradwbonn, setting permissions can be done for users as well as API keys by using the database module's share_database and unshare_database methods. On the topic of using Basic authentication with this library, you can still follow the work around that I presented but I think I was negligent before in not stating that extending the library is done at your risk. On a somewhat related note, relating to the work around that I presented: I am sure that you are aware that the _ prefix in Python denotes a "private" variable so it is not best practice to access these variables. However in Python you can pretty much do what you want so there are no mechanisms in place to keep you from doing so. I guess the point that I am trying to make here is keep letting us know about gaps in functionality. This will help us bridge those gaps but any work around should be treated as an exception rather than normal practice.

@vinomaster, the problem you are experiencing is simple. This library has not yet been scrubbed to be used with Python3. Unfortunately, I'm not sure that will help you much. Sorry for that.

@mikerhodes, on the topic of the main context manager, I believe the problem you encountered was simply a spelling mistake. Cloudant should have been cloudant. So I am pretty sure we can close #53. As far as the _security document goes, database level security is currently managed via the share_database and unshare_database methods found in the database module as I mentioned earlier. So we have means to update the security document. We can use #52 to add functionality that will allow us to manipulate the _security document directly since it seems as though this is a requirement now.

A lot of this along with other good stuff can be found in our docs.

@bradwbonn
Copy link
Author

This whole conversation has been super-insightful, thank you!

@alfinkel
Copy link
Contributor

@bradwbonn: I have an update on the whole setting of permissions question. See #52 (comment)

@vinomaster
Copy link

So to summarize:

  1. The python-cloudant is not yet supported on Python 3. This should be in-your-face on the readme.
  2. We are using Cloudant on Bluemix which does support API Keys. I seek only an simple pythonic way to authenticate to a db using an API key. Based on this thread, it seems this functionality is available but it is not intuitive that APIKey and USERNAME can be interchanged. Maybe two simple constructors would be more intuitive: UID/PW credentials, APIKey/Passcode credentials.

Remaining questions:

  1. When will Python3 support be available?

@vinomaster
Copy link

So after further testing... using Python 2 under Project Jupyter we are unable to pip install Cloudant.

!pip install --pre cloudant
Collecting cloudant
  Retrying (Retry(total=4, connect=None, read=None, redirect=None)) after connection broken by 'ProtocolError('Connection aborted.', gaierror(-2, 'Name or service not known'))': /simple/cloudant/

To be clear this Python 2 kernel functionality is provided via conda

!python --version
Python 3.4.3 :: Continuum Analytics, Inc.

To test try here.

At this juncture, until Python3 support it seems Project Jupyter integration with Cloudant is a non-starter.

@mikerhodes
Copy link
Member

@vinomaster Could you file a separate bug for Project Jupyter support? This bug is overloaded.

@alfinkel
Copy link
Contributor

@vinomaster, I've moved your issue with the pip install on Project Jupyter to #54.

@alfinkel
Copy link
Contributor

I think that all issues here have been either answered or moved off to their own separate issue.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

4 participants