• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla-releng / balrog / #4654

30 Jun 2025 12:06PM UTC coverage: 89.343%. First build
#4654

Pull #3435

circleci

jcristau
web: rename app to flask_app

We have a Connexion app which wraps a Flask app, so having something
called `app` can get confusing.
Pull Request #3435: web: rename app to flask_app

3151 of 3673 branches covered (85.79%)

Branch coverage included in aggregate %.

33 of 35 new or added lines in 2 files covered. (94.29%)

5685 of 6217 relevant lines covered (91.44%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.02
/src/auslib/web/admin/base.py
1
import logging
1✔
2
import re
1✔
3
from os import path
1✔
4

5
import connexion
1✔
6
from flask import request
1✔
7
from flask_compress import Compress
1✔
8
from sentry_sdk import capture_exception
1✔
9
from specsynthase.specbuilder import SpecBuilder
1✔
10

11
import auslib
1✔
12
from auslib.dockerflow import create_dockerflow_endpoints
1✔
13
from auslib.errors import BlobValidationError, PermissionDeniedError, ReadOnlyError, SignoffRequiredError
1✔
14
from auslib.util.auth import verified_userinfo
1✔
15
from auslib.web.admin.views.problem import problem
1✔
16
from auslib.web.admin.views.validators import BalrogRequestBodyValidator
1✔
17

18
log = logging.getLogger(__name__)
1✔
19

20
current_dir = path.dirname(__file__)
1✔
21
web_dir = path.dirname(auslib.web.__file__)
1✔
22

23
spec = (
1✔
24
    SpecBuilder()
25
    .add_spec(path.join(current_dir, "swagger/api.yml"))
26
    .add_spec(path.join(web_dir, "common/swagger/definitions.yml"))
27
    .add_spec(path.join(web_dir, "common/swagger/parameters.yml"))
28
    .add_spec(path.join(web_dir, "common/swagger/responses.yml"))
29
)
30

31
validator_map = {"body": BalrogRequestBodyValidator}
1✔
32

33
connexion_app = connexion.App(__name__, debug=False, options={"swagger_ui": False})
1✔
34
connexion_app.add_api(spec, validator_map=validator_map, strict_validation=True)
1✔
35
connexion_app.add_api(path.join(current_dir, "swagger", "api_v2.yml"), base_path="/v2", strict_validation=True, validate_responses=True)
1✔
36
flask_app = connexion_app.app
1✔
37

38
create_dockerflow_endpoints(flask_app)
1✔
39

40

41
@flask_app.before_request
1✔
42
def setup_request():
1✔
43
    if request.full_path.startswith("/v2"):
1✔
44
        from auslib.global_state import dbo
1✔
45

46
        request.transaction = dbo.begin()
1✔
47

48
        if request.method in ("POST", "PUT", "DELETE"):
1✔
49
            username = verified_userinfo(request, flask_app.config["AUTH_DOMAIN"], flask_app.config["AUTH_AUDIENCE"])["email"]
1✔
50
            if not username:
1!
51
                log.warning("Login Required")
×
52
                return problem(401, "Unauthenticated", "Login Required")
×
53
            # Machine to machine accounts are identified by uninformative clientIds
54
            # In order to keep Balrog permissions more readable, we map them to
55
            # more useful usernames, which are stored in the app config.
56
            if "@" not in username:
1!
57
                username = flask_app.config["M2M_ACCOUNT_MAPPING"].get(username, username)
1✔
58
            # Even if the user has provided a valid access token, we don't want to assume
59
            # that person should be able to access Balrog (in case auth0 is not configured
60
            # to be restrictive enough.
61
            elif not dbo.isKnownUser(username):
×
62
                log.warning("Authorization Required")
×
63
                return problem(403, "Forbidden", "Authorization Required")
×
64

65
            request.username = username
1✔
66

67

68
@flask_app.after_request
1✔
69
def complete_request(response):
1✔
70
    if hasattr(request, "transaction"):
1✔
71
        try:
1✔
72
            if response.status_code >= 400:
1✔
73
                request.transaction.rollback()
1✔
74
            else:
75
                request.transaction.commit()
1✔
76
        finally:
77
            request.transaction.close()
1✔
78

79
    return response
1✔
80

81

82
@flask_app.errorhandler(BlobValidationError)
1✔
83
def blob_validation_error(error):
1✔
84
    return problem(400, "Bad Request", "Invalid Blob", ext={"exception": error.errors})
1✔
85

86

87
@flask_app.errorhandler(SignoffRequiredError)
1✔
88
def signoff_required_error(error):
1✔
89
    return problem(400, "Bad Request", "Signoff Required", ext={"exception": f"{error}"})
1✔
90

91

92
@flask_app.errorhandler(ReadOnlyError)
1✔
93
def read_only_error(error):
1✔
94
    return problem(400, "Bad Request", "Read only", ext={"exception": f"{error}"})
1✔
95

96

97
@flask_app.errorhandler(PermissionDeniedError)
1✔
98
def permission_denied_error(error):
1✔
99
    return problem(403, "Forbidden", "Permission Denied", ext={"exception": f"{error}"})
1✔
100

101

102
@flask_app.errorhandler(ValueError)
1✔
103
def value_error(error):
1✔
104
    return problem(400, "Bad Request", "Unknown error", ext={"exception": f"{error}"})
1✔
105

106

107
# Connexion's error handling sometimes breaks when parameters contain
108
# unicode characters (https://github.com/zalando/connexion/issues/604).
109
# To work around, we catch them and return a 400 (which is what Connexion
110
# would do if it didn't hit this error).
111
@flask_app.errorhandler(UnicodeEncodeError)
1✔
112
def unicode(error):
1✔
113
    return problem(400, "Unicode Error", "Connexion was unable to parse some unicode data correctly.")
×
114

115

116
@flask_app.errorhandler(Exception)
1✔
117
def ise(error):
1✔
118
    capture_exception(error)
1✔
119
    log.exception("Caught ISE 500 error: %r", error)
1✔
120
    log.debug("Request path is: %s", request.path)
1✔
121
    log.debug("Request environment is: %s", request.environ)
1✔
122
    log.debug("Request headers are: %s", request.headers)
1✔
123
    return problem(500, "Internal Server Error", "Internal Server Error")
1✔
124

125

126
@flask_app.after_request
1✔
127
def add_security_headers(response):
1✔
128
    response.headers["X-Frame-Options"] = "DENY"
1✔
129
    response.headers["X-Content-Type-Options"] = "nosniff"
1✔
130
    response.headers["Strict-Transport-Security"] = flask_app.config.get("STRICT_TRANSPORT_SECURITY", "max-age=31536000;")
1✔
131
    response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"
1✔
132
    response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET, POST, PUT, DELETE"
1✔
133
    if "*" in flask_app.config["CORS_ORIGINS"]:
1!
134
        response.headers["Access-Control-Allow-Origin"] = "*"
1✔
NEW
135
    elif "Origin" in request.headers and request.headers["Origin"] in flask_app.config["CORS_ORIGINS"]:
×
136
        response.headers["Access-Control-Allow-Origin"] = request.headers["Origin"]
×
137
    if re.match("^/ui/", request.path):
1!
138
        # This enables swagger-ui to dynamically fetch and
139
        # load the swagger specification JSON file containing API definition and examples.
140
        response.headers["X-Frame-Options"] = "SAMEORIGIN"
×
141
    else:
142
        response.headers["Content-Security-Policy"] = flask_app.config.get("CONTENT_SECURITY_POLICY", "default-src 'none'; frame-ancestors 'none'")
1✔
143
    return response
1✔
144

145

146
Compress(flask_app)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc