trivial: apply black

This commit is contained in:
Frédéric Péters 2021-05-14 18:39:27 +02:00
parent 567ed17306
commit 872f39774a
184 changed files with 2929 additions and 2241 deletions

View File

@ -7,11 +7,11 @@ MEDIA_ROOT = 'media'
STATIC_ROOT = 'collected-static'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db.sqlite3',
}
}
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db.sqlite3',
}
}
## Django Mellon configuration
# you need to generate SSL certificates in your current directory to make it functionnal :
@ -21,13 +21,13 @@ DATABASES = {
#
# you also need to get the idp metadata and call it idp-metadata.xml
# Uncomment the following lines to enable SAML support
#INSTALLED_APPS += ('mellon',)
#AUTHENTICATION_BACKENDS = ( 'mellon.backends.SAMLBackend',)
#LOGIN_URL = '/accounts/mellon/login/'
#LOGOUT_URL = '/accounts/mellon/logout/'
#MELLON_PUBLIC_KEYS = ['cert.pem']
#MELLON_PRIVATE_KEY = 'key.cert'
#MELLON_IDENTITY_PROVIDERS = [
# INSTALLED_APPS += ('mellon',)
# AUTHENTICATION_BACKENDS = ( 'mellon.backends.SAMLBackend',)
# LOGIN_URL = '/accounts/mellon/login/'
# LOGOUT_URL = '/accounts/mellon/logout/'
# MELLON_PUBLIC_KEYS = ['cert.pem']
# MELLON_PRIVATE_KEY = 'key.cert'
# MELLON_IDENTITY_PROVIDERS = [
# {'METADATA': 'idp-metadata.xml',
# 'GROUP_ATTRIBUTE': 'role'},
# ]

View File

@ -29,9 +29,7 @@ HOBO_MANAGE_COMMAND = 'sudo -u hobo /usr/bin/hobo-manage'
CELERY_SETTINGS = {
'CELERY_SEND_TASK_ERROR_EMAILS': True,
'ADMINS': (
('Admins', 'root@localhost'),
),
'ADMINS': (('Admins', 'root@localhost'),),
}
# run additional settings snippets

View File

@ -21,9 +21,7 @@ DEBUG = False
SECRET_KEY = open('/etc/%s/secret' % PROJECT_NAME).read()
ADMINS = (
('Tous', 'root@localhost'),
)
ADMINS = (('Tous', 'root@localhost'),)
EMAIL_SUBJECT_PREFIX = ''
@ -74,12 +72,12 @@ LOGGING = {
'clamp_to_warning': {
'()': 'hobo.logger.ClampLogLevel',
'level': 'WARNING',
}
},
},
'formatters': {
'syslog': {
'format': '%(application)s %(levelname)s %(tenant)s %(ip)s %(user)s %(request_id)s'
' %(message)s',
' %(message)s',
},
'debug': {
'format': DEBUG_LOG_FORMAT,
@ -119,7 +117,7 @@ LOGGING = {
'backupCount': 1,
'interval': 1,
'filters': ['request_context', 'debug_log'],
}
},
},
'loggers': {
'django.db': {
@ -232,13 +230,14 @@ if 'TEMPLATE_DEBUG' in globals():
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS
if not 'django.core.context_processors.request' in TEMPLATE_CONTEXT_PROCESSORS:
TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.request', )\
+ TEMPLATE_CONTEXT_PROCESSORS
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request',
) + TEMPLATE_CONTEXT_PROCESSORS
TEMPLATE_CONTEXT_PROCESSORS = (
'hobo.context_processors.template_vars',
'hobo.context_processors.theme_base',
'hobo.context_processors.portal_agent_url',
) + TEMPLATE_CONTEXT_PROCESSORS
'hobo.context_processors.template_vars',
'hobo.context_processors.theme_base',
'hobo.context_processors.portal_agent_url',
) + TEMPLATE_CONTEXT_PROCESSORS
else:
assert len(TEMPLATES)
assert TEMPLATES[0]['BACKEND'] == 'django.template.backends.django.DjangoTemplates'
@ -246,31 +245,30 @@ else:
if not 'loaders' in TEMPLATES[0]['OPTIONS']:
TEMPLATES[0]['APP_DIRS'] = False
TEMPLATES[0]['OPTIONS']['loaders'] = [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader']
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
]
if PROJECT_NAME != 'wcs':
TEMPLATES[0]['OPTIONS']['loaders'].insert(0,
'hobo.multitenant.template_loader.FilesystemLoader')
TEMPLATES[0]['OPTIONS']['loaders'].insert(0, 'hobo.multitenant.template_loader.FilesystemLoader')
if not 'django.template.context_processors.request' in TEMPLATES[0]['OPTIONS']['context_processors']:
TEMPLATES[0]['OPTIONS']['context_processors'].insert(0,
'django.template.context_processors.request')
TEMPLATES[0]['OPTIONS']['context_processors'].insert(0, 'django.template.context_processors.request')
TEMPLATES[0]['OPTIONS']['context_processors'] = [
'hobo.context_processors.template_vars',
'hobo.context_processors.theme_base',
'hobo.context_processors.portal_agent_url',
] + TEMPLATES[0]['OPTIONS']['context_processors']
'hobo.context_processors.template_vars',
'hobo.context_processors.theme_base',
'hobo.context_processors.portal_agent_url',
] + TEMPLATES[0]['OPTIONS']['context_processors']
# needed by hobo.context_processors.theme_base:
#THEME_SKELETON_URL = 'https://www.example.net/__skeleton__'
# THEME_SKELETON_URL = 'https://www.example.net/__skeleton__'
# Browsers may ensure that cookies are only sent under an HTTPS connection
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_COOKIE_AGE = 36000 # 10h
SESSION_COOKIE_AGE = 36000 # 10h
CSRF_COOKIE_SAMESITE = None
# Apply sessionNotOnOrAfter on session expiration date
@ -297,9 +295,7 @@ if PROJECT_NAME != 'wcs' and 'authentic2' not in INSTALLED_APPS:
)
if 'authentic2' in INSTALLED_APPS:
MIDDLEWARE = MIDDLEWARE + (
'hobo.agent.authentic2.middleware.ProvisionningMiddleware',
)
MIDDLEWARE = MIDDLEWARE + ('hobo.agent.authentic2.middleware.ProvisionningMiddleware',)
if PROJECT_NAME != 'wcs':
@ -317,13 +313,11 @@ if PROJECT_NAME != 'wcs':
DATABASES = {
'default': {
'ENGINE': 'tenant_schemas.postgresql_backend',
'NAME': PROJECT_NAME.replace('-', '_'),
'NAME': PROJECT_NAME.replace('-', '_'),
}
}
DATABASE_ROUTERS = (
'tenant_schemas.routers.TenantSyncRouter',
)
DATABASE_ROUTERS = ('tenant_schemas.routers.TenantSyncRouter',)
TENANT_SETTINGS_LOADERS = (
'hobo.multitenant.settings_loaders.TemplateVars',
@ -386,8 +380,7 @@ MIDDLEWARE = (
'hobo.middleware.xforwardedfor.XForwardedForMiddleware',
) + MIDDLEWARE
MIDDLEWARE = MIDDLEWARE + (
'hobo.middleware.PrometheusStatsMiddleware',)
MIDDLEWARE = MIDDLEWARE + ('hobo.middleware.PrometheusStatsMiddleware',)
HOBO_MANAGER_HOMEPAGE_URL_VAR = 'portal_agent_url'

View File

@ -11,7 +11,7 @@ MELLON_ADAPTER = ('hobo.utils.MellonAdapter',)
MELLON_ADD_AUTHNREQUEST_NEXT_URL_EXTENSION = True
# add custom hobo agent module
INSTALLED_APPS = ('hobo.agent.hobo', ) + INSTALLED_APPS
INSTALLED_APPS = ('hobo.agent.hobo',) + INSTALLED_APPS
exec(open(os.path.join(ETC_DIR, 'settings.py')).read())

View File

@ -15,15 +15,15 @@
# DEBUG = False
# TEMPLATE_DEBUG = False
#ADMINS = (
# ADMINS = (
# ('User 1', 'poulpe@example.org'),
# ('User 2', 'janitor@example.net'),
#)
# )
# ALLOWED_HOSTS must be correct in production!
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = [
'*',
'*',
]
# If a tenant doesn't exist, the tenant middleware raise a 404 error. If you

View File

@ -15,13 +15,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.apps import AppConfig
from django.db.models.signals import pre_save, pre_delete, m2m_changed, post_save
from django.conf import settings
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
class Plugin:
def get_before_urls(self):
from . import urls
return urls.urlpatterns

View File

@ -1,28 +1,25 @@
import requests
import logging
import os
from time import sleep
import xml.etree.ElementTree as ET
from time import sleep
import requests
from authentic2 import app_settings
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.compat_lasso import lasso
from authentic2.models import Attribute
from authentic2.saml.models import LibertyProvider, SPOptionsIdPPolicy, \
SAMLAttribute, LibertyServiceProvider
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.saml.models import LibertyProvider, LibertyServiceProvider, SAMLAttribute, SPOptionsIdPPolicy
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _, activate
from django.core import serializers
from django_rbac.utils import get_role_model, get_ou_model
from django.conf import settings
from django.utils.translation import activate
from django.utils.translation import ugettext as _
from django_rbac.utils import get_ou_model, get_role_model
from tenant_schemas.utils import tenant_context
from hobo.agent.common.management.commands import hobo_deploy
from hobo.agent.authentic2.provisionning import Provisionning
from hobo.agent.common.management.commands import hobo_deploy
User = get_user_model()
@ -61,9 +58,7 @@ class Command(hobo_deploy.Command):
user_dict['is_staff'] = True
user_dict['is_superuser'] = True
user, created = User.objects.get_or_create(
defaults=user_dict,
username=user_dict['username'])
user, created = User.objects.get_or_create(defaults=user_dict, username=user_dict['username'])
# create/update user attributes
fields = []
@ -76,11 +71,18 @@ class Command(hobo_deploy.Command):
fields.append(attribute['name'])
continue
attr, created = Attribute.all_objects.update_or_create(
name=attribute['name'],
defaults={'kind': attribute['kind']})
for key in ('label', 'description', 'asked_on_registration',
'user_editable', 'user_visible', 'required',
'searchable', 'disabled'):
name=attribute['name'], defaults={'kind': attribute['kind']}
)
for key in (
'label',
'description',
'asked_on_registration',
'user_editable',
'user_visible',
'required',
'searchable',
'disabled',
):
if key in attribute:
setattr(attr, key, attribute[key])
attr.order = i
@ -106,9 +108,12 @@ class Command(hobo_deploy.Command):
names = ['username', 'is_superuser'] + fields + disabled_fields
for name in names:
attribute, created = SAMLAttribute.objects.get_or_create(
name=name, name_format='basic',
name=name,
name_format='basic',
attribute_name='django_user_%s' % name,
object_id=policy.id, content_type=policy_type)
object_id=policy.id,
content_type=policy_type,
)
attribute.enabled = not (name in disabled_fields)
attribute.save()
@ -118,7 +123,8 @@ class Command(hobo_deploy.Command):
name_format='basic',
attribute_name='@verified_attributes@',
object_id=policy.id,
content_type=policy_type)
content_type=policy_type,
)
# create or update Service Providers
services = hobo_environment['services']
@ -135,8 +141,7 @@ class Command(hobo_deploy.Command):
sp_url = service['saml-sp-metadata-url']
metadata_text = None
try:
metadata_response = requests.get(
sp_url, verify=app_settings.A2_VERIFY_SSL, timeout=5)
metadata_response = requests.get(sp_url, verify=app_settings.A2_VERIFY_SSL, timeout=5)
metadata_response.raise_for_status()
# verify metadata is correct
if self.check_saml_metadata(metadata_response.text):
@ -149,10 +154,9 @@ class Command(hobo_deploy.Command):
continue
metadata_text = metadata_response.text
provider, service_created = \
LibertyProvider.objects.get_or_create(
entity_id=sp_url,
protocol_conformance=lasso.PROTOCOL_SAML_2_0)
provider, service_created = LibertyProvider.objects.get_or_create(
entity_id=sp_url, protocol_conformance=lasso.PROTOCOL_SAML_2_0
)
provider.name = service['title']
provider.slug = service['slug']
provider.federation_source = 'hobo'
@ -161,7 +165,8 @@ class Command(hobo_deploy.Command):
variables = service.get('variables', {})
if variables.get('ou-slug'):
ou, created = get_ou_model().objects.get_or_create(
slug=service['variables']['ou-slug'])
slug=service['variables']['ou-slug']
)
ou.name = service['variables']['ou-label']
ou.save()
if service.get('secondary') and variables.get('ou-label'):
@ -178,55 +183,54 @@ class Command(hobo_deploy.Command):
continue
if s['slug'] == service['slug']:
continue
if LibertyProvider.objects.filter(
slug=s['slug']).exists():
if LibertyProvider.objects.filter(slug=s['slug']).exists():
create_ou = True
break
if create_ou:
ou, created = get_ou_model().objects.get_or_create(
name=service['title'])
ou, created = get_ou_model().objects.get_or_create(name=service['title'])
if service_created or not provider.ou:
provider.ou = ou
provision_target_ous[provider.ou.id] = provider.ou
provider.save()
if service_created:
service_provider = LibertyServiceProvider(
enabled=True, liberty_provider=provider,
enabled=True,
liberty_provider=provider,
sp_options_policy=policy,
users_can_manage_federations=False)
users_can_manage_federations=False,
)
service_provider.save()
# add a superuser role for the service
Role = get_role_model()
name = _('Superuser of %s') % service['title']
su_role, created = Role.objects.get_or_create(
service=provider, slug='_a2-hobo-superuser',
defaults={'name': name})
service=provider, slug='_a2-hobo-superuser', defaults={'name': name}
)
if su_role.name != name:
su_role.name = name
su_role.save()
su_role.attributes.get_or_create(name='is_superuser',
kind='string',
value='true')
su_role.attributes.get_or_create(name='is_superuser', kind='string', value='true')
# pass the new attribute to the service
SAMLAttribute.objects.get_or_create(
name='is_superuser',
name_format='basic',
attribute_name='is_superuser',
object_id=provider.pk,
content_type=provider_type)
content_type=provider_type,
)
SAMLAttribute.objects.get_or_create(
name='role-slug',
name_format='basic',
attribute_name='a2_service_ou_role_uuids',
object_id=provider.pk,
content_type=provider_type)
content_type=provider_type,
)
# load skeleton if service is new
if service.get('template_name'):
# if there are more of the same servie, we will create an
# ou
self.load_skeleton(provider, service['service-id'],
service['template_name'])
self.load_skeleton(provider, service['service-id'], service['template_name'])
service['$done'] = True
if all(service.get('$done') for service in services):
@ -249,17 +253,14 @@ class Command(hobo_deploy.Command):
sp_url = service['saml-sp-metadata-url']
self.stderr.write(self.style.WARNING('Error registering %s: %s\n' % (sp_url, last_error)))
def load_skeleton(self, provider, service_id, template_name,
create_ou=False):
def load_skeleton(self, provider, service_id, template_name, create_ou=False):
if not getattr(settings, 'HOBO_SKELETONS_DIR', None):
self.logger.debug('no skeleton: no HOBO_SKELETONS_DIR setting')
return
# ex.: /var/lib/authentic2-multitenant/skeletons/communes/wcs/
skeleton_dir = os.path.join(settings.HOBO_SKELETONS_DIR, template_name,
service_id)
skeleton_dir = os.path.join(settings.HOBO_SKELETONS_DIR, template_name, service_id)
if not os.path.exists(skeleton_dir):
self.logger.debug('no skeleton: skeleton dir %r does not exist',
skeleton_dir)
self.logger.debug('no skeleton: skeleton dir %r does not exist', skeleton_dir)
return
self.load_skeleton_roles(skeleton_dir, provider)
@ -267,8 +268,7 @@ class Command(hobo_deploy.Command):
'''Load default roles based on a template'''
roles_filename = os.path.join(skeleton_dir, 'roles.json')
if not os.path.exists(roles_filename):
self.logger.debug('no skeleton roles: roles file %r does not '
'exist', roles_filename)
self.logger.debug('no skeleton roles: roles file %r does not ' 'exist', roles_filename)
return
Role = get_role_model()
if Role.objects.filter(ou=provider.ou).exclude(slug__startswith='_').exists():

View File

@ -1,9 +1,8 @@
import time
from django.core.management.base import BaseCommand
from django_rbac.utils import get_role_model, get_ou_model
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django_rbac.utils import get_ou_model, get_role_model
from hobo.agent.authentic2.provisionning import Provisionning
@ -26,7 +25,13 @@ class Command(BaseCommand):
self.provision_roles(engine, ous)
if options['users']:
self.provision_users(engine, ous, batch_size=options['batch_size'], batch_sleep=options['batch_sleep'], verbosity=options['verbosity'])
self.provision_users(
engine,
ous,
batch_size=options['batch_size'],
batch_sleep=options['batch_sleep'],
verbosity=options['verbosity'],
)
if self.verbosity > 0:
self.stdout.write('Done.')
@ -47,7 +52,10 @@ class Command(BaseCommand):
users = list(qs[:batch_size])
while users:
if verbosity > 0:
self.stdout.write(' batch provisionning %d users and sleeping for %d seconds' % (len(users), batch_sleep))
self.stdout.write(
' batch provisionning %d users and sleeping for %d seconds'
% (len(users), batch_sleep)
)
engine.notify_users(ous, users)
users = list(qs.filter(id__gt=users[-1].pk)[:batch_size])
if users:

View File

@ -1,23 +1,23 @@
from itertools import chain, islice
import json
from django.utils.six.moves.urllib.parse import urljoin
import threading
import copy
import json
import logging
import requests
import threading
from itertools import chain, islice
import requests
from authentic2.a2_rbac.models import RoleAttribute
from authentic2.models import AttributeValue
from authentic2.saml.models import LibertyProvider
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import connection
from django.urls import reverse
from django.conf import settings
from django.utils.encoding import force_text
from django.utils.six.moves.urllib.parse import urljoin
from django_rbac.utils import get_ou_model, get_role_model, get_role_parenting_model
from django_rbac.utils import get_role_model, get_ou_model, get_role_parenting_model
from hobo.agent.common import notify_agents
from hobo.signature import sign_url
from authentic2.saml.models import LibertyProvider
from authentic2.a2_rbac.models import RoleAttribute
from authentic2.models import AttributeValue
User = get_user_model()
Role = get_role_model()
@ -44,10 +44,12 @@ class Provisionning(threading.local):
self.stack = []
def start(self):
self.stack.append({
'saved': {},
'deleted': {},
})
self.stack.append(
{
'saved': {},
'deleted': {},
}
)
def clear(self):
self.stack = []
@ -101,8 +103,11 @@ class Provisionning(threading.local):
allowed_technical_roles_prefixes = getattr(settings, 'HOBO_PROVISION_ROLE_PREFIXES', []) or []
if mode == 'provision':
users = (User.objects.filter(id__in=[u.id for u in users])
.select_related('ou').prefetch_related('attribute_values__attribute'))
users = (
User.objects.filter(id__in=[u.id for u in users])
.select_related('ou')
.prefetch_related('attribute_values__attribute')
)
else:
self.resolve_ou(users, ous)
@ -112,32 +117,44 @@ class Provisionning(threading.local):
ous.setdefault(ou, set()).add(user)
def is_forbidden_technical_role(role):
return role.slug.startswith('_') and not role.slug.startswith(tuple(allowed_technical_roles_prefixes))
return role.slug.startswith('_') and not role.slug.startswith(
tuple(allowed_technical_roles_prefixes)
)
issuer = force_text(self.get_entity_id())
if mode == 'provision':
def user_to_json(ou, service, user, user_roles):
from authentic2.api_views import BaseUserSerializer
data = {}
# filter user's roles visible by the service's ou
roles = [role for role in user_roles.get(user.id, [])
if (not is_forbidden_technical_role(role)
and (role.ou_id is None or (ou and role.ou_id == ou.id)))]
data.update({
'uuid': user.uuid,
'username': user.username,
'first_name': user.first_name,
'last_name': user.last_name,
'email': user.email,
'is_active': user.is_active,
'roles': [
{
'uuid': role.uuid,
'name': role.name,
'slug': role.slug,
} for role in roles],
})
roles = [
role
for role in user_roles.get(user.id, [])
if (
not is_forbidden_technical_role(role)
and (role.ou_id is None or (ou and role.ou_id == ou.id))
)
]
data.update(
{
'uuid': user.uuid,
'username': user.username,
'first_name': user.first_name,
'last_name': user.last_name,
'email': user.email,
'is_active': user.is_active,
'roles': [
{
'uuid': role.uuid,
'name': role.name,
'slug': role.slug,
}
for role in roles
],
}
)
data.update(BaseUserSerializer(user).data)
# check if user is superuser through a role
role_is_superuser = False
@ -150,13 +167,16 @@ class Provisionning(threading.local):
role_is_superuser = True
data['is_superuser'] = user.is_superuser or role_is_superuser
return data
# Find roles giving a superuser attribute
# If there is any role of this kind, we do one provisionning message for each user and
# each service.
roles_with_attributes = (Role.objects.filter(members__in=users)
.parents(include_self=True)
.filter(attributes__name='is_superuser')
.exists())
roles_with_attributes = (
Role.objects.filter(members__in=users)
.parents(include_self=True)
.filter(attributes__name='is_superuser')
.exists()
)
all_roles = Role.objects.all().prefetch_related('attributes')
roles = dict((r.id, r) for r in all_roles)
@ -184,56 +204,73 @@ class Provisionning(threading.local):
batched_users = list(batched_users)
for user in batched_users:
logger.info('provisionning user %s to %s', user, audience)
self.notify_agents({
'@type': 'provision',
'issuer': issuer,
'audience': [audience],
'full': False,
'objects': {
'@type': 'user',
'data': [user_to_json(ou, service, user, user_roles) for user in batched_users],
self.notify_agents(
{
'@type': 'provision',
'issuer': issuer,
'audience': [audience],
'full': False,
'objects': {
'@type': 'user',
'data': [
user_to_json(ou, service, user, user_roles)
for user in batched_users
],
},
}
})
)
else:
for ou, users in ous.items():
audience = [a for service, a in self.get_audience(ou)]
if not audience:
continue
logger.info(u'provisionning users %s to %s', u', '.join(
map(force_text, users)), u', '.join(audience))
self.notify_agents({
'@type': 'provision',
'issuer': issuer,
'audience': audience,
'full': False,
'objects': {
'@type': 'user',
'data': [user_to_json(ou, None, user, user_roles) for user in users],
logger.info(
u'provisionning users %s to %s',
u', '.join(map(force_text, users)),
u', '.join(audience),
)
self.notify_agents(
{
'@type': 'provision',
'issuer': issuer,
'audience': audience,
'full': False,
'objects': {
'@type': 'user',
'data': [user_to_json(ou, None, user, user_roles) for user in users],
},
}
})
)
elif users:
audience = [audience for ou in ous.keys()
for s, audience in self.get_audience(ou)]
logger.info(u'deprovisionning users %s from %s', u', '.join(
map(force_text, users)), u', '.join(audience))
self.notify_agents({
'@type': 'deprovision',
'issuer': issuer,
'audience': audience,
'full': False,
'objects': {
'@type': 'user',
'data': [{
'uuid': user.uuid,
} for user in users]
audience = [audience for ou in ous.keys() for s, audience in self.get_audience(ou)]
logger.info(
u'deprovisionning users %s from %s', u', '.join(map(force_text, users)), u', '.join(audience)
)
self.notify_agents(
{
'@type': 'deprovision',
'issuer': issuer,
'audience': audience,
'full': False,
'objects': {
'@type': 'user',
'data': [
{
'uuid': user.uuid,
}
for user in users
],
},
}
})
)
def notify_roles(self, ous, roles, mode='provision', full=False):
allowed_technical_roles_prefixes = getattr(settings, 'HOBO_PROVISION_ROLE_PREFIXES', []) or []
def is_forbidden_technical_role(role):
return role.slug.startswith('_') and not role.slug.startswith(tuple(allowed_technical_roles_prefixes))
return role.slug.startswith('_') and not role.slug.startswith(
tuple(allowed_technical_roles_prefixes)
)
roles = set([role for role in roles if not is_forbidden_technical_role(role)])
if mode == 'provision':
@ -258,26 +295,30 @@ class Provisionning(threading.local):
'details': role.details,
'emails': role.emails,
'emails_to_members': role.emails_to_members,
} for role in roles
}
for role in roles
]
else:
data = [
{
'uuid': role.uuid,
} for role in roles
}
for role in roles
]
audience = [entity_id for service, entity_id in self.get_audience(ou)]
logger.info(u'%sning roles %s to %s', mode, roles, audience)
self.notify_agents({
'@type': mode,
'audience': audience,
'full': full,
'objects': {
'@type': 'role',
'data': data,
self.notify_agents(
{
'@type': mode,
'audience': audience,
'full': full,
'objects': {
'@type': 'role',
'data': data,
},
}
})
)
global_roles = set(ous.get(None, []))
for ou, ou_roles in ous.items():
@ -289,17 +330,18 @@ class Provisionning(threading.local):
# - we are not in a tenant
# - provsionning is disabled
# - there is nothing to do
if (not hasattr(connection, 'tenant') or not connection.tenant or not
hasattr(connection.tenant, 'domain_url')):
if (
not hasattr(connection, 'tenant')
or not connection.tenant
or not hasattr(connection.tenant, 'domain_url')
):
return
if not getattr(settings, 'HOBO_ROLE_EXPORT', True):
return
if not (saved or deleted):
return
t = threading.Thread(
target=self.do_provision,
kwargs={'saved': saved, 'deleted': deleted})
t = threading.Thread(target=self.do_provision, kwargs={'saved': saved, 'deleted': deleted})
t.start()
self.threads.add(t)
@ -341,8 +383,7 @@ class Provisionning(threading.local):
role.emails_to_members = True
role.details = u''
for attribute in role.attributes.all():
if (attribute.name in ('emails', 'emails_to_members', 'details')
and attribute.kind == 'json'):
if attribute.name in ('emails', 'emails_to_members', 'details') and attribute.kind == 'json':
setattr(role, attribute.name, json.loads(attribute.value))
def get_entity_id(self):
@ -452,7 +493,8 @@ class Provisionning(threading.local):
try:
response = requests.put(
sign_url(service['provisionning-url'] + '?orig=%s' % service['orig'], service['secret']),
json=data)
json=data,
)
response.raise_for_status()
except requests.RequestException as e:
logger.error(u'error provisionning to %s (%s)', audience, e)

View File

@ -16,16 +16,15 @@
import json
from authentic2.a2_rbac.models import Role, RoleAttribute
from authentic2.manager.forms import RoleEditForm
from authentic2.validators import EmailValidator
from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils import six
from django.utils.translation import ugettext_lazy as _
from authentic2.a2_rbac.models import RoleAttribute, Role
from authentic2.validators import EmailValidator
from authentic2.manager.forms import RoleEditForm
class ListValidator(object):
def __init__(self, item_validator):
@ -36,8 +35,7 @@ class ListValidator(object):
try:
self.item_validator(item)
except ValidationError as e:
raise ValidationError(
_('Item {0} is invalid: {1}').format(i, e.args[0]))
raise ValidationError(_('Item {0} is invalid: {1}').format(i, e.args[0]))
class CommaSeparatedInput(forms.TextInput):
@ -52,8 +50,7 @@ class CommaSeparatedInput(forms.TextInput):
class CommaSeparatedCharField(forms.Field):
widget = CommaSeparatedInput
def __init__(self, dedup=True, max_length=None, min_length=None, *args,
**kwargs):
def __init__(self, dedup=True, max_length=None, min_length=None, *args, **kwargs):
self.dedup = dedup
self.max_length = max_length
self.min_length = min_length
@ -80,24 +77,23 @@ class CommaSeparatedCharField(forms.Field):
class RoleForm(RoleEditForm):
details = forms.CharField(label=_('Role details (frontoffice)'),
widget=forms.Textarea, initial='',
required=False)
emails = CommaSeparatedCharField(label=_('Emails'),
item_validators=[EmailValidator()],
required=False,
help_text=_('Emails must be separated by commas.'))
emails_to_members = forms.BooleanField(required=False,
initial=True,
label=_('Emails to members'))
details = forms.CharField(
label=_('Role details (frontoffice)'), widget=forms.Textarea, initial='', required=False
)
emails = CommaSeparatedCharField(
label=_('Emails'),
item_validators=[EmailValidator()],
required=False,
help_text=_('Emails must be separated by commas.'),
)
emails_to_members = forms.BooleanField(required=False, initial=True, label=_('Emails to members'))
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
if instance:
fields = [x.name for x in Role._meta.get_fields()]
initial = kwargs.setdefault('initial', {})
role_attributes = RoleAttribute.objects.filter(role=instance,
kind='json')
role_attributes = RoleAttribute.objects.filter(role=instance, kind='json')
for role_attribute in role_attributes:
if role_attribute.name in fields:
continue
@ -113,8 +109,8 @@ class RoleForm(RoleEditForm):
continue
value = json.dumps(self.cleaned_data[field])
ra, created = RoleAttribute.objects.get_or_create(
role=instance, name=field, kind='json',
defaults={'value': value})
role=instance, name=field, kind='json', defaults={'value': value}
)
if not created and ra.value != value:
ra.value = value
ra.save()

View File

@ -15,10 +15,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from rest_framework import permissions, serializers, status
from rest_framework.response import Response
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from . import provisionning

View File

@ -1,5 +1,6 @@
from django.apps import AppConfig
class ComboAgentConfig(AppConfig):
name = 'hobo.agent.combo'
label = 'combo_agent'

View File

@ -17,6 +17,7 @@
from hobo.agent.common.management.commands import import_template
class Command(import_template.Command):
def handle(self, *args, **kwargs):
try:
@ -24,6 +25,5 @@ class Command(import_template.Command):
except import_template.UnknownTemplateError:
# ignore errors if template name contains portal-user or portal-agent as
# those names do not actually require an existing file to work.
if not any(name in kwargs.get('template_name')
for name in ('portal-user', 'portal-agent')):
if not any(name in kwargs.get('template_name') for name in ('portal-user', 'portal-agent')):
raise

View File

@ -1,8 +1,7 @@
from celery import Celery
from kombu.common import Broadcast
from django.conf import settings
from django.db import connection
from kombu.common import Broadcast
def notify_agents(data):
@ -20,11 +19,10 @@ def notify_agents(data):
CELERY_TASK_SERIALIZER='json',
CELERY_ACCEPT_CONTENT=['json'],
CELERY_RESULT_SERIALIZER='json',
CELERY_QUEUES=(Broadcast('broadcast_tasks'), ),
CELERY_QUEUES=(Broadcast('broadcast_tasks'),),
CELERY_EVENT_QUEUE_TTL=10,
)
# see called method in hobo.agent.worker.celery
app.send_task('hobo-notify',
(notification,),
expires=settings.BROKER_TASK_EXPIRES,
queue='broadcast_tasks')
app.send_task(
'hobo-notify', (notification,), expires=settings.BROKER_TASK_EXPIRES, queue='broadcast_tasks'
)

View File

@ -2,30 +2,28 @@ from __future__ import print_function
import json
import os
import requests
import subprocess
import sys
import tempfile
import requests
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command, get_commands
from django.core.management.base import BaseCommand, CommandError
from django.utils.encoding import force_text
from django.utils.six.moves.urllib import parse as urlparse
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)+'-')
fd, temp = tempfile.mkstemp(dir=dirname, prefix='.tmp-' + os.path.basename(path) + '-')
f = os.fdopen(fd, 'w')
f.write(force_text(content))
f.flush()
@ -42,12 +40,12 @@ class Command(BaseCommand):
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('--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):
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():
@ -107,7 +105,6 @@ class Command(BaseCommand):
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)
@ -119,9 +116,23 @@ class Command(BaseCommand):
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))
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
@ -135,8 +146,9 @@ class Command(BaseCommand):
continue
if response.status_code != 200:
continue
tenant_idp_metadata = os.path.join(tenant.get_directory(),
'idp-metadata-%s.xml' % service.get('id'))
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
@ -161,8 +173,7 @@ class Command(BaseCommand):
tenant_dir = tenant.get_directory()
theme_dir = os.path.join(tenant_dir, 'theme')
target_dir = os.path.join(settings.THEMES_DIRECTORY,
theme.get('module'))
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:
@ -175,8 +186,9 @@ class Command(BaseCommand):
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)):
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:
@ -188,8 +200,7 @@ class Command(BaseCommand):
except OSError:
pass
else:
target_dir = os.path.join(settings.THEMES_DIRECTORY,
theme.get('overlay'), part)
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):

View File

@ -19,10 +19,9 @@ import os
import sys
from django.core.management.base import BaseCommand
from tenant_schemas.utils import tenant_context
from hobo.multitenant.middleware import TenantMiddleware
from hobo.multitenant.middleware import TenantMiddleware
from hobo.provisionning.utils import NotificationProcessing, TryAgain
@ -50,8 +49,7 @@ class Command(BaseCommand, NotificationProcessing):
@classmethod
def process_notification(cls, tenant, notification):
assert cls.check_valid_notification(notification), \
'invalid notification'
assert cls.check_valid_notification(notification), 'invalid notification'
service = tenant.get_service()
action = notification['@type']
audience = notification['audience']
@ -64,7 +62,9 @@ class Command(BaseCommand, NotificationProcessing):
object_type = notification['objects']['@type']
for i in range(20):
try:
getattr(cls, 'provision_' + object_type)(issuer, action, notification['objects']['data'], full=full)
getattr(cls, 'provision_' + object_type)(
issuer, action, notification['objects']['data'], full=full
)
except TryAgain:
continue
break

View File

@ -17,8 +17,8 @@
import os
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command, get_commands
from django.core.management.base import BaseCommand, CommandError
class UnknownTemplateError(CommandError):
@ -26,7 +26,6 @@ class UnknownTemplateError(CommandError):
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('template_name', type=str)
parser.add_argument('--basepath', type=str)

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,12 +14,21 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Role',
fields=[
('group_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='auth.Group', on_delete=models.CASCADE)),
(
'group_ptr',
models.OneToOneField(
parent_link=True,
auto_created=True,
primary_key=True,
serialize=False,
to='auth.Group',
on_delete=models.CASCADE,
),
),
('uuid', models.CharField(max_length=32)),
('description', models.TextField()),
],
options={
},
options={},
bases=('auth.group',),
),
]

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@ -16,7 +16,9 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='role',
name='emails',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), default=list, size=None),
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(max_length=128), default=list, size=None
),
),
migrations.AddField(
model_name='role',

View File

@ -1,7 +1,7 @@
from django.contrib.auth.models import Group
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.contrib.auth.models import Group
class Role(Group):
uuid = models.CharField(max_length=32)

View File

@ -1,5 +1,6 @@
from django.apps import AppConfig
class HoboAgentConfig(AppConfig):
name = 'hobo.agent.hobo'
label = 'hobo_agent'

View File

@ -1,6 +1,5 @@
from django.contrib.contenttypes.models import ContentType
from django.utils.timezone import now
from tenant_schemas.utils import tenant_context
from hobo.agent.common.management.commands import hobo_deploy
@ -58,16 +57,21 @@ class Command(hobo_deploy.Command):
# this is the primary hobo, receiving a notification
# because something changed in secondary hobo environment.
# mark services with ou-label/ou-slug
secondary_hobo = [x for x in services if x['service-id'] == 'hobo' and not x.get('secondary')][0]
secondary_hobo = [
x for x in services if x['service-id'] == 'hobo' and not x.get('secondary')
][0]
slug_prefix = '_%s_' % hobo_environment['variables']['ou-slug']
service_slug = '%s%s' % (slug_prefix, service_dict['slug'])
service, created = service_klass.objects.get_or_create(
base_url=service_dict['base_url'],
secondary=True,
defaults={'title': service_dict['title'],
'slug': service_slug,
'secret_key': service_dict.get('secret_key')})
base_url=service_dict['base_url'],
secondary=True,
defaults={
'title': service_dict['title'],
'slug': service_slug,
'secret_key': service_dict.get('secret_key'),
},
)
service.title = service_dict['title']
service.secret_key = service_dict.get('secret_key')
service.template_name = service_dict.get('template_name') or ''
@ -82,13 +86,13 @@ class Command(hobo_deploy.Command):
# organizational unit
service_type = ContentType.objects.get_for_model(service_klass)
variable, created = Variable.objects.get_or_create(
name='ou-label', auto=True,
service_type=service_type, service_pk=service.id)
name='ou-label', auto=True, service_type=service_type, service_pk=service.id
)
variable.value = hobo_environment['variables']['ou-label']
variable.save()
variable, created = Variable.objects.get_or_create(
name='ou-slug', auto=True,
service_type=service_type, service_pk=service.id)
name='ou-slug', auto=True, service_type=service_type, service_pk=service.id
)
variable.value = hobo_environment['variables']['ou-slug']
variable.save()
@ -97,11 +101,13 @@ class Command(hobo_deploy.Command):
# global variables so they can later be used to identify
# this "organizational unit".
variable, created = Variable.objects.get_or_create(
name='ou-label', auto=True, service_pk=None)
name='ou-label', auto=True, service_pk=None
)
variable.value = me['title']
variable.save()
variable, created = Variable.objects.get_or_create(
name='ou-slug', auto=True, service_pk=None)
name='ou-slug', auto=True, service_pk=None
)
variable.value = me['slug']
variable.save()
@ -111,8 +117,8 @@ class Command(hobo_deploy.Command):
changes = False
for i, field in enumerate(hobo_environment['profile']['fields']):
attribute, created = AttributeDefinition.objects.get_or_create(
name=field['name'],
defaults={'label': field['label']})
name=field['name'], defaults={'label': field['label']}
)
if created:
changes = True
for k, v in field.items():

View File

@ -18,20 +18,21 @@ from __future__ import absolute_import
from celery import Celery
from kombu.common import Broadcast
from . import settings
from . import services
from . import services, settings
app = Celery('hobo', broker=settings.BROKER_URL)
app.conf.update(
CELERY_TASK_SERIALIZER='json',
CELERY_ACCEPT_CONTENT=['json'],
CELERY_RESULT_SERIALIZER='json',
CELERY_QUEUES=(Broadcast('broadcast_tasks'), )
CELERY_QUEUES=(Broadcast('broadcast_tasks'),),
)
if hasattr(settings, 'CELERY_SETTINGS'):
app.conf.update(settings.CELERY_SETTINGS)
@app.task(name='hobo-deploy', bind=True)
def deploy(self, environment):
services.deploy(environment)

View File

@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
import fnmatch
import json
import multiprocessing
@ -79,8 +80,9 @@ class BaseService(object):
if not os.path.exists(self.service_manage_try_cmd):
return
cmd = self.service_manage_cmd + ' hobo_deploy ' + self.base_url + ' -'
cmd_process = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
cmd_process = subprocess.Popen(
cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, stderr = cmd_process.communicate(input=force_bytes(json.dumps(environment)))
if cmd_process.returncode != 0:
raise RuntimeError('command "%s" failed: %r %r' % (cmd, stdout, stderr))
@ -100,8 +102,9 @@ class BaseService(object):
return
cmd = cls.service_manage_cmd + ' hobo_notify -'
try:
cmd_process = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
cmd_process = subprocess.Popen(
cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except OSError:
return
stdout, stderr = cmd_process.communicate(input=force_bytes(json.dumps(data)))
@ -209,6 +212,7 @@ def deploy(environment):
pool = multiprocessing.pool.ThreadPool()
list(pool.imap_unordered(lambda x: x.execute(environment), services))
def notify(data):
services = []
for klassname, service in globals().items():

View File

@ -48,7 +48,8 @@ BIJOE_TENANTS_DIRECTORY = '/var/lib/bijoe/tenants'
HOBO_TENANTS_DIRECTORY = '/var/lib/hobo/tenants'
local_settings_file = os.environ.get('HOBO_AGENT_SETTINGS_FILE',
os.path.join(os.path.dirname(__file__), 'local_settings.py'))
local_settings_file = os.environ.get(
'HOBO_AGENT_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
)
if os.path.exists(local_settings_file):
exec(open(local_settings_file).read())

View File

@ -2,10 +2,10 @@ import datetime
import hashlib
import json
import logging
import requests
import threading
import os
import threading
import requests
from django.conf import settings
from django.core.cache import cache
from django.template import Template
@ -66,14 +66,16 @@ class RemoteTemplate(object):
@property
def cache_key(self):
return hashlib.md5(urlparse.urlunparse(
urlparse.urlparse(self.source)[:3] + ('', '', '')).encode('ascii')).hexdigest()
return hashlib.md5(
urlparse.urlunparse(urlparse.urlparse(self.source)[:3] + ('', '', '')).encode('ascii')
).hexdigest()
def get_template(self):
item = self.get_cached_item()
if 'hobo.environment' in settings.INSTALLED_APPS:
from hobo.multitenant.settings_loaders import TemplateVars
from hobo.deploy.utils import get_hobo_json
from hobo.multitenant.settings_loaders import TemplateVars
context = TemplateVars.get_hobo_json_variables(get_hobo_json())
if 'portal_url' not in context:
# serve a minimalistic template if no portal have been
@ -126,15 +128,13 @@ class RemoteTemplate(object):
page_cache[page_redirect_url] = r.text
expiry_time = datetime.datetime.now() + datetime.timedelta(seconds=CACHE_REFRESH_TIMEOUT)
cache.set(self.PAGE_CACHE_KEY,
(page_cache, expiry_time),
2592000) # bypass cache level expiration time
cache.set(
self.PAGE_CACHE_KEY, (page_cache, expiry_time), 2592000
) # bypass cache level expiration time
def cache(self, template_body):
expiry_time = datetime.datetime.now() + datetime.timedelta(seconds=CACHE_REFRESH_TIMEOUT)
cache.set(self.cache_key,
(template_body, expiry_time),
2592000) # bypass cache level expiration time
cache.set(self.cache_key, (template_body, expiry_time), 2592000) # bypass cache level expiration time
def theme_base(request):
@ -152,18 +152,20 @@ def theme_base(request):
# template by combo, it will be taken from template_vars and will
# default to theme.html.
source = request.build_absolute_uri()
return {'theme_base': RemoteTemplate(source).get_template,
'theme_404': RemoteTemplate('404').get_template,
'theme_base_filename': template_vars(request).get('theme_base_filename') or 'theme.html'
}
return {
'theme_base': RemoteTemplate(source).get_template,
'theme_404': RemoteTemplate('404').get_template,
'theme_base_filename': template_vars(request).get('theme_base_filename') or 'theme.html',
}
def portal_agent_url(request):
def get_portal_agent_url():
portal_agents = []
if 'authentic2' in settings.INSTALLED_APPS:
portal_agents = [x for x in settings.KNOWN_SERVICES.get('combo', {}).values()
if x.get('is-portal-agent')]
portal_agents = [
x for x in settings.KNOWN_SERVICES.get('combo', {}).values() if x.get('is-portal-agent')
]
if len(portal_agents) > 1 and request.user and request.user.is_authenticated and request.user.ou_id:
ou_slug = request.user.ou.slug
for portal_agent in portal_agents:
@ -179,8 +181,9 @@ def portal_agent_url(request):
def hobo_json(request):
# this context processor gives Hobo itself variables that would be defined
# from settings loaders based on hobo.json.
from hobo.multitenant.settings_loaders import TemplateVars
from hobo.deploy.utils import get_hobo_json
from hobo.multitenant.settings_loaders import TemplateVars
context = TemplateVars.get_hobo_json_variables(get_hobo_json())
context['manager_homepage_url'] = context.get(settings.HOBO_MANAGER_HOMEPAGE_URL_VAR)
return context

View File

@ -15,8 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django import forms
from django.core.validators import validate_ipv46_address
from django.core.exceptions import ValidationError
from django.core.validators import validate_ipv46_address
from django.utils.translation import ugettext_lazy as _
@ -48,10 +48,9 @@ class MultipleIPAddressField(forms.CharField):
class SettingsForm(forms.Form):
debug_log = forms.BooleanField(
required=False,
label=_('Enable Debug Logs'))
debug_log = forms.BooleanField(required=False, label=_('Enable Debug Logs'))
debug_ips = MultipleIPAddressField(
label=_('Internal IP addresses'),
required=False,
help_text=_('List of IP addresses for which to enable debugging'))
help_text=_('List of IP addresses for which to enable debugging'),
)

View File

@ -15,8 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.urls import reverse_lazy
from django.views.generic import FormView
from django.utils.functional import cached_property
from django.views.generic import FormView
from hobo.environment.utils import get_setting_variable
@ -69,4 +69,5 @@ class HomeView(FormView):
self.debug_ips_variable.save()
return super(HomeView, self).form_valid(form)
home = HomeView.as_view()

View File

@ -16,10 +16,12 @@
import django.apps
class AppConfig(django.apps.AppConfig):
name = 'hobo.deploy'
def ready(self):
from . import signals
default_app_config = 'hobo.deploy.AppConfig'

View File

@ -17,22 +17,22 @@
import threading
from celery import Celery
from kombu.common import Broadcast
from django.conf import settings
from django.core.signals import request_finished, request_started
from django.db.models.signals import post_save, post_delete
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from kombu.common import Broadcast
from hobo.environment.models import AVAILABLE_SERVICES, Variable
from hobo.profile.models import AttributeDefinition
from .utils import get_hobo_json
from hobo.environment.models import Variable, AVAILABLE_SERVICES
from hobo.profile.models import AttributeDefinition
class Local(threading.local):
MUST_NOTIFY = False
tls = Local()
@ -59,10 +59,9 @@ def notify_agents(sender, **kwargs):
CELERY_TASK_SERIALIZER='json',
CELERY_ACCEPT_CONTENT=['json'],
CELERY_RESULT_SERIALIZER='json',
CELERY_QUEUES=(Broadcast('broadcast_tasks'), )
CELERY_QUEUES=(Broadcast('broadcast_tasks'),),
)
# see called method in hobo.agent.worker.celery
app.send_task('hobo-deploy',
(get_hobo_json(),),
expires=settings.BROKER_TASK_EXPIRES,
queue='broadcast_tasks')
app.send_task(
'hobo-deploy', (get_hobo_json(),), expires=settings.BROKER_TASK_EXPIRES, queue='broadcast_tasks'
)

View File

@ -20,7 +20,7 @@ import time
from django.contrib.auth.models import User
from django.db.models import Max
from hobo.environment.models import Variable, AVAILABLE_SERVICES
from hobo.environment.models import AVAILABLE_SERVICES, Variable
from hobo.environment.utils import get_installed_services_dict
from hobo.profile.models import AttributeDefinition
from hobo.profile.utils import get_profile_dict
@ -32,11 +32,11 @@ def get_hobo_json():
# include the list of admin users so an agent can create them when
# deploying a service (according to its policy)
users = []
for user in User.objects.filter(is_superuser=True,
is_active=True, password__isnull=False).exclude(password=''):
for user in User.objects.filter(is_superuser=True, is_active=True, password__isnull=False).exclude(
password=''
):
user_dict = {}
for attribute in ('username', 'first_name', 'last_name', 'email',
'password'):
for attribute in ('username', 'first_name', 'last_name', 'email', 'password'):
user_dict[attribute] = getattr(user, attribute)
users.append(user_dict)
@ -48,12 +48,11 @@ def get_hobo_json():
# set a timestamp
timestamp = None
for klass in [AttributeDefinition, Variable] + AVAILABLE_SERVICES:
ts = klass.objects.all().aggregate(Max('last_update_timestamp')
).get('last_update_timestamp__max')
ts = klass.objects.all().aggregate(Max('last_update_timestamp')).get('last_update_timestamp__max')
if timestamp is None or (ts and ts > timestamp):
timestamp = ts
if timestamp is None:
timestamp = datetime.datetime.now()
hobo_json['timestamp'] = str(time.mktime(timestamp.timetuple()) + timestamp.microsecond/1e6)
hobo_json['timestamp'] = str(time.mktime(timestamp.timetuple()) + timestamp.microsecond / 1e6)
return hobo_json

View File

@ -14,8 +14,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
import django.core.mail.backends.smtp
from django.conf import settings
class EmailBackend(django.core.mail.backends.smtp.EmailBackend):
@ -32,7 +32,9 @@ class EmailBackend(django.core.mail.backends.smtp.EmailBackend):
try:
url = settings.TEMPLATE_VARS['email_abuse_report_url']
if url:
email_message.extra_headers['X-Report-Abuse'] = 'Please report abuse for this email here: %s' % url
email_message.extra_headers['X-Report-Abuse'] = (
'Please report abuse for this email here: %s' % url
)
except (KeyError, TypeError):
pass
return super()._send(email_message)

View File

@ -28,19 +28,26 @@ class ValidEmailField(forms.EmailField):
class EmailsForm(forms.Form):
default_from_email = ValidEmailField(label=_('Default From'),
help_text=_('Sender email address'))
email_sender_name = forms.CharField(label=_('Sender Name'), required=False,
help_text=_('Custom sender name (defaults to global title)'))
global_email_prefix = forms.CharField(label=_('Prefix'), required=False,
help_text=_('Custom prefix for emails subject (defaults to global title)'))
email_signature = forms.CharField(label=_('Signature'), required=False,
widget=forms.Textarea)
default_from_email = ValidEmailField(label=_('Default From'), help_text=_('Sender email address'))
email_sender_name = forms.CharField(
label=_('Sender Name'), required=False, help_text=_('Custom sender name (defaults to global title)')
)
global_email_prefix = forms.CharField(
label=_('Prefix'),
required=False,
help_text=_('Custom prefix for emails subject (defaults to global title)'),
)
email_signature = forms.CharField(label=_('Signature'), required=False, widget=forms.Textarea)
email_unsubscribe_info_url = forms.CharField(
label=_('URL with informations about emails sent by the platform (List-Unsubscribe header)'), required=False,
help_text=_('It should contain details such as what emails are sent and how not to receive them.'))
email_abuse_report_url = forms.CharField(label=_('URL for email abuse reports (X-Report-Abuse header)'), required=False,
help_text=_('It should contain a form to submit email abuse reports'))
label=_('URL with informations about emails sent by the platform (List-Unsubscribe header)'),
required=False,
help_text=_('It should contain details such as what emails are sent and how not to receive them.'),
)
email_abuse_report_url = forms.CharField(
label=_('URL for email abuse reports (X-Report-Abuse header)'),
required=False,
help_text=_('It should contain a form to submit email abuse reports'),
)
def __init__(self, *args, **kwargs):
super(EmailsForm, self).__init__(*args, **kwargs)

View File

@ -14,10 +14,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import dns.resolver
import smtplib
import socket
import dns.resolver
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.encoding import force_bytes
@ -29,7 +29,9 @@ def validate_email_address(value):
return
email_domain = value.split('@')[-1]
try:
mx_server = sorted(dns.resolver.query(email_domain, 'MX'), key=lambda rdata: rdata.preference)[0].exchange.to_text()
mx_server = sorted(dns.resolver.query(email_domain, 'MX'), key=lambda rdata: rdata.preference)[
0
].exchange.to_text()
except dns.exception.DNSException as e:
raise ValidationError(_('Error: %s') % str(e))
try:
@ -44,7 +46,9 @@ def validate_email_address(value):
finally:
smtp.close()
except (socket.error, IOError, OSError) as e:
raise ValidationError(_('Error while connecting to %(server)s: %(msg)s') % {'server': mx_server, 'msg': e})
raise ValidationError(
_('Error while connecting to %(server)s: %(msg)s') % {'server': mx_server, 'msg': e}
)
if status // 100 == 5:
raise ValidationError(_('Email address not found on %s') % mx_server)

View File

@ -19,17 +19,22 @@ from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
from hobo.environment.forms import VariablesFormMixin
from .forms import EmailsForm
class HomeView(VariablesFormMixin, TemplateView):
template_name = 'hobo/emails_home.html'
variables = ['default_from_email', 'email_sender_name',
'global_email_prefix', 'email_signature',
'email_unsubscribe_info_url',
'email_abuse_report_url']
variables = [
'default_from_email',
'email_sender_name',
'global_email_prefix',
'email_signature',
'email_unsubscribe_info_url',
'email_abuse_report_url',
]
form_class = EmailsForm
success_message = _('Emails settings have been updated. '
'It will take a few seconds to be effective.')
success_message = _('Emails settings have been updated. ' 'It will take a few seconds to be effective.')
home = HomeView.as_view()

View File

@ -21,15 +21,16 @@ from django.http import HttpResponseRedirect
from django.template.defaultfilters import slugify
from django.utils.translation import ugettext_lazy as _
from .models import (Authentic, Wcs, Passerelle, Variable, Combo, Fargo, Welco,
Chrono, BiJoe, Hobo)
from .models import Authentic, BiJoe, Chrono, Combo, Fargo, Hobo, Passerelle, Variable, Wcs, Welco
from .utils import get_variable
from .validators import validate_service_url
EXCLUDED_FIELDS = ('last_operational_check_timestamp',
'last_operational_success_timestamp', 'secret_key',
'secondary')
EXCLUDED_FIELDS = (
'last_operational_check_timestamp',
'last_operational_success_timestamp',
'secret_key',
'secondary',
)
class BaseForm(forms.ModelForm):
@ -46,8 +47,8 @@ class BaseForm(forms.ModelForm):
# choice that was selected as an additional, disabled, <select> widget.
if self.instance.id and len(choices) > 1:
self.fields['template_name_readonly'] = forms.fields.CharField(
label=_('Template'), required=False,
initial=self.instance.template_name)
label=_('Template'), required=False, initial=self.instance.template_name
)
self.fields['template_name_readonly'].widget = forms.Select(choices=choices)
self.fields['template_name_readonly'].widget.attrs['disabled'] = 'disabled'

View File

@ -6,6 +6,7 @@ from django.core.management.base import BaseCommand
from hobo.environment import models
class Command(BaseCommand):
help = '''Checks if all services are operational'''
@ -19,4 +20,8 @@ class Command(BaseCommand):
else:
print(self.style.NOTICE('%s is NOT operational' % obj.title))
if obj.last_operational_success_timestamp:
print(self.style.NOTICE(' last operational success: %s' % obj.last_operational_success_timestamp))
print(
self.style.NOTICE(
' last operational success: %s' % obj.last_operational_success_timestamp
)
)

View File

@ -17,33 +17,44 @@
from __future__ import print_function
import json
import os
import string
import subprocess
import sys
import time
import os
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand, CommandError
from django.core.exceptions import ValidationError
from django.core.management import call_command
from django.core.management.base import BaseCommand, CommandError
from django.db import connection
from django.db.models import Max
from django.core.exceptions import ValidationError
from django.utils import six
from django.utils.six.moves.urllib import parse as urlparse
from django.utils.text import slugify
from hobo.agent.common.management.commands.hobo_deploy import (
Command as HoboDeployCommand)
from hobo.multitenant.middleware import TenantMiddleware
from hobo.environment.models import (AVAILABLE_SERVICES, Authentic, Wcs, Hobo,
Passerelle, Combo, Fargo, Welco, Chrono, BiJoe,
Variable, AUTO_VARIABLES)
from hobo.environment.validators import validate_service_url
from hobo.agent.common.management.commands.hobo_deploy import Command as HoboDeployCommand
from hobo.deploy.signals import notify_agents
from hobo.theme.utils import set_theme
from hobo.environment.models import (
AUTO_VARIABLES,
AVAILABLE_SERVICES,
Authentic,
BiJoe,
Chrono,
Combo,
Fargo,
Hobo,
Passerelle,
Variable,
Wcs,
Welco,
)
from hobo.environment.validators import validate_service_url
from hobo.multitenant.middleware import TenantMiddleware
from hobo.profile.models import AttributeDefinition
from hobo.theme.utils import set_theme
def get_domain(url):
return urlparse.urlparse(url).netloc.split(':')[0]
@ -56,8 +67,12 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('recipe', metavar='RECIPE', type=str)
parser.add_argument(
'--timeout', type=int, action='store', default=120,
help='set the timeout for the wait_operationals method')
'--timeout',
type=int,
action='store',
default=120,
help='set the timeout for the wait_operationals method',
)
parser.add_argument('--permissive', action='store_true', help='ignore integrity checks')
def handle(self, recipe, *args, **kwargs):
@ -78,16 +93,16 @@ class Command(BaseCommand):
variables = {}
steps = []
if 'load-variables-from' in recipe:
variables.update(json.load(open(
os.path.join(os.path.dirname(filename), recipe['load-variables-from']))))
variables.update(
json.load(open(os.path.join(os.path.dirname(filename), recipe['load-variables-from'])))
)
variables.update(recipe.get('variables', {}))
for step in recipe.get('steps', []):
action, action_args = list(step.items())[0]
for arg in action_args:
if not isinstance(action_args[arg], six.string_types):
continue
action_args[arg] = string.Template(
action_args[arg]).substitute(variables)
action_args[arg] = string.Template(action_args[arg]).substitute(variables)
if not self.permissive:
self.check_action(action, action_args)
steps.append((action, action_args))
@ -137,8 +152,7 @@ class Command(BaseCommand):
if time.time() - t0 > timeout:
if self.verbosity:
sys.stderr.write('\n')
raise CommandError('timeout waiting for %s' % ', '.join(
[x.base_url for x in services]))
raise CommandError('timeout waiting for %s' % ', '.join([x.base_url for x in services]))
def create_hobo(self, url, primary=False, title=None, slug=None, **kwargs):
if connection.get_tenant().schema_name == 'public':
@ -149,8 +163,7 @@ class Command(BaseCommand):
if not primary:
if not slug:
slug = 'hobo-%s' % slugify(title)
self.create_site(Hobo, url, title, slug,
template_name='', variables=None)
self.create_site(Hobo, url, title, slug, template_name='', variables=None)
# deploy and wait for new site
notify_agents(None)
self.wait_operationals(timeout=self.timeout)
@ -175,10 +188,10 @@ class Command(BaseCommand):
connection.set_tenant(tenant)
def create_superuser(self, username='admin', email='admin@localhost',
first_name='Admin', last_name='', password=None):
user, created = User.objects.get_or_create(
username=username, email=email, is_superuser=True)
def create_superuser(
self, username='admin', email='admin@localhost', first_name='Admin', last_name='', password=None
):
user, created = User.objects.get_or_create(username=username, email=email, is_superuser=True)
if created:
user.first_name = first_name
user.last_name = last_name
@ -193,19 +206,17 @@ class Command(BaseCommand):
if slug is None:
slug = klass.Extra.service_default_slug
obj, must_save = klass.objects.get_or_create(
slug=slug,
defaults={
'title': title,
'base_url': base_url,
'template_name': template_name
})
slug=slug, defaults={'title': title, 'base_url': base_url, 'template_name': template_name}
)
for attr in ('title', 'base_url', 'template_name'):
if getattr(obj, attr) != locals().get(attr):
setattr(obj, attr, locals().get(attr))
must_save = True
if must_save:
try:
obj.full_clean(exclude=['last_operational_success_timestamp', 'last_operational_check_timestamp'])
obj.full_clean(
exclude=['last_operational_success_timestamp', 'last_operational_check_timestamp']
)
except ValidationError as e:
raise CommandError(str(e))
@ -215,10 +226,12 @@ class Command(BaseCommand):
obj_type = ContentType.objects.get_for_model(klass)
for variable_name in variables.keys():
label = variables[variable_name].get('label')
variable, created = Variable.objects.get_or_create(name=variable_name,
service_type=obj_type,
service_pk=obj.id,
defaults={'label': label or variable_name})
variable, created = Variable.objects.get_or_create(
name=variable_name,
service_type=obj_type,
service_pk=obj.id,
defaults={'label': label or variable_name},
)
if label:
variable.label = label
value = variables[variable_name].get('value')
@ -262,17 +275,16 @@ class Command(BaseCommand):
def set_theme(self, theme):
set_theme(theme)
HoboDeployCommand().configure_theme(
{'variables': {'theme': theme}},
connection.get_tenant())
HoboDeployCommand().configure_theme({'variables': {'theme': theme}}, connection.get_tenant())
def set_variable(self, name, value, label=None, auto=None):
if auto is None:
auto = bool(name in AUTO_VARIABLES)
else:
auto = bool(auto)
variable, created = Variable.objects.get_or_create(name=name,
defaults={'label': label or name, 'auto': auto})
variable, created = Variable.objects.get_or_create(
name=name, defaults={'label': label or name, 'auto': auto}
)
if isinstance(value, dict) or isinstance(value, list):
value = json.dumps(value)
if variable.label != label or variable.value != value or variable.auto != auto or created:
@ -304,7 +316,8 @@ class Command(BaseCommand):
# possible keys in kwargs are: description, required,
# asked_on_registration, user_editable, user_visible, kind, order
attribute, created = AttributeDefinition.objects.get_or_create(
name=name, defaults={'label': label, 'order': 0})
name=name, defaults={'label': label, 'order': 0}
)
kwargs['label'] = label
attribute_fields = [x.name for x in AttributeDefinition._meta.fields]
for arg in kwargs:
@ -312,8 +325,7 @@ class Command(BaseCommand):
setattr(attribute, arg, kwargs.get(arg))
if created and not attribute.order:
attribute.order = AttributeDefinition.objects.all().aggregate(
Max('order')).get('order__max') + 1
attribute.order = AttributeDefinition.objects.all().aggregate(Max('order')).get('order__max') + 1
attribute.save()
def cook(self, filename):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Authentic',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField()),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),
@ -33,7 +36,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Combo',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField()),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),
@ -52,7 +58,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Passerelle',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField()),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),
@ -71,21 +80,34 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Variable',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('name', models.CharField(max_length=100, verbose_name='name')),
('value', models.TextField(help_text='start with [ or { for a JSON document', verbose_name='value', blank=True)),
(
'value',
models.TextField(
help_text='start with [ or { for a JSON document', verbose_name='value', blank=True
),
),
('service_pk', models.PositiveIntegerField(null=True)),
('last_update_timestamp', models.DateTimeField(auto_now=True, null=True)),
('service_type', models.ForeignKey(to='contenttypes.ContentType', null=True, on_delete=models.CASCADE)),
(
'service_type',
models.ForeignKey(to='contenttypes.ContentType', null=True, on_delete=models.CASCADE),
),
],
options={
},
options={},
bases=(models.Model,),
),
migrations.CreateModel(
name='Wcs',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField()),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Fargo',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(verbose_name='Slug')),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Welco',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(verbose_name='Slug')),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -13,23 +13,43 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='authentic',
options={'ordering': ['title'], 'verbose_name': 'Authentic Identity Provider', 'verbose_name_plural': 'Authentic Identity Providers'},
options={
'ordering': ['title'],
'verbose_name': 'Authentic Identity Provider',
'verbose_name_plural': 'Authentic Identity Providers',
},
),
migrations.AlterModelOptions(
name='combo',
options={'ordering': ['title'], 'verbose_name': 'Combo Portal', 'verbose_name_plural': 'Combo Portals'},
options={
'ordering': ['title'],
'verbose_name': 'Combo Portal',
'verbose_name_plural': 'Combo Portals',
},
),
migrations.AlterModelOptions(
name='fargo',
options={'ordering': ['title'], 'verbose_name': 'Fargo document box', 'verbose_name_plural': 'Fargo document box'},
options={
'ordering': ['title'],
'verbose_name': 'Fargo document box',
'verbose_name_plural': 'Fargo document box',
},
),
migrations.AlterModelOptions(
name='passerelle',
options={'ordering': ['title'], 'verbose_name': 'Passerelle', 'verbose_name_plural': 'Passerelle'},
options={
'ordering': ['title'],
'verbose_name': 'Passerelle',
'verbose_name_plural': 'Passerelle',
},
),
migrations.AlterModelOptions(
name='wcs',
options={'ordering': ['title'], 'verbose_name': 'w.c.s. Web Forms', 'verbose_name_plural': 'w.c.s. Web Forms'},
options={
'ordering': ['title'],
'verbose_name': 'w.c.s. Web Forms',
'verbose_name_plural': 'w.c.s. Web Forms',
},
),
migrations.AlterModelOptions(
name='welco',

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='MandayeJS',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(verbose_name='Slug')),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Chrono',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(verbose_name='Slug')),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,7 +14,18 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='mandayejs',
name='site_app',
field=models.CharField(default=b'mandayejs.applications.Test', max_length=128, verbose_name='App Settings', choices=[(b'mandayejs.applications.Test', b'Test'), (b'mandayejs.applications.Duonet', b'Duonet'), (b'mandayejs.applications.Sezhame', b'Sezhame'), (b'mandayejs.applications.Archimed', b'Archimed'), (b'mandayejs.applications.ImuseFamilyMontpellier', b'ImuseFamilyMontpellier')]),
field=models.CharField(
default=b'mandayejs.applications.Test',
max_length=128,
verbose_name='App Settings',
choices=[
(b'mandayejs.applications.Test', b'Test'),
(b'mandayejs.applications.Duonet', b'Duonet'),
(b'mandayejs.applications.Sezhame', b'Sezhame'),
(b'mandayejs.applications.Archimed', b'Archimed'),
(b'mandayejs.applications.ImuseFamilyMontpellier', b'ImuseFamilyMontpellier'),
],
),
preserve_default=True,
),
]

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,7 +14,18 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='mandayejs',
name='site_app',
field=models.CharField(default=b'mandayejs.applications.Test', max_length=128, verbose_name='Site Application', choices=[(b'mandayejs.applications.Test', b'Test'), (b'mandayejs.applications.Duonet', b'Duonet'), (b'mandayejs.applications.Sezhame', b'Sezhame'), (b'mandayejs.applications.Archimed', b'Archimed'), (b'mandayejs.applications.ImuseFamilyMontpellier', b'ImuseFamilyMontpellier')]),
field=models.CharField(
default=b'mandayejs.applications.Test',
max_length=128,
verbose_name='Site Application',
choices=[
(b'mandayejs.applications.Test', b'Test'),
(b'mandayejs.applications.Duonet', b'Duonet'),
(b'mandayejs.applications.Sezhame', b'Sezhame'),
(b'mandayejs.applications.Archimed', b'Archimed'),
(b'mandayejs.applications.ImuseFamilyMontpellier', b'ImuseFamilyMontpellier'),
],
),
preserve_default=True,
),
]

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Piwik',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(verbose_name='Slug')),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Corbo',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(verbose_name='Slug')),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='BiJoe',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(verbose_name='Slug')),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),

View File

@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Hobo',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(verbose_name='Slug')),
('base_url', models.CharField(max_length=200, verbose_name='Base URL')),

View File

@ -14,27 +14,26 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import datetime
import json
import random
import requests
import re
import socket
import requests
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.db import models
from django.utils import six
from django.utils.crypto import get_random_string
from django.utils.encoding import force_text
from django.utils.six.moves.urllib.parse import urlparse
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils import six
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from .utils import Zone, get_installed_services
@ -43,25 +42,25 @@ FLOAT_RE = re.compile(r'^\s*[0-9]+\.[0-9]+\s*')
AUTO_VARIABLES = (
'default_from_email',
'global_email_prefix',
'email_signature',
'email_sender_name',
'global_title',
'robots_txt',
'meta_description',
'meta_keywords',
'sms_url',
'sms_sender',
'default_from_email',
'global_email_prefix',
'email_signature',
'email_sender_name',
'global_title',
'robots_txt',
'meta_description',
'meta_keywords',
'sms_url',
'sms_sender',
)
class Variable(models.Model):
name = models.CharField(max_length=100, verbose_name=_('name'))
label = models.CharField(max_length=100, blank=True, verbose_name=_('label'))
value = models.TextField(
verbose_name=_('value'),
blank=True,
help_text=_('start with [ or { for a JSON document'))
verbose_name=_('value'), blank=True, help_text=_('start with [ or { for a JSON document')
)
auto = models.BooleanField(default=False)
service_type = models.ForeignKey(ContentType, null=True, on_delete=models.CASCADE)
service_pk = models.PositiveIntegerField(null=True)
@ -75,9 +74,8 @@ class Variable(models.Model):
def _parse_value_as_json(self):
if self.value and (
self.value[0] in '{['
or self.value in ('true', 'false', 'null')
or FLOAT_RE.match(self.value)):
self.value[0] in '{[' or self.value in ('true', 'false', 'null') or FLOAT_RE.match(self.value)
):
try:
return json.loads(self.value)
except ValueError:
@ -114,18 +112,17 @@ class ServiceBase(models.Model):
last_operational_success_timestamp = models.DateTimeField(null=True)
last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
variables = GenericRelation(
Variable,
content_type_field='service_type',
object_id_field='service_pk')
variables = GenericRelation(Variable, content_type_field='service_type', object_id_field='service_pk')
@classmethod
def is_enabled(cls):
return True
def is_operational(self):
return (self.last_operational_success_timestamp is not None
and self.last_operational_success_timestamp == self.last_operational_check_timestamp)
return (
self.last_operational_success_timestamp is not None
and self.last_operational_success_timestamp == self.last_operational_check_timestamp
)
def check_operational(self):
once_now = now()
@ -154,8 +151,13 @@ class ServiceBase(models.Model):
return (self.last_operational_check_timestamp - self.last_update_timestamp) < two_minutes
def as_dict(self):
as_dict = dict([(x, y) for (x, y) in self.__dict__.items()
if isinstance(y, six.integer_types + six.string_types)])
as_dict = dict(
[
(x, y)
for (x, y) in self.__dict__.items()
if isinstance(y, six.integer_types + six.string_types)
]
)
as_dict['base_url'] = self.get_base_url_path()
as_dict['service-id'] = self.Extra.service_id
as_dict['service-label'] = force_text(self.Extra.service_label)
@ -192,7 +194,7 @@ class ServiceBase(models.Model):
if not self.secret_key:
self.secret_key = get_random_string(50, SECRET_CHARS)
is_new = (self.id is None)
is_new = self.id is None
super(ServiceBase, self).save(*args, **kwargs)
if is_new and settings.SERVICE_EXTRA_VARIABLES:
@ -247,7 +249,7 @@ class ServiceBase(models.Model):
if not self.is_resolvable():
return False
r = requests.get(self.get_admin_zones()[0].href, timeout=5, verify=False, allow_redirects=False)
return (r.status_code >= 200 and r.status_code < 400)
return r.status_code >= 200 and r.status_code < 400
def security_data(self):
security_data = {
@ -259,7 +261,12 @@ class ServiceBase(models.Model):
security_data['level'] = 0
resp = requests.get(self.base_url, timeout=5, verify=False, allow_redirects=False)
missing_headers = []
for header in ('X-Content-Type-Options', 'X-Frame-Options', 'X-XSS-Protection', 'Strict-Transport-Security'):
for header in (
'X-Content-Type-Options',
'X-Frame-Options',
'X-XSS-Protection',
'Strict-Transport-Security',
):
if not resp.headers.get(header):
missing_headers.append(header)
if missing_headers:
@ -289,9 +296,7 @@ class ServiceBase(models.Model):
class Authentic(ServiceBase):
use_as_idp_for_self = models.BooleanField(
verbose_name=_('Use as IdP'),
default=False)
use_as_idp_for_self = models.BooleanField(verbose_name=_('Use as IdP'), default=False)
class Meta:
verbose_name = _('Authentic Identity Provider')
@ -354,9 +359,7 @@ class Passerelle(ServiceBase):
service_default_slug = 'passerelle'
def get_admin_zones(self):
return [
Zone(self.title, 'webservices', self.get_base_url_path() + 'manage/')
]
return [Zone(self.title, 'webservices', self.get_base_url_path() + 'manage/')]
def get_saml_sp_metadata_url(self):
return self.get_base_url_path() + 'accounts/mellon/metadata/'
@ -377,9 +380,7 @@ class Combo(ServiceBase):
service_default_slug = 'portal'
def get_admin_zones(self):
return [
Zone(self.title, 'portal', self.get_base_url_path() + 'manage/')
]
return [Zone(self.title, 'portal', self.get_base_url_path() + 'manage/')]
def get_saml_sp_metadata_url(self):
return self.get_base_url_path() + 'accounts/mellon/metadata/'
@ -400,9 +401,7 @@ class Fargo(ServiceBase):
service_default_slug = 'porte-doc'
def get_admin_zones(self):
return [
Zone(self.title, 'document-box', self.get_base_url_path() + 'admin/')
]
return [Zone(self.title, 'document-box', self.get_base_url_path() + 'admin/')]
def get_saml_sp_metadata_url(self):
return self.get_base_url_path() + 'accounts/mellon/metadata/'
@ -419,9 +418,7 @@ class Welco(ServiceBase):
service_default_slug = 'courrier'
def get_admin_zones(self):
return [
Zone(self.title, 'multichannel-guichet', self.get_base_url_path() + 'admin/')
]
return [Zone(self.title, 'multichannel-guichet', self.get_base_url_path() + 'admin/')]
def get_saml_sp_metadata_url(self):
return self.get_base_url_path() + 'accounts/mellon/metadata/'
@ -442,9 +439,7 @@ class Chrono(ServiceBase):
service_default_slug = 'agendas'
def get_admin_zones(self):
return [
Zone(self.title, 'calendar', self.get_base_url_path() + 'manage/')
]
return [Zone(self.title, 'calendar', self.get_base_url_path() + 'manage/')]
def get_saml_sp_metadata_url(self):
return self.get_base_url_path() + 'accounts/mellon/metadata/'
@ -465,9 +460,7 @@ class Hobo(ServiceBase):
service_default_slug = 'hobo'
def get_admin_zones(self):
return [
Zone(self.title, 'hobo', self.get_base_url_path())
]
return [Zone(self.title, 'hobo', self.get_base_url_path())]
def get_saml_sp_metadata_url(self):
return self.get_base_url_path() + 'accounts/mellon/metadata/'
@ -488,9 +481,7 @@ class BiJoe(ServiceBase):
service_default_slug = 'statistics'
def get_admin_zones(self):
return [
Zone(self.title, 'bijoe', self.get_base_url_path())
]
return [Zone(self.title, 'bijoe', self.get_base_url_path())]
def get_saml_sp_metadata_url(self):
return self.get_base_url_path() + 'accounts/mellon/metadata/'
@ -499,5 +490,4 @@ class BiJoe(ServiceBase):
return self.get_base_url_path() + 'manage/menu.json'
AVAILABLE_SERVICES = [Authentic, Wcs, Passerelle, Combo, Fargo, Welco,
Chrono, BiJoe, Hobo]
AVAILABLE_SERVICES = [Authentic, Wcs, Passerelle, Combo, Fargo, Welco, Chrono, BiJoe, Hobo]

View File

@ -1,22 +1,21 @@
from django.urls import reverse_lazy
from django.template import Library
from django.urls import reverse_lazy
from .. import forms
register = Library()
@register.filter(name='as_update_form')
def as_update_form(object):
return getattr(forms, object.__class__.__name__ + 'Form')(instance=object).as_p()
@register.filter(name='save_url')
def save_url(object):
return reverse_lazy('save-service', kwargs={
'service': object.Extra.service_id,
'slug': object.slug})
return reverse_lazy('save-service', kwargs={'service': object.Extra.service_id, 'slug': object.slug})
@register.filter(name='delete_url')
def delete_url(object):
return reverse_lazy('delete-service', kwargs={
'service': object.Extra.service_id,
'slug': object.slug})
return reverse_lazy('delete-service', kwargs={'service': object.Extra.service_id, 'slug': object.slug})

View File

@ -21,20 +21,30 @@ from . import views
urlpatterns = [
url(r'^$', views.HomeView.as_view(), name='environment-home'),
url(r'^variables$', views.VariablesView.as_view(), name='environment-variables'),
url(r'^new-variable$', views.VariableCreateView.as_view(), name='new-variable',),
url(r'^update-variable/(?P<pk>\w+)$', views.VariableUpdateView.as_view(),
name='update-variable'),
url(r'^delete-variable/(?P<pk>\w+)$', views.VariableDeleteView.as_view(),
name='delete-variable'),
url(r'^check_operational/(?P<service>\w+)/(?P<slug>[\w-]+)$',
views.operational_check_view, name='operational-check'),
url(
r'^new-variable$',
views.VariableCreateView.as_view(),
name='new-variable',
),
url(r'^update-variable/(?P<pk>\w+)$', views.VariableUpdateView.as_view(), name='update-variable'),
url(r'^delete-variable/(?P<pk>\w+)$', views.VariableDeleteView.as_view(), name='delete-variable'),
url(
r'^check_operational/(?P<service>\w+)/(?P<slug>[\w-]+)$',
views.operational_check_view,
name='operational-check',
),
url(r'^new-(?P<service>\w+)$', views.ServiceCreateView.as_view(), name='create-service'),
url(r'^save-(?P<service>\w+)/(?P<slug>[\w-]+)$', views.ServiceUpdateView.as_view(), name='save-service'),
url(r'^delete-(?P<service>\w+)/(?P<slug>[\w-]+)$', views.ServiceDeleteView.as_view(), name='delete-service'),
url(r'^new-variable-(?P<service>\w+)/(?P<slug>[\w-]+)$',
views.VariableCreateView.as_view(), name='new-variable-service',),
url(
r'^delete-(?P<service>\w+)/(?P<slug>[\w-]+)$',
views.ServiceDeleteView.as_view(),
name='delete-service',
),
url(
r'^new-variable-(?P<service>\w+)/(?P<slug>[\w-]+)$',
views.VariableCreateView.as_view(),
name='new-variable-service',
),
url(r'^import/$', views.ImportView.as_view(), name='environment-import'),
url(r'^export/$', views.ExportView.as_view(), name='environment-export'),
url(r'^debug.json$', views.debug_json, name='debug-json'),

View File

@ -17,10 +17,10 @@
import hashlib
from django.conf import settings
from django.urls import reverse
from django.db import connection, transaction
from django.utils.six.moves.urllib.parse import urlparse
from django.urls import reverse
from django.utils.encoding import force_text
from django.utils.six.moves.urllib.parse import urlparse
from hobo.middleware.utils import StoreRequestMiddleware
from hobo.multitenant.settings_loaders import KnownServices
@ -29,6 +29,7 @@ from hobo.profile.utils import get_profile_dict
def get_installed_services():
from .models import AVAILABLE_SERVICES
installed_services = []
for available_service in AVAILABLE_SERVICES:
installed_services.extend(available_service.objects.all())
@ -70,13 +71,14 @@ def get_local_hobo_dict():
def get_installed_services_dict():
from .models import Variable
hobo_service = []
hobo_dict = get_local_hobo_dict()
if hobo_dict:
hobo_service.append(hobo_dict)
return {
'services': hobo_service + [x.as_dict() for x in get_installed_services()],
'variables': {v.name: v.json for v in Variable.objects.filter(service_pk__isnull=True)}
'variables': {v.name: v.json for v in Variable.objects.filter(service_pk__isnull=True)},
}
@ -93,6 +95,7 @@ class Zone:
def get_variable(name):
from .models import Variable
try:
variable = Variable.objects.get(name=name)
except Variable.DoesNotExist:
@ -102,8 +105,8 @@ def get_variable(name):
def set_variable(name, value):
from .models import Variable
variable, created = Variable.objects.get_or_create(
name=name, defaults={'auto': True})
variable, created = Variable.objects.get_or_create(name=name, defaults={'auto': True})
variable.value = value
variable.save()
@ -123,7 +126,7 @@ def create_base_url(hostname, service):
def get_setting_variable(setting_name, label=None, service=None):
from .models import Variable, ContentType
from .models import ContentType, Variable
kwargs = {
'name': 'SETTING_' + setting_name,
@ -138,11 +141,7 @@ def get_setting_variable(setting_name, label=None, service=None):
try:
variable = Variable.objects.get(**kwargs)
except Variable.DoesNotExist:
variable = Variable(
name='SETTING_' + setting_name,
label=label or '',
service=service,
auto=True)
variable = Variable(name='SETTING_' + setting_name, label=label or '', service=service, auto=True)
return variable
@ -152,20 +151,17 @@ def export_parameters():
variables = []
for var in Variable.objects.filter(service_pk__isnull=True):
variables.append({
'name': var.name,
'label': var.label,
'value': var.value,
'auto': var.auto})
variables.append({'name': var.name, 'label': var.label, 'value': var.value, 'auto': var.auto})
parameters = {'variables': variables}
parameters.update(get_profile_dict())
return parameters
def import_parameters(parameters):
from .models import Variable
from hobo.profile.models import AttributeDefinition
from .models import Variable
with transaction.atomic():
for variables in parameters.get('variables', []):
obj, created = Variable.objects.get_or_create(name=variables['name'])

View File

@ -11,11 +11,9 @@ def validate_service_url(url):
raise ValidationError(
_('Error: %(netloc)s is not resolvable in URL %(url)s'),
code='not-resolvable',
params={'netloc': urlparse.urlsplit(url).netloc, 'url': url}
params={'netloc': urlparse.urlsplit(url).netloc, 'url': url},
)
if not service.has_valid_certificate():
raise ValidationError(
_('Error: no valid certificate for %(url)s'),
code='invalid-certificate',
params={'url': url}
_('Error: no valid certificate for %(url)s'), code='invalid-certificate', params={'url': url}
)

View File

@ -19,17 +19,17 @@ import string
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse_lazy
from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.views.generic import View
from django.views.generic.base import TemplateView
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from .models import Variable, AVAILABLE_SERVICES
from . import forms, utils
from .models import AVAILABLE_SERVICES, Variable
class AvailableService(object):
@ -43,9 +43,7 @@ class HomeView(TemplateView):
def get_context_data(self, **kwargs):
context = super(HomeView, self).get_context_data(**kwargs)
context['available_services'] = [
AvailableService(x) for x in AVAILABLE_SERVICES if x.is_enabled()
]
context['available_services'] = [AvailableService(x) for x in AVAILABLE_SERVICES if x.is_enabled()]
context['installed_services'] = [x for x in utils.get_installed_services() if not x.secondary]
return context
@ -78,16 +76,12 @@ class VariableCreateView(CreateView):
if form.service:
service_kwargs = {
'service_pk': form.service.id,
'service_type': ContentType.objects.get_for_model(form.service)
'service_type': ContentType.objects.get_for_model(form.service),
}
else:
service_kwargs = {
'service_pk__isnull': True
}
service_kwargs = {'service_pk__isnull': True}
try:
self.object = Variable.objects.get(
name=form.instance.name,
**service_kwargs)
self.object = Variable.objects.get(name=form.instance.name, **service_kwargs)
except Variable.DoesNotExist:
self.object = form.save()
else:
@ -133,8 +127,8 @@ class ServiceCreateView(CreateView):
def get_initial(self):
initial = super(ServiceCreateView, self).get_initial()
initial['base_url'] = utils.create_base_url(
self.request.build_absolute_uri(),
self.model.Extra.service_default_slug)
self.request.build_absolute_uri(), self.model.Extra.service_default_slug
)
initial['slug'] = self.model.Extra.service_default_slug
return initial
@ -209,8 +203,7 @@ class ImportView(FormView):
def form_valid(self, form):
try:
parameters_json = json.loads(
force_text(self.request.FILES['parameters_json'].read()))
parameters_json = json.loads(force_text(self.request.FILES['parameters_json'].read()))
except ValueError:
form.add_error('parameters_json', _('File is not in the expected JSON format.'))
return self.form_invalid(form)
@ -220,7 +213,6 @@ class ImportView(FormView):
class ExportView(View):
def get(self, request, *args, **kwargs):
response = JsonResponse(utils.export_parameters(), json_dumps_params={'indent': 2})
response['Content-Disposition'] = 'attachment; filename="hobo-export.json"'

View File

@ -1,11 +1,12 @@
from django.forms import ModelForm
from django.conf import settings
from django.forms import ModelForm
if 'tenant_schemas' in settings.INSTALLED_APPS:
from tenant_schemas.utils import get_tenant_model
else:
get_tenant_model = lambda: None
class HoboForm(ModelForm):
required_css_class = 'required'
@ -26,8 +27,8 @@ class HoboForm(ModelForm):
model = get_tenant_model()
fields = ['domain_url', 'schema_name']
class HoboUpdateForm(ModelForm):
class HoboUpdateForm(ModelForm):
class Meta:
model = get_tenant_model()
fields = ['domain_url']

View File

@ -20,19 +20,21 @@ from django.utils.translation import ugettext_lazy as _
class SettingsForm(forms.Form):
platform = forms.ChoiceField(
label=_('Platform'),
choices=[
('prod', _('Production')),
('test', _('Integration')),
])
label=_('Platform'),
choices=[
('prod', _('Production')),
('test', _('Integration')),
],
)
client_id = forms.CharField(
label=_('Client ID'),
help_text=_('See <a href="https://partenaires.franceconnect.gouv.fr/fcp/fournisseur-service">'
'FranceConnect partners site</a> for getting client ID and secret.'),
widget=forms.TextInput(attrs={'size': 64}))
client_secret = forms.CharField(
label=_('Client Secret'),
widget=forms.TextInput(attrs={'size': 64}))
label=_('Client ID'),
help_text=_(
'See <a href="https://partenaires.franceconnect.gouv.fr/fcp/fournisseur-service">'
'FranceConnect partners site</a> for getting client ID and secret.'
),
widget=forms.TextInput(attrs={'size': 64}),
)
client_secret = forms.CharField(label=_('Client Secret'), widget=forms.TextInput(attrs={'size': 64}))
scopes = forms.MultipleChoiceField(
label=_('Scopes'),
choices=[
@ -51,7 +53,8 @@ class SettingsForm(forms.Form):
('birth', _('birth profile (birth)')),
],
widget=forms.CheckboxSelectMultiple,
help_text=_('These scopes will be requested in addition to openid'))
help_text=_('These scopes will be requested in addition to openid'),
)
class EnableForm(forms.Form):

View File

@ -21,13 +21,13 @@ from django.views.generic import FormView
from hobo.environment.models import Authentic
from hobo.environment.utils import get_setting_variable
from .forms import SettingsForm, EnableForm
from .forms import EnableForm, SettingsForm
def get_variable(setting_name):
return get_setting_variable(
setting_name,
service=Authentic.objects.get(secondary=False))
return get_setting_variable(setting_name, service=Authentic.objects.get(secondary=False))
PLATFORMS = {
'test': {
@ -41,7 +41,7 @@ PLATFORMS = {
'A2_FC_TOKEN_URL': 'https://app.franceconnect.gouv.fr/api/v1/token',
'A2_FC_USERINFO_URL': 'https://app.franceconnect.gouv.fr/api/v1/userinfo',
'A2_FC_LOGOUT_URL': 'https://app.franceconnect.gouv.fr/api/v1/logout',
}
},
}
@ -83,26 +83,28 @@ class HomeView(FormView):
variable.save()
variable = get_variable('A2_FC_USER_INFO_MAPPINGS')
variable.value = json.dumps({
'last_name': {
'ref': 'family_name',
'verified': True,
},
'first_name': {
'ref': 'given_name',
'verified': True,
},
'title': {
'ref': 'gender',
'translation': 'simple',
'translation_simple': {
'male': 'Monsieur',
'female': 'Madame',
variable.value = json.dumps(
{
'last_name': {
'ref': 'family_name',
'verified': True,
},
'verified': True,
},
'email': 'email',
})
'first_name': {
'ref': 'given_name',
'verified': True,
},
'title': {
'ref': 'gender',
'translation': 'simple',
'translation_simple': {
'male': 'Monsieur',
'female': 'Madame',
},
'verified': True,
},
'email': 'email',
}
)
variable.save()
variable = get_variable('A2_FC_SCOPES')
@ -116,6 +118,7 @@ class HomeView(FormView):
context['enabled'] = bool(get_variable('A2_FC_ENABLE').json)
return context
home = HomeView.as_view()
@ -130,6 +133,7 @@ class EnableView(FormView):
variable.save()
return super(EnableView, self).form_valid(form)
enable = EnableView.as_view()
@ -144,4 +148,5 @@ class DisableView(FormView):
variable.save()
return super(DisableView, self).form_valid(form)
disable = DisableView.as_view()

View File

@ -18,14 +18,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with python-systemd; If not, see <http://www.gnu.org/licenses/>.
import logging as _logging
import sys as _sys
import traceback as _traceback
import logging as _logging
from syslog import (LOG_ALERT, LOG_CRIT, LOG_ERR,
LOG_WARNING, LOG_INFO, LOG_DEBUG)
from syslog import LOG_ALERT, LOG_CRIT, LOG_DEBUG, LOG_ERR, LOG_INFO, LOG_WARNING
from systemd._journal import sendv
from django.utils import six
from systemd._journal import sendv
_IDENT_CHARACTER = set('ABCDEFGHIJKLMNOPQRTSUVWXYZ_0123456789')
@ -43,9 +42,7 @@ def _make_line(field, value):
return field + '=' + str(value)
def send(MESSAGE, MESSAGE_ID=None,
CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None,
**kwargs):
def send(MESSAGE, MESSAGE_ID=None, CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None, **kwargs):
r"""Send a message to the journal.
>>> from systemd import journal
@ -181,24 +178,32 @@ class JournalHandler(_logging.Handler):
# explicit arguments — highest priority
for key, value in record.__dict__.items():
new_key = key.upper()
if new_key in ['PRIORITY', 'LOGGER', 'THREAD_NAME',
'PROCESS_NAME', 'CODE_FILE', 'MESSAGE',
'CODE_LINE', 'CODE_FUNC']:
if new_key in [
'PRIORITY',
'LOGGER',
'THREAD_NAME',
'PROCESS_NAME',
'CODE_FILE',
'MESSAGE',
'CODE_LINE',
'CODE_FUNC',
]:
continue
if key in ['threadName', 'processName', 'pathname', 'lineno',
'funcName']:
if key in ['threadName', 'processName', 'pathname', 'lineno', 'funcName']:
continue
extras[key.upper()] = value
self.send(msg,
PRIORITY=format(pri),
LOGGER=record.name,
THREAD_NAME=record.threadName,
PROCESS_NAME=record.processName,
CODE_FILE=record.pathname,
CODE_LINE=record.lineno,
CODE_FUNC=record.funcName,
**extras)
self.send(
msg,
PRIORITY=format(pri),
LOGGER=record.name,
THREAD_NAME=record.threadName,
PROCESS_NAME=record.processName,
CODE_FILE=record.pathname,
CODE_LINE=record.lineno,
CODE_FUNC=record.funcName,
**extras,
)
except Exception:
self.handleError(record)

View File

@ -19,10 +19,10 @@ import datetime
import logging
import logging.handlers
import os
import pytz
import time
import warnings
import pytz
from django.conf import settings
from django.db import connection
@ -31,7 +31,9 @@ from hobo.middleware.utils import StoreRequestMiddleware
class SettingsLogLevel(str):
def __new__(cls, value):
warnings.warn('SettingsLogLevel is deprecated, use DEBUG_LOG instead.', DeprecationWarning, stacklevel=2)
warnings.warn(
'SettingsLogLevel is deprecated, use DEBUG_LOG instead.', DeprecationWarning, stacklevel=2
)
return super(SettingsLogLevel, cls).__new__(value)
@ -48,10 +50,10 @@ class RequestContextFilter(logging.Filter):
DEFAULT_APPLICATION = 'django'
def filter(self, record):
'''Add username, ip and request ID to the log record.
"""Add username, ip and request ID to the log record.
Inspired by django-log-request-id
'''
Inspired by django-log-request-id
"""
# prevent multiple execution on the same record
if getattr(record, 'request_context', False):
return True
@ -63,9 +65,11 @@ class RequestContextFilter(logging.Filter):
# lookup user from record then from request
if not hasattr(record, 'user'):
if (hasattr(request, 'user')
and hasattr(request.user, 'is_authenticated')
and request.user.is_authenticated):
if (
hasattr(request, 'user')
and hasattr(request.user, 'is_authenticated')
and request.user.is_authenticated
):
record.user = request.user
else:
record.user = None
@ -117,6 +121,7 @@ class ForceDebugFilter(logging.Filter):
class LogRecord(logging.LogRecord):
'''Subclass LogRecord to make multiline log parseable'''
def getMessage(self):
return super(LogRecord, self).getMessage().replace('\n', '\n ')
@ -146,8 +151,10 @@ class DebugLogFilter(object):
elif hasattr(debug_log, 'encode'):
# debug_log is a string
domains = [domain.strip() for domain in debug_log.split(',')]
return any(record.name == domain or (record.name.startswith(domain) and record.name[len(domain)] == '.')
for domain in domains)
return any(
record.name == domain or (record.name.startswith(domain) and record.name[len(domain)] == '.')
for domain in domains
)
else:
return bool(debug_log)
@ -201,11 +208,13 @@ class DebugLog(object):
d['user'] = ast.literal_eval(d['user'])
except SyntaxError:
pass
d.update({
'cursor': cursor,
'timestamp': timestamp,
'message': message,
})
d.update(
{
'cursor': cursor,
'timestamp': timestamp,
'message': message,
}
)
yield d
@classmethod

View File

@ -26,17 +26,18 @@ class SettingsForm(forms.Form):
'cnil_compliant_visits_tracking_js' else into 'visits_tracking_js'.
The goal is to display a banner advertising users about intrusive JS.
"""
tracking_js = forms.CharField(
label=_('Tracking Javascript'),
required=False,
widget=forms.Textarea())
tracking_js = forms.CharField(label=_('Tracking Javascript'), required=False, widget=forms.Textarea())
def clean_tracking_js(self):
value = self.cleaned_data['tracking_js']
if '<script' in value:
raise forms.ValidationError(
_('This field should only contain the Javascript code. '
'You should remove the surrounding <script> markup.'))
_(
'This field should only contain the Javascript code. '
'You should remove the surrounding <script> markup.'
)
)
return value

View File

@ -16,19 +16,18 @@
import hashlib
import re
import requests
import string
from lxml import etree
from random import choice, randint
from django.db import connection
import requests
from django.conf import settings
from django.core import exceptions
from django.db import connection
from django.utils.encoding import force_bytes
from django.utils.six.moves.urllib import parse as urlparse
from lxml import etree
from hobo.environment.models import Variable, Wcs, Combo, Fargo
from hobo.environment.models import Combo, Fargo, Variable, Wcs
CNIL_JS = """
// disallow cookie's time extension
@ -47,15 +46,15 @@ CNIL_JS = """
}]);
"""
def get_variable(name, default=''):
"""get hobo variables from DB
set it to '' into DB if not already created
"""
variable, created = Variable.objects.get_or_create(
name=name,
defaults={'auto': True, 'value': default})
variable, created = Variable.objects.get_or_create(name=name, defaults={'auto': True, 'value': default})
return variable
def get_variable_value(name, default=''):
"""get hobo variables's value from DB"""
try:
@ -64,12 +63,14 @@ def get_variable_value(name, default=''):
value = default
return value
def get_tracking_js():
"""merge JS code from the 2 above variables"""
tracking_js = get_variable_value('cnil_compliant_visits_tracking_js')
tracking_js += get_variable_value('visits_tracking_js')
return tracking_js
def put_tracking_js(tracking_js):
"""store JS code into only one of the 2 above variables"""
variable1 = get_variable('cnil_compliant_visits_tracking_js')
@ -87,11 +88,11 @@ def put_tracking_js(tracking_js):
variable1.delete()
variable2.delete()
def get_tenant_name_and_public_urls():
"""get an alias for our matomo's id and urls to monitor"""
tenant_name = None
services = [x for x in Combo.objects.all()
if 'portal-user' in x.template_name and not x.secondary]
services = [x for x in Combo.objects.all() if 'portal-user' in x.template_name and not x.secondary]
if services != [] and services[0] != '':
tenant_name = urlparse.urlparse(services[0].base_url).netloc
services += [x for x in Wcs.objects.all() if not x.secondary]
@ -99,12 +100,15 @@ def get_tenant_name_and_public_urls():
site_urls = [x.base_url for x in services if x.base_url != '']
return tenant_name, site_urls
class MatomoException(Exception):
"""unexpected Matomo internal error"""
class MatomoError(MatomoException):
"""expected Matomo error responses"""
class MatomoWS(object):
"""api for matomo webservices"""
@ -166,8 +170,7 @@ class MatomoWS(object):
return tree
def get_site_id_from_site_url(self, url):
data = {'method': 'SitesManager.getSitesIdFromSiteUrl',
'url': url}
data = {'method': 'SitesManager.getSitesIdFromSiteUrl', 'url': url}
tree = self.call(data)
try:
if tree.xpath('/result[not(*)]')[0].text is None:
@ -181,8 +184,7 @@ class MatomoWS(object):
return tag.text
def add_site(self, site_name):
data = {'method': 'SitesManager.addSite',
'siteName': site_name}
data = {'method': 'SitesManager.addSite', 'siteName': site_name}
tree = self.call(data)
try:
tag = tree.xpath('/result')[0]
@ -191,8 +193,7 @@ class MatomoWS(object):
return tag.text
def add_site_alias_urls(self, id_site, site_urls):
data = {'method': 'SitesManager.addSiteAliasUrls',
'idSite': id_site}
data = {'method': 'SitesManager.addSiteAliasUrls', 'idSite': id_site}
cpt = 0
for url in site_urls:
key = 'urls[%i]' % cpt
@ -206,23 +207,23 @@ class MatomoWS(object):
return tag.text
def add_user(self, user_login, password, initial_id_site):
data = {'method': 'UsersManager.addUser',
'userLogin': user_login,
'password': password,
'email': self.email_template % user_login,
'initialIdSite': initial_id_site}
data = {
'method': 'UsersManager.addUser',
'userLogin': user_login,
'password': password,
'email': self.email_template % user_login,
'initialIdSite': initial_id_site,
}
tree = self.call(data)
return self.assert_success(tree, 'add_user')
def del_user(self, user_login):
data = {'method': 'UsersManager.deleteUser',
'userLogin': user_login}
data = {'method': 'UsersManager.deleteUser', 'userLogin': user_login}
tree = self.call(data)
return self.assert_success(tree, 'del_user')
def get_javascript_tag(self, id_site):
data = {'method': 'SitesManager.getJavascriptTag',
'idSite': id_site}
data = {'method': 'SitesManager.getJavascriptTag', 'idSite': id_site}
tree = self.call(data)
try:
tag = tree.xpath('/result')[0]
@ -240,14 +241,11 @@ class MatomoWS(object):
try:
tree = resp.json()
except ValueError:
raise MatomoException(
'internal error on ping (JSON expected): %s' % resp.content)
raise MatomoException('internal error on ping (JSON expected): %s' % resp.content)
if not isinstance(tree, dict):
raise MatomoException(
'internal error on ping (dict expected): %s' % resp.content)
raise MatomoException('internal error on ping (dict expected): %s' % resp.content)
if 'status' not in tree:
raise MatomoException(
'internal error on ping (status expected): %s' % resp.content)
raise MatomoException('internal error on ping (status expected): %s' % resp.content)
if tree['status'] != 'success':
raise MatomoError('ping fails: %s' % resp.content)
return True
@ -265,6 +263,7 @@ def upgrade_site(matomo, tenant_name, site_urls):
matomo.add_site_alias_urls(id_site, site_urls)
return id_site
def upgrade_user(matomo, user_login, id_site):
# API is not obvious to change password (need the previous one)
try:
@ -280,9 +279,13 @@ def upgrade_user(matomo, user_login, id_site):
# build the user's login url
password_md5 = hashlib.md5(force_bytes(password)).hexdigest()
logme_url = '%s/index.php?module=Login&action=logme&login=%s&password=%s' % (
matomo.url_ws_base, user_login, password_md5)
matomo.url_ws_base,
user_login,
password_md5,
)
return logme_url
def upgrade_javascript_tag(matomo, id_site):
"""addapt JS return by Matomo and merge it whith previous JS code we have"""
matomo_tag = matomo.get_javascript_tag(id_site)
@ -306,7 +309,7 @@ def upgrade_javascript_tag(matomo, id_site):
regex = re.compile(r'\s*var _paq = window._paq \|\| \[\];')
for count, line in enumerate(lines):
if regex.match(line):
lines.insert(count+1, CNIL_JS)
lines.insert(count + 1, CNIL_JS)
break
enhanced_tag = '\n'.join(lines)

View File

@ -14,16 +14,23 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.urls import reverse_lazy
from django.conf import settings
from django.contrib import messages
from django.views.generic import RedirectView, FormView
from django.urls import reverse_lazy
from django.views.generic import FormView, RedirectView
from .forms import SettingsForm, EnableForm
from .utils import get_variable, get_variable_value, \
get_tracking_js, put_tracking_js, \
MatomoException, MatomoError, MatomoWS, \
compute_cnil_acknowledgment_level, auto_configure_matomo
from .forms import EnableForm, SettingsForm
from .utils import (
MatomoError,
MatomoException,
MatomoWS,
auto_configure_matomo,
compute_cnil_acknowledgment_level,
get_tracking_js,
get_variable,
get_variable_value,
put_tracking_js,
)
class HomeView(FormView):
@ -63,6 +70,7 @@ class HomeView(FormView):
context['mode'] = 'manual'
return context
home = HomeView.as_view()
@ -83,6 +91,7 @@ class EnableManualView(FormView):
logme_url.delete()
return super(EnableManualView, self).form_valid(form)
enable_manual = EnableManualView.as_view()
@ -96,14 +105,15 @@ class EnableAutoView(FormView):
try:
id_site = auto_configure_matomo(matomo)
except MatomoException as exc:
messages.error(self.request, 'matomo: '+str(exc))
messages.error(self.request, 'matomo: ' + str(exc))
else:
try:
matomo.create_fake_first_tracking_visit(id_site)
except MatomoException as exc:
messages.warning(self.request, 'ping: '+str(exc))
messages.warning(self.request, 'ping: ' + str(exc))
return super(EnableAutoView, self).form_valid(form)
enable_auto = EnableAutoView.as_view()
@ -118,4 +128,5 @@ class DisableView(FormView):
variable.delete()
return super(DisableView, self).form_valid(form)
disable = DisableView.as_view()

View File

@ -1,5 +1,5 @@
from .version import VersionMiddleware
from .cookies_samesite import CookiesSameSiteFixMiddleware
from .cors import CORSMiddleware
from .seo import RobotsTxtMiddleware
from .stats import PrometheusStatsMiddleware
from .version import VersionMiddleware

View File

@ -35,4 +35,5 @@ class CookiesSameSiteFixMiddleware(MiddlewareMixin):
# required for Python <3.8
import http.cookies
http.cookies.Morsel._reserved.setdefault('samesite', 'SameSite')

View File

@ -4,6 +4,7 @@ from django.conf import settings
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin
class CORSMiddleware(MiddlewareMixin):
def process_request(self, request):
"""
@ -13,8 +14,7 @@ class CORSMiddleware(MiddlewareMixin):
view/exception middleware along with the requested view;
it will call any response middlewares
"""
if request.method == 'OPTIONS' and \
"HTTP_ACCESS_CONTROL_REQUEST_METHOD" in request.META:
if request.method == 'OPTIONS' and "HTTP_ACCESS_CONTROL_REQUEST_METHOD" in request.META:
response = HttpResponse()
return response
return None

View File

@ -22,7 +22,5 @@ from django.utils.deprecation import MiddlewareMixin
class RobotsTxtMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.method == 'GET' and request.path == '/robots.txt' and hasattr(settings, 'TEMPLATE_VARS'):
return HttpResponse(
settings.TEMPLATE_VARS.get('robots_txt', ''),
content_type='text/plain')
return HttpResponse(settings.TEMPLATE_VARS.get('robots_txt', ''), content_type='text/plain')
return None

View File

@ -18,47 +18,50 @@ import inspect
import time
import django
import prometheus_client
from django.db import connection
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin
import prometheus_client
requests_total_by_host_view_status_method = prometheus_client.Counter(
'django_http_requests_total_by_host_view_status_method',
'Count of requests by host, view, status, method.',
['host', 'view', 'status', 'method'])
['host', 'view', 'status', 'method'],
)
requests_bytes_by_host_view_status_method = prometheus_client.Summary(
'django_http_requests_bytes_by_host_view_status_method',
'Bytes of requests by host, view, status, method.',
['host', 'view', 'status', 'method'])
['host', 'view', 'status', 'method'],
)
requests_times_by_host_view_status_method = prometheus_client.Summary(
'django_http_requests_times_by_host_view_status_method',
'Duration of requests by host, view, status, method.',
['host', 'view', 'status', 'method'])
['host', 'view', 'status', 'method'],
)
requests_queries_count_by_host_view_status_method = prometheus_client.Summary(
'django_http_requests_queries_count_by_host_view_status_method',
'Query count by host, view, status, method.',
['host', 'view', 'status', 'method'])
['host', 'view', 'status', 'method'],
)
requests_queries_time_by_host_view_status_method = prometheus_client.Summary(
'django_http_requests_queries_time_by_host_view_status_method',
'Query time by host, view, status, method.',
['host', 'view', 'status', 'method'])
['host', 'view', 'status', 'method'],
)
class PrometheusStatsMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.method == 'GET' and request.path == '/__metrics__/':
return HttpResponse(prometheus_client.generate_latest(),
content_type=prometheus_client.CONTENT_TYPE_LATEST)
return HttpResponse(
prometheus_client.generate_latest(), content_type=prometheus_client.CONTENT_TYPE_LATEST
)
connection.force_debug_cursor = True # to count queries
connection.force_debug_cursor = True # to count queries
request._stats_t0 = time.time()
return None
@ -80,23 +83,26 @@ class PrometheusStatsMiddleware(MiddlewareMixin):
status_code = response.status_code
view_name = getattr(request, '_view_name', '<unnamed view>')
requests_total_by_host_view_status_method.labels(
host_name, view_name, status_code, http_method).inc()
requests_total_by_host_view_status_method.labels(host_name, view_name, status_code, http_method).inc()
if hasattr(response, 'content'):
response_size = len(response.content)
requests_bytes_by_host_view_status_method.labels(
host_name, view_name, status_code, http_method).observe(response_size)
host_name, view_name, status_code, http_method
).observe(response_size)
requests_times_by_host_view_status_method.labels(
host_name, view_name, status_code, http_method).observe(total_time)
host_name, view_name, status_code, http_method
).observe(total_time)
if connection.queries_logged:
sql_queries_count = len(connection.queries)
sql_queries_time = sum([float(x['time']) for x in connection.queries])
requests_queries_count_by_host_view_status_method.labels(
host_name, view_name, status_code, http_method).observe(sql_queries_count)
host_name, view_name, status_code, http_method
).observe(sql_queries_count)
requests_queries_time_by_host_view_status_method.labels(
host_name, view_name, status_code, http_method).observe(sql_queries_time)
host_name, view_name, status_code, http_method
).observe(sql_queries_time)
return response

View File

@ -1,6 +1,8 @@
import threading
from django.utils.deprecation import MiddlewareMixin
class StoreRequestMiddleware(MiddlewareMixin):
collection = {}

View File

@ -5,11 +5,10 @@ from django.utils.deprecation import MiddlewareMixin
from hobo.scrutiny.wsgi import middleware
class VersionMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.method == 'GET' and (request.path == '/__version__' or
request.path == '/__version__/'):
if request.method == 'GET' and (request.path == '/__version__' or request.path == '/__version__/'):
packages_version = middleware.VersionMiddleware.get_packages_version()
return HttpResponse(json.dumps(packages_version),
content_type='application/json')
return HttpResponse(json.dumps(packages_version), content_type='application/json')
return None

View File

@ -3,15 +3,15 @@ from django.utils.deprecation import MiddlewareMixin
class XForwardedForMiddleware(MiddlewareMixin):
'''Copy the first address from X-Forwarded-For header to the REMOTE_ADDR meta.
"""Copy the first address from X-Forwarded-For header to the REMOTE_ADDR meta.
This middleware should only be used if you are sure the header cannot be
forged (behind a reverse proxy for example)."""
This middleware should only be used if you are sure the header cannot be
forged (behind a reverse proxy for example).'''
def process_request(self, request):
if getattr(settings, 'USE_X_FORWARDED_FOR', False):
if 'HTTP_X_FORWARDED_FOR' in request.META:
ip = request.META.get('HTTP_X_FORWARDED_FOR', '').split(",")[0].strip()
ip = request.META.get('HTTP_X_FORWARDED_FOR', '').split(",")[0].strip()
if ip:
request.META['REMOTE_ADDR'] = ip
return None

View File

@ -3,6 +3,8 @@ default_app_config = 'hobo.multitenant.apps.MultitenantAppConfig'
# import the context_processors module so it gets to compute the versions hash
# early on.
from hobo import context_processors
# install tenant aware thread classes to prevent other lib to use classic ones
from . import threads
threads.install_tenant_aware_threads()

View File

@ -8,15 +8,13 @@ class MultitenantAppConfig(AppConfig):
verbose_name = 'Multitenant'
def ready(self):
from django.db.migrations import operations
from django.db import migrations
from django import conf
from django.db import migrations
from django.db.migrations import operations
# Install tenant aware settings
if not isinstance(conf.settings._wrapped,
settings.TenantSettingsWrapper):
conf.settings._wrapped = settings.TenantSettingsWrapper(
conf.settings._wrapped)
if not isinstance(conf.settings._wrapped, settings.TenantSettingsWrapper):
conf.settings._wrapped = settings.TenantSettingsWrapper(conf.settings._wrapped)
# reset settings getattr method to a cache-less version, to cancel
# https://code.djangoproject.com/ticket/27625.
conf.LazySettings.__getattr__ = lambda self, name: getattr(self._wrapped, name)

View File

@ -1,16 +1,18 @@
from django.core.cache.backends.base import InvalidCacheBackendError
from django.core.exceptions import ImproperlyConfigured
from django.db import connection
from django.utils.module_loading import import_string
from django.core.cache.backends.base import InvalidCacheBackendError
class TenantBaseCache(object):
'''Prepend the tenant schema name to the cache prefix'''
def set_key_prefix(self, prefix):
self.__key_prefix = prefix
def get_key_prefix(self):
if connection.tenant:
return '%s_%s'% (connection.tenant.schema_name, self.__key_prefix)
return '%s_%s' % (connection.tenant.schema_name, self.__key_prefix)
return self.__key_prefix
key_prefix = property(get_key_prefix, set_key_prefix)
@ -18,6 +20,7 @@ class TenantBaseCache(object):
__DERIVED_CLASSES = {}
def TenantCache(host, params, **kwargs):
try:
backend = params['REAL_BACKEND']
@ -31,6 +34,5 @@ def TenantCache(host, params, **kwargs):
if derived_cls_name not in __DERIVED_CLASSES:
# dynamically create a new class with TenantBaseCache
# and the original class as parents
__DERIVED_CLASSES[derived_cls_name] = type(derived_cls_name,
(TenantBaseCache, backend_cls), {})
__DERIVED_CLASSES[derived_cls_name] = type(derived_cls_name, (TenantBaseCache, backend_cls), {})
return __DERIVED_CLASSES[derived_cls_name](host, params, **kwargs)

View File

@ -20,18 +20,17 @@ import os
import shutil
import time
import haystack.backends.whoosh_backend
from django.conf import settings
from django.db import connection
from tenant_schemas.postgresql_backend.base import FakeTenant
import haystack.backends.whoosh_backend
class WhooshSearchBackend(haystack.backends.whoosh_backend.WhooshSearchBackend):
@property
def use_file_storage(self):
tenant = connection.get_tenant()
return not(isinstance(connection.tenant, FakeTenant))
return not (isinstance(connection.tenant, FakeTenant))
@use_file_storage.setter
def use_file_storage(self, value):

View File

@ -14,13 +14,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.db import connection
import django.utils.log
from django.db import connection
class AdminEmailHandler(django.utils.log.AdminEmailHandler):
def format_subject(self, subject):
from .models import Tenant
subject = super(AdminEmailHandler, self).format_subject(subject)
try:
subject = '[%s] %s' % (connection.get_tenant().domain_url, subject)

View File

@ -4,16 +4,19 @@
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
from optparse import make_option
import django
from django.conf import settings
from django.core.management import call_command, get_commands, load_command_class
from django.core.management.base import BaseCommand, CommandError
from django.db import connection
try:
from django.utils.six.moves import input
except ImportError:
input = raw_input
from tenant_schemas.utils import get_public_schema_name
from hobo.multitenant.middleware import TenantMiddleware
@ -61,9 +64,11 @@ class BaseTenantCommand(BaseCommand):
if verbosity >= 1:
print()
print(self.style.NOTICE("=== Switching to schema '")
+ self.style.SQL_TABLE(tenant.schema_name)
+ self.style.NOTICE("' then calling %s:" % command_name))
print(
self.style.NOTICE("=== Switching to schema '")
+ self.style.SQL_TABLE(tenant.schema_name)
+ self.style.NOTICE("' then calling %s:" % command_name)
)
connection.set_tenant(tenant)
@ -77,7 +82,12 @@ class BaseTenantCommand(BaseCommand):
if options['domain']:
# only run on a particular schema
connection.set_schema_to_public()
self.execute_command(TenantMiddleware.get_tenant_by_hostname(options['domain']), self.COMMAND_NAME, *args, **options)
self.execute_command(
TenantMiddleware.get_tenant_by_hostname(options['domain']),
self.COMMAND_NAME,
*args,
**options,
)
else:
for tenant in TenantMiddleware.get_tenants():
self.execute_command(tenant, self.COMMAND_NAME, *args, **options)
@ -91,9 +101,11 @@ class InteractiveTenantOption(object):
all_tenants = list(TenantMiddleware.get_tenants())
if not all_tenants:
raise CommandError("""There are no tenants in the system.
raise CommandError(
"""There are no tenants in the system.
To learn how create a tenant, see:
https://django-tenant-schemas.readthedocs.org/en/latest/use.html#creating-a-tenant""")
https://django-tenant-schemas.readthedocs.org/en/latest/use.html#creating-a-tenant"""
)
if options.get('domain'):
domain = options['domain']
@ -106,7 +118,7 @@ https://django-tenant-schemas.readthedocs.org/en/latest/use.html#creating-a-tena
if domain in [t.domain_url for t in all_tenants]:
break
try:
domain = displayed_tenants[int(domain)-1].domain_url
domain = displayed_tenants[int(domain) - 1].domain_url
break
except (ValueError, IndexError):
pass
@ -115,8 +127,7 @@ https://django-tenant-schemas.readthedocs.org/en/latest/use.html#creating-a-tena
else:
displayed_tenants = [x for x in all_tenants if domain in x.domain_url]
for i, tenant in enumerate(displayed_tenants):
print("[%2d] %s (schema %s)" % (
i+1, tenant.domain_url, tenant.schema_name))
print("[%2d] %s (schema %s)" % (i + 1, tenant.domain_url, tenant.schema_name))
return TenantMiddleware.get_tenant_by_hostname(domain)
@ -145,13 +156,24 @@ class TenantWrappedCommand(InteractiveTenantOption, BaseCommand):
class SyncCommon(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--app_label', action='store', dest='app_label', nargs='?',
help='App label of an application to synchronize the state.')
parser.add_argument('--migration_name', action='store', dest='migration_name', nargs='?',
help=('Database state will be brought to the state after that '
'migration. Use the name "zero" to unapply all migrations.'))
parser.add_argument(
'--app_label',
action='store',
dest='app_label',
nargs='?',
help='App label of an application to synchronize the state.',
)
parser.add_argument(
'--migration_name',
action='store',
dest='migration_name',
nargs='?',
help=(
'Database state will be brought to the state after that '
'migration. Use the name "zero" to unapply all migrations.'
),
)
parser.add_argument("-d", "--domain", dest="domain")
parser.add_argument("-s", "--schema", dest="schema_name")
@ -177,8 +199,8 @@ class SyncCommon(BaseCommand):
def disable_global_logging():
import os
import logging
import os
import sys
if 'TERM' not in os.environ:
@ -187,6 +209,7 @@ def disable_global_logging():
# try to disable sentry
if 'sentry_sdk' in sys.modules:
import sentry_sdk
sentry_sdk.init()
# then try to disable journald, syslog logging and mail admins

View File

@ -1,13 +1,13 @@
import os
import sys
import subprocess
import sys
from django.core.management.base import CommandError
from hobo.agent.common.management.commands.hobo_deploy import Command as HoboDeployCommand
from hobo.multitenant.middleware import TenantMiddleware
from .create_tenant import Command as CreateTenantCommand
from hobo.agent.common.management.commands.hobo_deploy import Command as HoboDeployCommand
class Command(CreateTenantCommand):
@ -17,7 +17,7 @@ class Command(CreateTenantCommand):
if not hostnames:
raise CommandError("you must give at least one tenant hostname")
if '-' in hostnames: # get additional list of hostnames from stdin
if '-' in hostnames: # get additional list of hostnames from stdin
hostnames = list(hostnames)
hostnames.remove('-')
hostnames.extend([x.strip() for x in sys.stdin.readlines()])

View File

@ -1,9 +1,11 @@
from __future__ import print_function
from django.core.management.base import BaseCommand
from hobo.multitenant.middleware import TenantMiddleware
from django.db import connection
from hobo.multitenant.middleware import TenantMiddleware
class Command(BaseCommand):
help = "Create schemas for all declared tenants"
@ -15,7 +17,6 @@ class Command(BaseCommand):
for tenant in all_tenants:
if verbosity >= 1:
print()
print(self.style.NOTICE("=== Creating schema ")
+ self.style.SQL_TABLE(tenant.schema_name))
print(self.style.NOTICE("=== Creating schema ") + self.style.SQL_TABLE(tenant.schema_name))
tenant.create_schema(check_if_exists=True)

View File

@ -1,11 +1,12 @@
import os
import sys
from django.core.management.base import BaseCommand, CommandError
from django.db import connection, transaction
from django.core.management.base import CommandError, BaseCommand
from hobo.multitenant.middleware import TenantMiddleware, get_tenant_model
class Command(BaseCommand):
help = "Create tenant(s) by hostname(s)"
@ -17,7 +18,7 @@ class Command(BaseCommand):
if not hostnames:
raise CommandError("you must give at least one tenant hostname")
if '-' in hostnames: # get additional list of hostnames from stdin
if '-' in hostnames: # get additional list of hostnames from stdin
hostnames = list(hostnames)
hostnames.remove('-')
hostnames.extend([x.strip() for x in sys.stdin.readlines()])
@ -32,8 +33,7 @@ class Command(BaseCommand):
tenant_dir = os.path.join(tenant_base, hostname)
if os.path.exists(tenant_dir):
raise CommandError('tenant already exists')
tenant_dir_tmp = os.path.join(
tenant_base, hostname + '__CREATION_IN_PROGRESS.invalid')
tenant_dir_tmp = os.path.join(tenant_base, hostname + '__CREATION_IN_PROGRESS.invalid')
try:
os.mkdir(tenant_dir_tmp, 0o755)
except OSError as e:
@ -45,8 +45,9 @@ class Command(BaseCommand):
tenant = get_tenant_model()(schema_name=schema, domain_url=hostname)
if verbosity >= 1:
print()
print(self.style.NOTICE("=== Creating schema ")
+ self.style.SQL_TABLE(tenant.schema_name))
print(
self.style.NOTICE("=== Creating schema ") + self.style.SQL_TABLE(tenant.schema_name)
)
with transaction.atomic():
tenant.create_schema(check_if_exists=True)
except Exception as e:

View File

@ -3,9 +3,10 @@
# Email: carneiro.be@gmail.com
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
from hobo.multitenant.management.commands import TenantWrappedCommand
from django.contrib.auth.management.commands import createsuperuser
from hobo.multitenant.management.commands import TenantWrappedCommand
class Command(TenantWrappedCommand):
COMMAND = createsuperuser.Command

View File

@ -1,7 +1,8 @@
import sys
from optparse import make_option
from django.core.management.base import CommandError, BaseCommand
from django.core.management.base import BaseCommand, CommandError
from hobo.multitenant.middleware import TenantMiddleware
@ -9,15 +10,19 @@ class Command(BaseCommand):
help = "Delete tenant(s) by hostname(s)"
def add_arguments(self, parser):
parser.add_argument('--force-drop', action='store_true', default=False,
help='If you want the schema to be deleted from database')
parser.add_argument(
'--force-drop',
action='store_true',
default=False,
help='If you want the schema to be deleted from database',
)
parser.add_argument('hostnames', metavar='HOSTNAME', nargs='+')
def handle(self, hostnames, **options):
if not hostnames:
raise CommandError("you must give at least one tenant hostname")
if '-' in hostnames: # get additional list of hostnames from stdin
if '-' in hostnames: # get additional list of hostnames from stdin
hostnames = list(hostnames)
hostnames.remove('-')
hostnames.extend([x.strip() for x in sys.stdin.readlines()])

View File

@ -1,6 +1,8 @@
from django.core.management.base import BaseCommand
from hobo.multitenant.middleware import TenantMiddleware
class Command(BaseCommand):
requires_model_validation = True
can_import_settings = True
@ -10,4 +12,3 @@ class Command(BaseCommand):
for tenant in all_tenants:
print("{0} {1}".format(tenant.schema_name, tenant.domain_url))

View File

@ -5,20 +5,26 @@
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
import django
from django.conf import settings
from django.core.management.base import CommandError, BaseCommand
from django.core.management.base import BaseCommand, CommandError
from tenant_schemas.utils import django_is_in_test_mode
class Command(BaseCommand):
class Command(BaseCommand):
def handle(self, *args, **options):
database = options.get('database', 'default')
if (settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend' or
MigrateCommand is BaseCommand):
raise CommandError("migrate has been disabled, for database '{}'. Use migrate_schemas "
"instead. Please read the documentation if you don't know why you "
"shouldn't call migrate directly!".format(database))
if (
settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend'
or MigrateCommand is BaseCommand
):
raise CommandError(
"migrate has been disabled, for database '{}'. Use migrate_schemas "
"instead. Please read the documentation if you don't know why you "
"shouldn't call migrate directly!".format(database)
)
super(Command, self).handle(*args, **options)
if django_is_in_test_mode():
from .migrate_schemas import MigrateSchemasCommand
Command = MigrateSchemasCommand

View File

@ -15,18 +15,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import django
from django.apps import apps
from django.conf import settings
from django.core.management.commands.migrate import Command as MigrateCommand
from django.db import connection
from django.db.migrations.loader import MigrationLoader
from django.db.migrations.recorder import MigrationRecorder
from django.db import connection
from django.conf import settings
from tenant_schemas.utils import get_public_schema_name, schema_exists
from tenant_schemas.postgresql_backend.base import FakeTenant
from hobo.multitenant.middleware import TenantMiddleware, TenantNotFound
from tenant_schemas.utils import get_public_schema_name, schema_exists
from hobo.multitenant.management.commands import SyncCommon
from hobo.multitenant.middleware import TenantMiddleware, TenantNotFound
class MigrateSchemasCommand(SyncCommon):
@ -56,7 +55,9 @@ class MigrateSchemasCommand(SyncCommon):
app_labels.append(app.label)
loader = MigrationLoader(None)
loader.load_disk()
all_migrations = set([(app, migration) for app, migration in loader.disk_migrations if app in app_labels])
all_migrations = set(
[(app, migration) for app, migration in loader.disk_migrations if app in app_labels]
)
for tenant in TenantMiddleware.get_tenants():
connection.set_tenant(tenant, include_public=False)
applied_migrations = self.get_applied_migrations(app_labels)
@ -102,4 +103,5 @@ class MigrateSchemasCommand(SyncCommon):
def _notice(self, output):
self.stdout.write(self.style.NOTICE(output))
Command = MigrateSchemasCommand

View File

@ -2,13 +2,12 @@ import os
from optparse import make_option
from django.conf import settings
from django.core.management.commands.runserver import Command as RunserverCommand
from django.utils.six.moves.urllib.request import url2pathname
from django.views import static
from django.contrib.staticfiles.handlers import StaticFilesHandler
from django.contrib.staticfiles.management.commands.runserver import Command as StaticRunserverCommand
from django.contrib.staticfiles.views import serve
from django.core.management.commands.runserver import Command as RunserverCommand
from django.utils.six.moves.urllib.request import url2pathname
from django.views import static
from hobo.multitenant.middleware import TenantMiddleware, TenantNotFound

View File

@ -1,6 +1,7 @@
from hobo.multitenant.management.commands import TenantWrappedCommand, disable_global_logging
from django.core.management.commands import shell
from hobo.multitenant.management.commands import TenantWrappedCommand, disable_global_logging
class Command(TenantWrappedCommand):
COMMAND = shell.Command

View File

@ -14,12 +14,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.core.management.commands.showmigrations import Command as ShowMigrationsCommand
from django.db import connection
from django.conf import settings
from hobo.multitenant.middleware import TenantMiddleware, TenantNotFound
from hobo.multitenant.management.commands import SyncCommon
from hobo.multitenant.middleware import TenantMiddleware, TenantNotFound
class ShowMigrationsSchemasCommand(SyncCommon):
@ -36,8 +36,7 @@ class ShowMigrationsSchemasCommand(SyncCommon):
try:
tenant = TenantMiddleware.get_tenant_by_hostname(self.domain)
except TenantNotFound:
raise RuntimeError('Schema "{}" does not exist'.format(
self.schema_name))
raise RuntimeError('Schema "{}" does not exist'.format(self.schema_name))
self.run_showmigrations(tenant, settings.TENANT_APPS)
else:
for tenant in TenantMiddleware.get_tenants():
@ -50,4 +49,5 @@ class ShowMigrationsSchemasCommand(SyncCommon):
command.execute(*self.args, **self.options)
connection.set_schema_to_public()
Command = ShowMigrationsSchemasCommand

View File

@ -3,8 +3,8 @@
# Email: carneiro.be@gmail.com
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
from django.core.management.base import CommandError
from django.conf import settings
from django.core.management.base import CommandError
from tenant_schemas.utils import django_is_in_test_mode
if 'south' in settings.INSTALLED_APPS:
@ -14,13 +14,16 @@ else:
class Command(syncdb.Command):
def handle(self, *args, **options):
database = options.get('database', 'default')
if (settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend' and not
django_is_in_test_mode()):
raise CommandError("syncdb has been disabled, for database '{0}'. "
"Use sync_schemas instead. Please read the "
"documentation if you don't know why "
"you shouldn't call syncdb directly!".format(database))
if (
settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend'
and not django_is_in_test_mode()
):
raise CommandError(
"syncdb has been disabled, for database '{0}'. "
"Use sync_schemas instead. Please read the "
"documentation if you don't know why "
"you shouldn't call syncdb directly!".format(database)
)
super(Command, self).handle(*args, **options)

View File

@ -9,15 +9,13 @@ import argparse
import sys
import django
from django.utils import six
from django.utils.encoding import force_text
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import (BaseCommand, CommandError,
SystemCheckError,
handle_default_options)
from django.core.management import call_command, get_commands, load_command_class
from django.core.management.base import BaseCommand, CommandError, SystemCheckError, handle_default_options
from django.db import connection, connections
from django.utils import six
from django.utils.encoding import force_text
from hobo.multitenant.management.commands import InteractiveTenantOption
from hobo.multitenant.middleware import TenantMiddleware
@ -62,8 +60,9 @@ def run_command_from_argv(command, argv):
if isinstance(e, SystemCheckError):
command.stderr.write(str(e), lambda x: x)
else:
command.stderr.write('%s: %s: %s' % (
connection.get_tenant(), e.__class__.__name__, exception_to_text(e)))
command.stderr.write(
'%s: %s: %s' % (connection.get_tenant(), e.__class__.__name__, exception_to_text(e))
)
return e
finally:
try:
@ -103,26 +102,31 @@ class Command(InteractiveTenantOption, BaseCommand):
# and forward the rest of the arguments to the actual command being wrapped.
del argv[1]
args_parser = argparse.ArgumentParser()
args_parser.add_argument("--all-tenants", help="apply command to all tenants",
action='store_true')
args_parser.add_argument("--all-tenants", help="apply command to all tenants", action='store_true')
args_parser.add_argument("-d", "--domain", dest="domain_name", help="specify tenant domain name")
args_parser.add_argument(
'--force-job', dest='force_job', action='store_true',
help='Run command even if DISABLE_CRON_JOBS is set')
'--force-job',
dest='force_job',
action='store_true',
help='Run command even if DISABLE_CRON_JOBS is set',
)
args_namespace, args = args_parser.parse_known_args(argv)
# Continue weirdness: parse verbosity option and also leave it in args
# for subcommand consumption
verbosity_parser = argparse.ArgumentParser()
verbosity_parser.add_argument('-v', '--verbosity', action='store',
dest='verbosity', default=1, type=int)
verbosity_parser.add_argument(
'-v', '--verbosity', action='store', dest='verbosity', default=1, type=int
)
args_verbosity, _ = verbosity_parser.parse_known_args(args)
if (args_namespace.all_tenants and not args_namespace.force_job and
getattr(settings, 'DISABLE_CRON_JOBS', False)):
if (
args_namespace.all_tenants
and not args_namespace.force_job
and getattr(settings, 'DISABLE_CRON_JOBS', False)
):
if args_verbosity.verbosity > 1:
print('Command %s is ignored because DISABLE_CRON_JOBS is set'
% app_name)
print('Command %s is ignored because DISABLE_CRON_JOBS is set' % app_name)
return
if args_namespace.all_tenants:

View File

@ -1,11 +1,10 @@
from __future__ import absolute_import
import logging
import json
import logging
import os
from django.db import connection
from mellon.adapters import DefaultAdapter
from . import utils

View File

@ -1,14 +1,14 @@
import os
import glob
import hashlib
import os
from django.utils.deprecation import MiddlewareMixin
from django.utils.encoding import smart_bytes
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import connection
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.contrib.contenttypes.models import ContentType
from tenant_schemas.utils import get_tenant_model, get_public_schema_name
from django.utils.deprecation import MiddlewareMixin
from django.utils.encoding import smart_bytes
from tenant_schemas.utils import get_public_schema_name, get_tenant_model
SENTINEL = object()
@ -23,6 +23,7 @@ class TenantMiddleware(MiddlewareMixin):
Selects the proper database schema using the request host. Can fail in
various ways which is better than corrupting or revealing data...
"""
@classmethod
def base(cls):
return settings.TENANT_BASE
@ -55,9 +56,7 @@ class TenantMiddleware(MiddlewareMixin):
continue
if not os.path.isdir(path):
continue
yield get_tenant_model()(
schema_name=self.hostname2schema(hostname),
domain_url=hostname)
yield get_tenant_model()(schema_name=self.hostname2schema(hostname), domain_url=hostname)
def process_request(self, request):
# connection needs first to be at the public schema, as this is where the
@ -88,5 +87,8 @@ class TenantMiddleware(MiddlewareMixin):
ContentType.objects.clear_cache()
# do we have a public-specific token?
if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
if (
hasattr(settings, 'PUBLIC_SCHEMA_URLCONF')
and request.tenant.schema_name == get_public_schema_name()
):
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF

View File

@ -1,15 +1,14 @@
import os
import json
import os
from shutil import rmtree
from django.conf import settings
from django.db import connection
from django.utils import timezone
from django.utils.six.moves.urllib.parse import urljoin
from tenant_schemas.utils import get_public_schema_name
from tenant_schemas.models import TenantMixin
from tenant_schemas.utils import django_is_in_test_mode, schema_exists
from tenant_schemas.utils import django_is_in_test_mode, get_public_schema_name, schema_exists
class Tenant(TenantMixin):
# default true, schema will be automatically created and synced when it is saved
@ -34,9 +33,7 @@ class Tenant(TenantMixin):
def get_hobo_json(self):
if not self.__hobo_json:
self.__hobo_json = json.load(open(
os.path.join(self.get_directory(),
'hobo.json')))
self.__hobo_json = json.load(open(os.path.join(self.get_directory(), 'hobo.json')))
return self.__hobo_json
def get_service(self):
@ -66,9 +63,10 @@ class Tenant(TenantMixin):
auto_drop_schema set to True.
"""
if connection.schema_name not in (self.schema_name, get_public_schema_name()):
raise Exception("Can't delete tenant outside it's own schema or "
"the public schema. Current schema is %s."
% connection.schema_name)
raise Exception(
"Can't delete tenant outside it's own schema or "
"the public schema. Current schema is %s." % connection.schema_name
)
if force_drop:
rmtree(self.get_directory())
else:

Some files were not shown because too many files have changed in this diff Show More