1 import os
2 import base64
3 import datetime
4 import functools
5 from functools import wraps, partial
6
7 from netaddr import IPAddress, IPNetwork
8 import re
9 import flask
10 from flask import send_file
11
12 from urllib.parse import urlparse
13 from openid_teams.teams import TeamsRequest
14
15 from copr_common.enums import RoleEnum
16 from coprs import app
17 from coprs import db
18 from coprs import helpers
19 from coprs import models
20 from coprs import oid
21 from coprs.logic.complex_logic import ComplexLogic
22 from coprs.logic.users_logic import UsersLogic
23 from coprs.logic.coprs_logic import CoprsLogic
40
43 oidname_parse = urlparse(oidname)
44 if not oidname_parse.netloc:
45 return oidname
46 config_parse = urlparse(app.config["OPENID_PROVIDER_URL"])
47 return oidname_parse.netloc.replace(".{0}".format(config_parse.netloc), "")
48
62
67
80
81
82 @app.errorhandler(404)
83 -def page_not_found(message):
84 return flask.render_template("404.html", message=message), 404
85
90
93 """
94 :type message: str
95 :type err: CoprHttpException
96 """
97 return flask.render_template("_error.html",
98 message=message,
99 error_code=code,
100 error_title=title), code
101
102
103 server_error_handler = partial(generic_error, code=500, title="Internal Server Error")
104 bad_request_handler = partial(generic_error, code=400, title="Bad Request")
105
106 app.errorhandler(500)(server_error_handler)
107 app.errorhandler(400)(bad_request_handler)
108
109 misc = flask.Blueprint("misc", __name__)
110
111
112 @misc.route(app.config['KRB5_LOGIN_BASEURI'] + "<name>/", methods=["GET"])
114 """
115 Handle the Kerberos authentication.
116
117 Note that if we are able to get here, either the user is authenticated
118 correctly, or apache is mis-configured and it does not perform KRB
119 authentication at all. Note also, even if that can be considered ugly, we
120 are reusing oid's get_next_url feature with kerberos login.
121 """
122
123
124 if flask.g.user is not None:
125 return flask.redirect(oid.get_next_url())
126
127 krb_config = app.config['KRB5_LOGIN']
128
129 found = None
130 for key in krb_config.keys():
131 if krb_config[key]['URI'] == name:
132 found = key
133 break
134
135 if not found:
136
137 return flask.render_template("404.html"), 404
138
139 if app.config["DEBUG"] and 'TEST_REMOTE_USER' in os.environ:
140
141 flask.request.environ['REMOTE_USER'] = os.environ['TEST_REMOTE_USER']
142
143 if 'REMOTE_USER' not in flask.request.environ:
144 nocred = "Kerberos authentication failed (no credentials provided)"
145 return flask.render_template("403.html", message=nocred), 403
146
147 krb_username = flask.request.environ['REMOTE_USER']
148 app.logger.debug("krb5 login attempt: " + krb_username)
149 username = krb_straighten_username(krb_username)
150 if not username:
151 message = "invalid krb5 username: " + krb_username
152 return flask.render_template("403.html", message=message), 403
153
154 krb_login = (
155 models.Krb5Login.query
156 .filter(models.Krb5Login.config_name == key)
157 .filter(models.Krb5Login.primary == username)
158 .first()
159 )
160 if krb_login:
161 flask.g.user = krb_login.user
162 flask.session['krb5_login'] = krb_login.user.name
163 flask.flash(u"Welcome, {0}".format(flask.g.user.name), "success")
164 return flask.redirect(oid.get_next_url())
165
166
167 user = models.User.query.filter(models.User.username == username).first()
168 if not user:
169
170 email = username + "@" + krb_config[key]['email_domain']
171 user = create_user_wrapper(username, email)
172 db.session.add(user)
173
174 krb_login = models.Krb5Login(user=user, primary=username, config_name=key)
175 db.session.add(krb_login)
176 db.session.commit()
177
178 flask.flash(u"Welcome, {0}".format(user.name), "success")
179 flask.g.user = user
180 flask.session['krb5_login'] = user.name
181 return flask.redirect(oid.get_next_url())
182
183
184 @misc.route("/login/", methods=["GET"])
185 @oid.loginhandler
186 -def login():
187 if not app.config['FAS_LOGIN']:
188 if app.config['KRB5_LOGIN']:
189 return krb5_login_redirect(next=oid.get_next_url())
190 flask.flash("No auth method available", "error")
191 return flask.redirect(flask.url_for("coprs_ns.coprs_show"))
192
193 if flask.g.user is not None:
194 return flask.redirect(oid.get_next_url())
195 else:
196
197 team_req = TeamsRequest(["_FAS_ALL_GROUPS_"])
198 return oid.try_login(app.config["OPENID_PROVIDER_URL"],
199 ask_for=["email", "timezone"],
200 extensions=[team_req])
201
239
240
241 @misc.route("/logout/")
242 -def logout():
243 flask.session.pop("openid", None)
244 flask.session.pop("krb5_login", None)
245 flask.flash(u"You were signed out")
246 return flask.redirect(oid.get_next_url())
247
250 @functools.wraps(f)
251 def decorated_function(*args, **kwargs):
252 token = None
253 apt_login = None
254 if "Authorization" in flask.request.headers:
255 base64string = flask.request.headers["Authorization"]
256 base64string = base64string.split()[1].strip()
257 userstring = base64.b64decode(base64string)
258 (apt_login, token) = userstring.decode("utf-8").split(":")
259 token_auth = False
260 if token and apt_login:
261 user = UsersLogic.get_by_api_login(apt_login).first()
262 if (user and user.api_token == token and
263 user.api_token_expiration >= datetime.date.today()):
264
265 if user.proxy and "username" in flask.request.form:
266 user = UsersLogic.get(flask.request.form["username"]).first()
267
268 token_auth = True
269 flask.g.user = user
270 if not token_auth:
271 url = 'https://' + app.config["PUBLIC_COPR_HOSTNAME"]
272 url = helpers.fix_protocol_for_frontend(url)
273
274 output = {
275 "output": "notok",
276 "error": "Login invalid/expired. Please visit {0}/api to get or renew your API token.".format(url),
277 }
278 jsonout = flask.jsonify(output)
279 jsonout.status_code = 401
280 return jsonout
281 return f(*args, **kwargs)
282 return decorated_function
283
286 krbc = app.config['KRB5_LOGIN']
287 for key in krbc:
288
289 return flask.redirect(flask.url_for("misc.krb5_login",
290 name=krbc[key]['URI'],
291 next=next))
292 flask.flash("Unable to pick krb5 login page", "error")
293 return flask.redirect(flask.url_for("coprs_ns.coprs_show"))
294
297 def view_wrapper(f):
298 @functools.wraps(f)
299 def decorated_function(*args, **kwargs):
300 if flask.g.user is None:
301 return flask.redirect(flask.url_for("misc.login",
302 next=flask.request.url))
303
304 if role == RoleEnum("admin") and not flask.g.user.admin:
305 flask.flash("You are not allowed to access admin section.")
306 return flask.redirect(flask.url_for("coprs_ns.coprs_show"))
307
308 return f(*args, **kwargs)
309 return decorated_function
310
311
312
313
314
315 if callable(role):
316 return view_wrapper(role)
317 else:
318 return view_wrapper
319
323 @functools.wraps(f)
324 def decorated_function(*args, **kwargs):
325 auth = flask.request.authorization
326 if not auth or auth.password != app.config["BACKEND_PASSWORD"]:
327 return "You have to provide the correct password\n", 401
328
329 return f(*args, **kwargs)
330 return decorated_function
331
334 @functools.wraps(f)
335 def decorated_function(*args, **kwargs):
336 ip_addr = IPAddress(flask.request.remote_addr)
337 accept_ranges = set(app.config.get("INTRANET_IPS", []))
338 accept_ranges.add("127.0.0.1")
339 if not any(ip_addr in IPNetwork(addr_or_net) for addr_or_net in accept_ranges):
340 return ("Stats can be update only from intranet hosts, "
341 "not {}, check config\n".format(flask.request.remote_addr)), 403
342
343 return f(*args, **kwargs)
344 return decorated_function
345
358 return wrapper
359
360
361 @misc.route("/migration-report/")
362 @misc.route("/migration-report/<username>")
363 -def coprs_migration_report(username=None):
375
382
389
392 if not build:
393 return send_file("static/status_images/unknown.png",
394 mimetype='image/png')
395
396 if build.state in ["importing", "pending", "starting", "running"]:
397
398
399 response = send_file("static/status_images/in_progress.png",
400 mimetype='image/png')
401 response.headers['Cache-Control'] = 'no-cache'
402 return response
403
404 if build.state in ["succeeded", "skipped"]:
405 return send_file("static/status_images/succeeded.png",
406 mimetype='image/png')
407
408 if build.state == "failed":
409 return send_file("static/status_images/failed.png",
410 mimetype='image/png')
411
412 return send_file("static/status_images/unknown.png",
413 mimetype='image/png')
414