Skip to content

Commit eb82933

Browse files
authored
Merge pull request #3446 from jcristau/app-factory
Use a factory to create the app object
2 parents db45504 + 1cc2135 commit eb82933

File tree

11 files changed

+318
-342
lines changed

11 files changed

+318
-342
lines changed

src/auslib/web/admin/base.py

Lines changed: 132 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -30,146 +30,136 @@
3030

3131
validator_map = {"body": BalrogRequestBodyValidator}
3232

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

38-
create_dockerflow_endpoints(flask_app)
39-
40-
41-
@flask_app.before_request
42-
def setup_request():
43-
if request.full_path.startswith("/v2"):
44-
from auslib.global_state import dbo
45-
46-
request.transaction = dbo.begin()
47-
48-
if request.method in ("POST", "PUT", "DELETE"):
49-
username = verified_userinfo(request, flask_app.config["AUTH_DOMAIN"], flask_app.config["AUTH_AUDIENCE"])["email"]
50-
if not username:
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:
57-
username = flask_app.config["M2M_ACCOUNT_MAPPING"].get(username, username)
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
66-
67-
68-
@flask_app.after_request
69-
def complete_request(response):
70-
if hasattr(request, "transaction"):
71-
try:
72-
if response.status_code >= 400:
73-
request.transaction.rollback()
74-
else:
75-
request.transaction.commit()
76-
finally:
77-
request.transaction.close()
78-
79-
return response
80-
81-
82-
@flask_app.errorhandler(OutdatedDataError)
83-
def outdated_data_error(error):
84-
msg = "Couldn't perform the request %s. Outdated Data Version. old_data_version doesn't match current data_version" % request.method
85-
log.warning("Bad input: %s", msg)
86-
log.warning(error)
87-
return problem(400, "Bad Request", "OutdatedDataError", ext={"exception": msg})
88-
89-
90-
@flask_app.errorhandler(UpdateMergeError)
91-
def update_merge_error(error):
92-
msg = "Couldn't perform the request %s due to merge error. Is there a scheduled change that conflicts with yours?" % request.method
93-
log.warning("Bad input: %s", msg)
94-
log.warning(error)
95-
return problem(400, "Bad Request", "UpdateMergeError", ext={"exception": msg})
96-
97-
98-
@flask_app.errorhandler(ChangeScheduledError)
99-
def change_scheduled_error(error):
100-
msg = "Couldn't perform the request %s due a conflict with a scheduled change. " % request.method
101-
msg += str(error)
102-
log.warning("Bad input: %s", msg)
103-
log.warning(error)
104-
return problem(400, "Bad Request", "ChangeScheduledError", ext={"exception": msg})
105-
106-
107-
@flask_app.errorhandler(AuthError)
108-
def auth_error(error):
109-
msg = "Permission denied to perform the request. {}".format(error.error)
110-
log.warning(msg)
111-
return problem(error.status_code, "Forbidden", "PermissionDeniedError", ext={"exception": msg})
112-
113-
114-
@flask_app.errorhandler(BlobValidationError)
115-
def blob_validation_error(error):
116-
return problem(400, "Bad Request", "Invalid Blob", ext={"exception": error.errors})
117-
118-
119-
@flask_app.errorhandler(SignoffRequiredError)
120-
def signoff_required_error(error):
121-
return problem(400, "Bad Request", "Signoff Required", ext={"exception": f"{error}"})
122-
123-
124-
@flask_app.errorhandler(ReadOnlyError)
125-
def read_only_error(error):
126-
return problem(400, "Bad Request", "Read only", ext={"exception": f"{error}"})
127-
128-
129-
@flask_app.errorhandler(PermissionDeniedError)
130-
def permission_denied_error(error):
131-
return problem(403, "Forbidden", "Permission Denied", ext={"exception": f"{error}"})
132-
133-
134-
@flask_app.errorhandler(ValueError)
135-
def value_error(error):
136-
return problem(400, "Bad Request", "Unknown error", ext={"exception": f"{error}"})
137-
138-
139-
# Connexion's error handling sometimes breaks when parameters contain
140-
# unicode characters (https://github.com/zalando/connexion/issues/604).
141-
# To work around, we catch them and return a 400 (which is what Connexion
142-
# would do if it didn't hit this error).
143-
@flask_app.errorhandler(UnicodeEncodeError)
144-
def unicode(error):
145-
return problem(400, "Unicode Error", "Connexion was unable to parse some unicode data correctly.")
146-
147-
148-
@flask_app.errorhandler(Exception)
149-
def ise(error):
150-
capture_exception(error)
151-
log.exception("Caught ISE 500 error: %r", error)
152-
log.debug("Request path is: %s", request.path)
153-
log.debug("Request environment is: %s", request.environ)
154-
log.debug("Request headers are: %s", request.headers)
155-
return problem(500, "Internal Server Error", "Internal Server Error")
156-
157-
158-
@flask_app.after_request
159-
def add_security_headers(response):
160-
response.headers["X-Frame-Options"] = "DENY"
161-
response.headers["X-Content-Type-Options"] = "nosniff"
162-
response.headers["Strict-Transport-Security"] = flask_app.config.get("STRICT_TRANSPORT_SECURITY", "max-age=31536000;")
163-
response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"
164-
response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET, POST, PUT, DELETE"
165-
if "*" in flask_app.config["CORS_ORIGINS"]:
166-
response.headers["Access-Control-Allow-Origin"] = "*"
167-
elif "Origin" in request.headers and request.headers["Origin"] in flask_app.config["CORS_ORIGINS"]:
168-
response.headers["Access-Control-Allow-Origin"] = request.headers["Origin"]
169-
if re.match("^/ui/", request.path):
170-
# This enables swagger-ui to dynamically fetch and
171-
# load the swagger specification JSON file containing API definition and examples.
172-
response.headers["X-Frame-Options"] = "SAMEORIGIN"
173-
else:
174-
response.headers["Content-Security-Policy"] = flask_app.config.get("CONTENT_SECURITY_POLICY", "default-src 'none'; frame-ancestors 'none'")
175-
return response
34+
def create_app():
35+
connexion_app = connexion.App(__name__, debug=False, options={"swagger_ui": False})
36+
connexion_app.add_api(spec, validator_map=validator_map, strict_validation=True)
37+
connexion_app.add_api(path.join(current_dir, "swagger", "api_v2.yml"), base_path="/v2", strict_validation=True, validate_responses=True)
38+
flask_app = connexion_app.app
39+
40+
create_dockerflow_endpoints(flask_app)
41+
42+
@flask_app.before_request
43+
def setup_request():
44+
if request.full_path.startswith("/v2"):
45+
from auslib.global_state import dbo
46+
47+
request.transaction = dbo.begin()
48+
49+
if request.method in ("POST", "PUT", "DELETE"):
50+
username = verified_userinfo(request, flask_app.config["AUTH_DOMAIN"], flask_app.config["AUTH_AUDIENCE"])["email"]
51+
if not username:
52+
log.warning("Login Required")
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:
58+
username = flask_app.config["M2M_ACCOUNT_MAPPING"].get(username, username)
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.
62+
elif not dbo.isKnownUser(username):
63+
log.warning("Authorization Required")
64+
return problem(403, "Forbidden", "Authorization Required")
65+
66+
request.username = username
67+
68+
@flask_app.after_request
69+
def complete_request(response):
70+
if hasattr(request, "transaction"):
71+
try:
72+
if response.status_code >= 400:
73+
request.transaction.rollback()
74+
else:
75+
request.transaction.commit()
76+
finally:
77+
request.transaction.close()
78+
79+
return response
80+
81+
@flask_app.errorhandler(OutdatedDataError)
82+
def outdated_data_error(error):
83+
msg = "Couldn't perform the request %s. Outdated Data Version. old_data_version doesn't match current data_version" % request.method
84+
log.warning("Bad input: %s", msg)
85+
log.warning(error)
86+
return problem(400, "Bad Request", "OutdatedDataError", ext={"exception": msg})
87+
88+
@flask_app.errorhandler(UpdateMergeError)
89+
def update_merge_error(error):
90+
msg = "Couldn't perform the request %s due to merge error. Is there a scheduled change that conflicts with yours?" % request.method
91+
log.warning("Bad input: %s", msg)
92+
log.warning(error)
93+
return problem(400, "Bad Request", "UpdateMergeError", ext={"exception": msg})
94+
95+
@flask_app.errorhandler(ChangeScheduledError)
96+
def change_scheduled_error(error):
97+
msg = "Couldn't perform the request %s due a conflict with a scheduled change. " % request.method
98+
msg += str(error)
99+
log.warning("Bad input: %s", msg)
100+
log.warning(error)
101+
return problem(400, "Bad Request", "ChangeScheduledError", ext={"exception": msg})
102+
103+
@flask_app.errorhandler(AuthError)
104+
def auth_error(error):
105+
msg = "Permission denied to perform the request. {}".format(error.error)
106+
log.warning(msg)
107+
return problem(error.status_code, "Forbidden", "PermissionDeniedError", ext={"exception": msg})
108+
109+
@flask_app.errorhandler(BlobValidationError)
110+
def blob_validation_error(error):
111+
return problem(400, "Bad Request", "Invalid Blob", ext={"exception": error.errors})
112+
113+
@flask_app.errorhandler(SignoffRequiredError)
114+
def signoff_required_error(error):
115+
return problem(400, "Bad Request", "Signoff Required", ext={"exception": f"{error}"})
116+
117+
@flask_app.errorhandler(ReadOnlyError)
118+
def read_only_error(error):
119+
return problem(400, "Bad Request", "Read only", ext={"exception": f"{error}"})
120+
121+
@flask_app.errorhandler(PermissionDeniedError)
122+
def permission_denied_error(error):
123+
return problem(403, "Forbidden", "Permission Denied", ext={"exception": f"{error}"})
124+
125+
@flask_app.errorhandler(ValueError)
126+
def value_error(error):
127+
return problem(400, "Bad Request", "Unknown error", ext={"exception": f"{error}"})
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)
134+
def unicode(error):
135+
return problem(400, "Unicode Error", "Connexion was unable to parse some unicode data correctly.")
136+
137+
@flask_app.errorhandler(Exception)
138+
def ise(error):
139+
capture_exception(error)
140+
log.exception("Caught ISE 500 error: %r", error)
141+
log.debug("Request path is: %s", request.path)
142+
log.debug("Request environment is: %s", request.environ)
143+
log.debug("Request headers are: %s", request.headers)
144+
return problem(500, "Internal Server Error", "Internal Server Error")
145+
146+
@flask_app.after_request
147+
def add_security_headers(response):
148+
response.headers["X-Frame-Options"] = "DENY"
149+
response.headers["X-Content-Type-Options"] = "nosniff"
150+
response.headers["Strict-Transport-Security"] = flask_app.config.get("STRICT_TRANSPORT_SECURITY", "max-age=31536000;")
151+
response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"
152+
response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET, POST, PUT, DELETE"
153+
if "*" in flask_app.config["CORS_ORIGINS"]:
154+
response.headers["Access-Control-Allow-Origin"] = "*"
155+
elif "Origin" in request.headers and request.headers["Origin"] in flask_app.config["CORS_ORIGINS"]:
156+
response.headers["Access-Control-Allow-Origin"] = request.headers["Origin"]
157+
if re.match("^/ui/", request.path):
158+
# This enables swagger-ui to dynamically fetch and
159+
# load the swagger specification JSON file containing API definition and examples.
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'")
163+
return response
164+
165+
return connexion_app

0 commit comments

Comments
 (0)