Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to tornado (Python 3) #12

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
image: python:2.7

stages:
- test
- deploy

test:
stage: test
script:
- pip install -U nox-automation
- nox

deploy_production:
stage: deploy
only:
- master
before_script:
- echo "deb http://packages.cloud.google.com/apt cloud-sdk-jessie main" | tee /etc/apt/sources.list.d/google-cloud-sdk.list
- curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
- apt-get update && apt-get install -y google-cloud-sdk
script:
- pip install -U -t libs -r requirements-app.txt
- echo $DEPLOY_KEY_FILE_PRODUCTION > /tmp/$CI_PIPELINE_ID.json
- gcloud auth activate-service-account --key-file /tmp/$CI_PIPELINE_ID.json
- echo $ML2GROW_HTTP_AUTH > config.json
- cat config.json
- gcloud --verbosity=info --quiet --project $PROJECT_ID_PRODUCTION app deploy app.yaml
after_script:
- rm /tmp/$CI_PIPELINE_ID.json



2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Release 0.0.1
Initial release of GAEPyPI
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ Before deploying, install the required dependencies to the libs folder:
pip install -t libs -r requirements-app.txt
```
Next, have a look at config.json, you can set up multiple accounts for the http auth. The passwords are assumed to be hashed with sha1, you can obtain the hash as follows:

```python
from webapp2_extras import security
security.hash_password('password', method='sha1')
Expand All @@ -33,7 +32,6 @@ echo -n "password" | openssl sha1
# shasum
echo -n "password" | shasum
```

Now, you are ready to deploy to google app engine. Depending on how you set up your projects, this may require adjusting app.yaml, or setting the right project for gcloud. Then:
```
gcloud app deploy app.yaml
Expand Down
4 changes: 1 addition & 3 deletions app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

runtime: python27
runtime: python37
api_version: 1
threadsafe: yes

Expand All @@ -27,8 +27,6 @@ handlers:
script: gaepypi.app

libraries:
- name: webapp2
version: latest
- name: jinja2
version: latest
- name: six
Expand Down
51 changes: 26 additions & 25 deletions gaepypi/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,56 +17,57 @@
import base64
import json
from functools import wraps
from webapp2_extras import security
from hashlib import sha1


def basic_auth(required_roles=None):
"""
Decorator to require HTTP auth for request handler
"""
def decorator_basic_auth(func):
@wraps(func)
def callf(handler, *args, **kwargs):
auth_header = handler.request.headers.get('Authorization')
if auth_header is None:
__basic_login(handler)
else:
parts = base64.b64decode(auth_header.split(' ')[1]).split(':')
username = parts[0]
password = ':'.join(parts[1:])
account = __basic_lookup(username)
return __basic_login(handler)
if not auth_header.startswith('Basic '):
return __basic_login(handler)

# Return 401 Unauthorized if user did not specify credentials or password is mismatched
if not account or account["password"] != __basic_hash(password):
return __basic_login(handler)
(username, password) = base64.b64decode(auth_header.split(' ')[1]).split(':')
account = __basic_lookup(username)

# Return 403 Forbidden if user's account does not have any of the required access roles
user_roles = account['roles'] if 'roles' in account else []
if required_roles and any([(required_role not in user_roles) for required_role in required_roles]):
return __basic_forbidden(handler)
# Return 401 Unauthorized if user did not specify credentials or password is mismatched
if not account or account["password"] != __basic_hash(password):
return __basic_login(handler)

else:
return func(handler, *args, **kwargs)
# Return 403 Forbidden if user's account does not have any of the required access roles
user_roles = account['roles'] if 'roles' in account else []
if required_roles and any([(required_role not in user_roles) for required_role in required_roles]):
return __basic_forbidden(handler)

return func(handler, *args, **kwargs)
return callf
return decorator_basic_auth


def __basic_login(handler):
handler.response.set_status(401, message="Authorization Required")
handler.response.headers['WWW-Authenticate'] = 'Basic realm="Secure Area"'
handler.set_header('WWW-Authenticate', 'Basic realm="Secure Area"')
handler.set_status(401)
handler.finish()
return False


def __basic_forbidden(handler):
handler.response.set_status(403, message="Forbidden")
def __basic_hash(password):
return sha1(password.encode('utf-8')).hexdigest()


def __basic_lookup(username):
with open('config.json') as data_file:
config = json.load(data_file)
for account in config["accounts"]:
if account['username'] == username:
return account
return config["accounts"].get(username, None)


def __basic_forbidden(handler):
handler.set_status(403, message="Forbidden")


def __basic_hash(password):
return security.hash_password(password, method='sha1')
28 changes: 14 additions & 14 deletions gaepypi/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
from .exceptions import GAEPyPIError

import os
import webapp2
from tornado import web
from cloudstorage import NotFoundError
from _decorators import basic_auth
from ._decorators import basic_auth
from google.appengine.api import app_identity


class BaseHandler(webapp2.RequestHandler):
class BaseHandler(web.RequestHandler):
"""
Basic handler class for our GAE webapp2 application
"""
Expand All @@ -34,7 +34,7 @@ def write_page(self, body):
self.response.write('<html><body>{}</body></html>'.format(body))

def write404(self):
self.response.set_status(404)
self.set_status(404)
self.write_page('<h1>Not found</h1>')

def get_storage(self):
Expand All @@ -47,19 +47,17 @@ class IndexHandler(BaseHandler):
"""
Handles /
"""

@basic_auth()
def get(self):
self.write_page('<a href="/packages">packages</a>')

@basic_auth(required_roles=['write'])
def post(self):
name = self.request.get('name', default_value=None)
version = self.request.get('version', default_value=None)
action = self.request.get(':action', default_value=None)
upload = self.request.POST.getall('content')[0]
content = upload.file.read()
filename = upload.filename
name = self.get_argument('name')
version = self.get_argument('version')
action = self.get_argument(':action')
content = self.request.files['content'][0]['body']
filename = self.request.files['content'][0]['filename']

if name and version and content and action == 'file_upload':
try:
Expand Down Expand Up @@ -145,14 +143,16 @@ class PackageDownload(BaseHandler):
Handles /packages/package/version/filename
"""

def prepare(self):
self.set_header("Content-Type", 'application/octet-stream')

@basic_auth()
def get(self, name, version, filename):
try:
package = Package(self.get_storage(), name, version)
with package.get_file(filename) as gcs_file:
self.response.content_type = 'application/octet-stream'
self.response.headers.add('Content-Disposition', 'attachment; filename={0}'.format(filename))
self.response.write(gcs_file.read())
self.set_header('Content-Disposition', 'attachment; filename={0}'.format(filename))
self.write(gcs_file.read())
except NotFoundError:
self.write404()

Expand Down
2 changes: 1 addition & 1 deletion gaepypi/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

__version__ = '0.0.1'
__version__ = '0.0.2'
4 changes: 1 addition & 3 deletions gaepypi/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@
from .templates import __templates__
from .renderable import Renderable

import six
from contextlib import contextmanager
from abc import ABCMeta, abstractmethod
from abc import abstractmethod


@six.add_metaclass(ABCMeta)
class BucketObject(Renderable):
def __init__(self, storage, *args, **kwargs):
super(BucketObject, self).__init__(*args, **kwargs)
Expand Down
3 changes: 1 addition & 2 deletions gaepypi/renderable.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from abc import ABCMeta, abstractmethod

from abc import abstractmethod, ABCMeta
import six


Expand Down
8 changes: 3 additions & 5 deletions gaepypi/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
from .templates import __templates__
from .renderable import Renderable

import six
from abc import ABCMeta, abstractmethod
from abc import abstractmethod
import cloudstorage as gcs

my_default_retry_params = gcs.RetryParams(initial_delay=0.2,
Expand All @@ -29,7 +28,6 @@
gcs.set_default_retry_params(my_default_retry_params)


@six.add_metaclass(ABCMeta)
class Storage(Renderable):
"""
Storage abstract class, describing the interface assumed by the Package/PackageIndex classes
Expand Down Expand Up @@ -107,8 +105,8 @@ def to_html(self, full_index=True):
"""
package_indices = PackageIndex.get_all(self)
template_file = 'storage-index.html.j2' if not full_index else 'package-index.html.j2'
template = __templates__.get_template(template_file)
return template.render({'indices': package_indices})
template = __templates__.load(template_file)
return template.generate({'indices': package_indices})


class GCStorage(Storage):
Expand Down
8 changes: 3 additions & 5 deletions gaepypi/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os
import jinja2
from tornado import template

__templates__ = jinja2.Environment(loader=jinja2.FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates')),
extensions=['jinja2.ext.autoescape'],
autoescape=True)

__templates__ = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
41 changes: 28 additions & 13 deletions gaepypi/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,31 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import webapp2
from _handlers import *

app = webapp2.WSGIApplication([
('/', IndexHandler),
('/pypi/', PypiHandler),
('/pypi/([^/]+)/', PypiPackageHandler),
('/pypi/([^/]+)/([^/]+)', PackageVersionHandler),
('/packages', PackageBase),
('/packages/([^/]+)', PackageList),
('/packages/([^/]+)/([^/]+)', PackageVersionHandler),
('/packages/([^/]+)/([^/]+)/(.+)', PackageDownload)
], debug=True)
import os
from tornado import web
from tornado import wsgi
from ._handlers import *
from .templates import __templates__


settings = {
'template_loader': __templates__
}


class Application(web.Application):
def __init__(self):
routes = [
('/', IndexHandler),
('/pypi/', PypiHandler),
('/pypi/([^/]+)/', PypiPackageHandler),
('/pypi/([^/]+)/([^/]+)', PackageVersionHandler),
('/packages', PackageBase),
('/packages/([^/]+)', PackageList),
('/packages/([^/]+)/([^/]+)', PackageVersionHandler),
('/packages/([^/]+)/([^/]+)/(.+)', PackageDownload)
]
super(Application, self).__init__(routes, **settings)


app = wsgi.WSGIAdapter(Application())
14 changes: 0 additions & 14 deletions nox.py

This file was deleted.

11 changes: 11 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import nox


@nox.session(python="2.7")
def default(session):
session.install('mock', 'pytest', 'gae_installer', 'pyaml', 'webob', 'GoogleAppEngineCloudStorageClient', 'tornado', 'six')

session.run(
'pytest',
'tests/unit'
)
4 changes: 3 additions & 1 deletion requirements-app.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
GoogleAppEngineCloudStorageClient==1.9.22.1
GoogleAppEngineCloudStorageClient==1.9.22.1
tornado==6.0.3
six
Loading