1
0
mirror of https://github.com/quay/quay.git synced 2026-01-27 18:42:52 +03:00
Files
quay/endpoints/web.py
2019-12-02 12:23:08 -05:00

936 lines
28 KiB
Python

import os
import json
import logging
from datetime import timedelta, datetime
from cachetools.func import lru_cache
from flask import (
abort,
redirect,
request,
url_for,
make_response,
Response,
render_template,
Blueprint,
jsonify,
send_file,
session,
)
from flask_login import current_user
import features
from app import (
app,
billing as stripe,
build_logs,
avatar,
signer,
log_archive,
config_provider,
get_app_url,
instance_keys,
user_analytics,
storage,
)
from auth import scopes
from auth.auth_context import get_authenticated_user
from auth.basic import has_basic_auth
from auth.decorators import require_session_login, process_oauth, process_auth_or_cookie
from auth.permissions import (
AdministerOrganizationPermission,
ReadRepositoryPermission,
SuperUserPermission,
AdministerRepositoryPermission,
ModifyRepositoryPermission,
OrganizationMemberPermission,
)
from buildtrigger.basehandler import BuildTriggerHandler
from buildtrigger.bitbuckethandler import BitbucketBuildTrigger
from buildtrigger.customhandler import CustomBuildTrigger
from buildtrigger.triggerutil import TriggerProviderException
from data import model
from data.database import db, RepositoryTag, TagToRepositoryTag
from endpoints.api.discovery import swagger_route_data
from endpoints.common import common_login, render_page_template
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
from endpoints.decorators import (
anon_protect,
anon_allowed,
route_show_if,
parse_repository_name,
param_required,
)
from health.healthcheck import get_healthchecker
from util.cache import no_cache
from util.headers import parse_basic_auth
from util.invoice import renderInvoiceToPdf
from util.saas.useranalytics import build_error_callback
from util.useremails import send_email_changed
from util.registry.gzipinputstream import GzipInputStream
from util.request import get_request_ip
from _init import ROOT_DIR
PGP_KEY_MIMETYPE = "application/pgp-keys"
@lru_cache(maxsize=1)
def _get_route_data():
return swagger_route_data(include_internal=True, compact=True)
def render_page_template_with_routedata(name, *args, **kwargs):
return render_page_template(name, _get_route_data(), *args, **kwargs)
# Capture the unverified SSL errors.
logger = logging.getLogger(__name__)
logging.captureWarnings(True)
web = Blueprint("web", __name__)
STATUS_TAGS = app.config["STATUS_TAGS"]
@web.route("/", methods=["GET"], defaults={"path": ""})
@no_cache
def index(path, **kwargs):
return render_page_template_with_routedata("index.html", **kwargs)
@web.route("/_internal_ping")
@anon_allowed
def internal_ping():
return make_response("true", 200)
@web.route("/500", methods=["GET"])
def internal_error_display():
return render_page_template_with_routedata("500.html")
@web.errorhandler(404)
@web.route("/404", methods=["GET"])
def not_found_error_display(e=None):
resp = index("", error_code=404, error_info=dict(reason="notfound"))
resp.status_code = 404
return resp
@web.route("/opensearch.xml")
def opensearch():
template = render_template(
"opensearch.xml",
baseurl=get_app_url(),
registry_title=app.config.get("REGISTRY_TITLE", "Quay"),
)
resp = make_response(template)
resp.headers["Content-Type"] = "application/xml"
return resp
@web.route("/organization/<path:path>", methods=["GET"])
@web.route("/organization/<path:path>/", methods=["GET"])
@no_cache
def org_view(path):
return index("")
@web.route("/user/<path:path>", methods=["GET"])
@web.route("/user/<path:path>/", methods=["GET"])
@no_cache
def user_view(path):
return index("")
@route_show_if(features.ACI_CONVERSION)
@web.route("/aci-signing-key")
@no_cache
@anon_protect
def aci_signing_key():
if not signer.name:
abort(404)
return send_file(signer.open_public_key_file(), mimetype=PGP_KEY_MIMETYPE)
@web.route("/plans/")
@no_cache
@route_show_if(features.BILLING)
def plans():
return index("")
@web.route("/search")
@no_cache
def search():
return index("")
@web.route("/guide/")
@no_cache
def guide():
return index("")
@web.route("/tour/")
@web.route("/tour/<path:path>")
@no_cache
def tour(path=""):
return index("")
@web.route("/tutorial/")
@no_cache
def tutorial():
return index("")
@web.route("/organizations/")
@web.route("/organizations/new/")
@no_cache
def organizations():
return index("")
@web.route("/superuser/")
@no_cache
@route_show_if(features.SUPER_USERS)
def superuser():
return index("")
@web.route("/setup/")
@no_cache
@route_show_if(features.SUPER_USERS)
def setup():
return index("")
@web.route("/upgradeprogress/")
@no_cache
@route_show_if(not features.BILLING)
@route_show_if(app.config.get("V3_UPGRADE_MODE") == "background")
def upgrade_progress():
total_tags = RepositoryTag.select().where(RepositoryTag.hidden == False).count()
if total_tags == 0:
return jsonify({"progress": 1.0, "tags_remaining": 0, "total_tags": 0,})
upgraded_tags = TagToRepositoryTag.select().count()
return jsonify(
{
"progress": float(upgraded_tags) / total_tags,
"tags_remaining": total_tags - upgraded_tags,
"total_tags": total_tags,
}
)
@web.route("/signin/")
@no_cache
def signin(redirect=None):
return index("")
@web.route("/contact/")
@no_cache
def contact():
return index("")
@web.route("/about/")
@no_cache
def about():
return index("")
@web.route("/new/")
@no_cache
def new():
return index("")
@web.route("/updateuser")
@no_cache
def updateuser():
return index("")
@web.route("/confirminvite")
@no_cache
def confirm_invite():
code = request.values["code"]
return index("", code=code)
@web.route("/repository/", defaults={"path": ""})
@web.route("/repository/<path:path>", methods=["GET"])
@no_cache
def repository(path):
return index("")
@web.route("/repository/<path:path>/trigger/<trigger>", methods=["GET"])
@no_cache
def buildtrigger(path, trigger):
return index("")
@route_show_if(features.APP_REGISTRY)
@web.route("/application/", defaults={"path": ""})
@web.route("/application/<path:path>", methods=["GET"])
@no_cache
def application(path):
return index("")
@web.route("/security/")
@no_cache
def security():
return index("")
@web.route("/enterprise/")
@no_cache
@route_show_if(features.BILLING)
def enterprise():
return redirect("/plans?tab=enterprise")
@web.route("/__exp/<expname>")
@no_cache
def exp(expname):
return index("")
@web.route("/v1")
@web.route("/v1/")
@no_cache
def v1():
return index("")
@web.route("/tos", methods=["GET"])
@no_cache
def tos():
return index("")
@web.route("/privacy", methods=["GET"])
@no_cache
def privacy():
return index("")
@web.route("/health", methods=["GET"])
@web.route("/health/instance", methods=["GET"])
@process_auth_or_cookie
@no_cache
def instance_health():
checker = get_healthchecker(app, config_provider, instance_keys)
(data, status_code) = checker.check_instance()
response = jsonify(dict(data=data, status_code=status_code))
response.status_code = status_code
return response
@web.route("/status", methods=["GET"])
@web.route("/health/endtoend", methods=["GET"])
@process_auth_or_cookie
@no_cache
def endtoend_health():
checker = get_healthchecker(app, config_provider, instance_keys)
(data, status_code) = checker.check_endtoend()
response = jsonify(dict(data=data, status_code=status_code))
response.status_code = status_code
return response
@web.route("/health/warning", methods=["GET"])
@process_auth_or_cookie
@no_cache
def warning_health():
checker = get_healthchecker(app, config_provider, instance_keys)
(data, status_code) = checker.check_warning()
response = jsonify(dict(data=data, status_code=status_code))
response.status_code = status_code
return response
@web.route("/health/dbrevision", methods=["GET"])
@route_show_if(features.BILLING) # Since this is only used in production.
@process_auth_or_cookie
@no_cache
def dbrevision_health():
# Find the revision from the database.
result = db.execute_sql("select * from alembic_version limit 1").fetchone()
db_revision = result[0]
# Find the local revision from the file system.
with open(os.path.join(ROOT_DIR, "ALEMBIC_HEAD"), "r") as f:
local_revision = f.readline().split(" ")[0]
data = {
"db_revision": db_revision,
"local_revision": local_revision,
}
status_code = 200 if db_revision == local_revision else 400
response = jsonify(dict(data=data, status_code=status_code))
response.status_code = status_code
return response
@web.route("/health/enabledebug/<secret>", methods=["GET"])
@no_cache
def enable_health_debug(secret):
if not secret:
abort(404)
if not app.config.get("ENABLE_HEALTH_DEBUG_SECRET"):
abort(404)
if app.config.get("ENABLE_HEALTH_DEBUG_SECRET") != secret:
abort(404)
session["health_debug"] = True
return make_response("Health check debug information enabled")
@web.route("/robots.txt", methods=["GET"])
def robots():
robots_txt = make_response(render_template("robots.txt", baseurl=get_app_url()))
robots_txt.headers["Content-Type"] = "text/plain"
return robots_txt
@web.route("/buildlogs/<build_uuid>", methods=["GET"])
@route_show_if(features.BUILD_SUPPORT)
@process_auth_or_cookie
def buildlogs(build_uuid):
found_build = model.build.get_repository_build(build_uuid)
if not found_build:
abort(403)
repo = found_build.repository
has_permission = ModifyRepositoryPermission(repo.namespace_user.username, repo.name).can()
if features.READER_BUILD_LOGS and not has_permission:
if ReadRepositoryPermission(
repo.namespace_user.username, repo.name
).can() or model.repository.repository_is_public(repo.namespace_user.username, repo.name):
has_permission = True
if not has_permission:
abort(403)
# If the logs have been archived, just return a URL of the completed archive
if found_build.logs_archived:
return redirect(log_archive.get_file_url(found_build.uuid, get_request_ip()))
_, logs = build_logs.get_log_entries(found_build.uuid, 0)
response = jsonify({"logs": [log for log in logs]})
response.headers["Content-Disposition"] = "attachment;filename=" + found_build.uuid + ".json"
return response
@web.route("/exportedlogs/<file_id>", methods=["GET"])
def exportedlogs(file_id):
# Only enable this endpoint if local storage is available.
has_local_storage = False
for storage_type, _ in app.config.get("DISTRIBUTED_STORAGE_CONFIG", {}).values():
if storage_type == "LocalStorage":
has_local_storage = True
break
if not has_local_storage:
abort(404)
JSON_MIMETYPE = "application/json"
exported_logs_storage_path = app.config.get(
"EXPORT_ACTION_LOGS_STORAGE_PATH", "exportedactionlogs"
)
export_storage_path = os.path.join(exported_logs_storage_path, file_id)
if not storage.exists(storage.preferred_locations, export_storage_path):
abort(404)
try:
return send_file(
storage.stream_read_file(storage.preferred_locations, export_storage_path),
mimetype=JSON_MIMETYPE,
)
except IOError:
logger.exception("Could not read exported logs")
abort(403)
@web.route("/logarchive/<file_id>", methods=["GET"])
@route_show_if(features.BUILD_SUPPORT)
@process_auth_or_cookie
def logarchive(file_id):
JSON_MIMETYPE = "application/json"
try:
found_build = model.build.get_repository_build(file_id)
except model.InvalidRepositoryBuildException as ex:
logger.exception(ex, extra={"build_uuid": file_id})
abort(403)
repo = found_build.repository
has_permission = ModifyRepositoryPermission(repo.namespace_user.username, repo.name).can()
if features.READER_BUILD_LOGS and not has_permission:
if ReadRepositoryPermission(
repo.namespace_user.username, repo.name
).can() or model.repository.repository_is_public(repo.namespace_user.username, repo.name):
has_permission = True
if not has_permission:
abort(403)
try:
path = log_archive.get_file_id_path(file_id)
data_stream = log_archive._storage.stream_read_file(log_archive._locations, path)
return send_file(GzipInputStream(data_stream), mimetype=JSON_MIMETYPE)
except IOError:
logger.exception("Could not read archived logs")
abort(403)
@web.route("/receipt", methods=["GET"])
@route_show_if(features.BILLING)
@require_session_login
def receipt():
if not current_user.is_authenticated:
abort(401)
return
invoice_id = request.args.get("id")
if invoice_id:
invoice = stripe.Invoice.retrieve(invoice_id)
if invoice:
user_or_org = model.user.get_user_or_org_by_customer_id(invoice.customer)
if user_or_org:
if user_or_org.organization:
admin_org = AdministerOrganizationPermission(user_or_org.username)
if not admin_org.can():
abort(404)
return
else:
if not user_or_org.username == current_user.db_user().username:
abort(404)
return
def format_date(timestamp):
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d")
file_data = renderInvoiceToPdf(invoice, user_or_org)
receipt_filename = "quay-receipt-%s.pdf" % (format_date(invoice.date))
return Response(
file_data,
mimetype="application/pdf",
headers={"Content-Disposition": "attachment;filename=" + receipt_filename},
)
abort(404)
@web.route("/authrepoemail", methods=["GET"])
@route_show_if(features.MAILING)
def confirm_repo_email():
code = request.values["code"]
record = None
try:
record = model.repository.confirm_email_authorization_for_repo(code)
except model.DataModelException as ex:
return index("", error_info=dict(reason="confirmerror", error_message=ex.message))
message = """
Your E-mail address has been authorized to receive notifications for repository
<a href="%s://%s/repository/%s/%s">%s/%s</a>.
""" % (
app.config["PREFERRED_URL_SCHEME"],
app.config["SERVER_HOSTNAME"],
record.repository.namespace_user.username,
record.repository.name,
record.repository.namespace_user.username,
record.repository.name,
)
return render_page_template_with_routedata("message.html", message=message)
@web.route("/confirm", methods=["GET"])
@route_show_if(features.MAILING)
@anon_allowed
def confirm_email():
code = request.values["code"]
user = None
new_email = None
try:
user, new_email, old_email = model.user.confirm_user_email(code)
except model.DataModelException as ex:
return index("", error_info=dict(reason="confirmerror", error_message=ex.message))
if new_email:
send_email_changed(user.username, old_email, new_email)
change_email_future = user_analytics.change_email(old_email, new_email)
change_email_future.add_done_callback(build_error_callback("Change email failed"))
success, _ = common_login(user.uuid)
if not success:
return index(
"", error_info=dict(reason="confirmerror", error_message="Could not perform login")
)
if model.user.has_user_prompts(user):
return redirect(url_for("web.updateuser"))
elif new_email:
return redirect(url_for("web.user_view", path=user.username, tab="settings"))
else:
return redirect(url_for("web.index"))
@web.route("/recovery", methods=["GET"])
@route_show_if(features.MAILING)
@anon_allowed
def confirm_recovery():
code = request.values["code"]
user = model.user.validate_reset_code(code)
if user is not None:
success, _ = common_login(user.uuid)
if not success:
message = "Could not perform login."
return render_page_template_with_routedata("message.html", message=message)
return redirect(
url_for("web.user_view", path=user.username, tab="settings", action="password")
)
else:
message = "Invalid recovery code: This code is invalid or may have already been used."
return render_page_template_with_routedata("message.html", message=message)
@web.route("/repository/<repopath:repository>/status", methods=["GET"])
@parse_repository_name()
@anon_protect
def build_status_badge(namespace_name, repo_name):
token = request.args.get("token", None)
repo = model.repository.get_repository(namespace_name, repo_name)
if repo and repo.kind.name != "image":
abort(404)
is_public = model.repository.repository_is_public(namespace_name, repo_name)
if not is_public:
if not repo or token != repo.badge_token:
abort(404)
is_empty = model.repository.is_empty(namespace_name, repo_name)
recent_build = model.build.get_recent_repository_build(namespace_name, repo_name)
if not is_empty and (not recent_build or recent_build.phase == "complete"):
status_name = "ready"
elif recent_build and recent_build.phase == "error":
status_name = "failed"
elif recent_build and recent_build.phase == "cancelled":
status_name = "cancelled"
elif recent_build and recent_build.phase != "complete":
status_name = "building"
else:
status_name = "none"
if request.headers.get("If-None-Match") == status_name:
return Response(status=304)
response = make_response(STATUS_TAGS[status_name])
response.content_type = "image/svg+xml"
response.headers["Cache-Control"] = "no-cache"
response.headers["ETag"] = status_name
return response
class FlaskAuthorizationProvider(model.oauth.DatabaseAuthorizationProvider):
def get_authorized_user(self):
return get_authenticated_user()
def _make_response(self, body="", headers=None, status_code=200):
return make_response(body, status_code, headers)
@web.route("/oauth/authorizeapp", methods=["POST"])
@process_auth_or_cookie
def authorize_application():
# Check for an authenticated user.
if not get_authenticated_user():
abort(401)
return
# If direct OAuth is not enabled or the user is not directly authed, verify CSRF.
client_id = request.form.get("client_id", None)
whitelist = app.config.get("DIRECT_OAUTH_CLIENTID_WHITELIST", [])
if client_id not in whitelist or not has_basic_auth(get_authenticated_user().username):
verify_csrf()
provider = FlaskAuthorizationProvider()
redirect_uri = request.form.get("redirect_uri", None)
scope = request.form.get("scope", None)
# Add the access token.
return provider.get_token_response("token", client_id, redirect_uri, scope=scope)
@web.route(app.config["LOCAL_OAUTH_HANDLER"], methods=["GET"])
def oauth_local_handler():
if not current_user.is_authenticated:
abort(401)
return
if not request.args.get("scope"):
return render_page_template_with_routedata("message.html", message="Authorization canceled")
else:
return render_page_template_with_routedata("generatedtoken.html")
@web.route("/oauth/denyapp", methods=["POST"])
@csrf_protect()
def deny_application():
if not current_user.is_authenticated:
abort(401)
return
provider = FlaskAuthorizationProvider()
client_id = request.form.get("client_id", None)
redirect_uri = request.form.get("redirect_uri", None)
scope = request.form.get("scope", None)
# Add the access token.
return provider.get_auth_denied_response("token", client_id, redirect_uri, scope=scope)
@web.route("/oauth/authorize", methods=["GET"])
@no_cache
@param_required("client_id")
@param_required("redirect_uri")
@param_required("scope")
@process_auth_or_cookie
def request_authorization_code():
provider = FlaskAuthorizationProvider()
response_type = request.args.get("response_type", "code")
client_id = request.args.get("client_id", None)
redirect_uri = request.args.get("redirect_uri", None)
scope = request.args.get("scope", None)
if not current_user.is_authenticated or not provider.validate_has_scopes(
client_id, current_user.db_user().username, scope
):
if not provider.validate_redirect_uri(client_id, redirect_uri):
current_app = provider.get_application_for_client_id(client_id)
if not current_app:
abort(404)
return provider._make_redirect_error_response(
current_app.redirect_uri, "redirect_uri_mismatch"
)
# Load the scope information.
scope_info = scopes.get_scope_information(scope)
if not scope_info:
abort(404)
return
# Load the application information.
oauth_app = provider.get_application_for_client_id(client_id)
app_email = oauth_app.avatar_email or oauth_app.organization.email
oauth_app_view = {
"name": oauth_app.name,
"description": oauth_app.description,
"url": oauth_app.application_uri,
"avatar": json.dumps(avatar.get_data(oauth_app.name, app_email, "app")),
"organization": {
"name": oauth_app.organization.username,
"avatar": json.dumps(avatar.get_data_for_org(oauth_app.organization)),
},
}
# Show the authorization page.
has_dangerous_scopes = any([check_scope["dangerous"] for check_scope in scope_info])
return render_page_template_with_routedata(
"oauthorize.html",
scopes=scope_info,
has_dangerous_scopes=has_dangerous_scopes,
application=oauth_app_view,
enumerate=enumerate,
client_id=client_id,
redirect_uri=redirect_uri,
scope=scope,
csrf_token_val=generate_csrf_token(),
)
if response_type == "token":
return provider.get_token_response(response_type, client_id, redirect_uri, scope=scope)
else:
return provider.get_authorization_code(response_type, client_id, redirect_uri, scope=scope)
@web.route("/oauth/access_token", methods=["POST"])
@no_cache
@param_required("grant_type", allow_body=True)
@param_required("client_id", allow_body=True)
@param_required("redirect_uri", allow_body=True)
@param_required("code", allow_body=True)
@param_required("scope", allow_body=True)
def exchange_code_for_token():
grant_type = request.values.get("grant_type", None)
client_id = request.values.get("client_id", None)
client_secret = request.values.get("client_id", None)
redirect_uri = request.values.get("redirect_uri", None)
code = request.values.get("code", None)
scope = request.values.get("scope", None)
# Sometimes OAuth2 clients place the client id/secret in the Auth header.
basic_header = parse_basic_auth(request.headers.get("Authorization"))
if basic_header is not None:
client_id = basic_header[0] or client_id
client_secret = basic_header[1] or client_secret
provider = FlaskAuthorizationProvider()
return provider.get_token(grant_type, client_id, client_secret, redirect_uri, code, scope=scope)
@web.route("/bitbucket/setup/<repopath:repository>", methods=["GET"])
@require_session_login
@parse_repository_name()
@route_show_if(features.BITBUCKET_BUILD)
def attach_bitbucket_trigger(namespace_name, repo_name):
permission = AdministerRepositoryPermission(namespace_name, repo_name)
if permission.can():
repo = model.repository.get_repository(namespace_name, repo_name)
if not repo:
msg = "Invalid repository: %s/%s" % (namespace_name, repo_name)
abort(404, message=msg)
elif repo.kind.name != "image":
abort(501)
trigger = model.build.create_build_trigger(
repo, BitbucketBuildTrigger.service_name(), None, current_user.db_user()
)
try:
oauth_info = BuildTriggerHandler.get_handler(trigger).get_oauth_url()
except TriggerProviderException:
trigger.delete_instance()
logger.debug("Could not retrieve Bitbucket OAuth URL")
abort(500)
config = {"access_token": oauth_info["access_token"]}
access_token_secret = oauth_info["access_token_secret"]
model.build.update_build_trigger(trigger, config, auth_token=access_token_secret)
return redirect(oauth_info["url"])
abort(403)
@web.route("/customtrigger/setup/<repopath:repository>", methods=["GET"])
@require_session_login
@parse_repository_name()
def attach_custom_build_trigger(namespace_name, repo_name):
permission = AdministerRepositoryPermission(namespace_name, repo_name)
if permission.can():
repo = model.repository.get_repository(namespace_name, repo_name)
if not repo:
msg = "Invalid repository: %s/%s" % (namespace_name, repo_name)
abort(404, message=msg)
elif repo.kind.name != "image":
abort(501)
trigger = model.build.create_build_trigger(
repo, CustomBuildTrigger.service_name(), None, current_user.db_user()
)
repo_path = "%s/%s" % (namespace_name, repo_name)
full_url = url_for("web.buildtrigger", path=repo_path, trigger=trigger.uuid)
logger.debug("Redirecting to full url: %s", full_url)
return redirect(full_url)
abort(403)
@web.route("/<repopath:repository>")
@web.route("/<repopath:repository>/")
@no_cache
@process_oauth
@parse_repository_name(include_tag=True)
@anon_protect
def redirect_to_repository(namespace_name, repo_name, tag_name):
# Always return 200 for ac-discovery, to ensure that rkt and other ACI-compliant clients can
# find the metadata they need. Permissions will be checked in the registry API.
if request.args.get("ac-discovery", 0) == 1:
return index("")
# Redirect to the repository page if the user can see the repository.
is_public = model.repository.repository_is_public(namespace_name, repo_name)
permission = ReadRepositoryPermission(namespace_name, repo_name)
repo = model.repository.get_repository(namespace_name, repo_name)
if repo and (permission.can() or is_public):
repo_path = "/".join([namespace_name, repo_name])
if repo.kind.name == "application":
return redirect(url_for("web.application", path=repo_path))
else:
return redirect(url_for("web.repository", path=repo_path, tab="tags", tag=tag_name))
namespace_exists = bool(model.user.get_user_or_org(namespace_name))
namespace_permission = OrganizationMemberPermission(namespace_name).can()
if get_authenticated_user() and get_authenticated_user().username == namespace_name:
namespace_permission = True
# Otherwise, we display an error for the user. Which error we display depends on permissions:
# > If the namespace doesn't exist, 404.
# > If the user is a member of the namespace:
# - If the repository doesn't exist, 404
# - If the repository does exist (no access), 403
# > If the user is not a member of the namespace: 403
error_info = {
"reason": "notfound",
"for_repo": True,
"namespace_exists": namespace_exists,
"namespace": namespace_name,
"repo_name": repo_name,
}
if not namespace_exists or (namespace_permission and repo is None):
resp = index("", error_code=404, error_info=json.dumps(error_info))
resp.status_code = 404
return resp
else:
resp = index("", error_code=403, error_info=json.dumps(error_info))
resp.status_code = 403
return resp
@web.route("/<namespace>")
@web.route("/<namespace>/")
@no_cache
@process_oauth
@anon_protect
def redirect_to_namespace(namespace):
okay, _ = model.user.validate_username(namespace)
if not okay:
abort(404)
user_or_org = model.user.get_user_or_org(namespace)
if not user_or_org:
abort(404)
if user_or_org.organization:
return redirect(url_for("web.org_view", path=namespace))
else:
return redirect(url_for("web.user_view", path=namespace))