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:
parent
a7737722fc
commit
e8ff33e728
@ -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
17
auth/log.py
Normal 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,
|
||||
)
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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/"
|
||||
|
@ -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")
|
||||
)
|
||||
)
|
@ -41,6 +41,18 @@ class InvalidRobotException(DataModelException):
|
||||
pass
|
||||
|
||||
|
||||
class DeactivatedRobotOwnerException(InvalidRobotException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRobotCredentialException(InvalidRobotException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRobotOwnerException(InvalidRobotException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidUsernameException(DataModelException):
|
||||
pass
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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));
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 "
|
||||
|
@ -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);
|
||||
|
||||
|
||||
--
|
||||
|
Loading…
x
Reference in New Issue
Block a user