check_web_config_consistency()   D
last analyzed

Complexity

Conditions 8

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 45
rs 4
cc 8
1
'''
2
    This module contains administrative functionality
3
    that is available as command-line tool "opensubmit-web".
4
5
    All functions that demand a working Django ORM are implemented
6
    as Django management command and just called from here.
7
8
    Everything else is implemented here, so this file works without
9
    any of the install dependencies.
10
'''
11
12
import os
13
import pwd
14
import grp
15
import urllib.request
16
import urllib.parse
17
import urllib.error
18
import sys
19
import argparse
20
from base64 import b64encode
21
from configparser import RawConfigParser
22
23
DEFAULT_CONFIG = '''
24
# This is the configuration file for the OpenSubmit tool.
25
# https://github.com/troeger/opensubmit
26
#
27
# It is expected to be located at:
28
# /etc/opensubmit/settings.ini (on production system), or
29
# <project_root>/web/opensubmit/settings_dev.ini (on developer systems)
30
#
31
# For further information, check the output of 'opensubmit-web configcreate -h'.
32
#
33
34
[general]
35
DEBUG: {debug}
36
DEMO: {login_demo}
37
38
[server]
39
HOST: {server_host}
40
HOST_DIR: {server_hostdir}
41
HOST_ALIASES: {server_hostaliases}
42
MEDIA_ROOT: {server_mediaroot}
43
LOG_FILE: {server_logfile}
44
TIME_ZONE: {server_timezone}
45
SECRET_KEY: {server_secretkey}
46
47
[database]
48
DATABASE_ENGINE: {database_engine}
49
DATABASE_NAME: {database_name}
50
DATABASE_USER: {database_user}
51
DATABASE_PASSWORD: {database_password}
52
DATABASE_HOST: {database_host}
53
DATABASE_PORT: {database_port}
54
55
[executor]
56
# The shared secret with the job executor. This ensures that only authorized
57
# machines can fetch submitted solution attachments for validation, and not
58
# every student ...
59
# Change it, the value does not matter.
60
SHARED_SECRET: 49846zut93purfh977TTTiuhgalkjfnk89
61
62
[admin]
63
ADMIN_NAME: {admin_name}
64
ADMIN_EMAIL: {admin_email}
65
ADMIN_ADDRESS: {admin_address}
66
67
[login]
68
LOGIN_DESCRIPTION: {login_openid_title}
69
OPENID_PROVIDER: {login_openid_provider}
70
LOGIN_TWITTER_OAUTH_KEY: {login_twitter_oauth_key}
71
LOGIN_TWITTER_OAUTH_SECRET: {login_twitter_oauth_secret}
72
LOGIN_GOOGLE_OAUTH_KEY: {login_google_oauth_key}
73
LOGIN_GOOGLE_OAUTH_SECRET: {login_google_oauth_secret}
74
LOGIN_GITHUB_OAUTH_KEY: {login_github_oauth_key}
75
LOGIN_GITHUB_OAUTH_SECRET: {login_github_oauth_secret}
76
LOGIN_SHIB_DESCRIPTION: {login_shib_title}
77
LOGIN_DEMO: {login_demo}
78
'''
79
80
81
def django_admin(args):
82
    '''
83
        Run something like it would be done through Django's manage.py.
84
    '''
85
    from django.core.management import execute_from_command_line
86
    from django.core.exceptions import ImproperlyConfigured
87
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "opensubmit.settings")
88
    try:
89
        execute_from_command_line([sys.argv[0]] + args)
90
    except ImproperlyConfigured as e:
91
        print(str(e))
92
        exit(-1)
93
94
95
def apache_config(config, outputfile):
96
    '''
97
        Generate a valid Apache configuration file, based on the given settings.
98
    '''
99
    if os.path.exists(outputfile):
100
        os.rename(outputfile, outputfile + ".old")
101
        print("Renamed existing Apache config file to " + outputfile + ".old")
102
103
    from opensubmit import settings
104
    f = open(outputfile, 'w')
105
    print("Generating Apache configuration in " + outputfile)
106
    subdir = (len(settings.HOST_DIR) > 0)
107
    text = """
108
    # OpenSubmit Configuration for Apache 2.4
109
    # These directives are expected to live in some <VirtualHost> block
110
    """
111
    if subdir:
112
        text += "Alias /%s/static/ %s\n" % (settings.HOST_DIR,
113
                                            settings.STATIC_ROOT)
114
        text += "    WSGIScriptAlias /%s %s/wsgi.py\n" % (
115
            settings.HOST_DIR, settings.SCRIPT_ROOT)
116
    else:
117
        text += "Alias /static/ %s\n" % (settings.STATIC_ROOT)
118
        text += "    WSGIScriptAlias / %s/wsgi.py" % (settings.SCRIPT_ROOT)
119
    text += """
120
    WSGIPassAuthorization On
121
    <Directory {static_path}>
122
         Require all granted
123
    </Directory>
124
    <Directory {install_path}>
125
         <Files wsgi.py>
126
              Require all granted
127
         </Files>
128
    </Directory>
129
    """.format(static_path=settings.STATIC_ROOT, install_path=settings.SCRIPT_ROOT)
130
131
    f.write(text)
132
    f.close()
133
134
135
def check_path(file_path):
136
    '''
137
        Checks if the directories for this path exist, and creates them in case.
138
    '''
139
    directory = os.path.dirname(file_path)
140
    if directory != '':
141
        if not os.path.exists(directory):
142
            os.makedirs(directory, 0o775)   # rwxrwxr-x
143
144
145
def check_file(filepath):
146
    '''
147
        - Checks if the parent directories for this path exist.
148
        - Checks that the file exists.
149
        - Donates the file to the web server user.
150
151
        TODO: This is Debian / Ubuntu specific.
152
    '''
153
    check_path(filepath)
154
    if not os.path.exists(filepath):
155
        print("WARNING: File does not exist. Creating it: %s" % filepath)
156
        open(filepath, 'a').close()
157
    try:
158
        print("Setting access rights for %s for www-data user" % (filepath))
159
        uid = pwd.getpwnam("www-data").pw_uid
160
        gid = grp.getgrnam("www-data").gr_gid
161
        os.chown(filepath, uid, gid)
162
        os.chmod(filepath, 0o660)  # rw-rw---
163
    except:
164
        print("WARNING: Could not adjust file system permissions for %s. Make sure your web server can write into it." % filepath)
165
166
167
def check_web_config_consistency(config):
168
    '''
169
        Check the web application config file for consistency.
170
    '''
171
    login_conf_deps = {
172
        'LOGIN_TWITTER_OAUTH_KEY': ['LOGIN_TWITTER_OAUTH_SECRET'],
173
        'LOGIN_GOOGLE_OAUTH_KEY': ['LOGIN_GOOGLE_OAUTH_SECRET'],
174
        'LOGIN_GITHUB_OAUTH_KEY': ['LOGIN_GITHUB_OAUTH_SECRET'],
175
        'LOGIN_TWITTER_OAUTH_SECRET': ['LOGIN_TWITTER_OAUTH_KEY'],
176
        'LOGIN_GOOGLE_OAUTH_SECRET': ['LOGIN_GOOGLE_OAUTH_KEY'],
177
        'LOGIN_GITHUB_OAUTH_SECRET': ['LOGIN_GITHUB_OAUTH_KEY'],
178
    }
179
180
    print("Checking configuration of the OpenSubmit web application...")
181
    # Let Django's manage.py load the settings file, to see if this works in general
182
    django_admin(["check"])
183
    # Check configured host
184
    try:
185
        urllib.request.urlopen(config.get("server", "HOST"))
186
    except Exception as e:
187
        # This may be ok, when the admin is still setting up to server
188
        print("The configured HOST seems to be invalid at the moment: " + str(e))
189
    # Check configuration dependencies
190
    for k, v in list(login_conf_deps.items()):
191
        if config.get('login', k):
192
            for needed in v:
193
                if len(config.get('login', needed)) < 1:
194
                    print(
195
                        "ERROR: You have enabled %s in settings.ini, but %s is not set." % (k, needed))
196
                    return False
197
    # Check media path
198
    check_path(config.get('server', 'MEDIA_ROOT'))
199
    # Prepare empty log file, in case the web server has no creation rights
200
    log_file = config.get('server', 'LOG_FILE')
201
    print("Preparing log file at " + log_file)
202
    check_file(log_file)
203
    # If SQLite database, adjust file system permissions for the web server
204
    if config.get('database', 'DATABASE_ENGINE') == 'sqlite3':
205
        name = config.get('database', 'DATABASE_NAME')
206
        if not os.path.isabs(name):
207
            print("ERROR: Your SQLite database name must be an absolute path. The web server must have directory access permissions for this path.")
208
            return False
209
        check_file(config.get('database', 'DATABASE_NAME'))
210
    # everything ok
211
    return True
212
213
214
def check_web_config(config_fname):
215
    '''
216
        Try to load the Django settings.
217
        If this does not work, than settings file does not exist.
218
219
        Returns:
220
            Loaded configuration, or None.
221
    '''
222
    print("Looking for config file at {0} ...".format(config_fname))
223
    config = RawConfigParser()
224
    try:
225
        config.readfp(open(config_fname))
226
        return config
227
    except IOError:
228
        print("ERROR: Seems like the config file does not exist. Please call 'opensubmit-web configcreate' first, or specify a location with the '-c' option.")
229
        return None
230
231
232
def check_web_db():
233
    '''
234
        Everything related to database checks and updates.
235
    '''
236
    print("Testing for neccessary database migrations...")
237
    django_admin(["migrate"])             # apply schema migrations
238
    print("Checking the OpenSubmit permission system...")
239
    # configure permission system, of needed
240
    django_admin(["fixperms"])
241
    return True
242
243
244
def configcreate(config_fname, settings):
245
    settings['server_secretkey'] = b64encode(os.urandom(64)).decode('utf-8')
246
    url_parts = settings['server_url'].split('/', 3)
247
    settings['server_host'] = url_parts[0] + '//' + url_parts[2]
248
    if len(url_parts) > 3:
249
        settings['server_hostdir'] = url_parts[3]
250
    else:
251
        settings['server_hostdir'] = ''
252
    content = DEFAULT_CONFIG.format(**settings)
253
254
    try:
255
        check_path(config_fname)
256
        f = open(config_fname, 'wt')
257
        f.write(content)
258
        f.close()
259
        print("Config file %s generated" % (config_fname))
260
    except Exception as e:
261
        print("ERROR: Could not create config file at {0}: {1}".format(config_fname, str(e)))
262
263
264
def configtest(config_fname):
265
    print("Inspecting OpenSubmit configuration ...")
266
    config = check_web_config(config_fname)
267
    if not config:
268
        return          # Let them first fix the config file before trying a DB access
269
    if not check_web_config_consistency(config):
270
        return
271
    if not check_web_db():
272
        return
273
    print("Preparing static files for web server...")
274
    django_admin(["collectstatic", "--noinput", "--clear", "-v 0"])
275
276
277
def console_script(fsroot=''):
278
    '''
279
        The main entry point for the production administration script 'opensubmit-web'.
280
        The argument allows the test suite to override the root of all paths used in here.
281
    '''
282
283
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Administration for the OpenSubmit web application.')
284
    parser.add_argument('-c', '--config', default='/etc/opensubmit/settings.ini', help='OpenSubmit configuration file.')
285
    subparsers = parser.add_subparsers(dest='command', help='Supported administrative actions.')
286
    parser_configcreate = subparsers.add_parser('configcreate', help='Create initial config files for the OpenSubmit web server.')
287
    parser_configcreate.add_argument('--debug', default=bool(os.environ.get('OPENSUBMIT_DEBUG', 'False')), action='store_true', help='Enable debug mode, not for production systems.')
288
    parser_configcreate.add_argument('--server_url', default=os.environ.get('OPENSUBMIT_SERVER_URL', 'http://localhost:8000'), help='The main URL of the OpenSubmit installation, including sub-directories.')
289
    parser_configcreate.add_argument('--server_mediaroot', default=os.environ.get('OPENSUBMIT_SERVER_MEDIAROOT', '/tmp/'), help='Storage path for uploadeded files.')
290
    parser_configcreate.add_argument('--server_hostaliases', default=os.environ.get('OPENSUBMIT_SERVER_HOSTALIASES', '127.0.0.1'), help='Comma-separated list of alternative host names for the web server.')
291
    parser_configcreate.add_argument('--server_logfile', default=os.environ.get('OPENSUBMIT_SERVER_LOGFILE', '/tmp/opensubmit.log'), help='Log file for the OpenSubmit application.')
292
    parser_configcreate.add_argument('--server_timezone', default=os.environ.get('OPENSUBMIT_SERVER_TIMEZONE', 'Europe/Berlin'), help='Time zone for all dates and deadlines.')
293
    parser_configcreate.add_argument('--database_name', default=os.environ.get('OPENSUBMIT_DATABASE_NAME', '/tmp/database.sqlite'), help='Name of the database (file).'),
294
    parser_configcreate.add_argument('--database_engine', default=os.environ.get('OPENSUBMIT_DATABASE_ENGINE', 'sqlite3'), choices=['postgresql', 'mysql', 'sqlite3', 'oracle'])
295
    parser_configcreate.add_argument('--database_user', default=os.environ.get('OPENSUBMIT_DATABASE_USER', ''), help='The user name for accessing the database. Not needed for SQLite.')
296
    parser_configcreate.add_argument('--database_password', default=os.environ.get('OPENSUBMIT_DATABASE_PASSWORD', ''), help='The user password for accessing the database. Not needed for SQLite.')
297
    parser_configcreate.add_argument('--database_host', default=os.environ.get('OPENSUBMIT_DATABASE_HOST', ''), help='The host name for accessing the database. Not needed for SQLite. Default is localhost.')
298
    parser_configcreate.add_argument('--database_port', default=os.environ.get('OPENSUBMIT_DATABASE_PORT', ''), help='The port number for accessing the database. Not needed for SQLite.')
299
    parser_configcreate.add_argument('--login_google_oauth_key', default=os.environ.get('OPENSUBMIT_LOGIN_GOOGLE_OAUTH_KEY', ''), help='Google OAuth client key.')
300
    parser_configcreate.add_argument('--login_google_oauth_secret', default=os.environ.get('OPENSUBMIT_LOGIN_GOOGLE_OAUTH_SECRET', ''), help='Google OAuth client secret.')
301
    parser_configcreate.add_argument('--login_twitter_oauth_key', default=os.environ.get('OPENSUBMIT_LOGIN_TWITTER_OAUTH_KEY', ''), help='Twitter OAuth client key.')
302
    parser_configcreate.add_argument('--login_twitter_oauth_secret', default=os.environ.get('OPENSUBMIT_LOGIN_TWITTER_OAUTH_SECRET', ''), help='Twitter OAuth client secret.')
303
    parser_configcreate.add_argument('--login_github_oauth_key', default=os.environ.get('OPENSUBMIT_LOGIN_GITHUB_OAUTH_KEY', ''), help='GitHub OAuth client key.')
304
    parser_configcreate.add_argument('--login_github_oauth_secret', default=os.environ.get('OPENSUBMIT_LOGIN_GITHUB_OAUTH_SECRET', ''), help='GitHUb OAuth client secret.')
305
    parser_configcreate.add_argument('--login_openid_title', default=os.environ.get('OPENSUBMIT_LOGIN_OPENID_TITLE', 'StackExchange'), help='Title of the OpenID login button.')
306
    parser_configcreate.add_argument('--login_openid_provider', default=os.environ.get('OPENSUBMIT_LOGIN_OPENID_PROVIDER', 'https://openid.stackexchange.com'), help='URL of the OpenID provider.')
307
    parser_configcreate.add_argument('--login_shib_title', default=os.environ.get('OPENSUBMIT_LOGIN_SHIB_TITLE', ''), help='Title of the Shibboleth login button.')
308
    parser_configcreate.add_argument('--login_demo', default=bool(os.environ.get('OPENSUBMIT_LOGIN_DEMO', 'False')), action='store_true', help='Title of the Shibboleth login button.')
309
    parser_configcreate.add_argument('--admin_name', default=os.environ.get('OPENSUBMIT_ADMIN_NAME', 'OpenSubmit Administrator'), help='Name of the administrator, shown in privacy policy, impress and backend.')
310
    parser_configcreate.add_argument('--admin_email', default=os.environ.get('OPENSUBMIT_ADMIN_EMAIL', 'root@localhost'), help='eMail of the administrator, shown in privacy policy, impress and backend.')
311
    parser_configcreate.add_argument('--admin_address', default=os.environ.get('OPENSUBMIT_ADMIN_ADDRESS', '(address available by eMail)'), help='Address of the administrator, shown in privacy policy and impress.')
312
    parser_configcreate.add_argument('--admin_impress_page', default=os.environ.get('OPENSUBMIT_IMPRESS_PAGE', ''), help='Link to alternative impress page.')
313
    parser_configcreate.add_argument('--admin_privacy_page', default=os.environ.get('OPENSUBMIT_PRIVACY_PAGE', ''), help='Link to alternative privacy policy page.')
314
315
    parser_configtest = subparsers.add_parser('configtest', aliases=['configure'], help='Check config files and database for correct installation of the OpenSubmit web server.')
316
    parser_democreate = subparsers.add_parser('democreate', aliases=['createdemo'], help='Install some test data (courses, assignments, users).')
317
    parser_apachecreate = subparsers.add_parser('apachecreate', help='Create config file snippet for Apache 2.4.')
318
    parser_fixperms = subparsers.add_parser('fixperms', help='Check and fix student and tutor permissions.')
319
    parser_fixchecksums = subparsers.add_parser('fixchecksums', help='Re-create all student file checksums (for duplicate detection).')
320
321
    parser_makeadmin = subparsers.add_parser('makeadmin', help='Make this user an admin with backend rights.')
322
    parser_makeadmin.add_argument('email')
323
    parser_makeowner = subparsers.add_parser('makeowner', help='Make this user a course owner with backend rights.')
324
    parser_makeowner.add_argument('email')
325
    parser_maketutor = subparsers.add_parser('maketutor', help='Make this user a course tutor with backend rights.')
326
    parser_maketutor.add_argument('email')
327
    parser_makestudent = subparsers.add_parser('makestudent', help='Make this user a student without backend rights.')
328
    parser_makestudent.add_argument('email')
329
    args = parser.parse_args()
330
331
    config_file = fsroot + args.config
332
333
    if args.command == 'apachecreate':
334
        config = check_web_config(config_file)
335
        if config:
336
            apache_config(config, os.path.dirname(config_file) + os.sep + 'apache24.conf')
337
        return
338
339
    if args.command == 'configcreate':
340
        configcreate(config_file, vars(args))
341
        return
342
343
    if args.command == 'configtest':
344
        configtest(config_file)
345
        return
346
347
    if args.command in ['fixperms', 'fixchecksums', 'democreate']:
348
        django_admin([args.command])
349
        return
350
351
    if args.command in ['makeadmin', 'makeowner', 'maketutor', 'makestudent']:
352
        django_admin([args.command, args.email])
353
        return
354
355
356
if __name__ == "__main__":
357
    console_script()
358