attribuete_not_provided()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 1
1
# Copyright 2015 INFN
2
# All Rights Reserved.
3
#
4
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
#    not use this file except in compliance with the License. You may obtain
6
#    a copy of the License at
7
#
8
#         http://www.apache.org/licenses/LICENSE-2.0
9
#
10
#    Unless required by applicable law or agreed to in writing, software
11
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
#    License for the specific language governing permissions and limitations
14
#    under the License.
15
16
"""
17
SSO FLASK Application for Discourse
18
The configuration file is defined with the variable "DISCOURSE_SSO_CONFIG",
19
for the most significant values look at the sso/default.py file
20
"""
21
22
23
from flask import abort, Flask, redirect, render_template, request, url_for, \
24
    session
25
26
import base64
27
import hashlib
28
import hmac
29
import urllib
30
import re
31
32
app = Flask(__name__)
33
app.config.from_object('discourseSSO.default.Config')
34
app.config.from_pyfile('config.py')
35
app.config.from_envvar('DISCOURSE_SSO_CONFIG', True)
36
37
38
@app.route('/sso/login')
39
def payload_check():
40
    """
41
    Verify the payload and signature coming from a Discourse server and if
42
    correct redirect to the authentication page
43
    :return: The redirection page to the authentication page
44
    """
45
    payload = request.args.get('sso', '')
46
    signature = request.args.get('sig', '')
47
48
    app.logger.debug('Request to login with payload="%s" signature="%s"',
49
                     payload, signature)
50
    if not payload or not signature:
51
        abort(400)
52
53
    app.logger.debug('Session Secret Key: %s',
54
                     app.secret_key)
55
    app.logger.debug('SSO Secret Key: %s',
56
                     app.config.get('DISCOURSE_SECRET_KEY'))
57
    dig = hmac.new(
58
        app.config.get('DISCOURSE_SECRET_KEY'),
59
        payload,
60
        hashlib.sha256
61
    ).hexdigest()
62
    app.logger.debug('Calculated hash: %s', dig)
63
    if dig != signature:
64
        abort(400)
65
    decoded_msg = base64.decodestring(payload)
66
    session['nonce'] = decoded_msg
67
    return redirect(url_for('user_authz'))
68
69
70
@app.route('/sso/auth')
71
def user_authz():
72
    """
73
    Read the user attributes provided by the application server (generally
74
    it is apache httpd) as environment variables and create the payload to
75
    send to discourse
76
    :return: The redirection page to Discourse
77
    """
78
    attribute_map = app.config.get('DISCOURSE_USER_MAP')
79
    user_flag_filters = app.config.get('DISCOURSE_USER_FLAGS')
80
    email = request.environ.get(attribute_map['email'])
81
    external_id = request.environ.get(attribute_map['external_id'])
82
    if not (email and external_id):
83
        abort(403)
84
    name_list = []
85
    for name_to_map in attribute_map['name']:
86
        if request.environ.get(name_to_map):
87
            name_list.append(request.environ.get(name_to_map))
88
    name = ' '.join(name_list)
89
    if request.environ.get(attribute_map['username']):
90
        username = request.environ.get(attribute_map['username'])
91
    else:
92
        username = (name.replace(' ', '') +
93
                    "_" +
94
                    hashlib.md5(email).hexdigest()[0:4]
95
                    )
96
    avatar_url = request.environ.get(attribute_map['avatar_url'])
97
    bio = request.environ.get(attribute_map['bio'])
98
    app.logger.debug('Authenticating "%s" with username "%s" and email "%s"',
99
                     name, username, email)
100
    if 'nonce' not in session:
101
        abort(403)
102
    query = (session['nonce'] +
103
             '&name=' + name +
104
             '&username=' + username +
105
             '&email=' + urllib.quote(email) +
106
             '&external_id=' + urllib.quote(external_id))
107
    if avatar_url:
108
        query = query + '&avatar_url=' + urllib.quote(avatar_url)
109
    if bio:
110
        query = query + '&bio=' + urllib.quote(bio)
111
    flags = {}
112
    for user_flag in user_flag_filters:
113
        if 'filter' in user_flag:
114
            filter = user_flag['filter'].split('=')
115
            reg_exp = re.compile(filter[1])
116
            if (request.environ.get(filter[0]) and
117
                    reg_exp.match(request.environ.get(filter[0]))):
118
                flags[user_flag['name']] = user_flag['value']
119
        else:
120
            flags[user_flag['name']] = user_flag['value']
121
    for flags_name in sorted(flags.keys()):
122
        query = query + '&' + flags_name + '=' + flags[flags_name]
123
    app.logger.debug('Query string to return: %s', query)
124
    query_b64 = base64.encodestring(query)
125
    app.logger.debug('Base64 query string to return: %s', query_b64)
126
    query_urlenc = urllib.quote(query_b64)
127
    app.logger.debug('URLEnc query string to return: %s', query_urlenc)
128
    sig = hmac.new(
129
        app.config.get('DISCOURSE_SECRET_KEY'),
130
        query_b64,
131
        hashlib.sha256
132
    ).hexdigest()
133
    app.logger.debug('Signature: %s', sig)
134
    redirect_url = (app.config.get('DISCOURSE_URL') +
135
                    '/session/sso_login?'
136
                    'sso=' + query_urlenc +
137
                    '&sig=' + sig)
138
139
    return redirect(redirect_url)
140
141
142
@app.errorhandler(403)
143
def attribuete_not_provided(error):
144
    """
145
    Render a custom error page in case the IdP authenticate the user but does
146
    not provide the requested attributes
147
148
    :type error: object
149
    """
150
    return render_template('403.html'), 403
151