eidawsauth.py 10.7 KB
Newer Older
1
2
3
4
import re
import datetime
import random
import string
5
import logging
6
import os
7
from hashlib import md5, sha1
8
from flask import Flask, request, Response
9
import psycopg2
10
import gnupg
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
11
from config import Configurator
12
from version import __version__
13

Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
14

Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
15
application = Flask(__name__)
16
if os.getenv('RUNMODE') == 'production':
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
17
    application.logger.setLevel(logging.INFO)
18
else:
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
19
    application.logger.setLevel(logging.DEBUG)
20
21
# Loglevel can be overrinden by LOGLEVEL env var :
if os.getenv('DEBUG') == 'true':
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
22
    application.logger.setLevel(logging.DEBUG)
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
23
application.config.from_object(Configurator)
24

25

26
27
28
29
30
31
def wsshash(login, password):
    """
    Compute a hash suitable for the IRIS wss stack.
    """
    return md5(("%s:FDSN:%s"%(login,password)).encode()).hexdigest()

32
33
34
35
36
37
38
39
40
def verify_token_signature(data, gpg_homedir):
    # First we verify the signature
    gpg = gnupg.GPG(gnupghome=gpg_homedir)
    verified = gpg.verify(data)
    if not verified: raise ValueError("Signature could not be verified!")
    return True

def parse_input_data(data):
    # Then we get the token :
41
    token = re.search(r'{(?P<token>.*)}',str(data)).groupdict()['token']
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
42
    application.logger.debug(token)
43
    d = dict([i for i in kv.split(':',1)] for kv in token.replace('"','').replace(' ','').split(','))
44
45
    # Add the sha1 of the token to put in the credentials db
    d['shortSha1'] = sha1(token.encode()).hexdigest()[0:8]
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
46
    application.logger.debug("Transformed to dictionary : %s", d)
47
48
49
    return d


50
def register_privileges(login, tokendict):
51
    """
52
    - Check membership and get FDSN references
53
    - Connect to PRIVILEGEDB
54
    - For each fdsn reference, insert the privilege in the access table if not already there
55
    """
56
57
58
59
60
61
62
63
64
65
66
    fdsn_memberships = []
    for em in  tokendict['memberof'].split(';'):
        application.logger.debug("EPOS membership: "+em)
        if em in application.config['EPOS_FDSN_MAP']:
            application.logger.debug("   ... is in epos fdsn map")
            fdsn_memberships.append(application.config['EPOS_FDSN_MAP'][em])

    if len(fdsn_memberships) == 0:
        return

    application.logger.debug("FDSN memberships: %s"%(fdsn_memberships))
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
67
    try:
68
69
70
71
72
        conn = psycopg2.connect(dbname= application.config['RESIFINV_PGDATABASE'],
                            port = application.config['RESIFINV_PGPORT'],
                            host = application.config['RESIFINV_PGHOST'],
                            user= application.config['RESIFINV_PGUSER'],
                            password = application.config['RESIFINV_PGPASSWORD'])
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
73
        cur = conn.cursor()
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
74
        application.logger.debug("Connected to privileges database")
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
75
    except Exception as e:
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
76
        application.logger.error("Unable to connect to database %s as %s@%s:%s", application.config['RESIFINV_PGDATABASE'],
77
78
79
                                                                      application.config['RESIFINV_PGUSER'],
                                                                      application.config['RESIFINV_PGHOST'],
                                                                      application.config['RESIFINV_PGPORT'])
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
80
81
82
        raise e

    # Get the network id
83
    for ref in fdsn_memberships:
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
84
        ref['login'] = login
85
        ref['expires_at'] = datetime.datetime.now()+datetime.timedelta(days=1)
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
86
        application.logger.info(ref)
87
        sql_request = "select network_id from networks where start_year=%(startyear)s and end_year=%(endyear)s and network=%(networkcode)s"
88
89
        try:
            cur.execute(sql_request, ref)
90
        except psycopg2.Error as e:
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
91
            application.logger.error(e.pgerror)
92
        else:
93
            if cur.rowcount != 1:
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
94
95
                application.logger.info(cur.mogrify(sql_request, ref))
                application.logger.error("%d networks found for %s", cur.rowcount, ref)
96
                raise NameError(f"{cur.rowcount} networks found for {ref}")
97
            ref['networkid'] = cur.fetchone()[0]
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
98
            application.logger.info("Inserting tupple in %s.eida_temp_users: %s", application.config['RESIFINV_PGDATABASE'], ref)
99
            cur.execute("""
100
            insert into eida_temp_users (network_id, network, start_year, end_year, name, expires_at) values (%(networkid)s, %(networkcode)s, %(startyear)s, %(endyear)s, %(login)s, %(expires_at)s) ON CONFLICT DO NOTHING;
101
            """, ref)
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
102
103
104
    conn.commit()
    conn.close()

105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def get_login_password(tokendict):
    """
    Try to read the token's mail. If it exists and a valid credential already exists in the database, return this tuple
    else, generate a new login/password, register it in the database and return the tupple
    """
    # TODO Try to get an existing login/password from the token's short sha1

    login = ""
    password = ""
    try:
        conn = psycopg2.connect(dbname= application.config['RESIFAUTH_PGDATABASE'],
                                port = application.config['RESIFAUTH_PGPORT'],
                                host = application.config['RESIFAUTH_PGHOST'],
                                user= application.config['RESIFAUTH_PGUSER'],
                                password = application.config['RESIFAUTH_PGPASSWORD'])
        cur = conn.cursor()
        application.logger.debug("Connected to users database")
        cur.execute("select user_index,login from users where email=%s and expires_at between now()+'1 hour' and now()+'26 hours'", (tokendict['mail'],))
        application.logger.debug(cur.mogrify("select user_index,login from users where email=%s and expires_at between now()+'1 hour' and now()+'26 hours'", (tokendict['mail'],)))
        if cur.rowcount != 0:
            (uid, login) = cur.fetchone()
126
127
            application.logger.debug("Found a temporary account corresponding to %s: %s", tokendict['mail'], uid)
            cur.execute("select password_hash from credentials where user_index=%s", (uid,))
128
129
            if cur.rowcount != 0:
                password = cur.fetchone()[0]
130
                application.logger.debug("Found correspondig password also: %s", password)
131
132
133
134
135
136
137
138
139
140
141
142
143
        # If not found, compute a random login and password
        if not login or not password:
            application.logger.debug("No existing user found. Create a new one")
            login = ''.join(random.choices(string.ascii_uppercase + string.digits, k=14))
            password = ''.join(random.choices(string.ascii_uppercase + string.digits, k=14))
            expiration_time = datetime.datetime.now()+datetime.timedelta(days=1)
            # Register login in authentication database
            cur.execute("""
                INSERT INTO users VALUES (DEFAULT, %(login)s, %(givenName)s, %(sn)s, %(mail)s, %(expires_at)s);
                """,
                {'login': login, 'givenName': tokendict['givenName'], 'sn': tokendict['sn'], 'mail': tokendict['mail'], 'expires_at': expiration_time }
            )
            cur.execute("""
144
            INSERT INTO credentials (user_index, password_hash, ws_hash, expires_at) VALUES (CURRVAL('users_user_index_seq'), %(password)s, %(wsshash)s, %(expires_at)s);
145
            """,
146
                        {'wsshash': wsshash(login, password), 'expires_at': expiration_time, 'password': password }
147
148
149
150
151
152
153
154
155
            )
        conn.commit()
        conn.close()

    except psycopg2.Error as e:
        application.logger.error(e.pgerror)
        raise e

    return (login, password)
156

157
158
@application.route("/version", methods=['GET'])
def version():
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
159
    return Response("Version %s running in %s mode. Contact %s."%(__version__, application.config['ENVIRONMENT'], application.config['SUPPORT_EMAIL']), status=200)
160

Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
161
@application.route("/cleanup", methods=['GET'])
162
163
164
165
def cleanup():
    """
    Clean old temporary logins and passwords in both databases.
    """
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
166
    application.logger.info("Cleaning up expired temporary accounts")
167
    rows_deleted = 0
168
    try:
169
        conn = psycopg2.connect(dbname= application.config['RESIFAUTH_PGDATABASE'],
170
171
172
173
                                port = application.config['RESIFAUTH_PGPORT'],
                                host = application.config['RESIFAUTH_PGHOST'],
                                user= application.config['RESIFAUTH_PGUSER'],
                                password = application.config['RESIFAUTH_PGPASSWORD'])
174
        cur = conn.cursor()
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
175
        application.logger.debug("Connected to users database")
176
177
178
179
        cur.execute("delete from users where expires_at < now();")
        rows_deleted = cur.rowcount
        conn.commit()
        conn.close()
180
    except psycopg2.Error as e:
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
181
        application.logger.error(e.pgerror)
182
183
        raise e

184
    try:
185
        conn = psycopg2.connect(dbname= application.config['RESIFINV_PGDATABASE'],
186
187
188
189
                                port = application.config['RESIFINV_PGPORT'],
                                host = application.config['RESIFINV_PGHOST'],
                                user= application.config['RESIFINV_PGUSER'],
                                password = application.config['RESIFINV_PGPASSWORD'])
190
        cur = conn.cursor()
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
191
192
        application.logger.debug("Connected to privlieges database")
        application.logger.debug("Deleting from privileges database")
193
194
195
196
        cur.execute("delete from eida_temp_users where expires_at < now();")
        conn.commit()
        conn.close()
    except Exception as e:
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
197
        application.logger.error(e.pgerror)
198
        raise e
199
    return Response("Deleted %d expired accounts."%(rows_deleted), status=200)
200

201
@application.route("/", methods=['POST'])
202
203
204
def auth():
    login = ''
    password = ''
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
205
    application.logger.debug(request.mimetype)
206
    data = request.get_data()
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
207
    application.logger.debug("Data: %s", data)
208
    try:
209
        verify_token_signature(data, application.config['GNUPG_HOMEDIR'])
210
        tokendict = parse_input_data(data)
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
211
        application.logger.info("Token signature OK: %s"%str(tokendict))
212
    except ValueError as e:
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
213
        application.logger.info("Token signature could not be checked: %s"%str(data))
214
215
216
217
218
        return Response(str(e), status=415)
    # Now we have a dictionary corresponding to the token's content.
    # Verify validity
    expiration_ts=  datetime.datetime.strptime(tokendict['valid_until'], '%Y-%m-%dT%H:%M:%S.%fZ')
    if (expiration_ts - datetime.datetime.now()).total_seconds() < 0:
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
219
        application.logger.info("Token is expired")
220
        return Response('Token is expired. Please generate a new one at https://geofon.gfz-potsdam.de/eas/', status=400)
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
221
    application.logger.info("Token is valid")
222

223
224
225
226
227
    (login, password) = get_login_password(tokendict)
    if login and password:
        register_privileges(login, tokendict)
        return "%s:%s"%(login, password)
    else:
Jonathan Schaeffer's avatar
Details    
Jonathan Schaeffer committed
228
        return Response("Internal server error. Contact %s"%(application.config['SUPPORT_EMAIL']), status=500)
229
230

if __name__ == "__main__":
Jonathan Schaeffer's avatar
Jonathan Schaeffer committed
231
    application.logger.info("Running in %s mode"%(application.config['ENVIRONMENT']))
232
    application.run(host='0.0.0.0')