Coverage for webapp/login/views.py: 77%
43 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 22:07 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 22:07 +0000
1import os
2import talisker
3import flask
5from flask_wtf.csrf import generate_csrf, validate_csrf
7from canonicalwebteam.candid import CandidClient
8from webapp.helpers import is_safe_url
9from webapp import authentication
10from webapp.observability.utils import trace_function
11from webapp.store_api import publisher_gateway
13login = flask.Blueprint(
14 "login", __name__, template_folder="/templates", static_folder="/static"
15)
17LOGIN_URL = os.getenv("LOGIN_URL", "https://login.ubuntu.com")
18LOGIN_LAUNCHPAD_TEAM = os.getenv(
19 "LOGIN_LAUNCHPAD_TEAM", "canonical-webmonkeys"
20)
23request_session = talisker.requests.get_session()
24candid = CandidClient(request_session)
27@trace_function
28@login.route("/logout")
29def logout():
30 authentication.empty_session(flask.session)
31 return flask.redirect("/")
34@trace_function
35@login.route("/login")
36def publisher_login():
37 user_agent = flask.request.headers.get("User-Agent")
39 # Get a bakery v2 macaroon from the publisher API to be discharged
40 # and save it in the session
41 flask.session["account-macaroon"] = publisher_gateway.issue_macaroon(
42 [
43 "account-register-package",
44 "account-view-packages",
45 "package-manage",
46 "package-view",
47 ],
48 description=f"charmhub.io - {user_agent}",
49 )
51 login_url = candid.get_login_url(
52 macaroon=flask.session["account-macaroon"],
53 callback_url=flask.url_for("login.login_callback", _external=True),
54 state=generate_csrf(),
55 )
57 # Next URL to redirect the user after the login
58 next_url = flask.request.args.get("next")
60 if next_url:
61 if not is_safe_url(next_url):
62 return flask.abort(400)
63 flask.session["next_url"] = next_url
65 return flask.redirect(login_url, 302)
68@trace_function
69@login.route("/login/callback")
70def login_callback():
71 code = flask.request.args["code"]
72 state = flask.request.args["state"]
74 # Avoid CSRF attacks
75 validate_csrf(state)
77 discharged_token = candid.discharge_token(code)
78 candid_macaroon = candid.discharge_macaroon(
79 flask.session["account-macaroon"], discharged_token
80 )
82 # Store bakery authentication
83 issued_macaroon = candid.get_serialized_bakery_macaroon(
84 flask.session["account-macaroon"], candid_macaroon
85 )
87 flask.session["account-auth"] = publisher_gateway.exchange_macaroons(
88 issued_macaroon
89 )
91 # Set "account", "permissions" and other properties from the API response
92 flask.session.update(
93 publisher_gateway.macaroon_info(flask.session["account-auth"])
94 )
96 return flask.redirect(
97 flask.session.pop(
98 "next_url",
99 "/charms",
100 ),
101 302,
102 )