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

mozilla-releng / balrog / #4712

04 Jul 2025 04:22PM UTC coverage: 89.171%. First build
#4712

Pull #3446

circleci

jcristau
public: use a factory to create the app object
Pull Request #3446: Use a factory to create the app object

3015 of 3535 branches covered (85.29%)

Branch coverage included in aggregate %.

129 of 146 new or added lines in 2 files covered. (88.36%)

5623 of 6152 relevant lines covered (91.4%)

0.91 hits per line

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

89.63
/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 sentry_sdk import capture_exception
1✔
8
from specsynthase.specbuilder import SpecBuilder
1✔
9

10
import auslib
1✔
11
from auslib.db import ChangeScheduledError, OutdatedDataError, UpdateMergeError
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 AuthError, 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

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

40
    create_dockerflow_endpoints(flask_app)
1✔
41

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

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

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

66
                request.username = username
1✔
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
    @flask_app.errorhandler(OutdatedDataError)
1✔
82
    def outdated_data_error(error):
1✔
83
        msg = "Couldn't perform the request %s. Outdated Data Version. old_data_version doesn't match current data_version" % request.method
1✔
84
        log.warning("Bad input: %s", msg)
1✔
85
        log.warning(error)
1✔
86
        return problem(400, "Bad Request", "OutdatedDataError", ext={"exception": msg})
1✔
87

88
    @flask_app.errorhandler(UpdateMergeError)
1✔
89
    def update_merge_error(error):
1✔
90
        msg = "Couldn't perform the request %s due to merge error. Is there a scheduled change that conflicts with yours?" % request.method
1✔
91
        log.warning("Bad input: %s", msg)
1✔
92
        log.warning(error)
1✔
93
        return problem(400, "Bad Request", "UpdateMergeError", ext={"exception": msg})
1✔
94

95
    @flask_app.errorhandler(ChangeScheduledError)
1✔
96
    def change_scheduled_error(error):
1✔
97
        msg = "Couldn't perform the request %s due a conflict with a scheduled change. " % request.method
1✔
98
        msg += str(error)
1✔
99
        log.warning("Bad input: %s", msg)
1✔
100
        log.warning(error)
1✔
101
        return problem(400, "Bad Request", "ChangeScheduledError", ext={"exception": msg})
1✔
102

103
    @flask_app.errorhandler(AuthError)
1✔
104
    def auth_error(error):
1✔
105
        msg = "Permission denied to perform the request. {}".format(error.error)
1✔
106
        log.warning(msg)
1✔
107
        return problem(error.status_code, "Forbidden", "PermissionDeniedError", ext={"exception": msg})
1✔
108

109
    @flask_app.errorhandler(BlobValidationError)
1✔
110
    def blob_validation_error(error):
1✔
111
        return problem(400, "Bad Request", "Invalid Blob", ext={"exception": error.errors})
1✔
112

113
    @flask_app.errorhandler(SignoffRequiredError)
1✔
114
    def signoff_required_error(error):
1✔
115
        return problem(400, "Bad Request", "Signoff Required", ext={"exception": f"{error}"})
1✔
116

117
    @flask_app.errorhandler(ReadOnlyError)
1✔
118
    def read_only_error(error):
1✔
119
        return problem(400, "Bad Request", "Read only", ext={"exception": f"{error}"})
1✔
120

121
    @flask_app.errorhandler(PermissionDeniedError)
1✔
122
    def permission_denied_error(error):
1✔
123
        return problem(403, "Forbidden", "Permission Denied", ext={"exception": f"{error}"})
1✔
124

125
    @flask_app.errorhandler(ValueError)
1✔
126
    def value_error(error):
1✔
127
        return problem(400, "Bad Request", "Unknown error", ext={"exception": f"{error}"})
1✔
128

129
    # Connexion's error handling sometimes breaks when parameters contain
130
    # unicode characters (https://github.com/zalando/connexion/issues/604).
131
    # To work around, we catch them and return a 400 (which is what Connexion
132
    # would do if it didn't hit this error).
133
    @flask_app.errorhandler(UnicodeEncodeError)
1✔
134
    def unicode(error):
1✔
NEW
135
        return problem(400, "Unicode Error", "Connexion was unable to parse some unicode data correctly.")
×
136

137
    @flask_app.errorhandler(Exception)
1✔
138
    def ise(error):
1✔
139
        capture_exception(error)
1✔
140
        log.exception("Caught ISE 500 error: %r", error)
1✔
141
        log.debug("Request path is: %s", request.path)
1✔
142
        log.debug("Request environment is: %s", request.environ)
1✔
143
        log.debug("Request headers are: %s", request.headers)
1✔
144
        return problem(500, "Internal Server Error", "Internal Server Error")
1✔
145

146
    @flask_app.after_request
1✔
147
    def add_security_headers(response):
1✔
148
        response.headers["X-Frame-Options"] = "DENY"
1✔
149
        response.headers["X-Content-Type-Options"] = "nosniff"
1✔
150
        response.headers["Strict-Transport-Security"] = flask_app.config.get("STRICT_TRANSPORT_SECURITY", "max-age=31536000;")
1✔
151
        response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"
1✔
152
        response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET, POST, PUT, DELETE"
1✔
153
        if "*" in flask_app.config["CORS_ORIGINS"]:
1!
154
            response.headers["Access-Control-Allow-Origin"] = "*"
1✔
NEW
155
        elif "Origin" in request.headers and request.headers["Origin"] in flask_app.config["CORS_ORIGINS"]:
×
NEW
156
            response.headers["Access-Control-Allow-Origin"] = request.headers["Origin"]
×
157
        if re.match("^/ui/", request.path):
1!
158
            # This enables swagger-ui to dynamically fetch and
159
            # load the swagger specification JSON file containing API definition and examples.
NEW
160
            response.headers["X-Frame-Options"] = "SAMEORIGIN"
×
161
        else:
162
            response.headers["Content-Security-Policy"] = flask_app.config.get("CONTENT_SECURITY_POLICY", "default-src 'none'; frame-ancestors 'none'")
1✔
163
        return response
1✔
164

165
    return connexion_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