├── ckan_multisite ├── __init__.py ├── tests │ ├── __init__.py │ └── router_tests.py ├── templates │ ├── list.html │ ├── create.html │ ├── login.html │ └── edit.html ├── app.py ├── pw.py ├── login.py ├── task.py ├── config.py.template ├── site.py ├── static │ ├── edit.js │ └── main.css ├── admin.py ├── api.py └── router.py ├── diagrams ├── ckan-multisite.png └── ckan-multisite.graphml ├── requirements.txt ├── uwsgi.ini ├── .gitignore ├── setup.py ├── LICENSE ├── manage.sh ├── promoted.html ├── run.sh ├── README.md ├── development.ini └── redis.conf /ckan_multisite/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ckan_multisite/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /diagrams/ckan-multisite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datacats/ckan-multisite/HEAD/diagrams/ckan-multisite.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask-admin 3 | flask-login 4 | flask-sqlalchemy 5 | celery 6 | redis 7 | uwsgi 8 | datacats 9 | passlib 10 | -------------------------------------------------------------------------------- /ckan_multisite/templates/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/model/list.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | 7 | 8 | {% endblock %} -------------------------------------------------------------------------------- /ckan_multisite/templates/create.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/model/create.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | 7 | 8 | {% endblock %} -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket = /tmp/uwsgi.sock 3 | processes = 4 4 | master = 1 5 | module = ckan_multisite.app:app 6 | chmod-socket = 666 7 | logto=uwsgi.log 8 | harakiri-verbose = False 9 | log-maxsize = 10485760 10 | master = True 11 | 12 | max-requests = 5000 13 | buffer-size = 32768 14 | post-buffering = 4096 15 | processes = 4 16 | stats = :1717 17 | enable-threads = True 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python compiled 2 | *.pyc 3 | __pycache__ 4 | 5 | # Server logs 6 | redis-server.log 7 | 8 | # SQLite 9 | *.db 10 | 11 | # Pip stuffs 12 | *.egg* 13 | 14 | # Multisite environment generate by default script 15 | multisite 16 | 17 | # virtualenv generated by script 18 | virtualenv 19 | 20 | # Our configuration file built from the template 21 | ckan_multisite/config.py 22 | -------------------------------------------------------------------------------- /ckan_multisite/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2015 Boxkite Inc. 4 | 5 | # This file is part of the ckan-multisite package and is released 6 | # under the terms of the MIT License. 7 | # See LICENSE or http://opensource.org/licenses/MIT 8 | 9 | from setuptools import setup 10 | import sys 11 | 12 | install_requires=[ 13 | 'datacats', 14 | 'flask' 15 | ] 16 | 17 | __version__ = '0.01dev' 18 | 19 | setup( 20 | name='ckan-multisite', 21 | version=__version__, 22 | description='Web wrapper around Datacats child environment functionality', 23 | license='MIT', 24 | author='Boxkite', 25 | author_email='contact@boxkite.ca', 26 | url='https://github.com/boxkite/ckan-multisite', 27 | packages=[ 28 | 'ckan_multisite' 29 | ], 30 | install_requires=install_requires, 31 | include_package_data=True, 32 | zip_safe=False, 33 | entry_points = """ 34 | [console_scripts] 35 | ckan-multisite=ckan_multisite.api:main 36 | """, 37 | ) 38 | -------------------------------------------------------------------------------- /ckan_multisite/app.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Boxkite Inc. 2 | 3 | # This file is part of the ckan-multisite package and is released 4 | # under the terms of the MIT License. 5 | # See LICENSE or http://opensource.org/licenses/MIT 6 | 7 | from flask import Flask, redirect, request, url_for 8 | from flask.ext.admin import Admin 9 | from ckan_multisite.api import bp as api_bp 10 | from ckan_multisite.admin import admin 11 | from ckan_multisite import site 12 | from ckan_multisite.pw import check_login_cookie 13 | from ckan_multisite.config import SECRET_KEY, DEBUG, ADDRESS, PORT 14 | from ckan_multisite.login import bp as login_bp 15 | 16 | app = Flask(__name__) 17 | app.config['PROPAGATE_EXCEPTIONS'] = True 18 | app.secret_key = SECRET_KEY 19 | admin.init_app(app) 20 | app.register_blueprint(api_bp) 21 | app.register_blueprint(login_bp) 22 | 23 | 24 | @app.route('/') 25 | def index(): 26 | return redirect('/admin/site') 27 | 28 | 29 | if __name__ == '__main__': 30 | app.run(debug=DEBUG, use_reloader=False, host=ADDRESS, port=PORT) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 boxkite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /ckan_multisite/pw.py: -------------------------------------------------------------------------------- 1 | from passlib.hash import sha256_crypt 2 | 3 | from itertools import cycle, izip 4 | 5 | from flask import request, session 6 | 7 | try: 8 | from ckan_multisite.config import ADMIN_PW, SECRET_KEY 9 | except ImportError: 10 | pass 11 | 12 | def encrypt(password): 13 | return sha256_crypt.encrypt(password) 14 | 15 | 16 | def verify(txt, hash): 17 | return sha256_crypt.verify(txt, hash) 18 | 19 | 20 | def _xor_encrypt(thing, key): 21 | return ''.join([chr(ord(thing_c) ^ ord(key_c)) for thing_c, key_c in izip(thing, cycle(key))]) 22 | 23 | 24 | def _xor_decrypt(encrypted, key): 25 | return _xor_encrypt(encrypted, key) 26 | 27 | 28 | def remove_login_cookie(): 29 | session.clear() 30 | 31 | 32 | def place_login_cookie(pw): 33 | if verify(pw, ADMIN_PW): 34 | session['pwhash'] = _xor_encrypt(ADMIN_PW, SECRET_KEY) 35 | return True 36 | else: 37 | return False 38 | 39 | 40 | def check_login_cookie(): 41 | """ 42 | This function checks for the ADMIN_PW and the secret in config.py in 43 | the Flask request context's cookies. 44 | """ 45 | session_cookie = session.get('pwhash') 46 | if not session_cookie: 47 | return False 48 | else: 49 | pwhash = _xor_decrypt(session_cookie, SECRET_KEY) 50 | return pwhash == ADMIN_PW 51 | -------------------------------------------------------------------------------- /ckan_multisite/tests/router_tests.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Boxkite Inc. 2 | 3 | # This file is part of the ckan-multisite package and is released 4 | # under the terms of the MIT License. 5 | # See LICENSE or http://opensource.org/licenses/MIT 6 | 7 | from unittest import TestCase 8 | from ckan_multisite import router 9 | from ckan_multisite.router import DatacatsNginxConfig 10 | from tempfile import gettempdir 11 | from os.path import exists 12 | 13 | DEFAULT_PATH = router.BASE_PATH 14 | 15 | class RouterTest(TestCase): 16 | def setUp(self): 17 | self.router = DatacatsNginxConfig('testenv') 18 | self.tmpdir = gettempdir() 19 | router.BASE_PATH = self.tmpdir 20 | 21 | def test_config_names(self): 22 | router.BASE_PATH = DEFAULT_PATH 23 | name = router._get_site_config_name('testsite') 24 | self.assertEqual(name, '/etc/nginx/sites-available/testsite') 25 | 26 | def test_add_config(self): 27 | self.router.add_site('testaddsite', 2000) 28 | self.assert_(exists(self.tmpdir + '/testaddsite')) 29 | 30 | def test_remove_config(self): 31 | router.BASE_PATH = self.tmpdir 32 | fname = router._get_site_config_name('testremsite') 33 | with open(fname, 'a'): 34 | pass 35 | self.router.remove_site('testremsite', 2000) 36 | self.assert_(not exists(fname)) 37 | -------------------------------------------------------------------------------- /ckan_multisite/login.py: -------------------------------------------------------------------------------- 1 | from ckan_multisite.pw import check_login_cookie, place_login_cookie, remove_login_cookie 2 | 3 | from flask import request, url_for, render_template, redirect, Blueprint, flash 4 | 5 | from wtforms import Form, PasswordField, validators 6 | 7 | bp = Blueprint('login', __name__, template_folder='templates') 8 | 9 | 10 | @bp.route('/logout', methods=('GET',)) 11 | def logout(): 12 | remove_login_cookie() 13 | return redirect(url_for('index')) 14 | 15 | 16 | @bp.route('/login', methods=('GET', 'POST')) 17 | def login(): 18 | # If they're already logged in, forward them to their destination. 19 | if check_login_cookie(): 20 | print 'Redirecting for already auth' 21 | return redirect(request.values.get('next') if 'next' in request.values else url_for('index'), code=302) 22 | 23 | if request.method == 'POST': 24 | # Otherwise, we need to get the password from the form, validate it, and 25 | if 'pw' in request.values: 26 | if place_login_cookie(request.values['pw']): 27 | print 'Login successful!' 28 | return redirect(request.values.get('next') if 'next' in request.values else url_for('index'), code=302) 29 | else: 30 | flash('Incorrect password.') 31 | else: 32 | flash('Incomplete request.') 33 | return render_template('login.html') 34 | -------------------------------------------------------------------------------- /ckan_multisite/task.py: -------------------------------------------------------------------------------- 1 | """ 2 | A collection of tasks for the celeryd 3 | """ 4 | 5 | from celery import Celery 6 | from config import CELERY_BACKEND_URL, HOSTNAME 7 | from router import nginx 8 | from site import site_by_name 9 | from datacats.error import WebCommandError 10 | from datacats.cli.create import create_environment 11 | 12 | app = Celery('ckan-multisite', broker=CELERY_BACKEND_URL, backend=CELERY_BACKEND_URL) 13 | 14 | @app.task 15 | def create_site_task(site): 16 | try: 17 | environment = site.environment 18 | create_environment(environment.name, None, '2.3', 19 | True, environment.site_name, False, False, 20 | '0.0.0.0', False, True, True, 21 | site_url='{}.{}'.format(environment.site_name, HOSTNAME)) 22 | # Serialize the site display name to its datadir 23 | site.serialize_display_name() 24 | nginx.add_site(environment.site_name, environment.port) 25 | print 'create done!' 26 | except WebCommandError as e: 27 | raise 28 | 29 | 30 | @app.task 31 | def remove_site_task(site): 32 | print 'starting purge' 33 | environment = site.environment 34 | nginx.remove_site(environment.site_name) 35 | print 'site removed' 36 | environment.stop_ckan() 37 | environment.stop_supporting_containers() 38 | print 'containers stopped' 39 | assert environment.site_name in environment.sites, str(environment.sites) + ' ' + environment.site_name 40 | environment.purge_data([environment.site_name], never_delete=True) 41 | print 'Purge done!' 42 | -------------------------------------------------------------------------------- /ckan_multisite/templates/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/model/edit.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block body %} 12 | 13 |15 | Environment Actions: 16 | 17 | 18 | 19 |
20 | 31 | 32 | 33 |34 | {{ super() }} 35 |
36 | {% endblock %} 37 | 38 | {% block tail %} 39 | 40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /ckan_multisite/config.py.template: -------------------------------------------------------------------------------- 1 | """ 2 | Contains configuration options 3 | 4 | If this is your first time seeing this file - it's probably when you 5 | are running the run.sh script. In which case, hi! 6 | 7 | This is the config file for CKAN multisite. You should look through 8 | all of these options - hopefully the comments above them will guide 9 | you towards how you should set them. 10 | """ 11 | 12 | from os.path import expanduser 13 | 14 | # The base hostname of your site, e.x. datacats.com 15 | HOSTNAME = 'example.com' 16 | # This is generated by the script which installs ckan-multisite. If you are 17 | # manually installing this, you should definitely change this from its 18 | # (rather weak) development value. 19 | # If you're running from run.sh - ignore this option. 20 | #SECRET_KEY = 'my_key' 21 | # This is generated again by the script and is a hash of the admin password. 22 | # use manage.sh changepw. 23 | #ADMIN_PW 24 | # The name of the environment to use for multisite. 25 | # This must be created using the `datacats` command line tool prior to usage of this 26 | # application 27 | MAIN_ENV_NAME = 'multisite' 28 | # The datacats directory. This probably shouldn't change but 29 | # is in config to future-proof from new versions of datacats. 30 | DATACATS_DIRECTORY = expanduser('~/.datacats') 31 | # The URI for the backend (either RabbitMQ or Redis) for Celeryd. 32 | # We recommend redis. 33 | CELERY_BACKEND_URL = 'redis://localhost:6379/0' 34 | # An address to listen on 35 | ADDRESS = '0.0.0.0' 36 | PORT = 5000 37 | # True if the server should run in debugging mode (give tracebacks, etc). 38 | # THIS MUST BE FALSE ON A PRODUCTION SERVER 39 | DEBUG = True 40 | # This says that we should generate the default nginx configuration. 41 | GENERATE_NGINX_DEFAULT = False 42 | -------------------------------------------------------------------------------- /manage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ProgName=$(basename $0) 4 | 5 | sub_help(){ 6 | echo "Usage: $ProgName5 | {% trans %} 6 | Welcome to your new data catalog! 7 | Log in with the 8 | "admin" account password you created, then create a 9 | new dataset or a 10 | new organization. 11 | {% endtrans %} 12 |
13 |14 | {% if g.site_title == "multisite" %} 15 | Welcome to a CKAN-multisite environment! As you can see, it's 16 | effectively a template, and you can customize it as much as 17 | you'd like at the sysadmin config panel in the top right (the 18 | little hammer). 19 | {% endif %} 20 |
21 |22 | This is a new multisite environment. If you are the admin of this 23 | environment, you can look in the multisite/ckanext-multisitetheme 24 | directory and edit the templates to customize this site however 25 | you'd like! If you're having issues with this environment or the 26 | multisite administrative interface, visit the issues page 27 | and file an issue. We'll be glad to help you out! 28 |
29 |30 | Otherwise, you should get your admin password set by the admin 31 | and then change the name of your site in the settings. 32 |
33 |