Merge lp:~pedronis/u1db/naming-constraints into lp:u1db

Proposed by Samuele Pedroni
Status: Merged
Approved by: Samuele Pedroni
Approved revision: 137
Merged at revision: 137
Proposed branch: lp:~pedronis/u1db/naming-constraints
Merge into: lp:u1db
Diff against target: 237 lines (+81/-4)
12 files modified
u1db/__init__.py (+7/-0)
u1db/backends/__init__.py (+12/-0)
u1db/backends/inmemory.py (+1/-0)
u1db/backends/sqlite_backend.py (+1/-0)
u1db/errors.py (+1/-0)
u1db/remote/http_app.py (+3/-1)
u1db/remote/http_client.py (+6/-1)
u1db/remote/http_errors.py (+4/-2)
u1db/tests/test_backends.py (+6/-0)
u1db/tests/test_http_app.py (+26/-0)
u1db/tests/test_http_client.py (+5/-0)
u1db/tests/test_http_database.py (+9/-0)
To merge this branch: bzr merge lp:~pedronis/u1db/naming-constraints
Reviewer Review Type Date Requested Status
John A Meinel (community) Approve
Review via email: mp+84109@code.launchpad.net

Description of the change

* start imposing constraints on:

db names (for remote access): [a-zA-Z0-9][a-zA-Z0-9.-]*

doc ids: [^\\/]+

* fix quoting issues when constructing URLs in HTTPClient, e.g. %FFFF should be a supported doc id

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

I think:

ERROR_STATUSES.add(400)

is clearer than doing the set "|" operations.

Otherwise, looks good.

review: Approve
lp:~pedronis/u1db/naming-constraints updated
137. By Samuele Pedroni

style tweak

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'u1db/__init__.py'
2--- u1db/__init__.py 2011-11-30 13:14:11 +0000
3+++ u1db/__init__.py 2011-12-01 15:18:24 +0000
4@@ -33,6 +33,13 @@
5 return sqlite_backend.SQLiteDatabase.open_database(path, create=create)
6
7
8+# constraints on database names (relevant for remote access, as regex)
9+DBNAME_CONSTRAINTS = r"[a-zA-Z0-9][a-zA-Z0-9.-]*"
10+
11+# constraints on doc ids (as regex)
12+DOC_ID_CONSTRAINTS = r"[^/\\]+"
13+
14+
15 class Database(object):
16 """A JSON Document data store.
17
18
19=== modified file 'u1db/backends/__init__.py'
20--- u1db/backends/__init__.py 2011-11-28 15:16:41 +0000
21+++ u1db/backends/__init__.py 2011-12-01 15:18:24 +0000
22@@ -14,11 +14,19 @@
23
24 """"""
25
26+import re
27+
28 import u1db
29+from u1db import (
30+ errors,
31+)
32 import u1db.sync
33 from u1db.vectorclock import VectorClockRev
34
35
36+check_doc_id_re = re.compile("^" + u1db.DOC_ID_CONSTRAINTS + "$")
37+
38+
39 class CommonSyncTarget(u1db.sync.LocalSyncTarget):
40 pass
41
42@@ -34,6 +42,10 @@
43 vcr.increment(self._replica_uid)
44 return vcr.as_str()
45
46+ def _check_doc_id(self, doc_id):
47+ if not check_doc_id_re.match(doc_id):
48+ raise errors.InvalidDocId()
49+
50 def _get_generation(self):
51 raise NotImplementedError(self._get_generation)
52
53
54=== modified file 'u1db/backends/inmemory.py'
55--- u1db/backends/inmemory.py 2011-11-29 14:45:48 +0000
56+++ u1db/backends/inmemory.py 2011-12-01 15:18:24 +0000
57@@ -61,6 +61,7 @@
58 def put_doc(self, doc):
59 if doc.doc_id is None:
60 raise errors.InvalidDocId()
61+ self._check_doc_id(doc.doc_id)
62 if self._has_conflicts(doc.doc_id):
63 raise errors.ConflictedDoc()
64 old_doc = self._get_doc(doc.doc_id)
65
66=== modified file 'u1db/backends/sqlite_backend.py'
67--- u1db/backends/sqlite_backend.py 2011-12-01 14:53:45 +0000
68+++ u1db/backends/sqlite_backend.py 2011-12-01 15:18:24 +0000
69@@ -269,6 +269,7 @@
70 def put_doc(self, doc):
71 if doc.doc_id is None:
72 raise errors.InvalidDocId()
73+ self._check_doc_id(doc.doc_id)
74 with self._db_handle:
75 if self._has_conflicts(doc.doc_id):
76 raise errors.ConflictedDoc()
77
78=== modified file 'u1db/errors.py'
79--- u1db/errors.py 2011-11-28 19:49:13 +0000
80+++ u1db/errors.py 2011-12-01 15:18:24 +0000
81@@ -34,6 +34,7 @@
82 class InvalidDocId(U1DBError):
83 """A document was tried with an invalid document identifier."""
84
85+ wire_description = "invalid document id"
86
87 class ConflictedDoc(U1DBError):
88 """The document is conflicted, you must call resolve before put()"""
89
90=== modified file 'u1db/remote/http_app.py'
91--- u1db/remote/http_app.py 2011-11-30 14:25:18 +0000
92+++ u1db/remote/http_app.py 2011-12-01 15:18:24 +0000
93@@ -27,6 +27,7 @@
94 __version__ as _u1db_version,
95 Document,
96 errors,
97+ DBNAME_CONSTRAINTS,
98 )
99 from u1db.remote import (
100 http_errors,
101@@ -149,7 +150,8 @@
102 def register(self, resource_cls):
103 # register
104 self._map.connect(None, resource_cls.url_pattern,
105- resource_cls=resource_cls)
106+ resource_cls=resource_cls,
107+ requirements={"dbname": DBNAME_CONSTRAINTS})
108 self._map.create_regs()
109 return resource_cls
110
111
112=== modified file 'u1db/remote/http_client.py'
113--- u1db/remote/http_client.py 2011-11-29 10:26:37 +0000
114+++ u1db/remote/http_client.py 2011-12-01 15:18:24 +0000
115@@ -69,7 +69,12 @@
116 def _request(self, method, url_parts, params=None, body=None,
117 content_type=None):
118 self._ensure_connection()
119- url_query = '/'.join([self._url.path] + url_parts)
120+ url_query = self._url.path
121+ if url_parts:
122+ if not url_query.endswith('/'):
123+ url_query += '/'
124+ url_query += '/'.join(urllib.quote(part, safe='')
125+ for part in url_parts)
126 if params:
127 url_query += ('?' +
128 urllib.urlencode(dict((unicode(v).encode('utf-8'),
129
130=== modified file 'u1db/remote/http_errors.py'
131--- u1db/remote/http_errors.py 2011-11-29 17:12:28 +0000
132+++ u1db/remote/http_errors.py 2011-12-01 15:18:24 +0000
133@@ -21,6 +21,7 @@
134
135 # error wire descriptions mapping to HTTP status codes
136 wire_description_to_status = dict([
137+ (errors.InvalidDocId.wire_description, 400),
138 (errors.DatabaseDoesNotExist.wire_description, 404),
139 (errors.DocumentDoesNotExist.wire_description, 404),
140 (errors.DocumentAlreadyDeleted.wire_description, 404),
141@@ -30,6 +31,7 @@
142 ])
143
144
145-# 400 included for tests/anticipated usage
146-ERROR_STATUSES = set(wire_description_to_status.values())|set([400])
147
148+ERROR_STATUSES = set(wire_description_to_status.values())
149+# 400 included explicitly for tests
150+ERROR_STATUSES.add(400)
151
152=== modified file 'u1db/tests/test_backends.py'
153--- u1db/tests/test_backends.py 2011-11-29 14:45:48 +0000
154+++ u1db/tests/test_backends.py 2011-12-01 15:18:24 +0000
155@@ -89,6 +89,12 @@
156 doc = Document(None, None, simple_doc)
157 self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc)
158
159+ def test_put_doc_refuses_slashes(self):
160+ doc = Document('/a', None, simple_doc)
161+ self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc)
162+ doc = Document(r'\b', None, simple_doc)
163+ self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc)
164+
165 def test_put_doc_refuses_non_existing_old_rev(self):
166 doc = Document('doc-id', 'test:4', simple_doc)
167 self.assertRaises(errors.RevisionConflict, self.db.put_doc, doc)
168
169=== modified file 'u1db/tests/test_http_app.py'
170--- u1db/tests/test_http_app.py 2011-11-30 13:46:44 +0000
171+++ u1db/tests/test_http_app.py 2011-12-01 15:18:24 +0000
172@@ -439,6 +439,32 @@
173 self.assertEqual('application/json', resp.header('content-type'))
174 self.assertEqual({}, simplejson.loads(resp.body))
175
176+ def test_valid_database_names(self):
177+ resp = self.app.get('/a-database', expect_errors=True)
178+ self.assertEqual(404, resp.status)
179+
180+ resp = self.app.get('/db1', expect_errors=True)
181+ self.assertEqual(404, resp.status)
182+
183+ resp = self.app.get('/0', expect_errors=True)
184+ self.assertEqual(404, resp.status)
185+
186+ resp = self.app.get('/0-0', expect_errors=True)
187+ self.assertEqual(404, resp.status)
188+
189+ resp = self.app.get('/org.future', expect_errors=True)
190+ self.assertEqual(404, resp.status)
191+
192+ def test_invalid_database_names(self):
193+ resp = self.app.get('/.a', expect_errors=True)
194+ self.assertEqual(400, resp.status)
195+
196+ resp = self.app.get('/-a', expect_errors=True)
197+ self.assertEqual(400, resp.status)
198+
199+ resp = self.app.get('/_a', expect_errors=True)
200+ self.assertEqual(400, resp.status)
201+
202 def test_put_doc_create(self):
203 resp = self.app.put('/db0/doc/doc1', params='{"x": 1}',
204 headers={'content-type': 'application/json'})
205
206=== modified file 'u1db/tests/test_http_client.py'
207--- u1db/tests/test_http_client.py 2011-11-28 17:08:19 +0000
208+++ u1db/tests/test_http_client.py 2011-12-01 15:18:24 +0000
209@@ -111,6 +111,11 @@
210 'QUERY_STRING': 'a=1',
211 'REQUEST_METHOD': 'GET'}, simplejson.loads(res))
212
213+ res, headers = cli._request('GET', ['doc', '%FFFF', 'echo'], {'a': 1})
214+ self.assertEqual({'PATH_INFO': '/dbase/doc/%FFFF/echo',
215+ 'QUERY_STRING': 'a=1',
216+ 'REQUEST_METHOD': 'GET'}, simplejson.loads(res))
217+
218 res, headers = cli._request('POST', ['echo'], {'b': 2}, 'Body',
219 'application/x-test')
220 self.assertEqual({'CONTENT_TYPE': 'application/x-test',
221
222=== modified file 'u1db/tests/test_http_database.py'
223--- u1db/tests/test_http_database.py 2011-11-30 13:18:42 +0000
224+++ u1db/tests/test_http_database.py 2011-12-01 15:18:24 +0000
225@@ -181,3 +181,12 @@
226 db = http_database.HTTPDatabase.open_database(self.getURL('new'),
227 create=True)
228 self.assertIs(None, db.get_doc('doc1'))
229+
230+ def test_doc_ids_needing_quoting(self):
231+ db0 = self.request_state._create_database('db0')
232+ db = http_database.HTTPDatabase.open_database(self.getURL('db0'),
233+ create=False)
234+ doc = Document('%fff', None, '{}')
235+ db.put_doc(doc)
236+ self.assertGetDoc(db0, '%fff', doc.rev, '{}', False)
237+ self.assertGetDoc(db, '%fff', doc.rev, '{}', False)

Subscribers

People subscribed via source and target branches