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

logs: add failure logging for login, push, pull and delete events (PROJQUAY-5411) (#1903)

* add login failure logging

Signed-off-by: dmesser <dmesser@redhat.com>

* move failure logging into credential validation

Signed-off-by: dmesser <dmesser@redhat.com>

* more precise tracking of affected users

Signed-off-by: dmesser <dmesser@redhat.com>

* fix indent

Signed-off-by: dmesser <dmesser@redhat.com>

* differentiate robots with wrong credentials

Signed-off-by: dmesser <dmesser@redhat.com>

* don't audit failures by default

Signed-off-by: dmesser <dmesser@redhat.com>

* discrete failure tracking for logins, push, pulls and deletes

Signed-off-by: dmesser <dmesser@redhat.com>

* refine log metadata

Signed-off-by: dmesser <dmesser@redhat.com>

* login failure log visualization

Signed-off-by: dmesser <dmesser@redhat.com>

* properly use data model

Signed-off-by: dmesser <dmesser@redhat.com>

* fix unit test bug

Signed-off-by: dmesser <dmesser@redhat.com>

* track non-existing repos differently

Signed-off-by: dmesser <dmesser@redhat.com>

* log view visualization of failed pushes and pulls

Signed-off-by: dmesser <dmesser@redhat.com>

* ensure all tests are conducted with failure logging

Signed-off-by: dmesser <dmesser@redhat.com>

* additional unicode protection

Signed-off-by: dmesser <dmesser@redhat.com>

* python black formatting

Signed-off-by: dmesser <dmesser@redhat.com>

* add cypress test data

Signed-off-by: dmesser <dmesser@redhat.com>

* add safety checks for ascii conversion attempts

Signed-off-by: dmesser <dmesser@redhat.com>

* adjusting unit test with correct error message

Signed-off-by: dmesser <dmesser@redhat.com>

* update to alembic head

Signed-off-by: dmesser <dmesser@redhat.com>

* add standard oauth token metadata in audit

Signed-off-by: dmesser <dmesser@redhat.com>

* update alembic head

Signed-off-by: dmesser <dmesser@redhat.com>

* correct field name

Signed-off-by: dmesser <dmesser@redhat.com>

* formatting

Signed-off-by: dmesser <dmesser@redhat.com>

* bump alembic head

Signed-off-by: dmesser <dmesser@redhat.com>

* refactor auth logging imports

Signed-off-by: dmesser <dmesser@redhat.com>

* bump alembic head

Signed-off-by: dmesser <dmesser@redhat.com>

* formatting

Signed-off-by: dmesser <dmesser@redhat.com>

* restore module

Signed-off-by: dmesser <dmesser@redhat.com>

* pre-commit fixes

Signed-off-by: dmesser <dmesser@redhat.com>

* adding missing default

Signed-off-by: dmesser <dmesser@redhat.com>

* bump alembic head

Signed-off-by: dmesser <dmesser@redhat.com>

* update test data

Signed-off-by: dmesser <dmesser@redhat.com>

* refactoring to save db calls

Signed-off-by: dmesser <dmesser@redhat.com>

* fix unit tests

Signed-off-by: dmesser <dmesser@redhat.com>

* handle unicode conversion errors on email look up

Signed-off-by: dmesser <dmesser@redhat.com>

* bump alembic head

Signed-off-by: dmesser <dmesser@redhat.com>

* proper debug logging and conditional db calls

Signed-off-by: dmesser <dmesser@redhat.com>

* omit wildcard import

Signed-off-by: dmesser <dmesser@redhat.com>

* re-add import

Signed-off-by: dmesser <dmesser@redhat.com>

---------

Signed-off-by: dmesser <dmesser@redhat.com>
This commit is contained in:
Daniel Messer 2024-01-16 16:46:20 +01:00 committed by GitHub
parent a7737722fc
commit e8ff33e728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 693 additions and 119 deletions

View File

@ -1,16 +1,20 @@
import logging
from enum import Enum
from flask import request
import features
from app import authentication
from app import app, authentication
from auth.credential_consts import (
ACCESS_TOKEN_USERNAME,
APP_SPECIFIC_TOKEN_USERNAME,
OAUTH_TOKEN_USERNAME,
)
from auth.log import log_action
from auth.oauth import validate_oauth_token
from auth.validateresult import AuthKind, ValidateResult
from data import model
from data.database import User
from util.names import parse_robot_username
logger = logging.getLogger(__name__)
@ -52,17 +56,50 @@ def validate_credentials(auth_username, auth_password_or_token):
logger.debug(
"Failed to validate credentials for app specific token: %s", auth_password_or_token
)
error_message = "Invalid token"
if app.config.get("ACTION_LOG_AUDIT_LOGIN_FAILURES"):
log_action(
"login_failure",
None,
{
"type": "v2auth",
"kind": "app_specific_token",
"useragent": request.user_agent.string,
"message": error_message,
},
)
return (
ValidateResult(AuthKind.credentials, error_message="Invalid token"),
ValidateResult(AuthKind.credentials, error_message=error_message),
CredentialKind.app_specific_token,
)
if not token.user.enabled:
logger.debug("Tried to use an app specific token for a disabled user: %s", token.uuid)
error_message = "This user has been disabled. Please contact your administrator."
if app.config.get("ACTION_LOG_AUDIT_LOGIN_FAILURES"):
log_action(
"login_failure",
token.user.username,
{
"type": "v2auth",
"kind": "app_specific_token",
"app_specific_token_title": token.title,
"username": token.user.username,
"useragent": request.user_agent.string,
"message": error_message,
},
performer=token.user,
)
return (
ValidateResult(
AuthKind.credentials,
error_message="This user has been disabled. Please contact your administrator.",
error_message=error_message,
),
CredentialKind.app_specific_token,
)
@ -83,10 +120,97 @@ def validate_credentials(auth_username, auth_password_or_token):
logger.debug("Found credentials header for robot %s", auth_username)
try:
robot = model.user.verify_robot(auth_username, auth_password_or_token)
logger.debug("Successfully validated credentials for robot %s", auth_username)
return ValidateResult(AuthKind.credentials, robot=robot), CredentialKind.robot
except model.DeactivatedRobotOwnerException as dre:
robot_owner, robot_name = parse_robot_username(auth_username)
logger.debug(
"Tried to use the robot %s for a disabled user: %s", robot_name, robot_owner
)
if app.config.get("ACTION_LOG_AUDIT_LOGIN_FAILURES"):
try:
performer = model.user.lookup_robot(auth_username)
except User.DoesNotExist:
performer = None
log_action(
"login_failure",
robot_owner,
{
"type": "v2auth",
"kind": "robot",
"robot": auth_username,
"username": robot_owner,
"useragent": request.user_agent.string,
"message": str(dre),
},
performer=performer,
)
return (
ValidateResult(AuthKind.credentials, error_message=str(dre)),
CredentialKind.robot,
)
except model.InvalidRobotCredentialException as ire:
logger.debug("Failed to validate credentials for robot %s: %s", auth_username, ire)
if app.config.get("ACTION_LOG_AUDIT_LOGIN_FAILURES"):
robot_owner, _ = parse_robot_username(auth_username)
try:
performer = model.user.lookup_robot(auth_username)
except User.DoesNotExist:
performer = None
log_action(
"login_failure",
robot_owner,
{
"type": "v2auth",
"kind": "robot",
"robot": auth_username,
"username": robot_owner,
"useragent": request.user_agent.string,
"message": str(ire),
},
performer=performer,
)
return (
ValidateResult(AuthKind.credentials, error_message=str(ire)),
CredentialKind.robot,
)
except model.InvalidRobotException as ire:
logger.warning("Failed to validate credentials for robot %s: %s", auth_username, ire)
if isinstance(ire, model.InvalidRobotException):
logger.debug("Failed to validate credentials for robot %s: %s", auth_username, ire)
elif isinstance(ire, model.InvalidRobotOwnerException):
logger.debug(
"Tried to use the robot %s for a non-existing user: %s", auth_username, ire
)
if app.config.get("ACTION_LOG_AUDIT_LOGIN_FAILURES"):
robot_owner, _ = parse_robot_username(auth_username)
# need to get the owner here in case it wasn't found due to a non-existing user
owner = model.user.get_nonrobot_user(robot_owner)
log_action(
"login_failure",
owner.username if owner else None,
{
"type": "v2auth",
"kind": "robot",
"robot": auth_username,
"username": robot_owner,
"useragent": request.user_agent.string,
"message": str(ire),
},
)
return (
ValidateResult(AuthKind.credentials, error_message=str(ire)),
CredentialKind.robot,
@ -101,4 +225,18 @@ def validate_credentials(auth_username, auth_password_or_token):
return ValidateResult(AuthKind.credentials, user=authenticated), CredentialKind.user
else:
logger.warning("Failed to validate credentials for user %s: %s", auth_username, err)
if app.config.get("ACTION_LOG_AUDIT_LOGIN_FAILURES"):
log_action(
"login_failure",
None,
{
"type": "v2auth",
"kind": "user",
"username": auth_username,
"useragent": request.user_agent.string,
"message": err,
},
)
return ValidateResult(AuthKind.credentials, error_message=err), CredentialKind.user

17
auth/log.py Normal file
View File

@ -0,0 +1,17 @@
from flask import request
from data.logs_model import logs_model
def log_action(kind, user_or_orgname, metadata=None, repo=None, repo_name=None, performer=None):
if not metadata:
metadata = {}
logs_model.log_action(
kind,
user_or_orgname,
repository=repo,
performer=performer,
ip=request.remote_addr or None,
metadata=metadata,
)

View File

@ -1,9 +1,11 @@
import logging
from datetime import datetime
from flask import request
from jwt import ExpiredSignatureError, InvalidTokenError
from app import analytics, app, authentication, oauth_login
from auth.log import log_action
from auth.scopes import scopes_from_scope_string
from auth.validateresult import AuthKind, ValidateResult
from data import model
@ -100,18 +102,72 @@ def validate_app_oauth_token(token):
validated = model.oauth.validate_access_token(token)
if not validated:
logger.warning("OAuth access token could not be validated: %s", token)
return ValidateResult(
AuthKind.oauth, error_message="OAuth access token could not be validated"
)
error_message = "OAuth access token could not be validated"
if app.config.get("ACTION_LOG_AUDIT_LOGIN_FAILURES"):
log_action(
"login_failure",
None,
{
"type": "quayauth",
"kind": "oauth",
"useragent": request.user_agent.string,
"message": error_message,
},
)
return ValidateResult(AuthKind.oauth, error_message=error_message)
if validated.expires_at <= datetime.utcnow():
logger.warning("OAuth access with an expired token: %s", token)
return ValidateResult(AuthKind.oauth, error_message="OAuth access token has expired")
error_message = "OAuth access token has expired"
if app.config.get("ACTION_LOG_AUDIT_LOGIN_FAILURES"):
log_action(
"login_failure",
validated.application.organization.username,
{
"type": "quayauth",
"kind": "oauth",
"token": validated.token_name,
"application_name": validated.application.name,
"oauth_token_id": validated.id,
"oauth_token_application_id": validated.application.client_id,
"oauth_token_application": validated.application.name,
"username": validated.authorized_user.username,
"useragent": request.user_agent.string,
"message": error_message,
},
performer=validated,
)
return ValidateResult(AuthKind.oauth, error_message=error_message)
# Don't allow disabled users to login.
if not validated.authorized_user.enabled:
error_message = "Granter of the oauth access token is disabled"
if app.config.get("ACTION_LOG_AUDIT_LOGIN_FAILURES"):
log_action(
"login_failure",
validated.application.organization.username,
{
"type": "quayauth",
"kind": "oauth",
"token": validated.token_name,
"application_name": validated.application.name,
"username": validated.authorized_user.username,
"useragent": request.user_agent.string,
"message": error_message,
},
performer=validated.authorized_user,
)
return ValidateResult(
AuthKind.oauth, error_message="Granter of the oauth access token is disabled"
AuthKind.oauth,
error_message=error_message,
)
# We have a valid token

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from base64 import b64encode
from test.fixtures import *
import pytest
@ -13,6 +12,7 @@ from auth.credentials import (
)
from auth.validateresult import AuthKind, ValidateResult
from data import model
from test.fixtures import *
def _token(username, password):

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
from test.fixtures import *
from auth.credential_consts import (
ACCESS_TOKEN_USERNAME,
APP_SPECIFIC_TOKEN_USERNAME,
@ -10,6 +8,8 @@ from auth.credential_consts import (
from auth.credentials import CredentialKind, validate_credentials
from auth.validateresult import AuthKind, ValidateResult
from data import model
from data.database import RobotAccountToken
from test.fixtures import *
def test_valid_user(app):
@ -27,14 +27,37 @@ def test_valid_robot(app):
def test_valid_robot_for_disabled_user(app):
user = model.user.get_user("devtable")
robot, password = model.user.create_robot("somerobot", user)
user.enabled = False
user.save()
robot, password = model.user.create_robot("somerobot", user)
result, kind = validate_credentials(robot.username, password)
assert kind == CredentialKind.robot
err = "This user has been disabled. Please contact your administrator."
err = "Robot %s owner %s is disabled" % (robot.username, user.username)
assert result == ValidateResult(AuthKind.credentials, error_message=err)
def test_valid_robot_with_invalid_password(app):
robot, _ = model.user.create_robot("somerobot", model.user.get_user("devtable"))
result, kind = validate_credentials(robot.username, "wrongpassword")
assert kind == CredentialKind.robot
err = "Could not find robot with username: %s and supplied password." % robot.username
assert result == ValidateResult(AuthKind.credentials, error_message=err)
def test_valid_robot_with_invalid_token(app):
robot, password = model.user.create_robot("somerobot", model.user.get_user("devtable"))
token = RobotAccountToken.get(robot_account=robot)
token.delete_instance()
result, kind = validate_credentials(robot.username, password)
assert kind == CredentialKind.robot
err = "Could not find robot with username: %s and supplied password." % robot.username
assert result == ValidateResult(AuthKind.credentials, error_message=err)

View File

@ -441,6 +441,12 @@ class DefaultConfig(ImmutableConfig):
# Action logs configuration for advanced events
ACTION_LOG_AUDIT_LOGINS = True
# Action logs configuration for failure tracking
ACTION_LOG_AUDIT_LOGIN_FAILURES = False
ACTION_LOG_AUDIT_PULL_FAILURES = False
ACTION_LOG_AUDIT_PUSH_FAILURES = False
ACTION_LOG_AUDIT_DELETE_FAILURES = False
# Action logs archive
ACTION_LOG_ARCHIVE_LOCATION: Optional[str] = "local_us"
ACTION_LOG_ARCHIVE_PATH: Optional[str] = "actionlogarchive/"

View File

@ -0,0 +1,37 @@
"""add login and pull failure logentrykind
Revision ID: 3f8e3657bb67
Revises: 8d47693829a0
Create Date: 2023-05-06 14:21:16.580825
"""
# revision identifiers, used by Alembic.
revision = "3f8e3657bb67"
down_revision = "8d47693829a0"
import sqlalchemy as sa
def upgrade(op, tables, tester):
op.bulk_insert(
tables.logentrykind,
[
{"name": "login_failure"},
{"name": "push_repo_failed"},
{"name": "pull_repo_failed"},
{"name": "delete_tag_failed"},
],
)
def downgrade(op, tables, tester):
op.execute(
tables.logentrykind.delete().where(
tables.logentrykind.name
== op.inline_literal("login_failure") | tables.logentrykind.name
== op.inline_literal("push_repo_failed") | tables.logentrykind.name
== op.inline_literal("pull_repo_failed") | tables.logentrykind.name
== op.inline_literal("delete_tag_failed")
)
)

View File

@ -41,6 +41,18 @@ class InvalidRobotException(DataModelException):
pass
class DeactivatedRobotOwnerException(InvalidRobotException):
pass
class InvalidRobotCredentialException(InvalidRobotException):
pass
class InvalidRobotOwnerException(InvalidRobotException):
pass
class InvalidUsernameException(DataModelException):
pass

View File

@ -45,10 +45,13 @@ from data.database import (
from data.fields import Credential
from data.model import (
DataModelException,
DeactivatedRobotOwnerException,
InvalidEmailAddressException,
InvalidNamespaceException,
InvalidPasswordException,
InvalidRobotCredentialException,
InvalidRobotException,
InvalidRobotOwnerException,
InvalidUsernameException,
TooManyLoginAttemptsException,
_basequery,
@ -459,30 +462,27 @@ def verify_robot(robot_username, password):
robot = lookup_robot(robot_username)
assert robot.robot
# Find the owner user and ensure it is not disabled.
try:
owner = User.get(User.username == result[0])
except User.DoesNotExist:
raise InvalidRobotOwnerException("Robot %s owner does not exist" % robot_username)
if not owner.enabled:
raise DeactivatedRobotOwnerException(
"Robot %s owner %s is disabled" % (robot_username, owner.username)
)
# Lookup the token for the robot.
try:
token_data = RobotAccountToken.get(robot_account=robot)
if not token_data.token.matches(password):
msg = "Could not find robot with username: %s and supplied password." % robot_username
raise InvalidRobotException(msg)
raise InvalidRobotCredentialException(msg)
except RobotAccountToken.DoesNotExist:
msg = "Could not find robot with username: %s and supplied password." % robot_username
raise InvalidRobotException(msg)
raise InvalidRobotCredentialException(msg)
# Find the owner user and ensure it is not disabled.
try:
owner = User.get(User.username == result[0])
except User.DoesNotExist:
raise InvalidRobotException("Robot %s owner does not exist" % robot_username)
if not owner.enabled:
raise InvalidRobotException(
"This user has been disabled. Please contact your administrator."
)
# Mark that the robot was accessed.
_basequery.update_last_accessed(robot)
return robot
@ -790,6 +790,13 @@ def validate_reset_code(token):
def find_user_by_email(email):
# Make sure we didn't get any unicode for the email.
try:
if email and isinstance(email, str):
email.encode("ascii")
except UnicodeEncodeError:
return None
try:
return User.get(User.email == email)
except User.DoesNotExist:
@ -797,6 +804,13 @@ def find_user_by_email(email):
def get_nonrobot_user(username):
# Make sure we didn't get any unicode for the username.
try:
if username and isinstance(username, str):
username.encode("ascii")
except UnicodeEncodeError:
return None
try:
return User.get(User.username == username, User.organization == False, User.robot == False)
except User.DoesNotExist:
@ -804,6 +818,13 @@ def get_nonrobot_user(username):
def get_user(username):
# Make sure we didn't get any unicode for the username.
try:
if username and isinstance(username, str):
username.encode("ascii")
except UnicodeEncodeError:
return None
try:
return User.get(User.username == username, User.organization == False)
except User.DoesNotExist:
@ -811,6 +832,13 @@ def get_user(username):
def get_namespace_user(username):
# Make sure we didn't get any unicode for the username.
try:
if username and isinstance(username, str):
username.encode("ascii")
except UnicodeEncodeError:
return None
try:
return User.get(User.username == username)
except User.DoesNotExist:
@ -818,6 +846,13 @@ def get_namespace_user(username):
def get_user_or_org(username):
# Make sure we didn't get any unicode for the username.
try:
if username and isinstance(username, str):
username.encode("ascii")
except UnicodeEncodeError:
return None
try:
return User.get(User.username == username, User.robot == False)
except User.DoesNotExist:
@ -986,7 +1021,8 @@ def verify_user(username_or_email, password):
# Make sure we didn't get any unicode for the username.
try:
username_or_email.encode("ascii")
if username_or_email and isinstance(username_or_email, str):
username_or_email.encode("ascii")
except UnicodeEncodeError:
return None

View File

@ -32,6 +32,7 @@ from auth.permissions import (
from data import model as data_model
from data.database import RepositoryState
from data.logs_model import logs_model
from digest import digest_tools
from endpoints.csrf import csrf_protect
from endpoints.decorators import (
check_anon_protection,
@ -400,6 +401,87 @@ require_user_read = require_user_permission(UserReadPermission, scopes.READ_USER
require_user_admin = require_user_permission(UserAdminPermission, scopes.ADMIN_USER)
def log_unauthorized(audit_event):
def inner(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except endpoints.v2.errors.Unauthorized as e:
if (
(
app.config.get("ACTION_LOG_AUDIT_PUSH_FAILURES")
and audit_event == "push_repo_failed"
)
or (
app.config.get("ACTION_LOG_AUDIT_PULL_FAILURES")
and audit_event == "pull_repo_failed"
)
or (
app.config.get("ACTION_LOG_AUDIT_DELETE_FAILURES")
and audit_event == "delete_tag_failed"
)
):
if "namespace_name" in kwargs and "repo_name" in kwargs:
metadata = {
"namespace": kwargs["namespace_name"],
"repo": kwargs["repo_name"],
}
if "manifest_ref" in kwargs:
try:
digest = digest_tools.Digest.parse_digest(kwargs["manifest_ref"])
metadata["manifest_digest"] = str(digest)
except digest_tools.InvalidDigestException:
metadata["tag"] = kwargs["manifest_ref"]
user_or_orgname = data_model.user.get_user_or_org(kwargs["namespace_name"])
if user_or_orgname is not None:
repo = data_model.repository.get_repository(
user_or_orgname.username, kwargs["repo_name"]
)
else:
repo = None
if user_or_orgname is None:
metadata["message"] = "Namespace does not exist"
log_action(
kind=audit_event,
user_or_orgname=None,
metadata=metadata,
)
elif repo is None:
metadata["message"] = "Repository does not exist"
log_action(
kind=audit_event,
user_or_orgname=user_or_orgname.username,
metadata=metadata,
)
else:
metadata["message"] = str(e)
log_action(
kind=audit_event,
user_or_orgname=user_or_orgname.username,
repo_name=repo.name,
metadata=metadata,
)
logger.debug("Unauthorized request: %s", e)
raise e
return wrapper
return inner
log_unauthorized_pull = log_unauthorized("pull_repo_failed")
log_unauthorized_push = log_unauthorized("push_repo_failed")
log_unauthorized_delete = log_unauthorized("delete_tag_failed")
def allow_if_superuser():
return app.config.get("FEATURE_SUPERUSERS_FULL_ACCESS", False) and SuperUserPermission().can()
@ -507,7 +589,7 @@ def request_error(exception=None, **kwargs):
raise InvalidRequest(message, data)
def log_action(kind, user_or_orgname, metadata=None, repo=None, repo_name=None):
def log_action(kind, user_or_orgname, metadata=None, repo=None, repo_name=None, performer=None):
if not metadata:
metadata = {}
@ -517,7 +599,8 @@ def log_action(kind, user_or_orgname, metadata=None, repo=None, repo_name=None):
metadata["oauth_token_application_id"] = oauth_token.application.client_id
metadata["oauth_token_application"] = oauth_token.application.name
performer = get_authenticated_user()
if performer is None:
performer = get_authenticated_user()
if repo_name is not None:
repo = data_model.repository.get_repository(user_or_orgname, repo_name)

View File

@ -720,6 +720,22 @@ def conduct_signin(username_or_email, password, invite_code=None):
needs_email_verification = True
else:
if app.config.get("ACTION_LOG_AUDIT_LOGIN_FAILURES"):
possible_user = model.user.get_nonrobot_user(
username_or_email
) or model.user.find_user_by_email(username_or_email)
log_action(
"login_failure",
possible_user.username if possible_user else None,
{
"type": "quayauth",
"kind": "user",
"useragent": request.user_agent.string,
"username": username_or_email,
"message": error_message,
},
performer=possible_user,
)
invalid_credentials = True
return (

View File

@ -5,7 +5,6 @@ from functools import wraps
from flask import jsonify, make_response, request, session
import features
from app import app, docker_v2_signing_key, storage, userevents
from auth.auth_context import get_authenticated_context, get_authenticated_user
from auth.credentials import CredentialKind, validate_credentials

View File

@ -22,6 +22,7 @@ from data.registry_model.blobuploader import (
retrieve_blob_upload_manager,
)
from digest import digest_tools
from endpoints.api import log_unauthorized
from endpoints.decorators import (
anon_allowed,
anon_protect,
@ -240,6 +241,7 @@ def _try_to_mount_blob(repository_ref, mount_blob_digest):
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull", "push"])
@log_unauthorized("push_repo_failed")
@require_repo_write(allow_for_superuser=True, disallow_for_restricted_users=True)
@anon_protect
@check_readonly

View File

@ -13,12 +13,16 @@ from data.model import (
RepositoryDoesNotExist,
TagDoesNotExist,
namespacequota,
repository,
)
from data.model.oci.manifest import CreateManifestException
from data.model.oci.tag import RetargetTagException
from data.registry_model import registry_model
from digest import digest_tools
from endpoints.api import (
log_unauthorized_delete,
log_unauthorized_pull,
log_unauthorized_push,
)
from endpoints.decorators import (
anon_protect,
check_readonly,
@ -62,6 +66,7 @@ MANIFEST_TAGNAME_ROUTE = BASE_MANIFEST_ROUTE.format(VALID_TAG_PATTERN)
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull"])
@log_unauthorized_pull
@require_repo_read(allow_for_superuser=True)
@anon_protect
@inject_registry_model()
@ -135,6 +140,7 @@ def fetch_manifest_by_tagname(namespace_name, repo_name, manifest_ref, registry_
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull"])
@log_unauthorized_pull
@require_repo_read(allow_for_superuser=True)
@anon_protect
@inject_registry_model()
@ -262,6 +268,7 @@ def _doesnt_accept_schema_v1():
@parse_repository_name()
@_reject_manifest2_schema2
@process_registry_jwt_auth(scopes=["pull", "push"])
@log_unauthorized_push
@require_repo_write(allow_for_superuser=True, disallow_for_restricted_users=True)
@anon_protect
@check_readonly
@ -285,6 +292,7 @@ def _enqueue_blobs_for_replication(manifest, storage, namespace_name):
@parse_repository_name()
@_reject_manifest2_schema2
@process_registry_jwt_auth(scopes=["pull", "push"])
@log_unauthorized_push
@require_repo_write(allow_for_superuser=True, disallow_for_restricted_users=True)
@anon_protect
@check_readonly
@ -361,6 +369,7 @@ def _parse_manifest(content_type, request_data):
@disallow_for_account_recovery_mode
@parse_repository_name()
@process_registry_jwt_auth(scopes=["pull", "push"])
@log_unauthorized_delete
@require_repo_write(allow_for_superuser=True, disallow_for_restricted_users=True)
@anon_protect
@check_readonly

View File

@ -354,11 +354,14 @@ def initialize_database():
LogEntryKind.create(name="create_repo")
LogEntryKind.create(name="push_repo")
LogEntryKind.create(name="push_repo_failed")
LogEntryKind.create(name="pull_repo")
LogEntryKind.create(name="pull_repo_failed")
LogEntryKind.create(name="delete_repo")
LogEntryKind.create(name="create_tag")
LogEntryKind.create(name="move_tag")
LogEntryKind.create(name="delete_tag")
LogEntryKind.create(name="delete_tag_failed")
LogEntryKind.create(name="revert_tag")
LogEntryKind.create(name="add_repo_permission")
LogEntryKind.create(name="change_repo_permission")
@ -451,6 +454,7 @@ def initialize_database():
LogEntryKind.create(name="cancel_build")
LogEntryKind.create(name="login_success")
LogEntryKind.create(name="login_failure")
LogEntryKind.create(name="logout_success")
LogEntryKind.create(name="permanently_delete_tag")

View File

@ -21,9 +21,9 @@ angular.module('quay').directive('logsView', function () {
'repository': '=repository',
'allLogs': '@allLogs'
},
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService,
StringBuilderService, ExternalNotificationData, UtilService,
Features, humanizeIntervalFilter, humanizeDateFilter, StateService) {
controller: function ($scope, $element, $sce, Restangular, ApiService, TriggerService,
StringBuilderService, ExternalNotificationData, UtilService,
Features, humanizeIntervalFilter, humanizeDateFilter, StateService) {
$scope.inReadOnlyMode = StateService.inReadOnlyMode();
$scope.Features = Features;
$scope.loading = true;
@ -43,18 +43,18 @@ angular.module('quay').directive('logsView', function () {
$scope.options.monthAgo = moment().subtract(1, 'month').calendar();
$scope.options.now = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate());
var getOffsetDate = function(date, days) {
var getOffsetDate = function (date, days) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
};
var defaultPermSuffix = function(metadata) {
var defaultPermSuffix = function (metadata) {
if (metadata.activating_username) {
return ', when creating user is {activating_username}';
}
return '';
};
var getServiceKeyTitle = function(metadata) {
var getServiceKeyTitle = function (metadata) {
if (metadata.name) {
return metadata.name;
}
@ -63,59 +63,59 @@ angular.module('quay').directive('logsView', function () {
};
var logDescriptions = {
'user_create': function(metadata) {
if(metadata.superuser) {
'user_create': function (metadata) {
if (metadata.superuser) {
return 'Superuser {superuser} created user {username}';
} else {
return 'User {username} created';
}
},
'user_delete': function(metadata) {
if(metadata.superuser) {
'user_delete': function (metadata) {
if (metadata.superuser) {
return 'Superuser {superuser} deleted user {username}';
} else {
return 'User {username} deleted';
}
},
'user_enable': function(metadata) {
if(metadata.superuser) {
'user_enable': function (metadata) {
if (metadata.superuser) {
return 'Superuser {superuser} enabled user {username}';
} else {
return 'User {username} enabled';
}
},
'user_disable': function(metadata) {
if(metadata.superuser) {
'user_disable': function (metadata) {
if (metadata.superuser) {
return 'Superuser {superuser} disabled user {username}';
} else {
return 'User {username} disabled';
}
},
'user_change_password': function(metadata) {
if(metadata.superuser) {
'user_change_password': function (metadata) {
if (metadata.superuser) {
return 'Superuser {superuser} changed password of user {username}';
} else {
return 'User {username} changed password';
}
},
'user_change_email': function(metadata) {
if(metadata.superuser) {
'user_change_email': function (metadata) {
if (metadata.superuser) {
return 'Superuser {superuser} changed email from {old_email} to {email}';
} else {
return 'Changed email from {old_email} to {email}';
}
},
'user_change_name': function(metadata) {
if(metadata.superuser) {
'user_change_name': function (metadata) {
if (metadata.superuser) {
return 'Superuser {superuser} renamed user {old_username} to {username}';
} else {
return 'User {old_username} changed name to {username}';
}
},
'user_change_invoicing': function(metadata) {
'user_change_invoicing': function (metadata) {
if (metadata.invoice_email) {
return 'Enabled email invoicing';
} else if(metadata.invoice_email_address) {
} else if (metadata.invoice_email_address) {
return 'Set email invoicing address to {invoice_email_address}';
} else {
return 'Disabled email invoicing';
@ -136,7 +136,7 @@ angular.module('quay').directive('logsView', function () {
'repo_mirror_sync_failed': 'Mirror finished unsuccessfully for {message}[[{tags}]][[ {stdout}]][[ {stderr}]]',
'repo_mirror_sync_tag_success': 'Mirror of {tag} successful[[ to repository {namespace}/{repo}]][[ {message}]][[ {stdout}]][[ {stderr}]]',
'repo_mirror_sync_tag_failed': 'Mirror of {tag} failure[[ to repository {namespace}/{repo}]][[ {message}]][[ {stdout}]][[ {stderr}]]',
'repo_mirror_config_changed': function(metadata) {
'repo_mirror_config_changed': function (metadata) {
switch (metadata.changed) {
case 'sync_status':
if (metadata.to === 'SYNC_CANCEL') {
@ -192,10 +192,10 @@ angular.module('quay').directive('logsView', function () {
return 'Mirror {changed} changed to {to}';
default:
return 'Mirror {changed} changed to {to}';
}
}
},
'change_repo_state': 'Repository state changed to {state_changed}',
'push_repo': function(metadata) {
'push_repo': function (metadata) {
if (metadata.tag) {
return 'Push of {tag}[[ to repository {namespace}/{repo}]]';
} else if (metadata.release) {
@ -204,7 +204,7 @@ angular.module('quay').directive('logsView', function () {
return 'Repository push[[ to {namespace}/{repo}]]';
}
},
'repo_verb': function(metadata) {
'repo_verb': function (metadata) {
var prefix = '';
if (metadata.verb == 'squash') {
prefix = 'Pull of squashed tag {tag}[[ from {namespace}/{repo}]]'
@ -228,7 +228,7 @@ angular.module('quay').directive('logsView', function () {
return prefix;
},
'pull_repo': function(metadata) {
'pull_repo': function (metadata) {
var description = 'repository {namespace}/{repo}';
if (metadata.tag) {
description = 'tag {tag}[[ from repository {namespace}/{repo}]]';
@ -259,25 +259,25 @@ angular.module('quay').directive('logsView', function () {
}
},
'delete_repo': 'Delete repository {repo}',
'change_repo_permission': function(metadata) {
'change_repo_permission': function (metadata) {
if (metadata.username) {
return 'Change permission for [[user ]]{username}[[ in repository {namespace}/{repo}]] to {role}';
} else if (metadata.team) {
return 'Change permission for [[team ]]{team}[[ in repository {namespace}/{repo}]] to {role}';
return 'Change permission for [[team ]]{team}[[ in repository {namespace}/{repo}]] to {role}';
} else if (metadata.token) {
return 'Change permission for [[token ]]{token}[[ in repository {namespace}/{repo}]] to {role}';
}
},
'delete_repo_permission': function(metadata) {
'delete_repo_permission': function (metadata) {
if (metadata.username) {
return 'Remove permission for [[user ]]{username}[[ from repository {namespace}/{repo}]]';
} else if (metadata.team) {
return 'Remove permission for [[team ]]{team}[[ from repository {namespace}/{repo}]]';
return 'Remove permission for [[team ]]{team}[[ from repository {namespace}/{repo}]]';
} else if (metadata.token) {
return 'Remove permission for [[token ]]{token}[[ from repository {namespace}/{repo}]]';
}
},
'revert_tag': function(metadata) {
'revert_tag': function (metadata) {
if (metadata.manifest_digest) {
return 'Tag {tag} restored to {manifest_digest}';
} else {
@ -286,15 +286,15 @@ angular.module('quay').directive('logsView', function () {
},
'autoprune_tag_delete': 'Tag {tag} pruned[[ in repository {namespace}/{repo} by {performer}]]',
'delete_tag': 'Tag {tag} deleted[[ in repository {namespace}/{repo} by user {username}]]',
'permanently_delete_tag': function(metadata){
if (metadata.manifest_digest){
'permanently_delete_tag': function (metadata) {
if (metadata.manifest_digest) {
return 'Tag {tag} referencing {manifest_digest} permanently deleted[[ in repository {namespace}/{repo} by user {username}]]';
} else {
return 'Tag {tag} permanently deleted[[ in repository {namespace}/{repo} by user {username}]]'
}
},
'create_tag': 'Tag {tag} created[[ in repository {namespace}/{repo} on image {image} by user {username}]]',
'move_tag': function(metadata) {
'move_tag': function (metadata) {
if (metadata.manifest_digest) {
return 'Tag {tag} moved[[ from {original_manifest_digest}]] to {manifest_digest}[[ in repository {namespace}/{repo} by user {username}]]';
} else {
@ -302,7 +302,7 @@ angular.module('quay').directive('logsView', function () {
}
},
'change_repo_visibility': 'Change visibility[[ for repository {namespace}/{repo}]] to {visibility}',
'change_repo_trust': function(metadata) {
'change_repo_trust': function (metadata) {
if (metadata.trust_enabled) {
return 'Trust enabled[[ for {namespace}/{repo}]]';
} else {
@ -312,7 +312,7 @@ angular.module('quay').directive('logsView', function () {
'add_repo_accesstoken': 'Create access token {token}[[ in repository {repo}]]',
'delete_repo_accesstoken': 'Delete access token {token}[[ in repository {repo}]]',
'set_repo_description': 'Change description[[ for repository {namespace}/{repo} to {description}]]',
'build_dockerfile': function(metadata) {
'build_dockerfile': function (metadata) {
if (metadata.trigger_id) {
var triggerDescription = TriggerService.getDescription(
metadata['service'], metadata['config']);
@ -323,17 +323,17 @@ angular.module('quay').directive('logsView', function () {
'org_create': 'Organization {namespace} created',
'org_delete': 'Organization {namespace} deleted',
'org_change_email': 'Change organization email from {old_email} to {email}',
'org_change_invoicing': function(metadata) {
'org_change_invoicing': function (metadata) {
if (metadata.invoice_email) {
return 'Enabled email invoicing';
} else if(metadata.invoice_email_address) {
} else if (metadata.invoice_email_address) {
return 'Set email invoicing address to {invoice_email_address}';
} else {
return 'Disabled email invoicing';
}
},
'org_change_tag_expiration': 'Change time machine window to {tag_expiration}',
'org_change_name': function(metadata) {
'org_change_name': function (metadata) {
if (metadata.superuser) {
return 'Superuser {superuser} renamed organization from {old_name} to {new_name}';
} else {
@ -344,14 +344,14 @@ angular.module('quay').directive('logsView', function () {
'org_delete_team': 'Delete team {team}',
'org_add_team_member': 'Add member {member} to team {team}',
'org_remove_team_member': 'Remove member {member} from team {team}',
'org_invite_team_member': function(metadata) {
'org_invite_team_member': function (metadata) {
if (metadata.user) {
return 'Invite {user} to team {team}';
} else {
return 'Invite {email} to team {team}';
}
},
'org_delete_team_member_invite': function(metadata) {
'org_delete_team_member_invite': function (metadata) {
if (metadata.user) {
return 'Rescind invite of {user} to team {team}';
} else {
@ -364,38 +364,38 @@ angular.module('quay').directive('logsView', function () {
'org_set_team_description': 'Change description of team {team}[[ to {description}]]',
'org_set_team_role': 'Change permission of team {team} to {role}',
'create_prototype_permission': function(metadata) {
'create_prototype_permission': function (metadata) {
if (metadata.delegate_user) {
return 'Create default permission: {role} for {delegate_user}' + defaultPermSuffix(metadata);
} else if (metadata.delegate_team) {
return 'Create default permission: {role} for {delegate_team}' + defaultPermSuffix(metadata);
}
},
'modify_prototype_permission': function(metadata) {
'modify_prototype_permission': function (metadata) {
if (metadata.delegate_user) {
return 'Modify default permission: {role} (from {original_role}) for {delegate_user}' + defaultPermSuffix(metadata);
} else if (metadata.delegate_team) {
return 'Modify default permission: {role} (from {original_role}) for {delegate_team}' + defaultPermSuffix(metadata);
}
},
'delete_prototype_permission': function(metadata) {
'delete_prototype_permission': function (metadata) {
if (metadata.delegate_user) {
return 'Delete default permission: {role} for {delegate_user}' + defaultPermSuffix(metadata);
} else if (metadata.delegate_team) {
return 'Delete default permission: {role} for {delegate_team}' + defaultPermSuffix(metadata);
}
},
'setup_repo_trigger': function(metadata) {
'setup_repo_trigger': function (metadata) {
var triggerDescription = TriggerService.getDescription(
metadata['service'], metadata['config']);
return 'Setup build trigger[[ - ' + triggerDescription + ']]';
},
'delete_repo_trigger': function(metadata) {
'delete_repo_trigger': function (metadata) {
var triggerDescription = TriggerService.getDescription(
metadata['service'], metadata['config']);
return 'Delete build trigger[[ - ' + triggerDescription + ']]';
},
'toggle_repo_trigger': function(metadata) {
'toggle_repo_trigger': function (metadata) {
var triggerDescription = TriggerService.getDescription(
metadata['service'], metadata['config']);
if (metadata.enabled) {
@ -410,24 +410,24 @@ angular.module('quay').directive('logsView', function () {
'reset_application_client_secret': 'Reset the client secret of application {application_name}[[ ' +
'with client ID {client_id}]]',
'add_repo_notification': function(metadata) {
'add_repo_notification': function (metadata) {
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
return 'Add notification of event "' + eventData['title'] + '"[[ for repository {namespace}/{repo}]]';
},
'delete_repo_notification': function(metadata) {
'delete_repo_notification': function (metadata) {
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
return 'Delete notification of event "' + eventData['title'] + '"[[ for repository {namespace}/{repo}]]';
},
'reset_repo_notification': function(metadata) {
'reset_repo_notification': function (metadata) {
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
return 'Re-enable notification of event "' + eventData['title'] + '"[[ for repository {namespace}/{repo}]]';
},
'regenerate_robot_token': 'Regenerated token for robot {robot}',
'service_key_create': function(metadata) {
'service_key_create': function (metadata) {
if (metadata.preshared) {
return 'Manual creation of[[ preshared service]] key {kid}[[ for service {service}]]';
} else {
@ -441,7 +441,7 @@ angular.module('quay').directive('logsView', function () {
'service_key_extend': 'Change of expiration of service key {kid}[[ from {old_expiration_date}] to {expiration_date}',
'service_key_rotate': 'Automatic rotation of service key {kid} by {user_agent}',
'take_ownership': function(metadata) {
'take_ownership': function (metadata) {
if (metadata.was_user) {
return 'Superuser {superuser} took ownership of user namespace {namespace}';
} else {
@ -452,7 +452,7 @@ angular.module('quay').directive('logsView', function () {
'manifest_label_add': 'Label {key} added to[[ manifest]] {manifest_digest}[[ under repository {namespace}/{repo}]]',
'manifest_label_delete': 'Label {key} deleted from[[ manifest]] {manifest_digest}[[ under repository {namespace}/{repo}]]',
'change_tag_expiration': function(metadata) {
'change_tag_expiration': function (metadata) {
if (metadata.expiration_date && metadata.old_expiration_date) {
return 'Tag {tag} set to expire on {expiration_date}[[ (previously {old_expiration_date})]]';
} else if (metadata.expiration_date) {
@ -466,7 +466,7 @@ angular.module('quay').directive('logsView', function () {
'create_app_specific_token': 'Created external application token {app_specific_token_title}',
'revoke_app_specific_token': 'Revoked external application token {app_specific_token_title}',
'repo_mirror': function(metadata) {
'repo_mirror': function (metadata) {
if (metadata.message) {
return 'Repository mirror {verb} by Skopeo: {message}';
} else {
@ -481,14 +481,14 @@ angular.module('quay').directive('logsView', function () {
'create_proxy_cache_config': 'Create proxy cache for {namespace}[[ with upstream {upstream_registry}]]',
'delete_proxy_cache_config': 'Create proxy cache for {namespace}',
'start_build_trigger': function(metadata) {
'start_build_trigger': function (metadata) {
var triggerDescription = TriggerService.getDescription(
metadata['service'], metadata['config']);
return 'Manually start build from trigger[[ - ' + triggerDescription + ']]';
},
'cancel_build': 'Cancel build {build_uuid}',
'login_success': function(metadata) {
metadata["useragent"] = metadata.useragent.substring(0, 64)+ '...';
'login_success': function (metadata) {
metadata["useragent"] = metadata.useragent.length > 64 ? metadata.useragent.slice(0, 64) + '...' : metadata.useragent;
if (metadata.type == 'v2auth') {
var message = 'Login to registry[[ with';
@ -505,11 +505,107 @@ angular.module('quay').directive('logsView', function () {
return 'Login to Quay[[ with user-agent {useragent}]]';
}
},
'login_failure': function (metadata) {
metadata["useragent"] = metadata.useragent.length > 64 ? metadata.useragent.slice(0, 64) + '...' : metadata.useragent;
if (metadata.type == 'v2auth') {
var message = 'Login to registry failed[[ with';
if (metadata.kind == 'app_specific_token') {
message += ' app-specific token';
if (metadata.app_specific_token_title) {
message += ' {app_specific_token_title}';
}
if (metadata.username) {
message += ' (owned by {username})';
}
message += ' and'
}
else if (metadata.kind == 'robot') {
message += ' robot {robot} (owned by {username}) and';
}
else if (metadata.kind == 'user') {
message += ' user {username} and';
}
message += ' user-agent {useragent}';
if (metadata.message) {
message += ' with message: {message}';
}
return message + ']]';
} else if (metadata.type == 'quayauth') {
if (metadata.kind == 'user') {
return 'Login to Quay failed[[ with username {username} and user-agent {useragent} and message: {message}]]';
}
else if (metadata.kind == 'oauth') {
var message = 'API access to Quay failed[[ with';
if (metadata.token) {
message += ' token {token} (owned by {username} in application {application_name}) and';
}
message += ' user-agent {useragent}';
if (metadata.message) {
message += ' with message: {message}';
}
return message + ']]';
}
} else {
return 'Login to Quay failed[[ with user-agent {useragent}]]';
}
},
'logout_success': 'Logout from Quay',
'create_namespace_autoprune_policy': 'Autoprune policy created for namespace {namespace}[[ with policy method {method} and value {value}]]',
'update_namespace_autoprune_policy': 'Autoprune policy updated for namespace {namespace}[[ with policy method {method} and value {value}]]',
'delete_namespace_autoprune_policy': 'Autoprune policy deleted for namespace {namespace}[[ with uuid {policy_uuid}]]',
};
'pull_repo_failed': function (metadata) {
var message = 'Pull from repository {namespace}/{repo} failed';
if (metadata.tag) {
message += '[[ for tag {tag} with message {message}]]';
} else if (metadata.manifest_digest) {
message += '[[ for manifest {manifest_digest} with message {message}]]';
} else if (metadata.message) {
message += '[[ with message {message}]]'
}
return message;
},
'push_repo_failed': function (metadata) {
var message = 'Push to repository {namespace}/{repo} failed';
if (metadata.tag) {
message += '[[ for tag {tag} with message {message}]]';
} else if (metadata.manifest_digest) {
message += '[[ for manifest {manifest_digest} with message {message}]]';
} else if (metadata.message) {
message += '[[ with message {message}]]'
}
return message;
},
'delete_tag_failed': function (metadata) {
var message = 'Delete tag {namespace}/{repo} failed';
if (metadata.tag) {
message += '[[ for tag {tag} with message {message}]]';
} else if (metadata.manifest_digest) {
message += '[[ for manifest {manifest_digest} with message {message}]]';
} else if (metadata.message) {
message += '[[ with message {message}]]'
}
return message;
}
};
var logKinds = {
'user_create': 'Create user',
@ -531,8 +627,10 @@ angular.module('quay').directive('logsView', function () {
'delete_robot': 'Delete Robot Account',
'create_repo': 'Create Repository',
'push_repo': 'Push to repository',
'push_repo_failed': 'Push to repository failed',
'repo_verb': 'Pull Repo Verb',
'pull_repo': 'Pull repository',
'pull_repo_failed': 'Pull repository failed',
'delete_repo': 'Delete repository',
'change_repo_permission': 'Change repository permission',
'delete_repo_permission': 'Remove user permission from repository',
@ -543,9 +641,10 @@ angular.module('quay').directive('logsView', function () {
'set_repo_description': 'Change repository description',
'build_dockerfile': 'Build image from Dockerfile',
'delete_tag': 'Delete Tag',
'delete_tag_failed': 'Delete Tag Failed',
'create_tag': 'Create Tag',
'move_tag': 'Move Tag',
'revert_tag':'Restore Tag',
'revert_tag': 'Restore Tag',
'org_create': 'Create organization',
'org_delete': 'Delete organization',
'org_change_email': 'Change organization email',
@ -606,6 +705,7 @@ angular.module('quay').directive('logsView', function () {
'cancel_build': 'Cancel build',
'login_success': 'Login success',
'permanently_delete_tag': 'Permanently Delete Tag',
'login_failure': 'Login failure',
'autoprune_tag_delete': 'Autoprune worker tag deletion',
// Note: these are deprecated.
@ -613,11 +713,11 @@ angular.module('quay').directive('logsView', function () {
'delete_repo_webhook': 'Delete webhook'
};
var getDateString = function(date) {
var getDateString = function (date) {
return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear();
};
var getUrl = function(suffix) {
var getUrl = function (suffix) {
var url = UtilService.getRestUrl('user/' + suffix);
if ($scope.organization) {
url = UtilService.getRestUrl('organization', $scope.organization.name, suffix);
@ -635,7 +735,7 @@ angular.module('quay').directive('logsView', function () {
return url;
};
var update = function() {
var update = function () {
var hasValidUser = !!$scope.user;
var hasValidOrg = !!$scope.organization;
var hasValidRepo = $scope.repository && $scope.repository.namespace;
@ -655,14 +755,14 @@ angular.module('quay').directive('logsView', function () {
var aggregateUrl = getUrl('aggregatelogs').toString();
var loadAggregate = Restangular.one(aggregateUrl);
loadAggregate.customGET().then(function(resp) {
loadAggregate.customGET().then(function (resp) {
$scope.chart = new LogUsageChart(logKinds);
$($scope.chart).bind('filteringChanged', function(e) {
$scope.$apply(function() { $scope.kindsAllowed = e.allowed; });
$($scope.chart).bind('filteringChanged', function (e) {
$scope.$apply(function () { $scope.kindsAllowed = e.allowed; });
});
$scope.chart.draw('bar-chart', resp.aggregated, $scope.options.logStartDate,
$scope.options.logEndDate);
$scope.options.logEndDate);
$scope.chartLoading = false;
}).catch(function (resp) {
if (resp.status === 501) {
@ -679,7 +779,7 @@ angular.module('quay').directive('logsView', function () {
$scope.nextPage();
};
$scope.nextPage = function() {
$scope.nextPage = function () {
if ($scope.loading || !$scope.hasAdditional) { return; }
$scope.loading = true;
@ -690,7 +790,7 @@ angular.module('quay').directive('logsView', function () {
url.setQueryParameter('next_page', $scope.nextPageToken);
var loadLogs = Restangular.one(url.toString());
loadLogs.customGET().then(function(resp) {
loadLogs.customGET().then(function (resp) {
if ($scope.loadCounter != currentCounter) {
// Loaded logs data is out of date.
return;
@ -704,7 +804,7 @@ angular.module('quay').directive('logsView', function () {
$scope.nextPage();
}
resp.logs.forEach(function(log) {
resp.logs.forEach(function (log) {
$scope.logs.push(log);
});
@ -718,24 +818,24 @@ angular.module('quay').directive('logsView', function () {
});
};
$scope.toggleChart = function() {
$scope.toggleChart = function () {
$scope.chartVisible = !$scope.chartVisible;
};
$scope.isVisible = function(allowed, kind) {
$scope.isVisible = function (allowed, kind) {
return allowed == null || allowed.hasOwnProperty(kind);
};
$scope.toggleExpanded = function(log) {
$scope.toggleExpanded = function (log) {
log._expanded = !log._expanded;
};
$scope.getColor = function(kind, chart) {
$scope.getColor = function (kind, chart) {
if (!chart) { return 'gray'; }
return chart.getColor(kind);
};
$scope.getDescription = function(log, full_description) {
$scope.getDescription = function (log, full_description) {
log.metadata['_ip'] = log.ip ? log.ip : null;
// Note: This is for back-compat for logs that previously did not have namespaces.
@ -746,14 +846,14 @@ angular.module('quay').directive('logsView', function () {
log.metadata['namespace'] = log.metadata['namespace'] || namespace || '';
return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata,
null, !full_description);
null, !full_description);
};
$scope.showExportLogs = function() {
$scope.showExportLogs = function () {
$scope.exportLogsInfo = {};
};
$scope.exportLogs = function(exportLogsInfo, callback) {
$scope.exportLogs = function (exportLogsInfo, callback) {
if (!exportLogsInfo.urlOrEmail) {
callback(false);
return;
@ -770,7 +870,7 @@ angular.module('quay').directive('logsView', function () {
data['callback_email'] = urlOrEmail;
}
runExport.customPOST(data).then(function(resp) {
runExport.customPOST(data).then(function (resp) {
bootbox.alert('Usage logs export queued with ID `' + resp['export_id'] + '`')
callback(true);
}, ApiService.errorDisplay('Could not start logs export', callback));

View File

@ -37,6 +37,7 @@ angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', f
'new_name': 'sitemap',
'app_specific_token_title': 'key',
'useragent': 'user-secret',
'message': 'exclamation-triangle',
};
var allowMarkdown = {

View File

@ -3,7 +3,6 @@ import inspect
import os
import shutil
from collections import namedtuple
from test.testconfig import FakeTransaction
import pytest
from flask import Flask, jsonify
@ -34,6 +33,7 @@ from path_converters import (
RepositoryPathRedirectConverter,
V1CreateRepositoryPathConverter,
)
from test.testconfig import FakeTransaction
INIT_DB_PATH = 0
@ -182,6 +182,11 @@ def appconfig(database_uri):
"MAIL_DEFAULT_SENDER": "admin@example.com",
"DATABASE_SECRET_KEY": "anothercrazykey!",
"FEATURE_PROXY_CACHE": True,
"ACTION_LOG_AUDIT_LOGINS": True,
"ACTION_LOG_AUDIT_LOGIN_FAILURES": True,
"ACTION_LOG_AUDIT_PULL_FAILURES": True,
"ACTION_LOG_AUDIT_PUSH_FAILURES": True,
"ACTION_LOG_AUDIT_DELETE_FAILURES": True,
}
return conf

View File

@ -116,5 +116,10 @@ class TestConfig(DefaultConfig):
FEATURE_RH_MARKETPLACE = True
FEATURE_AUTO_PRUNE = True
ACTION_LOG_AUDIT_LOGINS = True
ACTION_LOG_AUDIT_LOGIN_FAILURES = True
ACTION_LOG_AUDIT_PULL_FAILURES = True
ACTION_LOG_AUDIT_PUSH_FAILURES = True
ACTION_LOG_AUDIT_DELETE_FAILURES = True
AUTOPRUNE_FETCH_TAGS_PAGE_LIMIT = 2
AUTOPRUNE_FETCH_REPOSITORIES_PAGE_LIMIT = 2

View File

@ -347,6 +347,26 @@ CONFIG_SCHEMA = {
"description": "Whether to log all registry API and Quay API/UI logins event to the action log. Defaults to True",
"x-example": False,
},
"ACTION_LOG_AUDIT_LOGIN_FAILURES": {
"type": "boolean",
"description": "Whether logging of failed logins attempts is enabled. Defaults to False",
"x-example": True,
},
"ACTION_LOG_AUDIT_PULL_FAILURES": {
"type": "boolean",
"description": "Whether logging of failed image pull attempts is enabled. Defaults to False",
"x-example": True,
},
"ACTION_LOG_AUDIT_PUSH_FAILURES": {
"type": "boolean",
"description": "Whether logging of failed image push attempts is enabled. Defaults to False",
"x-example": True,
},
"ACTION_LOG_AUDIT_DELETE_FAILURES": {
"type": "boolean",
"description": "Whether logging of failed image delete attempts is enabled. Defaults to False",
"x-example": True,
},
"ACTION_LOG_ARCHIVE_LOCATION": {
"type": "string",
"description": "If action log archiving is enabled, the storage engine in which to place the "

View File

@ -5569,7 +5569,7 @@ COPY public.accesstokenkind (id, name) FROM stdin;
--
COPY public.alembic_version (version_num) FROM stdin;
8d47693829a0
3f8e3657bb67
\.
@ -6237,6 +6237,10 @@ COPY public.logentrykind (id, name) FROM stdin;
101 create_namespace_autoprune_policy
102 update_namespace_autoprune_policy
103 delete_namespace_autoprune_policy
104 login_failure
105 push_repo_failed
106 pull_repo_failed
107 delete_tag_failed
\.
@ -8079,7 +8083,8 @@ SELECT pg_catalog.setval('public.logentry_id_seq', 1, false);
-- Name: logentrykind_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
--
SELECT pg_catalog.setval('public.logentrykind_id_seq', 103, true);
SELECT pg_catalog.setval('public.logentrykind_id_seq', 107, true);
--
@ -8163,7 +8168,7 @@ SELECT pg_catalog.setval('public.namespacegeorestriction_id_seq', 1, false);
-- Name: notification_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
--
SELECT pg_catalog.setval('public.notification_id_seq', 1, true);
SELECT pg_catalog.setval('public.notification_id_seq', 1, false);
--
@ -8401,14 +8406,14 @@ SELECT pg_catalog.setval('public.role_id_seq', 3, true);
-- Name: servicekey_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
--
SELECT pg_catalog.setval('public.servicekey_id_seq', 1, true);
SELECT pg_catalog.setval('public.servicekey_id_seq', 1, false);
--
-- Name: servicekeyapproval_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
--
SELECT pg_catalog.setval('public.servicekeyapproval_id_seq', 1, true);
SELECT pg_catalog.setval('public.servicekeyapproval_id_seq', 1, false);
--