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

mozilla-releng / balrog / #5214

16 Feb 2026 01:14AM UTC coverage: 16.352% (-73.5%) from 89.9%
#5214

Pull #3672

circleci

renovate-bot
chore(deps): lock file maintenance (pep621)
Pull Request #3672: chore(deps): lock file maintenance (pep621)

267 of 2546 branches covered (10.49%)

Branch coverage included in aggregate %.

1169 of 6236 relevant lines covered (18.75%)

0.19 hits per line

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

5.88
/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 g, request
1✔
7
from sentry_sdk import capture_exception
1✔
8
from specsynthase.specbuilder import SpecBuilder
1✔
9
from statsd.defaults.env import statsd
1✔
10

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

19
log = logging.getLogger(__name__)
×
20

21
current_dir = path.dirname(__file__)
×
22
web_dir = path.dirname(auslib.web.__file__)
×
23

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

32
validator_map = {"body": BalrogRequestBodyValidator}
×
33

34

35
def should_time_request():
×
36
    # don't time OPTIONS requests
37
    if request.method == "OPTIONS":
×
38
        return False
×
39
    # don't time requests that don't match a valid route
40
    if request.url_rule is None:
×
41
        return False
×
42
    # don't time dockerflow endpoints
43
    if request.path.startswith("/__"):
×
44
        return False
×
45

46
    return True
×
47

48

49
def create_app():
×
50
    connexion_app = connexion.App(__name__, debug=False, options={"swagger_ui": False})
×
51
    connexion_app.add_api(spec, validator_map=validator_map, strict_validation=True)
×
52
    connexion_app.add_api(path.join(current_dir, "swagger", "api_v2.yml"), base_path="/v2", strict_validation=True, validate_responses=True)
×
53
    flask_app = connexion_app.app
×
54

55
    create_dockerflow_endpoints(flask_app)
×
56

57
    @flask_app.before_request
×
58
    def setup_statsd():
×
59
        g.statsd = statsd.pipeline()
×
60
        g.request_timer = None
×
61
        if should_time_request():
×
62
            # do some massaging to get the metric name right
63
            # * get rid of the `/v2` prefix on v2 endpoints added by `base_path` further up
64
            # * remove various module prefixes
65
            # * add a common prefix to ensure that we can mark these metrics as gauges for
66
            #   statsd
67
            metric = (
×
68
                request.url_rule.endpoint.removeprefix("/v2.")
69
                .removeprefix("/.")
70
                .removeprefix("auslib_web_admin_views_")
71
                .removeprefix("auslib_web_admin_")
72
                .removeprefix("auslib_web_common_")
73
            )
74
            metric = f"endpoint_{metric}"
×
75
            g.request_timer = g.statsd.timer(metric)
×
76
            g.request_timer.start()
×
77

78
    @flask_app.before_request
×
79
    def setup_request():
×
80
        if request.full_path.startswith("/v2"):
×
81
            from auslib.global_state import dbo
×
82

83
            request.transaction = dbo.begin()
×
84

85
            if request.method in ("POST", "PUT", "DELETE"):
×
86
                username = verified_userinfo(request, flask_app.config["AUTH_DOMAIN"], flask_app.config["AUTH_AUDIENCE"])["email"]
×
87
                if not username:
×
88
                    log.warning("Login Required")
×
89
                    return problem(401, "Unauthenticated", "Login Required")
×
90
                # Machine to machine accounts are identified by uninformative clientIds
91
                # In order to keep Balrog permissions more readable, we map them to
92
                # more useful usernames, which are stored in the app config.
93
                if "@" not in username:
×
94
                    username = flask_app.config["M2M_ACCOUNT_MAPPING"].get(username, username)
×
95
                # Even if the user has provided a valid access token, we don't want to assume
96
                # that person should be able to access Balrog (in case auth0 is not configured
97
                # to be restrictive enough.
98
                elif not dbo.isKnownUser(username):
×
99
                    log.warning("Authorization Required")
×
100
                    return problem(403, "Forbidden", "Authorization Required")
×
101

102
                request.username = username
×
103

104
    @flask_app.after_request
×
105
    def complete_request(response):
×
106
        if hasattr(request, "transaction"):
×
107
            try:
×
108
                if response.status_code >= 400:
×
109
                    request.transaction.rollback()
×
110
                else:
111
                    request.transaction.commit()
×
112
            finally:
113
                request.transaction.close()
×
114

115
        return response
×
116

117
    @flask_app.errorhandler(OutdatedDataError)
×
118
    def outdated_data_error(error):
×
119
        msg = "Couldn't perform the request %s. Outdated Data Version. old_data_version doesn't match current data_version" % request.method
×
120
        log.warning("Bad input: %s", msg)
×
121
        log.warning(error)
×
122
        return problem(400, "Bad Request", "OutdatedDataError", ext={"exception": msg})
×
123

124
    @flask_app.errorhandler(UpdateMergeError)
×
125
    def update_merge_error(error):
×
126
        msg = "Couldn't perform the request %s due to merge error. Is there a scheduled change that conflicts with yours?" % request.method
×
127
        log.warning("Bad input: %s", msg)
×
128
        log.warning(error)
×
129
        return problem(400, "Bad Request", "UpdateMergeError", ext={"exception": msg})
×
130

131
    @flask_app.errorhandler(ChangeScheduledError)
×
132
    def change_scheduled_error(error):
×
133
        msg = "Couldn't perform the request %s due a conflict with a scheduled change. " % request.method
×
134
        msg += str(error)
×
135
        log.warning("Bad input: %s", msg)
×
136
        log.warning(error)
×
137
        return problem(400, "Bad Request", "ChangeScheduledError", ext={"exception": msg})
×
138

139
    @flask_app.errorhandler(AuthError)
×
140
    def auth_error(error):
×
141
        msg = "Permission denied to perform the request. {}".format(error.error)
×
142
        log.warning(msg)
×
143
        return problem(error.status_code, "Forbidden", "PermissionDeniedError", ext={"exception": msg})
×
144

145
    @flask_app.errorhandler(BlobValidationError)
×
146
    def blob_validation_error(error):
×
147
        return problem(400, "Bad Request", "Invalid Blob", ext={"exception": error.errors})
×
148

149
    @flask_app.errorhandler(SignoffRequiredError)
×
150
    def signoff_required_error(error):
×
151
        return problem(400, "Bad Request", "Signoff Required", ext={"exception": f"{error}"})
×
152

153
    @flask_app.errorhandler(ReadOnlyError)
×
154
    def read_only_error(error):
×
155
        return problem(400, "Bad Request", "Read only", ext={"exception": f"{error}"})
×
156

157
    @flask_app.errorhandler(PermissionDeniedError)
×
158
    def permission_denied_error(error):
×
159
        return problem(403, "Forbidden", "Permission Denied", ext={"exception": f"{error}"})
×
160

161
    @flask_app.errorhandler(ValueError)
×
162
    def value_error(error):
×
163
        return problem(400, "Bad Request", "Unknown error", ext={"exception": f"{error}"})
×
164

165
    # Connexion's error handling sometimes breaks when parameters contain
166
    # unicode characters (https://github.com/zalando/connexion/issues/604).
167
    # To work around, we catch them and return a 400 (which is what Connexion
168
    # would do if it didn't hit this error).
169
    @flask_app.errorhandler(UnicodeEncodeError)
×
170
    def unicode(error):
×
171
        return problem(400, "Unicode Error", "Connexion was unable to parse some unicode data correctly.")
×
172

173
    @flask_app.errorhandler(Exception)
×
174
    def ise(error):
×
175
        capture_exception(error)
×
176
        log.exception("Caught ISE 500 error: %r", error)
×
177
        log.debug("Request path is: %s", request.path)
×
178
        log.debug("Request environment is: %s", request.environ)
×
179
        log.debug("Request headers are: %s", request.headers)
×
180
        return problem(500, "Internal Server Error", "Internal Server Error")
×
181

182
    @flask_app.after_request
×
183
    def add_security_headers(response):
×
184
        response.headers["X-Frame-Options"] = "DENY"
×
185
        response.headers["X-Content-Type-Options"] = "nosniff"
×
186
        response.headers["Strict-Transport-Security"] = flask_app.config.get("STRICT_TRANSPORT_SECURITY", "max-age=31536000;")
×
187
        response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"
×
188
        response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET, POST, PUT, DELETE"
×
189
        if "*" in flask_app.config["CORS_ORIGINS"]:
×
190
            response.headers["Access-Control-Allow-Origin"] = "*"
×
191
        elif "Origin" in request.headers and request.headers["Origin"] in flask_app.config["CORS_ORIGINS"]:
×
192
            response.headers["Access-Control-Allow-Origin"] = request.headers["Origin"]
×
193
        if re.match("^/ui/", request.path):
×
194
            # This enables swagger-ui to dynamically fetch and
195
            # load the swagger specification JSON file containing API definition and examples.
196
            response.headers["X-Frame-Options"] = "SAMEORIGIN"
×
197
        else:
198
            response.headers["Content-Security-Policy"] = flask_app.config.get("CONTENT_SECURITY_POLICY", "default-src 'none'; frame-ancestors 'none'")
×
199
        return response
×
200

201
    # this is specifically set-up last before after_request handlers are called
202
    # in reverse order of registering, and we want this one to be called first
203
    # to avoid it being skipped if another one raises an exception
204
    @flask_app.after_request
×
205
    def send_stats(response):
×
206
        if hasattr(g, "request_timer") and g.request_timer:
×
207
            g.request_timer.stop()
×
208
        g.statsd.send()
×
209

210
        return response
×
211

212
    return connexion_app
×
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