Coverage for webapp/authentication.py: 84%

50 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-08 12:33 +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] 

35 

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

37SESSION_INTEGRATION_KEYS = [ 

38 "github_auth_secret", 

39] 

40 

41SESSION_DATA_KEYS = SESSION_AUTH_KEYS + SESSION_INTEGRATION_KEYS 

42 

43 

44def get_authorization_header(macaroon): 

45 """ 

46 Return the authorization header value for an exchanged macaroon. 

47 """ 

48 return f"Macaroon {macaroon}" 

49 

50 

51def get_session_authorization_headers(session): 

52 """ 

53 Return the correct authorization headers for the current session. 

54 """ 

55 if "macaroon_exchanged" in session: 

56 return { 

57 "Authorization": get_authorization_header( 

58 session["macaroon_exchanged"] 

59 ) 

60 } 

61 

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

63 root = session["macaroon_root"] 

64 discharge = session["macaroon_discharge"] 

65 

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

67 Macaroon.deserialize(discharge) 

68 ) 

69 

70 return { 

71 "Authorization": ( 

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

73 ) 

74 } 

75 

76 if "macaroons" in session: 

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

78 

79 return {"Macaroons": ""} 

80 

81 

82def get_publishergw_authorization_header(developer_token): 

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

84 

85 

86def is_authenticated(session): 

87 """ 

88 Checks if the user is authenticated from the session 

89 Returns True if the user is authenticated 

90 """ 

91 return ( 

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

93 or ( 

94 "publisher" in session 

95 and "macaroon_discharge" in session 

96 and "macaroon_root" in session 

97 ) 

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

99 ) 

100 

101 

102def empty_session(session): 

103 """ 

104 Empty the session, used to logout. 

105 """ 

106 for key in SESSION_DATA_KEYS: 

107 session.pop(key, None) 

108 

109 

110def reset_auth_session(session): 

111 """ 

112 Clear Snapcraft auth keys while preserving integration tokens 

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

114 but third-party tokens are still valid. 

115 """ 

116 for key in SESSION_AUTH_KEYS: 

117 session.pop(key, None) 

118 

119 

120def get_caveat_id(root): 

121 """ 

122 Returns the caveat_id generated by the SSO 

123 """ 

124 location = urlparse(LOGIN_URL).hostname 

125 (caveat,) = [ 

126 c 

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

128 if c.location == location 

129 ] 

130 

131 return caveat.caveat_id 

132 

133 

134def request_macaroon(): 

135 """ 

136 Request a macaroon from dashboard. 

137 Returns the macaroon. 

138 """ 

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

140 

141 return response["macaroon"] 

142 

143 

144def get_refreshed_discharge(discharge): 

145 """ 

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

147 Returns the new discharge macaroon. 

148 """ 

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

150 

151 return response["discharge_macaroon"] 

152 

153 

154def is_macaroon_expired(headers): 

155 """ 

156 Returns True if the macaroon needs to be refreshed from 

157 the header response. 

158 """ 

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

160 

161 

162def _store_api_authorization_header(_self, session): 

163 return get_session_authorization_headers(session) 

164 

165 

166store_api_dashboard.Dashboard._get_authorization_header = ( 

167 _store_api_authorization_header 

168) 

169store_api_publishergw.dashboard_authorization_header = ( 

170 get_session_authorization_headers 

171)