Passed
Push — master ( 7e1d33...a505d5 )
by Peter
01:26
created

check_web_config()   B

Complexity

Conditions 3

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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