def swagger_route_data(include_internal=False, compact=False): def swagger_parameter(name, description, kind="path", param_type="string", required=True, enum=None, schema=None): # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#parameterObject parameter_info = {"name": name, "in": kind, "required": required} if not compact: parameter_info["description"] = description or "" if schema: parameter_info["schema"] = {"$ref": "#/definitions/%s" % schema} else: parameter_info["type"] = param_type if enum is not None and len(list(enum)) > 0: parameter_info["enum"] = list(enum) return parameter_info paths = {} models = {} tags = [] tags_added = set() operationIds = set() for rule in app.url_map.iter_rules(): endpoint_method = app.view_functions[rule.endpoint] # Verify that we have a view class for this API method. if not "view_class" in dir(endpoint_method): continue view_class = endpoint_method.view_class # Hide the class if it is internal. internal = method_metadata(view_class, "internal") if not include_internal and internal: continue # Build the tag. parts = fully_qualified_name(view_class).split(".") tag_name = parts[-2] if not tag_name in tags_added: tags_added.add(tag_name) tags.append({ "name": tag_name, "description": (sys.modules[view_class.__module__].__doc__ or "").strip(), }) # Build the Swagger data for the path. swagger_path = PARAM_REGEX.sub(r"{\2}", rule.rule) full_name = fully_qualified_name(view_class) path_swagger = { "x-name": full_name, "x-path": swagger_path, "x-tag": tag_name } if include_internal: related_user_res = method_metadata(view_class, "related_user_resource") if related_user_res is not None: path_swagger["x-user-related"] = fully_qualified_name( related_user_res) paths[swagger_path] = path_swagger # Add any global path parameters. param_data_map = (view_class.__api_path_params if "__api_path_params" in dir(view_class) else {}) if param_data_map: path_parameters_swagger = [] for path_parameter in param_data_map: description = param_data_map[path_parameter].get("description") path_parameters_swagger.append( swagger_parameter(path_parameter, description)) path_swagger["parameters"] = path_parameters_swagger # Add the individual HTTP operations. method_names = list(rule.methods.difference(["HEAD", "OPTIONS"])) for method_name in method_names: # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#operation-object method = getattr(view_class, method_name.lower(), None) if method is None: logger.debug("Unable to find method for %s in class %s", method_name, view_class) continue _operationId = method_metadata(method, "nickname") if isinstance(_operationId, list): operationId = None for oid in _operationId: if oid in operationIds: continue else: operationId = oid break if operationId is None: raise Exception("Duplicate operation Id: %s" % operationId) else: operationId = _operationId operation_swagger = { "operationId": operationId, "parameters": [], } if operationId is None: continue if operationId in operationIds: raise Exception("Duplicate operation Id: %s" % operationId) operationIds.add(operationId) if not compact: operation_swagger.update({ "description": method.__doc__.strip() if method.__doc__ else "", "tags": [tag_name], }) # Mark the method as internal. internal = method_metadata(method, "internal") if internal is not None: operation_swagger["x-internal"] = True if include_internal: requires_fresh_login = method_metadata(method, "requires_fresh_login") if requires_fresh_login is not None: operation_swagger["x-requires-fresh-login"] = True # Add the path parameters. if rule.arguments: for path_parameter in rule.arguments: description = param_data_map.get(path_parameter, {}).get("description") operation_swagger["parameters"].append( swagger_parameter(path_parameter, description)) # Add the query parameters. if "__api_query_params" in dir(method): for query_parameter_info in method.__api_query_params: name = query_parameter_info["name"] description = query_parameter_info["help"] param_type = TYPE_CONVERTER[query_parameter_info["type"]] required = query_parameter_info["required"] operation_swagger["parameters"].append( swagger_parameter( name, description, kind="query", param_type=param_type, required=required, enum=query_parameter_info["choices"], )) # Add the OAuth security block. # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#securityRequirementObject scope = method_metadata(method, "oauth2_scope") if scope and not compact: operation_swagger["security"] = [{ "oauth2_implicit": [scope.scope] }] # Add the responses block. # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#responsesObject response_schema_name = method_metadata(method, "response_schema") if not compact: if response_schema_name: models[response_schema_name] = view_class.schemas[ response_schema_name] models["ApiError"] = { "type": "object", "properties": { "status": { "type": "integer", "description": "Status code of the response.", }, "type": { "type": "string", "description": "Reference to the type of the error.", }, "detail": { "type": "string", "description": "Details about the specific instance of the error.", }, "title": { "type": "string", "description": "Unique error code to identify the type of error.", }, "error_message": { "type": "string", "description": "Deprecated; alias for detail", }, "error_type": { "type": "string", "description": "Deprecated; alias for detail", }, }, "required": [ "status", "type", "title", ], } responses = { "400": { "description": "Bad Request", }, "401": { "description": "Session required", }, "403": { "description": "Unauthorized access", }, "404": { "description": "Not found", }, } for _, body in list(responses.items()): body["schema"] = {"$ref": "#/definitions/ApiError"} if method_name == "DELETE": responses["204"] = {"description": "Deleted"} elif method_name == "POST": responses["201"] = {"description": "Successful creation"} else: responses["200"] = {"description": "Successful invocation"} if response_schema_name: responses["200"]["schema"] = { "$ref": "#/definitions/%s" % response_schema_name } operation_swagger["responses"] = responses # Add the request block. request_schema_name = method_metadata(method, "request_schema") if request_schema_name and not compact: models[request_schema_name] = view_class.schemas[ request_schema_name] operation_swagger["parameters"].append( swagger_parameter("body", "Request body contents.", kind="body", schema=request_schema_name)) # Add the operation to the parent path. if not internal or (internal and include_internal): path_swagger[method_name.lower()] = operation_swagger tags.sort(key=lambda t: t["name"]) paths = OrderedDict( sorted(list(paths.items()), key=lambda p: p[1]["x-tag"])) if compact: return {"paths": paths} swagger_data = { "swagger": "2.0", "host": SERVER_HOSTNAME, "basePath": "/", "schemes": [PREFERRED_URL_SCHEME], "info": { "version": "v1", "title": "Quay Frontend", "description": ("This API allows you to perform many of the operations required to work " "with Quay repositories, users, and organizations. You can find out more " 'at <a href="https://quay.io">Quay</a>.'), "termsOfService": "https://quay.io/tos", "contact": { "email": "*****@*****.**" }, }, "securityDefinitions": { "oauth2_implicit": { "type": "oauth2", "flow": "implicit", "authorizationUrl": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME), "scopes": { scope.scope: scope.description for scope in list(scopes.app_scopes(app.config).values()) }, }, }, "paths": paths, "definitions": models, "tags": tags, } return swagger_data
def render_page_template(name, route_data=None, **kwargs): """ Renders the page template with the given name as the response and returns its contents. """ main_scripts = _list_files("build", "js", JS_BUNDLE_NAME) use_cdn = app.config.get("USE_CDN", True) if request.args.get("use_cdn") is not None: use_cdn = request.args.get("use_cdn") == "true" external_styles = get_external_css(local=not use_cdn, exclude=FONT_AWESOME_5) external_scripts = get_external_javascript(local=not use_cdn) # Add Stripe checkout if billing is enabled. if features.BILLING: external_scripts.append("//checkout.stripe.com/checkout.js") def get_external_login_config(): login_config = [] for login_service in oauth_login.services: login_config.append({ "id": login_service.service_id(), "title": login_service.service_name(), "config": login_service.get_public_config(), "icon": login_service.get_icon(), }) return login_config def get_oauth_config(): oauth_config = {} for oauth_app in oauth_apps: oauth_config[oauth_app.key_name] = oauth_app.get_public_config() return oauth_config has_contact = len(app.config.get("CONTACT_INFO", [])) > 0 contact_href = None if len(app.config.get("CONTACT_INFO", [])) == 1: contact_href = app.config["CONTACT_INFO"][0] version_number = "" if not features.BILLING: version_number = "Quay %s" % __version__ scopes_set = { scope.scope: scope._asdict() for scope in scopes.app_scopes(app.config).values() } contents = render_template( name, registry_state=app.config.get("REGISTRY_STATE", "normal"), route_data=route_data, external_styles=external_styles, external_scripts=external_scripts, main_scripts=main_scripts, feature_set=features.get_features(), config_set=frontend_visible_config(app.config), oauth_set=get_oauth_config(), external_login_set=get_external_login_config(), scope_set=scopes_set, vuln_priority_set=PRIORITY_LEVELS, mixpanel_key=app.config.get("MIXPANEL_KEY", ""), munchkin_key=app.config.get("MARKETO_MUNCHKIN_ID", ""), recaptcha_key=app.config.get("RECAPTCHA_SITE_KEY", ""), google_tagmanager_key=app.config.get("GOOGLE_TAGMANAGER_KEY", ""), google_anaytics_key=app.config.get("GOOGLE_ANALYTICS_KEY", ""), sentry_public_dsn=app.config.get("SENTRY_PUBLIC_DSN", ""), is_debug=str(app.config.get("DEBUGGING", False)).lower(), aci_conversion=features.ACI_CONVERSION, has_billing=features.BILLING, onprem=not app.config.get("FEATURE_BILLING", False), contact_href=contact_href, has_contact=has_contact, hostname=app.config["SERVER_HOSTNAME"], preferred_scheme=app.config["PREFERRED_URL_SCHEME"], version_number=version_number, current_year=datetime.datetime.now().year, kubernetes_namespace=IS_KUBERNETES and QE_NAMESPACE, **kwargs) resp = make_response(contents) resp.headers["X-FRAME-OPTIONS"] = "DENY" return resp
def swagger_route_data(include_internal=False, compact=False): def swagger_parameter(name, description, kind='path', param_type='string', required=True, enum=None, schema=None): # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#parameterObject parameter_info = {'name': name, 'in': kind, 'required': required} if not compact: parameter_info['description'] = description or '' if schema: parameter_info['schema'] = {'$ref': '#/definitions/%s' % schema} else: parameter_info['type'] = param_type if enum is not None and len(list(enum)) > 0: parameter_info['enum'] = list(enum) return parameter_info paths = {} models = {} tags = [] tags_added = set() operationIds = set() for rule in app.url_map.iter_rules(): endpoint_method = app.view_functions[rule.endpoint] # Verify that we have a view class for this API method. if not 'view_class' in dir(endpoint_method): continue view_class = endpoint_method.view_class # Hide the class if it is internal. internal = method_metadata(view_class, 'internal') if not include_internal and internal: continue # Build the tag. parts = fully_qualified_name(view_class).split('.') tag_name = parts[-2] if not tag_name in tags_added: tags_added.add(tag_name) tags.append({ 'name': tag_name, 'description': (sys.modules[view_class.__module__].__doc__ or '').strip() }) # Build the Swagger data for the path. swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule) full_name = fully_qualified_name(view_class) path_swagger = { 'x-name': full_name, 'x-path': swagger_path, 'x-tag': tag_name } if include_internal: related_user_res = method_metadata(view_class, 'related_user_resource') if related_user_res is not None: path_swagger['x-user-related'] = fully_qualified_name( related_user_res) paths[swagger_path] = path_swagger # Add any global path parameters. param_data_map = view_class.__api_path_params if '__api_path_params' in dir( view_class) else {} if param_data_map: path_parameters_swagger = [] for path_parameter in param_data_map: description = param_data_map[path_parameter].get('description') path_parameters_swagger.append( swagger_parameter(path_parameter, description)) path_swagger['parameters'] = path_parameters_swagger # Add the individual HTTP operations. method_names = list(rule.methods.difference(['HEAD', 'OPTIONS'])) for method_name in method_names: # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#operation-object method = getattr(view_class, method_name.lower(), None) if method is None: logger.debug('Unable to find method for %s in class %s', method_name, view_class) continue operationId = method_metadata(method, 'nickname') operation_swagger = { 'operationId': operationId, 'parameters': [], } if operationId is None: continue if operationId in operationIds: raise Exception('Duplicate operation Id: %s' % operationId) operationIds.add(operationId) if not compact: operation_swagger.update({ 'description': method.__doc__.strip() if method.__doc__ else '', 'tags': [tag_name] }) # Mark the method as internal. internal = method_metadata(method, 'internal') if internal is not None: operation_swagger['x-internal'] = True if include_internal: requires_fresh_login = method_metadata(method, 'requires_fresh_login') if requires_fresh_login is not None: operation_swagger['x-requires-fresh-login'] = True # Add the path parameters. if rule.arguments: for path_parameter in rule.arguments: description = param_data_map.get(path_parameter, {}).get('description') operation_swagger['parameters'].append( swagger_parameter(path_parameter, description)) # Add the query parameters. if '__api_query_params' in dir(method): for query_parameter_info in method.__api_query_params: name = query_parameter_info['name'] description = query_parameter_info['help'] param_type = TYPE_CONVERTER[query_parameter_info['type']] required = query_parameter_info['required'] operation_swagger['parameters'].append( swagger_parameter( name, description, kind='query', param_type=param_type, required=required, enum=query_parameter_info['choices'])) # Add the OAuth security block. # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#securityRequirementObject scope = method_metadata(method, 'oauth2_scope') if scope and not compact: operation_swagger['security'] = [{ 'oauth2_implicit': [scope.scope] }] # Add the responses block. # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#responsesObject response_schema_name = method_metadata(method, 'response_schema') if not compact: if response_schema_name: models[response_schema_name] = view_class.schemas[ response_schema_name] models['ApiError'] = { 'type': 'object', 'properties': { 'status': { 'type': 'integer', 'description': 'Status code of the response.' }, 'type': { 'type': 'string', 'description': 'Reference to the type of the error.' }, 'detail': { 'type': 'string', 'description': 'Details about the specific instance of the error.' }, 'title': { 'type': 'string', 'description': 'Unique error code to identify the type of error.' }, 'error_message': { 'type': 'string', 'description': 'Deprecated; alias for detail' }, 'error_type': { 'type': 'string', 'description': 'Deprecated; alias for detail' } }, 'required': [ 'status', 'type', 'title', ] } responses = { '400': { 'description': 'Bad Request', }, '401': { 'description': 'Session required', }, '403': { 'description': 'Unauthorized access', }, '404': { 'description': 'Not found', }, } for _, body in responses.items(): body['schema'] = {'$ref': '#/definitions/ApiError'} if method_name == 'DELETE': responses['204'] = {'description': 'Deleted'} elif method_name == 'POST': responses['201'] = {'description': 'Successful creation'} else: responses['200'] = {'description': 'Successful invocation'} if response_schema_name: responses['200']['schema'] = { '$ref': '#/definitions/%s' % response_schema_name } operation_swagger['responses'] = responses # Add the request block. request_schema_name = method_metadata(method, 'request_schema') if request_schema_name and not compact: models[request_schema_name] = view_class.schemas[ request_schema_name] operation_swagger['parameters'].append( swagger_parameter('body', 'Request body contents.', kind='body', schema=request_schema_name)) # Add the operation to the parent path. if not internal or (internal and include_internal): path_swagger[method_name.lower()] = operation_swagger tags.sort(key=lambda t: t['name']) paths = OrderedDict(sorted(paths.items(), key=lambda p: p[1]['x-tag'])) if compact: return {'paths': paths} swagger_data = { 'swagger': '2.0', 'host': SERVER_HOSTNAME, 'basePath': '/', 'schemes': [PREFERRED_URL_SCHEME], 'info': { 'version': 'v1', 'title': 'Quay Frontend', 'description': ('This API allows you to perform many of the operations required to work ' 'with Quay repositories, users, and organizations. You can find out more ' 'at <a href="https://quay.io">Quay</a>.'), 'termsOfService': 'https://quay.io/tos', 'contact': { 'email': '*****@*****.**' } }, 'securityDefinitions': { 'oauth2_implicit': { "type": "oauth2", "flow": "implicit", "authorizationUrl": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME), 'scopes': { scope.scope: scope.description for scope in scopes.app_scopes(app.config).values() }, }, }, 'paths': paths, 'definitions': models, 'tags': tags } return swagger_data