1
0
mirror of https://github.com/quay/quay.git synced 2025-04-18 10:44:06 +03:00

chore: allows Quay to run for account recoveries (PROJQUAY-970) (#793)

Adds ACCOUNT_RECOVERY_MODE to allow Quay to run with some core
features disabled. When this is set, the instance should only be used
in order by existing users who hasn't linked their account to an
external login service, after database authentication has been
disabled.
This commit is contained in:
Kenny Lee Sin Cheong 2021-07-07 12:45:24 -04:00 committed by GitHub
parent 95ec9478fc
commit a839a78eb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 178 additions and 11 deletions

View File

@ -34,17 +34,20 @@ DEFAULT_CONTROLLER_PORT = 8686
def run_build_manager():
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.BUILD_SUPPORT:
logger.debug("Building is disabled. Please enable the feature flag")
while True:
time.sleep(1000)
return
if app.config.get("REGISTRY_STATE", "normal") == "readonly":
logger.debug("Building is disabled while in read-only mode.")
while True:
time.sleep(1000)
return
build_manager_config = app.config.get("BUILD_MANAGER")
if build_manager_config is None:

View File

@ -773,3 +773,6 @@ class DefaultConfig(ImmutableConfig):
# Create organization on push if it does not exist
CREATE_NAMESPACE_ON_PUSH = False
# Account recovery mode
ACCOUNT_RECOVERY_MODE = False

View File

@ -158,6 +158,7 @@ def render_page_template(name, route_data=None, **kwargs):
version_number=version_number,
current_year=datetime.datetime.now().year,
kubernetes_namespace=IS_KUBERNETES and QE_NAMESPACE,
account_recovery_mode=app.config.get("ACCOUNT_RECOVERY_MODE", False),
**kwargs,
)

View File

@ -171,6 +171,21 @@ def route_show_if(value):
return decorator
def disallow_for_account_recovery_mode(func):
"""
Disable route if ACCOUNT_RECOVERY_MODE is set.
"""
@wraps(func)
def wrapper(*args, **kwargs):
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
abort(405, "Quay running for account recoveries only.")
return func(*args, **kwargs)
return wrapper
def require_xhr_from_browser(func):
"""
Requires that API GET calls made from browsers are made via XHR, in order to prevent reflected

View File

@ -21,6 +21,7 @@ from digest import digest_tools
from endpoints.decorators import (
anon_protect,
anon_allowed,
disallow_for_account_recovery_mode,
parse_repository_name,
check_region_blacklisted,
check_readonly,
@ -51,6 +52,7 @@ BLOB_CONTENT_TYPE = "application/octet-stream"
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=["HEAD"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull"])
@require_repo_read
@ -78,6 +80,7 @@ def check_blob_exists(namespace_name, repo_name, digest):
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=["GET"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull"])
@require_repo_read
@ -217,6 +220,7 @@ def _try_to_mount_blob(repository_ref, mount_blob_digest):
@v2_bp.route("/<repopath:repository>/blobs/uploads/", methods=["POST"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull", "push"])
@require_repo_write
@ -278,6 +282,7 @@ def start_blob_upload(namespace_name, repo_name):
@v2_bp.route("/<repopath:repository>/blobs/uploads/<upload_uuid>", methods=["GET"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull"])
@require_repo_write
@ -305,6 +310,7 @@ def fetch_existing_upload(namespace_name, repo_name, upload_uuid):
@v2_bp.route("/<repopath:repository>/blobs/uploads/<upload_uuid>", methods=["PATCH"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull", "push"])
@require_repo_write
@ -336,6 +342,7 @@ def upload_chunk(namespace_name, repo_name, upload_uuid):
@v2_bp.route("/<repopath:repository>/blobs/uploads/<upload_uuid>", methods=["PUT"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull", "push"])
@require_repo_write
@ -376,6 +383,7 @@ def monolithic_upload_or_last_chunk(namespace_name, repo_name, upload_uuid):
@v2_bp.route("/<repopath:repository>/blobs/uploads/<upload_uuid>", methods=["DELETE"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull", "push"])
@require_repo_write
@ -397,6 +405,7 @@ def cancel_upload(namespace_name, repo_name, upload_uuid):
@v2_bp.route("/<repopath:repository>/blobs/<digest>", methods=["DELETE"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull", "push"])
@require_repo_write

View File

@ -9,7 +9,7 @@ from auth.auth_context import get_authenticated_user, get_authenticated_context
from auth.registry_jwt_auth import process_registry_jwt_auth
from data import model
from data.cache import cache_key
from endpoints.decorators import anon_protect
from endpoints.decorators import anon_protect, disallow_for_account_recovery_mode, route_show_if
from endpoints.v2 import v2_bp, paginate
@ -18,6 +18,7 @@ class Repository(namedtuple("Repository", ["id", "namespace_name", "name"])):
@v2_bp.route("/_catalog", methods=["GET"])
@disallow_for_account_recovery_mode
@process_registry_jwt_auth()
@anon_protect
@paginate()

View File

@ -13,7 +13,12 @@ from data.database import db_disallow_replica_use
from data.registry_model import registry_model
from data.model.oci.manifest import CreateManifestException
from data.model.oci.tag import RetargetTagException
from endpoints.decorators import anon_protect, parse_repository_name, check_readonly
from endpoints.decorators import (
anon_protect,
disallow_for_account_recovery_mode,
parse_repository_name,
check_readonly,
)
from endpoints.metrics import image_pulls, image_pushes
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write
from endpoints.v2.errors import (
@ -43,6 +48,7 @@ MANIFEST_TAGNAME_ROUTE = BASE_MANIFEST_ROUTE.format(VALID_TAG_PATTERN)
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=["GET"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull"])
@require_repo_read
@ -101,6 +107,7 @@ def fetch_manifest_by_tagname(namespace_name, repo_name, manifest_ref):
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=["GET"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull"])
@require_repo_read
@ -213,6 +220,7 @@ def _doesnt_accept_schema_v1():
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=["PUT"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@_reject_manifest2_schema2
@process_registry_jwt_auth(scopes=["pull", "push"])
@ -225,6 +233,7 @@ def write_manifest_by_tagname(namespace_name, repo_name, manifest_ref):
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=["PUT"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@_reject_manifest2_schema2
@process_registry_jwt_auth(scopes=["pull", "push"])
@ -285,6 +294,7 @@ def _parse_manifest():
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=["DELETE"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull", "push"])
@require_repo_write

View File

@ -1,14 +1,20 @@
from flask import jsonify
from app import model_cache
from app import app, model_cache
from auth.registry_jwt_auth import process_registry_jwt_auth
from data.registry_model import registry_model
from endpoints.decorators import anon_protect, parse_repository_name
from endpoints.decorators import (
anon_protect,
disallow_for_account_recovery_mode,
parse_repository_name,
route_show_if,
)
from endpoints.v2 import v2_bp, require_repo_read, paginate
from endpoints.v2.errors import NameUnknown
@v2_bp.route("/<repopath:repository>/tags/list", methods=["GET"])
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull"])
@require_repo_read

View File

@ -1,5 +1,5 @@
from app import app as application
from endpoints.secscan import secscan
if not application.config.get("ACCOUNT_RECOVERY_MODE", False):
application.register_blueprint(secscan, url_prefix="/secscan")

View File

@ -1,10 +1,17 @@
<div class="announcement inline quay-message-bar-element" ng-show="messages.length || inReadOnlyMode">
<div class="announcement inline quay-message-bar-element" ng-show="messages.length || inReadOnlyMode || inAccountRecoveryMode">
<div class="quay-service-status-description info" ng-if="inReadOnlyMode">
<div style="display: inline-block">
<span class="registry-name"></span>&nbsp;is currently in read-only mode. Pulls and other read-only operations
will succeed but all other operations are currently suspended.
</div>
</div>
<div class="quay-service-status-description warning" ng-if="inAccountRecoveryMode">
<div style="display: inline-block">
<span class="registry-name"></span>&nbsp;is currently in account recovery mode. This instance should only be
used to link accounts to an external login service. e.g RedHat. Registry operations such as pushes/pulls
will not work.
</div>
</div>
<div ng-repeat="token in NotificationService.expiringAppTokens">
<div class="quay-service-status-description warning">
Your external application token <strong style="display: inline-block; padding: 4px;">{{ token.title }}</strong>

View File

@ -16,6 +16,7 @@ angular.module('quay').directive('quayMessageBar', function () {
StateService.updateStateIn($scope, function(state) {
$scope.inReadOnlyMode = state.inReadOnlyMode;
$scope.inAccountRecoveryMode = state.inAccountRecoveryMode;
});
ApiService.getGlobalMessages().then(function (data) {

View File

@ -33,6 +33,10 @@ export function provideRun($rootScope: QuayRunScope,
stateService.setInReadOnlyMode();
}
if ((<any>window).__account_recovery_mode) {
stateService.setInAccountRecoveryMode();
}
// Handle session security.
restangular.setDefaultHeaders({
'X-Requested-With': 'XMLHttpRequest',

View File

@ -6,7 +6,8 @@ angular.module('quay')
var stateService = {};
var currentState = {
'inReadOnlyMode': false
'inReadOnlyMode': false,
'inAccountRecoveryMode': false
};
stateService.inReadOnlyMode = function() {
@ -17,6 +18,15 @@ angular.module('quay')
currentState.inReadOnlyMode = true;
};
stateService.inAccountRecoveryMode = function() {
return currentState.inAccountRecoveryMode;
};
stateService.setInAccountRecoveryMode = function() {
currentState.inAccountRecoveryMode = true;
};
stateService.updateStateIn = function(scope, opt_callback) {
scope.$watch(function () { return stateService.currentState(); }, function (currentState) {
$timeout(function(){

View File

@ -47,6 +47,7 @@
window.__token = '{{ csrf_token() }}';
window.__kubernetes_namespace = {{ kubernetes_namespace|tojson|safe }};
window.__registry_state = '{{ registry_state }}';
window.__account_recovery_mode = '{{ account_recovery_mode }}';
{% if error_code %}
window.__error_code = {{ error_code }};

View File

@ -99,6 +99,7 @@ INTERNAL_ONLY_PROPERTIES = {
"LOGS_MODEL_CONFIG",
"APP_REGISTRY_RESULTS_LIMIT",
"V3_UPGRADE_MODE", # Deprecated old flag
"ACCOUNT_RECOVERY_MODE",
}
CONFIG_SCHEMA = {

View File

@ -1,5 +1,6 @@
import logging
import logging.config
import time
from datetime import timedelta, datetime
@ -85,6 +86,11 @@ def create_gunicorn_worker():
if __name__ == "__main__":
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
GlobalLock.configure(app.config)
worker = BlobUploadCleanupWorker()

View File

@ -1,4 +1,5 @@
import logging
import time
from gzip import GzipFile
from tempfile import SpooledTemporaryFile
@ -79,5 +80,10 @@ def create_gunicorn_worker():
if __name__ == "__main__":
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
worker = ArchiveBuildLogsWorker()
worker.start()

View File

@ -59,6 +59,11 @@ def create_gunicorn_worker():
if __name__ == "__main__":
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
engines = set(
[config[0] for config in list(app.config.get("DISTRIBUTED_STORAGE_CONFIG", {}).values())]
)

View File

@ -56,6 +56,11 @@ def create_gunicorn_worker():
if __name__ == "__main__":
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.APP_SPECIFIC_TOKENS:
logger.debug("App specific tokens disabled; skipping")
while True:

View File

@ -86,6 +86,11 @@ def create_gunicorn_worker():
if __name__ == "__main__":
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.GARBAGE_COLLECTION:
logger.debug("Garbage collection is disabled; skipping")
while True:

View File

@ -82,6 +82,11 @@ def create_gunicorn_worker():
def main():
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not app.config.get("PROMETHEUS_PUSHGATEWAY_URL"):
logger.debug("Prometheus not enabled; skipping global stats reporting")
while True:

View File

@ -148,6 +148,11 @@ def create_gunicorn_worker():
def main():
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.ACTION_LOG_ROTATION or None in [SAVE_PATH, SAVE_LOCATION]:
logger.debug("Action log rotation worker not enabled; skipping")
while True:

View File

@ -1,4 +1,5 @@
import logging
import time
from peewee import fn
@ -103,6 +104,11 @@ def create_gunicorn_worker():
def main():
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.MANIFEST_SIZE_BACKFILL:
logger.debug("Manifest backfill worker not enabled; skipping")
while True:

View File

@ -64,6 +64,11 @@ def create_gunicorn_worker():
if __name__ == "__main__":
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.NAMESPACE_GARBAGE_COLLECTION:
logger.debug("Namespace garbage collection is disabled; skipping")
while True:

View File

@ -1,4 +1,5 @@
import logging
import time
from app import app, notification_queue
from notifications.notificationmethod import NotificationMethod, InvalidNotificationMethodException
@ -55,6 +56,11 @@ def create_gunicorn_worker():
if __name__ == "__main__":
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
worker = NotificationWorker(
notification_queue, poll_period_seconds=10, reservation_seconds=30, retry_after_seconds=30
)

View File

@ -1,4 +1,5 @@
import logging
import time
from datetime import timedelta, datetime
@ -51,5 +52,10 @@ def create_gunicorn_worker():
if __name__ == "__main__":
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
worker = QueueCleanupWorker()
worker.start()

View File

@ -68,6 +68,11 @@ if __name__ == "__main__":
)
args = parser.parse_args()
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.REPO_MIRROR:
logger.debug("Repository mirror disabled; skipping RepoMirrorWorker")
while True:

View File

@ -121,6 +121,11 @@ def create_gunicorn_worker():
if __name__ == "__main__":
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.REPOSITORY_ACTION_COUNTER:
logger.info("Repository action count is disabled; skipping")
while True:

View File

@ -70,6 +70,11 @@ def create_gunicorn_worker():
if __name__ == "__main__":
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.REPOSITORY_GARBAGE_COLLECTION:
logger.info("Repository garbage collection is disabled; skipping")
while True:

View File

@ -152,6 +152,11 @@ def create_gunicorn_worker():
if __name__ == "__main__":
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.SECURITY_SCANNER:
logger.debug("Security scanner disabled; sleeping")
while True:

View File

@ -57,6 +57,11 @@ if __name__ == "__main__":
app.register_blueprint(v2_bp, url_prefix="/v2")
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.SECURITY_SCANNER:
logger.debug("Security scanner disabled; skipping SecurityWorker")
while True:

View File

@ -202,6 +202,11 @@ if __name__ == "__main__":
has_local_storage = False
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if features.STORAGE_REPLICATION:
for storage_type, _ in list(app.config.get("DISTRIBUTED_STORAGE_CONFIG", {}).values()):
if storage_type == "LocalStorage":

View File

@ -47,6 +47,11 @@ def create_gunicorn_worker():
def main():
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
if app.config.get("ACCOUNT_RECOVERY_MODE", False):
logger.debug("Quay running in account recovery mode")
while True:
time.sleep(100000)
if not features.TEAM_SYNCING or not authentication.federated_service:
logger.debug("Team syncing is disabled; sleeping")
while True: