metricq / aiocouch

🛋 An asynchronous client library for CouchDB 2.x and 3.x
https://aiocouch.readthedocs.io/en/latest/
BSD 3-Clause "New" or "Revised" License
29 stars 10 forks source link

HTTP 403 (Forbidden) not wrapped #14

Closed adrienverge closed 4 years ago

adrienverge commented 4 years ago

In some cases, CouchDB can return HTTP 403 errors (for instance with usage of _security and/or validation functions). Here is a fully working example* to demonstrate 2 types of errors 403:

(* needs https://github.com/metricq/aiocouch/pull/13)

import asyncio

from aiocouch import CouchDB
from aiocouch.document import Document

URL = 'http://localhost:5984'

async def prepare():
    async with CouchDB(URL, user='root', password='password') as couch:
        users = await couch['_users']

        stranger = Document(users, 'org.couchdb.user:stranger')
        stranger.update({'name': 'stranger', 'password': 'pass', 'type': 'user', 'roles': []})
        await stranger.save()

        reader = Document(users, 'org.couchdb.user:reader')
        reader.update({'name': 'reader', 'password': 'pass', 'type': 'user', 'roles': []})
        await reader.save()

        writer = Document(users, 'org.couchdb.user:writer')
        writer.update({'name': 'writer', 'password': 'pass', 'type': 'user', 'roles': []})
        await writer.save()

        db = await couch.create('db')
        # Only 'reader' and 'writer' will be able to access this base:
        await db.set_security({'members': {'names': ['reader', 'writer']}})

        ddoc = await db.design_doc('_design/write_access')
        # Only 'writer' will be able to create or update documents:
        ddoc.update({
            'validate_doc_update': '''
                function(newDoc, oldDoc, userCtx, secObj) {
                  if (userCtx.name == 'writer')
                    return;
                  throw {forbidden: 'You are not allowed to write to this database'};
                }''',
        })
        await ddoc.save()

async def test():
    async with CouchDB(URL, user='stranger', password='pass') as couch:
        db = await couch['db']  # HTTP 403

    async with CouchDB(URL, user='writer', password='pass') as couch:
        db = await couch['db']                # OK
        await(await db.create('doc')).save()  # OK
        print(await db['doc'])                # OK

    async with CouchDB(URL, user='reader', password='pass') as couch:
        db = await couch['db']  # OK
        doc = await db['doc']   # OK
        doc["changed"] = True
        await doc.save()        # HTTP 403

asyncio.get_event_loop().run_until_complete(prepare())
asyncio.get_event_loop().run_until_complete(test())

I'll work on a commit to encapsulate aiohttp.client_exceptions.ClientResponseError(403) (currently raised) in the same manner as UnauthorizedError, BadRequestError, etc.