Skip to content

Commit b22b029

Browse files
committed
Add validation for a couple of GMN settings
1 parent d9cabd0 commit b22b029

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed

gmn/src/d1_gmn/app/gmn.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# This work was created by participants in the DataONE project, and is
4+
# jointly copyrighted by participating institutions in DataONE. For
5+
# more information on DataONE, see our web site at http://dataone.org.
6+
#
7+
# Copyright 2009-2016 DataONE
8+
#
9+
# Licensed under the Apache License, Version 2.0 (the "License");
10+
# you may not use this file except in compliance with the License.
11+
# You may obtain a copy of the License at
12+
#
13+
# http://www.apache.org/licenses/LICENSE-2.0
14+
#
15+
# Unless required by applicable law or agreed to in writing, software
16+
# distributed under the License is distributed on an "AS IS" BASIS,
17+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
# See the License for the specific language governing permissions and
19+
# limitations under the License.
20+
"""App performing filesystem setup and basic sanity checks on configuration
21+
values in settings.py before GMN starts servicing requests.
22+
23+
Django loads apps into the Application Registry in the order specified in
24+
settings.INSTALLED_APPS. This app must be set to load before the main GMN app
25+
by listing it above the main app in settings.INSTALLED_APPS.
26+
"""
27+
28+
import functools
29+
import collections
30+
import logging
31+
import mimetypes
32+
import os
33+
import random
34+
import string
35+
36+
import d1_gmn.app.sciobj_store
37+
import d1_gmn.app.util
38+
39+
import django.apps
40+
import django.conf
41+
import django.core.exceptions
42+
43+
RESOURCE_MAP_CREATE_MODE_LIST = ['block', 'open']
44+
45+
46+
class Startup(django.apps.AppConfig):
47+
name = 'd1_gmn.app'
48+
49+
def ready(self):
50+
"""Called once per Django process instance
51+
52+
If the filesystem setup fails or if an error is found in settings.py,
53+
django.core.exceptions.ImproperlyConfigured is raised, causing Django not to
54+
launch the main GMN app.
55+
"""
56+
# Stop the startup code from running automatically from pytest unit tests.
57+
# When running tests in parallel with xdist, an instance of GMN is launched
58+
# before thread specific settings have been applied.
59+
# if hasattr(sys, '_launched_by_pytest'):
60+
# return
61+
self._assert_readable_file_if_set('CLIENT_CERT_PATH')
62+
self._assert_readable_file_if_set('CLIENT_CERT_PRIVATE_KEY_PATH')
63+
64+
self._assert_is_type('SCIMETA_VALIDATION_ENABLED', bool)
65+
self._assert_is_type('SCIMETA_VALIDATION_MAX_SIZE', int)
66+
self._assert_is_in(
67+
'SCIMETA_VALIDATION_OVER_SIZE_ACTION', ('reject', 'accept')
68+
)
69+
70+
self._warn_unsafe_for_prod()
71+
self._check_resource_map_create()
72+
if not d1_gmn.app.sciobj_store.is_existing_store():
73+
self._create_sciobj_store_root()
74+
75+
self._add_xslt_mimetype()
76+
77+
78+
def _assert_is_type(self, setting_name, valid_type):
79+
v = self._get_setting(setting_name)
80+
if not isinstance(v, valid_type):
81+
self.raise_config_error(setting_name, v, valid_type)
82+
83+
def _assert_is_in(self, setting_name, valid_list):
84+
v = self._get_setting(setting_name)
85+
if v not in valid_list:
86+
self.raise_config_error(setting_name, v, valid_list)
87+
88+
def _assert_readable_file_if_set(self, setting_name):
89+
v = self._get_setting(setting_name)
90+
if v is None:
91+
return
92+
self._assert_is_type(setting_name, str)
93+
if not os.path.isfile(v):
94+
self.raise_config_error(
95+
setting_name, v, str, 'a path to a readable file', is_none_allowed=True
96+
)
97+
try:
98+
with open(v, 'r') as f:
99+
f.read(1)
100+
except EnvironmentError as e:
101+
self.raise_config_error(
102+
setting_name, v, str, 'a path to a readable file. error="{}"'
103+
.format(str(e), is_none_allowed=True)
104+
)
105+
106+
def raise_config_error(
107+
self, setting_name, cur_val, exp_type, valid_str=None,
108+
is_none_allowed=False
109+
):
110+
valid_str = valid_str if valid_str is not None else \
111+
'a whole number' if exp_type is int else \
112+
'a number' if exp_type is float else \
113+
'a string' if (exp_type is str or exp_type is str) else \
114+
'True or False' if exp_type is bool else \
115+
' or '.join(['"{}"'.format(s) for s in exp_type]) \
116+
if isinstance(exp_type, collections.Iterable) else \
117+
'of type {}'.format(exp_type.__name__)
118+
119+
msg_str = 'Configuration error: {} {} must be {}. current="{}"'.format(
120+
'If set, setting'
121+
if is_none_allowed else 'Setting', setting_name, valid_str, str(cur_val)
122+
)
123+
logging.error(msg_str)
124+
raise django.core.exceptions.ImproperlyConfigured(msg_str)
125+
126+
def _warn_unsafe_for_prod(self):
127+
"""Warn on settings that are not safe for production"""
128+
safe_settings_list = [
129+
('DEBUG', False),
130+
('DEBUG_GMN', False),
131+
('DEBUG_PYCHARM', False),
132+
('STAND_ALONE', False),
133+
('DATABASES.default.ATOMIC_REQUESTS', True),
134+
('SECRET_KEY', '<Do not modify this placeholder value>'),
135+
('STATIC_SERVER', False),
136+
]
137+
for setting_str, setting_safe in safe_settings_list:
138+
setting_current = self._get_setting(setting_str)
139+
if setting_current != setting_safe:
140+
logging.warning(
141+
'Setting is unsafe for use in production. setting="{}" current="{}" '
142+
'safe="{}"'.format(setting_str, setting_current, setting_safe)
143+
)
144+
145+
def _check_resource_map_create(self):
146+
if (
147+
django.conf.settings.RESOURCE_MAP_CREATE not in RESOURCE_MAP_CREATE_MODE_LIST
148+
):
149+
raise django.core.exceptions.ImproperlyConfigured(
150+
'Configuration error: Invalid RESOURCE_MAP_CREATE setting. '
151+
'valid="{}" current="{}"'.format(
152+
', '.join(RESOURCE_MAP_CREATE_MODE_LIST),
153+
django.conf.settings.RESOURCE_MAP_CREATE
154+
)
155+
)
156+
157+
def _set_secret_key(self):
158+
try:
159+
with open(django.conf.settings.SECRET_KEY_PATH, 'rb') as f:
160+
django.conf.settings.SECRET_KEY = f.read().strip()
161+
except EnvironmentError:
162+
django.conf.settings.SECRET_KEY = self._create_secret_key_file()
163+
164+
def _create_secret_key_file(self):
165+
secret_key_str = ''.join([
166+
random.SystemRandom()
167+
.choice('{}{}'.format(string.ascii_letters, string.digits))
168+
for _ in range(64)
169+
])
170+
try:
171+
with open(django.conf.settings.SECRET_KEY_PATH, 'wb') as f:
172+
f.write(secret_key_str.encode('utf-8'))
173+
except EnvironmentError:
174+
raise django.core.exceptions.ImproperlyConfigured(
175+
'Configuration error: Secret key file not found and unable to write '
176+
'new. path="{}"'.format(django.conf.settings.SECRET_KEY_PATH)
177+
)
178+
else:
179+
logging.info(
180+
'Generated new secret key file. path="{}"'.
181+
format(django.conf.settings.SECRET_KEY_PATH)
182+
)
183+
return secret_key_str
184+
185+
def _create_sciobj_store_root(self):
186+
try:
187+
d1_gmn.app.sciobj_store.create_store()
188+
except EnvironmentError as e:
189+
raise django.core.exceptions.ImproperlyConfigured(
190+
'Configuration error: Invalid object store root path. '
191+
'path="{}". msg="{}"'.format(
192+
django.conf.settings.OBJECT_STORE_PATH, str(e)
193+
)
194+
)
195+
if not d1_gmn.app.sciobj_store.is_matching_version():
196+
logging.warning(
197+
'Configuration error: Incorrect object store version. '
198+
'store="{}" gmn="{}"'.format(
199+
d1_gmn.app.sciobj_store.get_store_version(),
200+
d1_gmn.app.sciobj_store.get_gmn_version(),
201+
)
202+
)
203+
204+
def _add_xslt_mimetype(self):
205+
"""Register the mimetype for .xsl files in order for Django to serve static
206+
.xsl files with the correct mimetype
207+
"""
208+
# 'application/xslt+xml'
209+
mimetypes.add_type('text/xsl', '.xsl')
210+
211+
def _get_setting(self, setting_dotted_name, default=None):
212+
"""Return the value of a potentially nested dict setting. E.g.,
213+
'DATABASES.default.NAME
214+
"""
215+
name_list = setting_dotted_name.split('.')
216+
setting_obj = getattr(django.conf.settings, name_list[0], default)
217+
# if len(name_list) == 1:
218+
# return setting_obj
219+
220+
return functools.reduce(
221+
lambda o, a: o.get(a, default), [setting_obj] + name_list[1:]
222+
)

0 commit comments

Comments
 (0)