Passed
Push — master ( 6ae5df...15ae70 )
by Peter
01:25
created

check_web_config()   A

Complexity

Conditions 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 16
rs 9.4285
cc 2
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
from configparser import RawConfigParser
20
21
DEFAULT_CONFIG = '''
22
# This is the configuration file for the OpenSubmit tool.
23
# https://github.com/troeger/opensubmit
24
#
25
# It is expected to be located at:
26
# /etc/opensubmit/settings.ini (on production system), or
27
# ./settings_dev.ini (on developer systems)
28
29
[general]
30
# Enabling this will lead to detailed developer error information as result page
31
# whenever something goes wrong on server side.
32
# In production systems, you never want that to be enabled, for obvious security reasons.
33
DEBUG: False
34
35
[server]
36
# This is the root host url were the OpenSubmit tool is offered by your web server.
37
# If you serve the content from a subdirectory, please specify it too, without leading or trailing slashes,
38
# otherwise leave it empty.
39
HOST: {server-host}
40
HOST_DIR: submit
41
42
# This is the local directory were the uploaded assignment attachments are stored.
43
# Your probably need a lot of space here.
44
# Make sure that the path starts and ends with a slash.
45
MEDIA_ROOT: ***not configured***
46
47
# This is the logging file. The web server must be allowed to write into it.
48
LOG_FILE: /var/log/opensubmit.log
49
50
# This is the timezone all dates and deadlines are specified in.
51
# This setting overrides your web server default for the time zone.
52
# The list of available zones is here:
53
# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
54
TIME_ZONE: Europe/Berlin
55
56
# This is a unique string needed for some of the security features.
57
# Change it, the value does not matter.
58
SECRET_KEY: uzfp=4gv1u((#hb*#o3*4^v#u#g9k8-)us2nw^)@rz0-$2-23)
59
60
[database]
61
# The database you are using. Possible choices are
62
# - postgresql_psycopg2
63
# - mysql
64
# - sqlite3
65
# - oracle
66
DATABASE_ENGINE: sqlite3
67
68
# The name of the database. It must be already available for being used.
69
# In SQLite, this is the path to the database file.
70
DATABASE_NAME: database.sqlite
71
72
# The user name for accessing the database. Not needed for SQLite.
73
DATABASE_USER:
74
75
# The user password for accessing the database. Not needed for SQLite.
76
DATABASE_PASSWORD:
77
78
# The host name for accessing the database. Not needed for SQLite.
79
# An empty settings means that the database is on the same host as the web server.
80
DATABASE_HOST:
81
82
# The port number for accessing the database. Not needed for SQLite.
83
# An empty settings means that the database default use used.
84
DATABASE_PORT:
85
86
[executor]
87
# The shared secret with the job executor. This ensures that only authorized
88
# machines can fetch submitted solution attachments for validation, and not
89
# every student ...
90
# Change it, the value does not matter.
91
SHARED_SECRET: 49846zut93purfh977TTTiuhgalkjfnk89
92
93
[admin]
94
# The administrator for this installation. Course administrators
95
# are stored in the database, so this is only the technical contact for problems
96
# with the tool itself. Exceptions that happen due to bugs or other issues
97
# are sent to this address.
98
ADMIN_NAME: Super Admin
99
ADMIN_EMAIL: root@localhost
100
101
[login]
102
# Enables or disables login with OpenID
103
LOGIN_OPENID: True
104
105
# Text shown beside the OpenID login icon.
106
LOGIN_DESCRIPTION: StackExchange
107
108
# OpenID provider URL to be used for login.
109
OPENID_PROVIDER: https://openid.stackexchange.com
110
111
# Enables or disables login with Twitter
112
LOGIN_TWITTER: False
113
114
# OAuth application credentials for Twitter
115
LOGIN_TWITTER_OAUTH_KEY:
116
LOGIN_TWITTER_OAUTH_SECRET:
117
118
# Enables or disables login with Google
119
LOGIN_GOOGLE: False
120
121
# OAuth application credentials for Google
122
LOGIN_GOOGLE_OAUTH_KEY:
123
LOGIN_GOOGLE_OAUTH_SECRET:
124
125
# Enables or disables login with GitHub
126
LOGIN_GITHUB: False
127
128
# OAuth application credentials for GitHub
129
LOGIN_GITHUB_OAUTH_KEY:
130
LOGIN_GITHUB_OAUTH_SECRET:
131
132
# Enables or diables login through Apache 2.4 mod_shib authentication
133
LOGIN_SHIB: False
134
LOGIN_SHIB_DESCRIPTION: Shibboleth
135
'''
136
137
138
def django_admin(args):
139
    '''
140
        Run something like it would be done through Django's manage.py.
141
    '''
142
    from django.core.management import execute_from_command_line
143
    from django.core.exceptions import ImproperlyConfigured
144
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "opensubmit.settings")
145
    try:
146
        execute_from_command_line([sys.argv[0]] + args)
147
    except ImproperlyConfigured as e:
148
        print(str(e))
149
        exit(-1)
150
151
152
def apache_config(config, outputfile):
153
    '''
154
        Generate a valid Apache configuration file, based on the given settings.
155
    '''
156
    if os.path.exists(outputfile):
157
        os.rename(outputfile, outputfile + ".old")
158
        print("Renamed existing Apache config file to " + outputfile + ".old")
159
160
    from opensubmit import settings
161
    f = open(outputfile, 'w')
162
    print("Generating Apache configuration in " + outputfile)
163
    subdir = (len(settings.HOST_DIR) > 0)
164
    text = """
165
    # OpenSubmit Configuration for Apache 2.4
166
    # These directives are expected to live in some <VirtualHost> block
167
    """
168
    if subdir:
169
        text += "Alias /%s/static/ %s\n" % (settings.HOST_DIR,
170
                                            settings.STATIC_ROOT)
171
        text += "    WSGIScriptAlias /%s %s/wsgi.py\n" % (
172
            settings.HOST_DIR, settings.SCRIPT_ROOT)
173
    else:
174
        text += "Alias /static/ %s\n" % (settings.STATIC_ROOT)
175
        text += "    WSGIScriptAlias / %s/wsgi.py" % (settings.SCRIPT_ROOT)
176
    text += """
177
    WSGIPassAuthorization On
178
    <Directory {static_path}>
179
         Require all granted
180
    </Directory>
181
    <Directory {install_path}>
182
         <Files wsgi.py>
183
              Require all granted
184
         </Files>
185
    </Directory>
186
    """.format(static_path=settings.STATIC_ROOT, install_path=settings.SCRIPT_ROOT)
187
188
    f.write(text)
189
    f.close()
190
191
192
def check_path(directory):
193
    '''
194
        Checks if the directories for this path exist, and creates them in case.
195
    '''
196
    if directory != '':
197
        if not os.path.exists(directory):
198
            os.makedirs(directory, 0o775)   # rwxrwxr-x
199
200
201
def check_file(filepath):
202
    '''
203
        - Checks if the parent directories for this path exist.
204
        - Checks that the file exists.
205
        - Donates the file to the web server user.
206
207
        TODO: This is Debian / Ubuntu specific.
208
    '''
209
    check_path(os.path.dirname(filepath))
210
    if not os.path.exists(filepath):
211
        print("WARNING: File does not exist. Creating it: %s" % filepath)
212
        open(filepath, 'a').close()
213
    try:
214
        print("Setting access rights for %s for www-data user" % (filepath))
215
        uid = pwd.getpwnam("www-data").pw_uid
216
        gid = grp.getgrnam("www-data").gr_gid
217
        os.chown(filepath, uid, gid)
218
        os.chmod(filepath, 0o660)  # rw-rw---
219
    except:
220
        print("WARNING: Could not adjust file system permissions for %s. Make sure your web server can write into it." % filepath)
221
222
223
def check_web_config_consistency(config):
224
    '''
225
        Check the web application config file for consistency.
226
    '''
227
    login_conf_deps = {
228
        'LOGIN_TWITTER': ['LOGIN_TWITTER_OAUTH_KEY', 'LOGIN_TWITTER_OAUTH_SECRET'],
229
        'LOGIN_GOOGLE': ['LOGIN_GOOGLE_OAUTH_KEY', 'LOGIN_GOOGLE_OAUTH_SECRET'],
230
        'LOGIN_GITHUB': ['LOGIN_GITHUB_OAUTH_KEY', 'LOGIN_GITHUB_OAUTH_SECRET']
231
    }
232
233
    print("Checking configuration of the OpenSubmit web application...")
234
    # Let Django's manage.py load the settings file, to see if this works in general
235
    django_admin(["check"])
236
    # Check configured host
237
    try:
238
        urllib.request.urlopen(config.get("server", "HOST"))
239
    except Exception as e:
240
        # This may be ok, when the admin is still setting up to server
241
        print("The configured HOST seems to be invalid at the moment: " + str(e))
242
    # Check configuration dependencies
243
    for k, v in list(login_conf_deps.items()):
244
        if config.getboolean('login', k):
245
            for needed in v:
246
                if len(config.get('login', needed)) < 1:
247
                    print(
248
                        "ERROR: You have enabled %s in settings.ini, but %s is not set." % (k, needed))
249
                    return False
250
    # Check media path
251
    check_path(config.get('server', 'MEDIA_ROOT'))
252
    # Prepare empty log file, in case the web server has no creation rights
253
    log_file = config.get('server', 'LOG_FILE')
254
    print("Preparing log file at " + log_file)
255
    check_file(log_file)
256
    # If SQLite database, adjust file system permissions for the web server
257
    if config.get('database', 'DATABASE_ENGINE') == 'sqlite3':
258
        name = config.get('database', 'DATABASE_NAME')
259
        if not os.path.isabs(name):
260
            print("ERROR: Your SQLite database name must be an absolute path. The web server must have directory access permissions for this path.")
261
            return False
262
        check_file(config.get('database', 'DATABASE_NAME'))
263
    # everything ok
264
    return True
265
266
267
def check_web_config(config_fname):
268
    '''
269
        Try to load the Django settings.
270
        If this does not work, than settings file does not exist.
271
272
        Returns:
273
            Loaded configuration, or None.
274
    '''
275
    print("Looking for config file at {0} ...".format(config_fname))
276
    config = RawConfigParser()
277
    try:
278
        config.readfp(open(config_fname))
279
        return config
280
    except IOError:
281
        print("ERROR: Seems like the config file does not exist. Please call 'opensubmit-web configcreate'.")
282
        return None
283
284
285
def check_web_db():
286
    '''
287
        Everything related to database checks and updates.
288
    '''
289
    print("Testing for neccessary database migrations...")
290
    django_admin(["migrate"])             # apply schema migrations
291
    print("Checking the OpenSubmit permission system...")
292
    # configure permission system, of needed
293
    django_admin(["fixperms"])
294
    return True
295
296
297
def configcreate(config_path, config_fname, open_options):
298
    content = DEFAULT_CONFIG.format(**open_options)
299
300
    try:
301
        check_path(config_path)
302
        f = open(config_path + config_fname, 'wt')
303
        f.write(content)
304
        f.close()
305
        print("Config file %s generated at %s. Please edit it." % (config_fname. config_path))
306
    except Exception:
307
        print("ERROR: Could not create config file at {0}. Please use sudo or become root.".format(
308
            config_path + config_fname))
309
310
311
def configtest(config_path, config_fname):
312
    print("Inspecting OpenSubmit configuration ...")
313
    config = check_web_config(config_path + config_fname)
314
    if not config:
315
        return          # Let them first fix the config file before trying a DB access
316
    if not check_web_config_consistency(config):
317
        return
318
    if not check_web_db():
319
        return
320
    print("Preparing static files for web server...")
321
    django_admin(["collectstatic", "--noinput", "--clear", "-v 0"])
322
    apache_config(config, config_path + 'apache24.conf')
323
324
325
def print_help():
326
    print("configcreate:        Create initial config files for the OpenSubmit web server.")
327
    print("configtest:          Check config files and database for correct installation of the OpenSubmit web server.")
328
    print("democreate:          Install some test data (courses, assignments, users).")
329
    print("fixperms:            Check and fix student and tutor permissions")
330
    print("fixchecksums:        Re-create all student file checksums (for duplicate detection)")
331
    print("makeadmin   <email>: Make this user an admin with backend rights.")
332
    print("makeowner   <email>: Make this user a course owner with backend rights.")
333
    print("maketutor   <email>: Make this user a course tutor with backend rights.")
334
    print("makestudent <email>: Make this user a student without backend rights.")
335
336
337
def console_script(fsroot='/'):
338
    '''
339
        The main entry point for the production administration script 'opensubmit-web'.
340
        The argument allows the test suite to override the root of all paths used in here.
341
    '''
342
343
    if len(sys.argv) == 1:
344
        print_help()
345
        return
346
347
    # Translate legacy commands
348
    if sys.argv[1] == "configure":
349
        sys.argv[1] = 'configtest'
350
    if sys.argv[1] == "createdemo":
351
        sys.argv[1] = 'democreate'
352
353
    if sys.argv[1] == 'configcreate':
354
        # TODO: Hack, do the arg handling with a proper library
355
356
        # Config name, default value, character pos of argument
357
        poss_options = [['server-host', '***not configured***']]
358
        options = {}
359
360
        for optionname, default in poss_options:
361
            options[optionname] = default
362
            for index, text in enumerate(sys.argv[2:]):
363
                if text.startswith('--' + optionname + '='):
364
                    options[optionname] = text[len(optionname) + 3:]
365
        configcreate(fsroot + 'etc/opensubmit/', 'settings.ini', options)
366
        return
367
368
    if sys.argv[1] == 'configtest':
369
        configtest(fsroot + 'etc/opensubmit/', 'settings.ini')
370
        return
371
372
    if sys.argv[1] in ['fixperms', 'fixchecksums', 'democreate']:
373
        django_admin([sys.argv[1]])
374
        return
375
376
    if sys.argv[1] in ['makeadmin', 'makeowner', 'maketutor', 'makestudent']:
377
        django_admin([sys.argv[1], sys.argv[2]])
378
        return
379
380
    print_help()
381
382
383
if __name__ == "__main__":
384
    console_script()
385