import json import os import subprocess import sys import tempfile import urllib.parse import requests from django.conf import settings from django.core.management import call_command, get_commands from django.core.management.base import BaseCommand, CommandError from django.utils.encoding import force_str from tenant_schemas.utils import tenant_context from hobo.multitenant.middleware import TenantMiddleware, TenantNotFound from hobo.theme.utils import get_theme KEY_SIZE = 2048 DAYS = 3652 def replace_file(path, content): dirname = os.path.dirname(path) fd, temp = tempfile.mkstemp(dir=dirname, prefix='.tmp-' + os.path.basename(path) + '-') f = os.fdopen(fd, 'w') f.write(force_str(content)) f.flush() os.fsync(f.fileno()) f.close() os.rename(temp, path) class Command(BaseCommand): early_secondary_exit = True requires_system_checks = [] me = None def add_arguments(self, parser): parser.add_argument('base_url', metavar='BASE_URL', nargs='?', type=str) parser.add_argument('json_filename', metavar='JSON_FILENAME', nargs='?', type=str) parser.add_argument('--ignore-timestamp', dest='ignore_timestamp', action="store_true", default=False) parser.add_argument('--redeploy', action="store_true", default=False) def handle( self, base_url=None, json_filename=None, ignore_timestamp=None, redeploy=None, *args, **kwargs ): self.redeploy = redeploy if redeploy: for tenant in TenantMiddleware.get_tenants(): try: hobo_environment = tenant.get_hobo_json() except OSError: continue try: me = [x for x in hobo_environment.get('services') if x.get('this') is True][0] except IndexError: continue self.deploy(me['base_url'], hobo_environment, True) print('Redeployed', me['base_url']) else: if not base_url or not json_filename: raise CommandError('missing args') if json_filename == '-': hobo_environment = json.load(sys.stdin) else: hobo_environment = json.load(open(json_filename)) self.deploy(base_url, hobo_environment, ignore_timestamp) def deploy(self, base_url, hobo_environment, ignore_timestamp): self.me = [x for x in hobo_environment.get('services') if x.get('base_url') == base_url][0] if self.me.get('secondary') and self.early_secondary_exit: # early exit, we don't redeploy secondary services return domain = urllib.parse.urlparse(self.me.get('base_url')).netloc.split(':')[0] legacy_domain = None try: tenant = TenantMiddleware.get_tenant_by_hostname(domain) except TenantNotFound: # might be a domain change request for legacy_urls in self.me.get('legacy_urls', []): old_domain = urllib.parse.urlparse(legacy_urls['base_url']).netloc.split(':')[0] try: tenant = TenantMiddleware.get_tenant_by_hostname(old_domain) legacy_domain = old_domain break except TenantNotFound: pass call_command('create_tenant', domain, legacy_hostname=legacy_domain) tenant = TenantMiddleware.get_tenant_by_hostname(domain) timestamp = hobo_environment.get('timestamp') tenant_hobo_json = os.path.join(tenant.get_directory(), 'hobo.json') if os.path.exists(tenant_hobo_json): if not ignore_timestamp and json.load(open(tenant_hobo_json)).get('timestamp') == timestamp: return # add an attribute to current tenant for easier retrieval self.me['this'] = True self.deploy_specifics(hobo_environment, tenant) if not self.me.get('secondary'): replace_file(tenant_hobo_json, json.dumps(hobo_environment, indent=2)) def deploy_specifics(self, hobo_environment, tenant): # configure things that are quite common but may actually not be # common to *all* tenants. self.generate_saml_keys(tenant, prefix='sp-') self.configure_service_provider(hobo_environment, tenant) self.configure_theme(hobo_environment, tenant) self.configure_template(hobo_environment, tenant) def generate_saml_keys(self, tenant, prefix=''): def openssl(*args): with open('/dev/null', 'w') as dev_null: subprocess.check_call(['openssl'] + list(args), stdout=dev_null, stderr=dev_null) key_file = os.path.join(tenant.get_directory(), '%ssaml.key' % prefix) cert_file = os.path.join(tenant.get_directory(), '%ssaml.crt' % prefix) # if files exist don't regenerate them if os.path.exists(key_file) and os.path.exists(cert_file): return openssl( 'req', '-x509', '-sha256', '-newkey', 'rsa:%s' % KEY_SIZE, '-nodes', '-keyout', key_file, '-out', cert_file, '-batch', '-subj', '/CN=%s' % tenant.domain_url[:60], '-days', str(DAYS), ) def configure_service_provider(self, hobo_environment, tenant): # configure authentication against identity provider for service in hobo_environment.get('services'): idp_url = service.get('saml-idp-metadata-url') if not idp_url: continue try: response = requests.get(idp_url, verify=False) except requests.exceptions.RequestException: continue if response.status_code != 200: continue tenant_idp_metadata = os.path.join( tenant.get_directory(), 'idp-metadata-%s.xml' % service.get('id') ) replace_file(tenant_idp_metadata, response.text) # break now, only a single IdP is supported break def get_theme(self, hobo_environment): theme_id = None if self.me: theme_id = self.me.get('variables', {}).get('theme') if not theme_id: theme_id = hobo_environment.get('variables', {}).get('theme') if not theme_id: return theme = get_theme(theme_id) if not theme: return return theme def configure_theme(self, hobo_environment, tenant): theme = self.get_theme(hobo_environment) if not theme: return tenant_dir = tenant.get_directory() theme_dir = os.path.join(tenant_dir, 'theme') target_dir = os.path.join(settings.THEMES_DIRECTORY, theme.get('module')) def atomic_symlink(src, dst): if os.path.exists(dst) and os.readlink(dst) == src: return if os.path.exists(dst + '.tmp'): os.unlink(dst + '.tmp') os.symlink(src, dst + '.tmp') os.rename(dst + '.tmp', dst) atomic_symlink(target_dir, theme_dir) for part in ('static', 'templates'): if not os.path.islink(os.path.join(tenant_dir, part)) and os.path.isdir( os.path.join(tenant_dir, part) ): try: os.rmdir(os.path.join(tenant_dir, part)) except OSError: continue if not theme.get('overlay'): try: os.unlink(os.path.join(tenant_dir, part)) except OSError: pass else: target_dir = os.path.join(settings.THEMES_DIRECTORY, theme.get('overlay'), part) atomic_symlink(target_dir, os.path.join(tenant_dir, part)) def configure_template(self, hobo_environment, tenant): me = [x for x in hobo_environment.get('services') if x.get('this') is True][0] if 'import_template' in get_commands() and me.get('template_name'): with tenant_context(tenant): call_command('import_template', me['template_name'])