Merge lp:~cjwatson/turnip/turnipcake-auth-uid into lp:turnip

Proposed by Colin Watson
Status: Superseded
Proposed branch: lp:~cjwatson/turnip/turnipcake-auth-uid
Merge into: lp:turnip
Diff against target: 322 lines (+291/-0) (has conflicts)
6 files modified
README.rst (+4/-0)
setup.py (+40/-0)
turnipcake.ini (+49/-0)
turnipcake/__init__.py (+22/-0)
turnipcake/models.py (+38/-0)
turnipcake/views.py (+138/-0)
Conflict adding file setup.py.  Moved existing file to setup.py.moved.
To merge this branch: bzr merge lp:~cjwatson/turnip/turnipcake-auth-uid
Reviewer Review Type Date Requested Status
Canonical Launchpad Branches Pending
Review via email: mp+248421@code.launchpad.net

This proposal has been superseded by a proposal from 2015-02-03.

Commit message

Return user.id as an additional dict entry from authenticateWithPassword; require user ID rather than name in translatePath.

Description of the change

Return user.id as an additional dict entry from authenticateWithPassword; require user ID rather than name in translatePath.

This is an incompatible API change, and goes together with https://code.launchpad.net/~cjwatson/turnip/auth-uid/+merge/248420.

To post a comment you must log in.

Unmerged revisions

10. By Colin Watson

Return user.id as an additional dict entry from authenticateWithPassword; require user ID rather than name in translatePath.

9. By William Grant

Repos have owners, and only the owner can write to them.

8. By William Grant

Actual user authentication.

7. By William Grant

Add a trivial authenticateWithPassword.

6. By William Grant

Support username-based authorisation.

5. By William Grant

Check repo existence before failing on read-onlyness.

4. By William Grant

Update translatePath to check permission itself.

3. By William Grant

Update turnip.endpoint to the new port layout.

2. By William Grant

Basic SQLite-backed turnip virtinfo service, with a JSON API to create and list repositories.

1. By William Grant

Initial paster cornice template.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'README.rst'
2--- README.rst 1970-01-01 00:00:00 +0000
3+++ README.rst 2015-02-03 17:12:01 +0000
4@@ -0,0 +1,4 @@
5+Documentation
6+=============
7+
8+Put a brief description of 'turnipcake'.
9
10=== added file 'setup.py'
11--- setup.py 1970-01-01 00:00:00 +0000
12+++ setup.py 2015-02-03 17:12:01 +0000
13@@ -0,0 +1,40 @@
14+import os
15+from setuptools import setup, find_packages
16+
17+here = os.path.abspath(os.path.dirname(__file__))
18+
19+with open(os.path.join(here, 'README.rst')) as f:
20+ README = f.read()
21+
22+
23+setup(name='turnipcake',
24+ version=0.1,
25+ description='turnipcake',
26+ long_description=README,
27+ classifiers=[
28+ "Programming Language :: Python",
29+ "Framework :: Pylons",
30+ "Topic :: Internet :: WWW/HTTP",
31+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application"
32+ ],
33+ keywords="web services",
34+ author='',
35+ author_email='',
36+ url='',
37+ packages=find_packages(),
38+ include_package_data=True,
39+ zip_safe=False,
40+ install_requires=[
41+ 'cornice',
42+ 'pyramid_rpc',
43+ 'pyramid_tm',
44+ 'requests',
45+ 'sqlalchemy',
46+ 'waitress',
47+ 'zope.sqlalchemy',
48+ ],
49+ entry_points="""\
50+ [paste.app_factory]
51+ main = turnipcake:main
52+ """,
53+ paster_plugins=['pyramid'])
54
55=== renamed file 'setup.py' => 'setup.py.moved'
56=== added directory 'turnipcake'
57=== added file 'turnipcake.ini'
58--- turnipcake.ini 1970-01-01 00:00:00 +0000
59+++ turnipcake.ini 2015-02-03 17:12:01 +0000
60@@ -0,0 +1,49 @@
61+[app:main]
62+use = egg:turnipcake
63+
64+pyramid.reload_templates = true
65+pyramid.debug_authorization = false
66+pyramid.debug_notfound = false
67+pyramid.debug_routematch = false
68+pyramid.debug_templates = true
69+pyramid.default_locale_name = en
70+
71+sqlalchemy.url = sqlite:///turnipcake.db
72+
73+turnip.endpoint = http://localhost:19417/
74+
75+[server:main]
76+use = egg:waitress#main
77+host = 0.0.0.0
78+port = 6543
79+
80+# Begin logging configuration
81+
82+[loggers]
83+keys = root, turnipcake
84+
85+[handlers]
86+keys = console
87+
88+[formatters]
89+keys = generic
90+
91+[logger_root]
92+level = INFO
93+handlers = console
94+
95+[logger_turnipcake]
96+level = DEBUG
97+handlers =
98+qualname = turnipcake
99+
100+[handler_console]
101+class = StreamHandler
102+args = (sys.stderr,)
103+level = NOTSET
104+formatter = generic
105+
106+[formatter_generic]
107+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
108+
109+# End logging configuration
110
111=== added file 'turnipcake/__init__.py'
112--- turnipcake/__init__.py 1970-01-01 00:00:00 +0000
113+++ turnipcake/__init__.py 2015-02-03 17:12:01 +0000
114@@ -0,0 +1,22 @@
115+"""Main entry point
116+"""
117+from pyramid.config import Configurator
118+from sqlalchemy import engine_from_config
119+
120+from .models import (
121+ Base,
122+ DBSession,
123+ )
124+
125+
126+def main(global_config, **settings):
127+ engine = engine_from_config(settings, 'sqlalchemy.')
128+ DBSession.configure(bind=engine)
129+ Base.metadata.bind = engine
130+ config = Configurator(settings=settings)
131+ config.include("cornice")
132+ config.include("pyramid_rpc.xmlrpc")
133+ config.include("pyramid_tm")
134+ config.add_xmlrpc_endpoint('githosting', '/githosting')
135+ config.scan("turnipcake.views")
136+ return config.make_wsgi_app()
137
138=== added file 'turnipcake/models.py'
139--- turnipcake/models.py 1970-01-01 00:00:00 +0000
140+++ turnipcake/models.py 2015-02-03 17:12:01 +0000
141@@ -0,0 +1,38 @@
142+import datetime
143+
144+from sqlalchemy import (
145+ Column,
146+ DateTime,
147+ ForeignKey,
148+ Integer,
149+ Text,
150+ )
151+from sqlalchemy.ext.declarative import declarative_base
152+from sqlalchemy.orm import (
153+ relationship,
154+ scoped_session,
155+ sessionmaker,
156+ )
157+
158+from zope.sqlalchemy import ZopeTransactionExtension
159+
160+DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
161+Base = declarative_base()
162+
163+
164+class Repo(Base):
165+ __tablename__ = "repo"
166+ id = Column(Integer, primary_key=True)
167+ name = Column(Text)
168+ date_created = Column(DateTime, default=datetime.datetime.utcnow)
169+ owner_id = Column(Integer, ForeignKey('user.id'))
170+ owner = relationship('User', backref='repos')
171+ turnip_id = Column(Text)
172+
173+
174+class User(Base):
175+ __tablename__ = "user"
176+ id = Column(Integer, primary_key=True)
177+ name = Column(Text)
178+ date_created = Column(DateTime, default=datetime.datetime.utcnow)
179+ password = Column(Text)
180
181=== added file 'turnipcake/views.py'
182--- turnipcake/views.py 1970-01-01 00:00:00 +0000
183+++ turnipcake/views.py 2015-02-03 17:12:01 +0000
184@@ -0,0 +1,138 @@
185+import uuid
186+import xmlrpclib
187+
188+from cornice.resource import resource
189+from cornice.util import extract_json_data
190+from pyramid.httpexceptions import (
191+ HTTPCreated,
192+ HTTPNotFound,
193+ )
194+from pyramid_rpc.xmlrpc import xmlrpc_method
195+import requests
196+
197+from .models import (
198+ DBSession,
199+ Repo,
200+ User,
201+ )
202+
203+from sqlalchemy.orm.exc import NoResultFound
204+
205+
206+@resource(collection_path='/repos', path='/repos/{name}')
207+class RepoAPI(object):
208+
209+ def __init__(self, request):
210+ self.request = request
211+
212+ def collection_get(self):
213+ return {'repos': [repo.name for repo in DBSession.query(Repo)]}
214+
215+ def collection_post(self):
216+ name = extract_json_data(self.request).get('name')
217+ if not name:
218+ self.request.errors.add('body', 'name', 'name is missing')
219+ return
220+ try:
221+ DBSession.query(Repo).filter(Repo.name == name).one()
222+ except NoResultFound:
223+ pass
224+ else:
225+ self.request.errors.add('body', 'name', 'name already exists')
226+ return
227+ owner_name = extract_json_data(self.request).get('owner')
228+ if not owner_name:
229+ self.request.errors.add('body', 'owner', 'owner_name is missing')
230+ return
231+ try:
232+ owner = DBSession.query(User).filter(User.name == owner_name).one()
233+ except NoResultFound:
234+ self.request.errors.add('body', 'owner', 'owner does not exist')
235+ return
236+ repo = Repo(name=name, owner=owner, turnip_id=str(uuid.uuid4()))
237+ DBSession.add(repo)
238+ create_resp = requests.post(
239+ self.request.registry.settings['turnip.endpoint'] + 'create',
240+ data={'path': repo.turnip_id})
241+ if create_resp.status_code != 200:
242+ raise Exception("turnip failed to create the repository")
243+ return HTTPCreated(
244+ self.request.route_url('repoapi', name=repo.name))
245+
246+ def get(self):
247+ name = self.request.matchdict['name']
248+ try:
249+ DBSession.query(Repo).filter(Repo.name == name).one()
250+ except NoResultFound:
251+ return HTTPNotFound('Repository not found')
252+ return {'name': self.request.matchdict['name']}
253+
254+
255+@resource(collection_path='/users', path='/users/{name}')
256+class UserAPI(object):
257+
258+ def __init__(self, request):
259+ self.request = request
260+
261+ def collection_get(self):
262+ return {'users': [user.name for user in DBSession.query(User)]}
263+
264+ def collection_post(self):
265+ name = extract_json_data(self.request).get('name')
266+ if not name:
267+ self.request.errors.add('body', 'name', 'name is missing')
268+ return
269+ password = extract_json_data(self.request).get('password')
270+ if not name:
271+ self.request.errors.add('body', 'password', 'password is missing')
272+ return
273+ try:
274+ DBSession.query(User).filter(User.name == name).one()
275+ except NoResultFound:
276+ pass
277+ else:
278+ self.request.errors.add('body', 'name', 'name already exists')
279+ return
280+ user = User(name=name, password=password)
281+ DBSession.add(user)
282+ return HTTPCreated(
283+ self.request.route_url('userapi', name=user.name))
284+
285+ def get(self):
286+ name = self.request.matchdict['name']
287+ try:
288+ DBSession.query(User).filter(User.name == name).one()
289+ except NoResultFound:
290+ return HTTPNotFound('User not found')
291+ return {'name': self.request.matchdict['name']}
292+
293+
294+@xmlrpc_method(endpoint='githosting')
295+def translatePath(request, path, permission, authenticated_uid,
296+ can_authenticate):
297+ try:
298+ repo = DBSession.query(Repo).filter(
299+ Repo.name == path.lstrip('/')).one()
300+ except NoResultFound:
301+ raise xmlrpclib.Fault(1, "Repo does not exist")
302+
303+ writable = authenticated_uid == repo.owner_id
304+
305+ if permission != b'read' and not writable:
306+ if not can_authenticate or authenticated_uid is not None:
307+ raise xmlrpclib.Fault(2, "Repo is read-only")
308+ else:
309+ raise xmlrpclib.Fault(3, "Authorisation required")
310+
311+ return {'path': repo.turnip_id, 'writable': writable}
312+
313+
314+@xmlrpc_method(endpoint='githosting')
315+def authenticateWithPassword(request, username, password):
316+ try:
317+ user = DBSession.query(User).filter(User.name == username).one()
318+ except NoResultFound:
319+ raise xmlrpclib.Fault(3, "Invalid username or password")
320+ if password != user.password:
321+ raise xmlrpclib.Fault(3, "Invalid username or password")
322+ return {'user': username, 'uid': user.id}

Subscribers

People subscribed via source and target branches

to all changes: