Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auditlogs 2 in vfolder #1822

Open
wants to merge 56 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
26de045
feature: add audit log's database structure and graphql structure
mirageoasis Jan 3, 2024
925a4d4
feature: audit log table new column and enum type refactor
mirageoasis Jan 4, 2024
b2cfb56
feature: vfolder create temp
mirageoasis Jan 5, 2024
6c077cc
fixed: rollbacked changes
mirageoasis Jan 7, 2024
13f2546
feature: vfolder create audit log
mirageoasis Jan 8, 2024
c3b26a2
fixed: removed todo
mirageoasis Jan 8, 2024
04437a1
feature: added rest_api_path and gql_query in field
mirageoasis Jan 8, 2024
52120c7
refactor: moved audit_log_data to util to use from other module
mirageoasis Jan 9, 2024
dbc5921
Merge branch 'main' into feature/auditlogs-2
mirageoasis Jan 9, 2024
f2636ba
refactor: moved audit_logs and fixed before after data image
mirageoasis Jan 10, 2024
2853bb4
feature: fixed AuditLogTargetType enum
mirageoasis Jan 10, 2024
3292f12
refactor: moved file to resolve package import error
mirageoasis Jan 10, 2024
6f84906
feature: added purge type
mirageoasis Jan 10, 2024
d5955f0
feature: added empty after data function and changed
mirageoasis Jan 10, 2024
1b1164b
feature: purge finished
mirageoasis Jan 10, 2024
6a1dc4c
feature: rename
mirageoasis Jan 10, 2024
912f079
refactor: changed hard coded audit_log_data value setting to decorato…
mirageoasis Jan 11, 2024
b2bdcb0
Merge branch 'main' into feature/auditlogs-2
mirageoasis Jan 11, 2024
676a23b
refactor: update target field fixed
mirageoasis Jan 11, 2024
4f332f9
feature: changed target to nullable and merged heads
mirageoasis Jan 12, 2024
6ac2fad
refactor: refactored decorator
mirageoasis Jan 12, 2024
bd852c6
Merge branch 'main' into feature/auditlogs-2
mirageoasis Jan 12, 2024
3b0e93c
feature: added update_options and added returning statement
mirageoasis Jan 13, 2024
171aa67
Merge branch 'main' into feature/auditlogs-2
mirageoasis Jan 13, 2024
b3f2ebe
feature: added mkdir
mirageoasis Jan 13, 2024
2d01618
refactor: made audit_log_util's some function private
mirageoasis Jan 15, 2024
91b995b
refactor: rename vfolder and entry stringify function
mirageoasis Jan 15, 2024
8c6cc1c
refactor: refactored overall structure and made contextvars encapsulated
mirageoasis Jan 15, 2024
d30765e
Merge branch 'main' into feature/auditlogs-2
mirageoasis Jan 15, 2024
e107cee
fixed: convert target name into string
mirageoasis Jan 16, 2024
c1f44fa
feature: added audit logs
mirageoasis Jan 16, 2024
2f45ddd
Merge branch 'main' into feature/auditlogs-2
mirageoasis Jan 16, 2024
0f51580
refactor: stringify entry values invisible from outside
mirageoasis Jan 17, 2024
38b9c45
refactor: stringify entry values removed and fixed change ownership q…
mirageoasis Jan 17, 2024
31ed1a2
fix : fix errors and audit log
mirageoasis Jan 19, 2024
df1f99a
Merge branch 'main' into feature/auditlogs-2
kyujin-cho Mar 31, 2024
d2088bf
chore: update GraphQL schema dump
mirageoasis Mar 31, 2024
4cccefa
fix invalid db migration script
kyujin-cho Mar 31, 2024
4b2060e
remove audit log creation feature
kyujin-cho Mar 31, 2024
c431c9c
Merge branch 'main' into feature/auditlogs-2
kyujin-cho Mar 31, 2024
8dd7cbe
chore: update GraphQL schema dump
mirageoasis Mar 31, 2024
b6edce6
narrow audit log scope
kyujin-cho Mar 31, 2024
d5be253
add missing file
kyujin-cho Mar 31, 2024
1ca0349
Merge branch 'main' into feature/auditlogs-2
kyujin-cho Jun 27, 2024
1bb775d
chore: update GraphQL schema dump
kyujin-cho Jun 27, 2024
57de9c1
fix logics
kyujin-cho Jun 27, 2024
ea8e6d5
fix logics
kyujin-cho Jun 27, 2024
f7f35d3
Merge branch 'main' into feature/auditlogs-2
fregataa Jul 1, 2024
81236b5
remove __init__() method of AuditLogRow ORM class
fregataa Jul 1, 2024
8339be8
fix alembic migration to add id field and remove enum type
fregataa Jul 1, 2024
d3ede04
fix missing arguments and parse UUID to string when jsonify data
fregataa Jul 1, 2024
3549f4d
refactor by applying strict type to audit_log_data schema
fregataa Jul 1, 2024
314bc56
fix vfolder creation audit
fregataa Jul 1, 2024
6ec1369
Merge branch 'main' into feature/auditlogs-2
fregataa Jul 1, 2024
1cbd956
Merge branch 'main' into feature/auditlogs-2
achimnol Jul 6, 2024
defa0b3
Merge branch 'main' into feature/auditlogs-2
achimnol Jul 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added src/ai/__init__.py
Empty file.
Empty file added src/ai/backend/__init__.py
Empty file.
1 change: 1 addition & 0 deletions src/ai/backend/client/cli/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def admin():
from . import ( # noqa
acl,
agent,
audit_logs,
domain,
etcd,
group,
Expand Down
62 changes: 62 additions & 0 deletions src/ai/backend/client/cli/admin/audit_logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from __future__ import annotations

import sys

import click

from ai.backend.client.output.fields import auditlog_fields
from ai.backend.client.session import Session

from ..extensions import pass_ctx_obj
from ..types import CLIContext
from . import admin


@admin.group()
def audit_logs() -> None:
"""
Events audit logs commands.
"""


@audit_logs.command()
# @click.pass_obj
@pass_ctx_obj
@click.option("-u", "--user-id", type=str, default=None, help="User ID to audit.")
@click.option("--filter", "filter_", default=None, help="Set the query filter expression.")
@click.option("--order", default=None, help="Set the query ordering expression.")
@click.option("--offset", default=0, help="The index of the current page start for pagination.")
@click.option("--limit", default=None, help="The page size for pagination.")
def list(ctx: CLIContext, user_id, filter_, order, offset, limit) -> None:
"""
List audit logs.
(admin privilege required)
"""
fields = [
auditlog_fields["user_id"],
auditlog_fields["access_key"],
auditlog_fields["email"],
auditlog_fields["action"],
auditlog_fields["target_type"],
auditlog_fields["target"],
auditlog_fields["data"],
auditlog_fields["created_at"],
]
try:
with Session() as session:
fetch_func = lambda pg_offset, pg_size: session.AuditLog.paginated_list(
user_id,
fields=fields,
page_offset=pg_offset,
page_size=pg_size,
filter=filter_,
order=order,
)
ctx.output.print_paginated_list(
fetch_func,
initial_page_offset=offset,
page_size=limit,
)
except Exception as e:
ctx.output.print_error(e)
sys.exit(1)
59 changes: 59 additions & 0 deletions src/ai/backend/client/func/audit_logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from typing import Sequence, Union

from ai.backend.client.output.fields import auditlog_fields
from ai.backend.client.output.types import FieldSpec, PaginatedResult
from ai.backend.client.pagination import fetch_paginated_result

from .base import BaseFunction, api_function

__all__ = ("AuditLog",)


_default_list_fields = [
auditlog_fields["user_id"],
auditlog_fields["access_key"],
auditlog_fields["email"],
auditlog_fields["action"],
auditlog_fields["data"],
auditlog_fields["target_type"],
auditlog_fields["target"],
auditlog_fields["created_at"],
]


class AuditLog(BaseFunction):
"""
Provides management of audit logs.
"""

@api_function
@classmethod
async def paginated_list(
cls,
user_id: Union[str, str] = None,
*,
fields: Sequence[FieldSpec] = _default_list_fields,
page_offset: int = 0,
page_size: int = 20,
filter: str = None,
order: str = None,
) -> PaginatedResult[dict]:
"""
Fetches the list of audit logs.
:param user_id: Fetches audit log from a user
"""
variables = {
"user_id": (user_id, "String"), # list by user_id
"filter": (filter, "String"),
"order": (order, "String"),
}

return await fetch_paginated_result(
"auditlog_list",
variables,
fields,
page_offset=page_offset,
page_size=page_size,
)
2 changes: 2 additions & 0 deletions src/ai/backend/client/func/keypair.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
keypair_fields["secret_key"],
keypair_fields["is_active"],
keypair_fields["is_admin"],
keypair_fields["rate_limit"],
keypair_fields["resource_policy"],
)

_default_result_fields = (
Expand Down
45 changes: 45 additions & 0 deletions src/ai/backend/client/func/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@
user_fields["main_access_key"],
)

_user_info_fields = (
user_fields["username"],
user_fields["full_name"],
user_fields["domain_name"],
user_fields["role"],
user_fields["status"],
user_fields["description"],
)


class UserRole(enum.StrEnum):
"""
Expand Down Expand Up @@ -257,6 +266,42 @@ async def detail_by_uuid(
)
return data["user_from_uuid"]

@api_function
@classmethod
async def detail_by_email(
cls,
email: str = None,
fields: Sequence[FieldSpec] = _user_info_fields,
) -> Sequence[dict]:
"""
Fetch basic information of a user by user's email. If email is not specified,
requester's information will be returned.
:param email: email of the user to fetch.
:param fields: Additional per-user query fields to fetch.
"""
if email is None:
query = textwrap.dedent(
"""\
query {
user {$fields}
}
"""
)
else:
query = textwrap.dedent(
"""\
query($email: String!) {
user_from_email(email: $email) {$fields}
}
"""
)
query = query.replace("$fields", " ".join(f.field_ref for f in fields))
print("query{0}", query)
variables = {"email": email}
data = await api_session.get().Admin._query(query, variables if email is not None else None)
print("data from func", data["user_from_email"])
return data["user_from_email"]

@api_function
@classmethod
async def create(
Expand Down
13 changes: 13 additions & 0 deletions src/ai/backend/client/output/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,19 @@
])


auditlog_fields = FieldSet([
FieldSpec("type"),
FieldSpec("user_id"),
FieldSpec("access_key"),
FieldSpec("email"),
FieldSpec("action"),
FieldSpec("data"),
FieldSpec("target_type"),
FieldSpec("target"),
FieldSpec("created_at"),
])


routing_fields = FieldSet([
FieldSpec("routing_id"),
FieldSpec("status"),
Expand Down
3 changes: 3 additions & 0 deletions src/ai/backend/client/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ class BaseSession(metaclass=abc.ABCMeta):
"VFolder",
"Dotfile",
"ServerLog",
"AuditLog",
"Permission",
"Service",
"Model",
Expand Down Expand Up @@ -294,6 +295,7 @@ def __init__(
from .func.acl import Permission
from .func.admin import Admin
from .func.agent import Agent, AgentWatcher
from .func.audit_logs import AuditLog
from .func.auth import Auth
from .func.bgtask import BackgroundTask
from .func.domain import Domain
Expand Down Expand Up @@ -339,6 +341,7 @@ def __init__(
self.VFolder = VFolder
self.Dotfile = Dotfile
self.ServerLog = ServerLog
self.AuditLog = AuditLog
self.Permission = Permission
self.Service = Service
self.Model = Model
Expand Down
85 changes: 85 additions & 0 deletions src/ai/backend/manager/api/audit_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import contextvars
import json
from collections.abc import Mapping, Sequence
from typing import Any, NamedTuple

from aiohttp import web
from aiohttp.typedefs import Handler

from .context import RootContext


class AuditLogData(NamedTuple):
previous: str = json.dumps({})
current: str = json.dumps({})

def to_dict(self) -> Mapping[str, str]:
return {
"previous": self.previous,
"current": self.current,
}


audit_log_data: contextvars.ContextVar[AuditLogData] = contextvars.ContextVar(
"audit_log_data", default=AuditLogData()
)

audit_log_target: contextvars.ContextVar[str | None] = contextvars.ContextVar(
"audit_log_target", default=None
)


@web.middleware
async def audit_log_middleware(request: web.Request, handler: Handler) -> web.StreamResponse:
from ai.backend.manager.models import AuditLogRow

root_ctx: RootContext = request.app["_root.context"]
success = False
exc = None

try:
res = await handler(request)
success = True
return res
except Exception as e:
exc = e
raise
finally:
if request.get("audit_log"):
async with root_ctx.db.begin_session() as sess:
new_log = AuditLogRow(
user_id=request["user"]["uuid"],
access_key=request["keypair"]["access_key"],
email=request["user"]["email"],
action=request["audit_log_action"],
data=audit_log_data.get().to_dict(),
target_type=request["audit_log_target_type"],
success=success,
target=audit_log_target.get(),
rest_resource=f"{request.method} {request.path}",
)
if exc:
new_log.error = str(exc)
sess.add(new_log)


def set_target(target: Any) -> None:
audit_log_target.set(str(target))


def update_previous(
data_to_insert: Mapping[str, Any] | Sequence[Any],
) -> None:
prev_audit_log_data = AuditLogData(
previous=json.dumps(data_to_insert), current=audit_log_data.get().current
)
audit_log_data.set(prev_audit_log_data)


def update_current(
data_to_insert: Mapping[str, Any] | Sequence[Any],
) -> None:
prev_audit_log_data = AuditLogData(
previous=audit_log_data.get().previous, current=json.dumps(data_to_insert)
)
audit_log_data.set(prev_audit_log_data)
22 changes: 22 additions & 0 deletions src/ai/backend/manager/api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
agent(agent_id: String!): Agent
agent_list(limit: Int!, offset: Int!, filter: String, order: String, scaling_group: String, status: String): AgentList
agents(scaling_group: String, status: String): [Agent]
auditlog_list(limit: Int!, offset: Int!, filter: String, order: String, user_id: String): AuditLogList

Check failure on line 15 in src/ai/backend/manager/api/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

New fields must include a description with a version number in the format "Added in XX.XX.X.", Field 'auditlog_list' was added to object type 'Queries'

New fields must include a description with a version number in the format "Added in XX.XX.X."
auditlog(user_id: ID): [AuditLog]

Check failure on line 16 in src/ai/backend/manager/api/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

New fields must include a description with a version number in the format "Added in XX.XX.X.", Field 'auditlog' was added to object type 'Queries'

New fields must include a description with a version number in the format "Added in XX.XX.X."
agent_summary(agent_id: String!): AgentSummary
agent_summary_list(limit: Int!, offset: Int!, filter: String, order: String, scaling_group: String, status: String): AgentSummaryList
domain(name: String): Domain
Expand Down Expand Up @@ -61,6 +63,7 @@
user(domain_name: String, email: String): User
user_from_uuid(domain_name: String, user_id: ID): User
users(domain_name: String, group_id: UUID, is_active: Boolean, status: String): [User]
user_from_email(email: String): User

Check failure on line 66 in src/ai/backend/manager/api/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

New fields must include a description with a version number in the format "Added in XX.XX.X.", Field 'user_from_email' was added to object type 'Queries'

New fields must include a description with a version number in the format "Added in XX.XX.X."
user_list(limit: Int!, offset: Int!, filter: String, order: String, domain_name: String, group_id: UUID, is_active: Boolean, status: String): UserList

"""Added in 24.03.0."""
Expand Down Expand Up @@ -279,6 +282,25 @@
total_count: Int!
}

type AuditLogList implements PaginatedList {

Check failure on line 285 in src/ai/backend/manager/api/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

New types must include a description with a version number in the format "Added in XX.XX.X.", Type 'AuditLogList' was added

New types must include a description with a version number in the format "Added in XX.XX.X."
items: [AuditLog]!
total_count: Int!
}

type AuditLog implements Item {

Check failure on line 290 in src/ai/backend/manager/api/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

New types must include a description with a version number in the format "Added in XX.XX.X.", Type 'AuditLog' was added

New types must include a description with a version number in the format "Added in XX.XX.X."
id: ID
user_id: String
access_key: String
email: String
action: String
data: JSONString
target_type: String
target: String
created_at: DateTime
rest_resource: String
gql_query: String
}

"""A schema for normal users."""
type AgentSummary implements Item {
id: ID
Expand Down
Loading
Loading