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

1import os 

2import talisker 

3import flask 

4 

5from flask_wtf.csrf import generate_csrf, validate_csrf 

6 

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 

12 

13login = flask.Blueprint( 

14 "login", __name__, template_folder="/templates", static_folder="/static" 

15) 

16 

17LOGIN_URL = os.getenv("LOGIN_URL", "https://login.ubuntu.com") 

18LOGIN_LAUNCHPAD_TEAM = os.getenv( 

19 "LOGIN_LAUNCHPAD_TEAM", "canonical-webmonkeys" 

20) 

21 

22 

23request_session = talisker.requests.get_session() 

24candid = CandidClient(request_session) 

25 

26 

27@trace_function 

28@login.route("/logout") 

29def logout(): 

30 authentication.empty_session(flask.session) 

31 return flask.redirect("/") 

32 

33 

34@trace_function 

35@login.route("/login") 

36def publisher_login(): 

37 user_agent = flask.request.headers.get("User-Agent") 

38 

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 ) 

50 

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 ) 

56 

57 # Next URL to redirect the user after the login 

58 next_url = flask.request.args.get("next") 

59 

60 if next_url: 

61 if not is_safe_url(next_url): 

62 return flask.abort(400) 

63 flask.session["next_url"] = next_url 

64 

65 return flask.redirect(login_url, 302) 

66 

67 

68@trace_function 

69@login.route("/login/callback") 

70def login_callback(): 

71 code = flask.request.args["code"] 

72 state = flask.request.args["state"] 

73 

74 # Avoid CSRF attacks 

75 validate_csrf(state) 

76 

77 discharged_token = candid.discharge_token(code) 

78 candid_macaroon = candid.discharge_macaroon( 

79 flask.session["account-macaroon"], discharged_token 

80 ) 

81 

82 # Store bakery authentication 

83 issued_macaroon = candid.get_serialized_bakery_macaroon( 

84 flask.session["account-macaroon"], candid_macaroon 

85 ) 

86 

87 flask.session["account-auth"] = publisher_gateway.exchange_macaroons( 

88 issued_macaroon 

89 ) 

90 

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 ) 

95 

96 return flask.redirect( 

97 flask.session.pop( 

98 "next_url", 

99 "/charms", 

100 ), 

101 302, 

102 )