Coverage for webapp/authentication.py: 92%

50 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-25 22:09 +0000

1import os 

2 

3from urllib.parse import urlparse 

4 

5from canonicalwebteam.store_api import dashboard as store_api_dashboard 

6from canonicalwebteam.store_api import publishergw as store_api_publishergw 

7from pymacaroons import Macaroon 

8from webapp.api import sso 

9 

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

11 

12PERMISSIONS = [ 

13 "edit_account", 

14 "package_access", 

15 "package_manage", 

16 "package_metrics", 

17 "package_register", 

18 "package_release", 

19 "package_update", 

20 "package_upload_request", 

21 "store_admin", 

22] 

23 

24# keys for session data that should be cleared on auth refresh 

25SESSION_AUTH_KEYS = [ 

26 "macaroons", 

27 "macaroon_exchanged", 

28 "macaroon_root", 

29 "macaroon_discharge", 

30 "publisher", 

31 "developer_token", 

32 "exchanged_developer_token", 

33 "csrf_token", 

34 "pending_snap_unregister", 

35] 

36 

37# keys for session data that should NOT be cleared on auth refresh 

38SESSION_INTEGRATION_KEYS = [ 

39 "github_auth_secret", 

40] 

41 

42SESSION_DATA_KEYS = SESSION_AUTH_KEYS + SESSION_INTEGRATION_KEYS 

43 

44 

45def get_authorization_header(macaroon): 

46 """ 

47 Return the authorization header value for an exchanged macaroon. 

48 """ 

49 return f"Macaroon {macaroon}" 

50 

51 

52def get_session_authorization_headers(session): 

53 """ 

54 Return the correct authorization headers for the current session. 

55 """ 

56 if "macaroon_exchanged" in session: 

57 return { 

58 "Authorization": get_authorization_header( 

59 session["macaroon_exchanged"] 

60 ) 

61 } 

62 

63 if "macaroon_root" in session and "macaroon_discharge" in session: 

64 root = session["macaroon_root"] 

65 discharge = session["macaroon_discharge"] 

66 

67 bound = Macaroon.deserialize(root).prepare_for_request( 

68 Macaroon.deserialize(discharge) 

69 ) 

70 

71 return { 

72 "Authorization": ( 

73 f"macaroon root={root}, discharge={bound.serialize()}" 

74 ) 

75 } 

76 

77 if "macaroons" in session: 

78 return {"Macaroons": session["macaroons"]} 

79 

80 return {"Macaroons": ""} 

81 

82 

83def get_publishergw_authorization_header(developer_token): 

84 return {"Authorization ": f"Macaroon {developer_token}"} 

85 

86 

87def is_authenticated(session): 

88 """ 

89 Checks if the user is authenticated from the session 

90 Returns True if the user is authenticated 

91 """ 

92 return ( 

93 ("publisher" in session and "macaroon_exchanged" in session) 

94 or ( 

95 "publisher" in session 

96 and "macaroon_discharge" in session 

97 and "macaroon_root" in session 

98 ) 

99 or ("publisher" in session and "macaroons" in session) 

100 ) 

101 

102 

103def empty_session(session): 

104 """ 

105 Empty the session, used to logout. 

106 """ 

107 for key in SESSION_DATA_KEYS: 

108 session.pop(key, None) 

109 

110 

111def reset_auth_session(session): 

112 """ 

113 Clear Snapcraft auth keys while preserving integration tokens 

114 (e.g. github_auth_secret). Used when re-authentication is needed 

115 but third-party tokens are still valid. 

116 """ 

117 for key in SESSION_AUTH_KEYS: 

118 session.pop(key, None) 

119 

120 

121def get_caveat_id(root): 

122 """ 

123 Returns the caveat_id generated by the SSO 

124 """ 

125 location = urlparse(LOGIN_URL).hostname 

126 (caveat,) = [ 

127 c 

128 for c in Macaroon.deserialize(root).third_party_caveats() 

129 if c.location == location 

130 ] 

131 

132 return caveat.caveat_id 

133 

134 

135def request_macaroon(): 

136 """ 

137 Request a macaroon from dashboard. 

138 Returns the macaroon. 

139 """ 

140 response = sso.post_macaroon({"permissions": PERMISSIONS}) 

141 

142 return response["macaroon"] 

143 

144 

145def get_refreshed_discharge(discharge): 

146 """ 

147 Get a refresh macaroon if the macaroon is not valid anymore. 

148 Returns the new discharge macaroon. 

149 """ 

150 response = sso.get_refreshed_discharge({"discharge_macaroon": discharge}) 

151 

152 return response["discharge_macaroon"] 

153 

154 

155def is_macaroon_expired(headers): 

156 """ 

157 Returns True if the macaroon needs to be refreshed from 

158 the header response. 

159 """ 

160 return headers.get("WWW-Authenticate") == ("Macaroon needs_refresh=1") 

161 

162 

163def _store_api_authorization_header(_self, session): 

164 return get_session_authorization_headers(session) 

165 

166 

167store_api_dashboard.Dashboard._get_authorization_header = ( 

168 _store_api_authorization_header 

169) 

170store_api_publishergw.dashboard_authorization_header = ( 

171 get_session_authorization_headers 

172)