Coverage for webapp/decorators.py: 94%

54 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-18 22:11 +0000

1# Core packages 

2import os 

3import functools 

4import logging 

5from datetime import datetime, timezone 

6 

7# Third party packages 

8import flask 

9from webapp import authentication 

10from webapp.helpers import param_redirect_capture, param_redirect_exec 

11 

12logger = logging.getLogger(__name__) 

13 

14 

15def strtobool(s: str) -> bool: 

16 """Convert a string representation of truth to true (1) or false (0).""" 

17 return s.lower() in ("yes", "true", "on", "1") 

18 

19 

20def cached_redirect(func): 

21 """ 

22 Decorator that check for a param_redirect cookie and redirects 

23 to the appropriate URL. 

24 """ 

25 

26 @functools.wraps(func) 

27 def param_redirect(*args, **kwargs): 

28 resp = param_redirect_exec( 

29 req=flask.request, 

30 make_response=flask.make_response, 

31 redirect=flask.redirect, 

32 ) 

33 if resp: 

34 return resp 

35 return func(*args, **kwargs) 

36 

37 return param_redirect 

38 

39 

40def login_required(func): 

41 """ 

42 Decorator that checks if a user is logged in, and redirects 

43 to login page if not. 

44 """ 

45 

46 @functools.wraps(func) 

47 def is_user_logged_in(*args, **kwargs): 

48 date = datetime.now(timezone.utc) 

49 date_str = date.strftime("%Y-%m-%dT%H:%M:%S") 

50 

51 if not authentication.is_authenticated(flask.session): 

52 logger.warning( 

53 "User login failed", 

54 extra={ 

55 "datetime": date_str, 

56 "appid": "charmhub-io", 

57 "event": "authn_login_fail", 

58 }, 

59 ) 

60 

61 response = flask.make_response( 

62 flask.redirect("/login?next=" + flask.request.path) 

63 ) 

64 

65 response = param_redirect_capture(flask.request, response) 

66 

67 return response 

68 

69 account = flask.session.get("account") 

70 user = account["email"] 

71 

72 logger.info( 

73 f"User {user} login successfully", 

74 extra={ 

75 "datetime": date_str, 

76 "appid": "charmhub-io", 

77 "event": f"authn_login_successafterfail:{user}", 

78 }, 

79 ) 

80 

81 return func(*args, **kwargs) 

82 

83 return is_user_logged_in 

84 

85 

86def store_maintenance(func): 

87 """ 

88 Decorator that checks if the maintence mode is enabled 

89 """ 

90 

91 @functools.wraps(func) 

92 def is_store_in_maintenance(*args, **kwargs): 

93 # TODO: this will be a config option for the charm 

94 # or used from the app config using from_prefexed_env 

95 if strtobool(os.getenv("MAINTENANCE", "false")): 

96 return flask.render_template("maintenance.html") 

97 

98 return func(*args, **kwargs) 

99 

100 return is_store_in_maintenance 

101 

102 

103def redirect_uppercase_to_lowercase(func): 

104 """ 

105 Decorator that redirect package names containing upper case 

106 to the lower case URL 

107 

108 The route must have the entity_name parameter 

109 """ 

110 

111 @functools.wraps(func) 

112 def is_uppercase(*args, **kwargs): 

113 if "entity_name" in kwargs: 

114 name = kwargs["entity_name"] 

115 else: 

116 # For solutions - fallback to name if entity_name is not provided 

117 name = kwargs["name"] 

118 

119 ENV = os.getenv("ENVIRONMENT", "devel").strip() 

120 redirect = flask.request.url.lower() 

121 

122 if any(char.isupper() for char in name): 

123 if ( 

124 (ENV == "devel" and redirect.startswith("http://localhost:")) 

125 or ( 

126 ENV == "production" 

127 and redirect.startswith("https://charmhub.io/") 

128 ) 

129 or ( 

130 ENV == "staging" 

131 and redirect.startswith("https://staging.charmhub.io/") 

132 ) 

133 ): 

134 return flask.redirect(redirect) 

135 else: 

136 flask.abort(404) 

137 

138 return func(*args, **kwargs) 

139 

140 return is_uppercase