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

Commit 68a1751

Browse files
authored
Merge pull request #236 from cloudant/52-add-support-for-security-document
Add security document management support
2 parents 514387f + f75c788 commit 68a1751

File tree

9 files changed

+327
-8
lines changed

9 files changed

+327
-8
lines changed

CHANGES.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
2.3.0 (Unreleased)
22
==================
3-
- [FIXED] Issue where the custom JSON encoder was not used when transforming
4-
data.
3+
- [FIXED] Resolved issue where the custom JSON encoder was not used when transforming data.
4+
- [NEW] Added support for managing the database security document through the SecurityDocument class and CouchDatabase convenience method ``get_security_document``.
55

66
2.2.0 (2016-10-20)
77
==================

Jenkinsfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ def test_python(pythonVersion)
1010
sh """ virtualenv tmp -p /usr/local/lib/python${pythonVersion}/bin/${pythonVersion.startsWith('3') ? "python3" : "python"}
1111
. ./tmp/bin/activate
1212
echo \$DB_USER
13-
export ADMIN_PARTY=true
1413
export RUN_CLOUDANT_TESTS=1
1514
export CLOUDANT_ACCOUNT=\$DB_USER
1615
# Temporarily disable the _db_updates tests pending resolution of case 71610

docs/modules.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Modules
88
database
99
document
1010
design_document
11+
security_document
1112
view
1213
query
1314
module_index

docs/security_document.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
security_document
2+
=================
3+
4+
.. automodule:: cloudant.security_document
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

src/cloudant/database.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
get_docs)
3131
from .document import Document
3232
from .design_document import DesignDocument
33+
from .security_document import SecurityDocument
3334
from .view import View
3435
from .index import Index, TextIndex, SpecialIndex
3536
from .query import Query
@@ -226,6 +227,19 @@ def get_design_document(self, ddoc_id):
226227

227228
return ddoc
228229

230+
def get_security_document(self):
231+
"""
232+
Retrieves the database security document as a SecurityDocument object.
233+
The returned object is useful for viewing as well as updating the
234+
the database's security document.
235+
236+
:returns: A SecurityDocument instance representing the database
237+
security document
238+
"""
239+
sdoc = SecurityDocument(self)
240+
sdoc.fetch()
241+
return sdoc
242+
229243
def get_view_result(self, ddoc_id, view_name, raw_result=False, **kwargs):
230244
"""
231245
Retrieves the view result based on the design document and view name.
@@ -930,11 +944,9 @@ def security_document(self):
930944
containing information about the users that the database
931945
is shared with.
932946
933-
:returns: Security document in JSON format
947+
:returns: Security document as a ``dict``
934948
"""
935-
resp = self.r_session.get(self.security_url)
936-
resp.raise_for_status()
937-
return resp.json()
949+
return dict(self.get_security_document())
938950

939951
@property
940952
def security_url(self):

src/cloudant/security_document.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env python
2+
# Copyright (c) 2016 IBM. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""
16+
API module/class for interacting with a security document in a database.
17+
"""
18+
import json
19+
20+
from ._2to3 import url_quote_plus
21+
22+
class SecurityDocument(dict):
23+
"""
24+
Encapsulates a JSON security document. A SecurityDocument object is
25+
instantiated with a reference to a database and used to manipulate security
26+
document content in a CouchDB or Cloudant database instance.
27+
28+
In addition to basic read/write operations, a SecurityDocument object also
29+
provides a convenient context manager. This context manager removes having
30+
to explicitly :func:`~cloudant.security_document.SecurityDocument.fetch`
31+
the security document from the remote database before commencing work on it
32+
as well as explicitly having to
33+
:func:`~cloudant.security_document.SecurityDocument.save` the security
34+
document once work is complete.
35+
36+
For example:
37+
38+
.. code-block:: python
39+
40+
# Upon entry into the security document context, fetches the security
41+
# document from the remote database, if it exists. Upon exit from the
42+
# context, saves the security document to the remote database with
43+
# changes made within the context.
44+
with SecurityDocument(database) as security_document:
45+
# The security document is fetched from the remote database
46+
# Changes are made locally
47+
security_document['Cloudant']['julia'] = ['_reader', '_writer']
48+
security_document['Cloudant']['ruby'] = ['_admin', '_replicator']
49+
# The security document is saved to the remote database
50+
51+
:param database: A database instance used by the SecurityDocument. Can be
52+
either a ``CouchDatabase`` or ``CloudantDatabase`` instance.
53+
"""
54+
def __init__(self, database):
55+
super(SecurityDocument, self).__init__()
56+
self._client = database.client
57+
self._database = database
58+
self._database_host = self._client.server_url
59+
self._database_name = database.database_name
60+
self.encoder = self._client.encoder
61+
62+
@property
63+
def document_url(self):
64+
"""
65+
Constructs and returns the security document URL.
66+
67+
:returns: Security document URL
68+
"""
69+
return '/'.join([
70+
self._database_host,
71+
url_quote_plus(self._database_name),
72+
'_security'
73+
])
74+
75+
@property
76+
def r_session(self):
77+
"""
78+
Returns the Python requests session used by the security document.
79+
80+
:returns: The Python requests session
81+
"""
82+
return self._client.r_session
83+
84+
def json(self):
85+
"""
86+
Retrieves the JSON string representation of the current locally cached
87+
security document object, encoded by the encoder specified in the
88+
associated client object.
89+
90+
:returns: Encoded JSON string containing the security document data
91+
"""
92+
return json.dumps(dict(self), cls=self.encoder)
93+
94+
def fetch(self):
95+
"""
96+
Retrieves the content of the current security document from the remote
97+
database and populates the locally cached SecurityDocument object with
98+
that content. A call to fetch will overwrite any dictionary content
99+
currently in the locally cached SecurityDocument object.
100+
"""
101+
resp = self.r_session.get(self.document_url)
102+
resp.raise_for_status()
103+
self.clear()
104+
self.update(resp.json())
105+
106+
def save(self):
107+
"""
108+
Saves changes made to the locally cached SecurityDocument object's data
109+
structures to the remote database.
110+
"""
111+
resp = self.r_session.put(
112+
self.document_url,
113+
data=self.json(),
114+
headers={'Content-Type': 'application/json'}
115+
)
116+
resp.raise_for_status()
117+
118+
def __enter__(self):
119+
"""
120+
Supports context like editing of security document fields.
121+
Handles context entry logic. Executes a
122+
:func:`~cloudant.security_document.SecurityDocument.fetch` upon entry.
123+
"""
124+
self.fetch()
125+
return self
126+
127+
def __exit__(self, *args):
128+
"""
129+
Support context like editing of security document fields.
130+
Handles context exit logic. Executes a
131+
:func:`~cloudant.security_document.SecurityDocument.save` upon exit.
132+
"""
133+
self.save()

tests/unit/database_tests.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from cloudant.error import CloudantException, CloudantArgumentError
3434
from cloudant.document import Document
3535
from cloudant.design_document import DesignDocument
36+
from cloudant.security_document import SecurityDocument
3637
from cloudant.index import Index, TextIndex, SpecialIndex
3738
from cloudant.feed import Feed, InfiniteFeed
3839

@@ -315,6 +316,15 @@ def test_retrieve_design_document(self):
315316
ddoc = self.db.get_design_document('_design/ddoc01')
316317
self.assertEqual(ddoc, local_ddoc)
317318

319+
def test_get_security_document(self):
320+
"""
321+
Test retrieving the database security document
322+
"""
323+
self.load_security_document_data()
324+
sdoc = self.db.get_security_document()
325+
self.assertIsInstance(sdoc, SecurityDocument)
326+
self.assertDictEqual(sdoc, self.sdoc)
327+
318328
def test_retrieve_view_results(self):
319329
"""
320330
Test retrieving Result wrapped output from a design document view
@@ -878,7 +888,7 @@ def test_unshare_database_uses_custom_encoder(self):
878888
with self.assertRaises(TypeError):
879889
database.unshare_database(share)
880890

881-
def test_get_security_document(self):
891+
def test_security_document(self):
882892
"""
883893
Test the retrieval of the security document.
884894
"""
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env python
2+
# Copyright (c) 2016 IBM. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""
16+
security_document module - Unit tests for the SecurityDocument class
17+
18+
See configuration options for environment variables in unit_t_db_base
19+
module docstring.
20+
"""
21+
22+
import unittest
23+
import requests
24+
import json
25+
import os
26+
27+
from cloudant.security_document import SecurityDocument
28+
29+
from .unit_t_db_base import UnitTestDbBase
30+
31+
32+
class SecurityDocumentTests(UnitTestDbBase):
33+
"""
34+
SecurityDocument unit tests
35+
"""
36+
37+
def setUp(self):
38+
"""
39+
Set up test attributes
40+
"""
41+
super(SecurityDocumentTests, self).setUp()
42+
self.db_set_up()
43+
self.load_security_document_data()
44+
45+
def tearDown(self):
46+
"""
47+
Reset test attributes
48+
"""
49+
self.db_tear_down()
50+
super(SecurityDocumentTests, self).tearDown()
51+
52+
def test_constructor(self):
53+
"""
54+
Test constructing a SecurityDocument
55+
"""
56+
sdoc = SecurityDocument(self.db)
57+
self.assertIsInstance(sdoc, SecurityDocument)
58+
self.assertEqual(sdoc.r_session, self.db.r_session)
59+
60+
def test_document_url(self):
61+
"""
62+
Test that the document url is populated correctly
63+
"""
64+
sdoc = SecurityDocument(self.db)
65+
self.assertEqual(
66+
sdoc.document_url,
67+
'/'.join([self.db.database_url, '_security'])
68+
)
69+
70+
def test_json(self):
71+
"""
72+
Test the security document dictionary renders as a JSON string
73+
"""
74+
sdoc = SecurityDocument(self.db)
75+
sdoc.fetch()
76+
sdoc_as_json_string = sdoc.json()
77+
self.assertIsInstance(sdoc_as_json_string, str)
78+
sdoc_as_a_dict = json.loads(sdoc_as_json_string)
79+
self.assertDictEqual(sdoc_as_a_dict, sdoc)
80+
81+
def test_fetch(self):
82+
"""
83+
Test that the security document is retrieved as expected
84+
"""
85+
sdoc = SecurityDocument(self.db)
86+
sdoc.fetch()
87+
self.assertDictEqual(sdoc, self.sdoc)
88+
89+
def test_save(self):
90+
"""
91+
Test that the security document is updated correctly
92+
"""
93+
sdoc = SecurityDocument(self.db)
94+
sdoc.fetch()
95+
sdoc.update(self.mod_sdoc)
96+
sdoc.save()
97+
mod_sdoc = SecurityDocument(self.db)
98+
mod_sdoc.fetch()
99+
self.assertDictEqual(mod_sdoc, self.mod_sdoc)
100+
101+
def test_context_manager(self):
102+
"""
103+
Test that the context SecurityDocument context manager enter and exit
104+
routines work as expected.
105+
"""
106+
with SecurityDocument(self.db) as sdoc:
107+
self.assertDictEqual(sdoc, self.sdoc)
108+
sdoc.update(self.mod_sdoc)
109+
mod_sdoc = SecurityDocument(self.db)
110+
mod_sdoc.fetch()
111+
self.assertDictEqual(mod_sdoc, self.mod_sdoc)
112+
113+
114+
if __name__ == '__main__':
115+
unittest.main()

0 commit comments

Comments
 (0)