Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion ydb/core/tx/schemeshard/schemeshard__operation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ struct TSchemeShard::TTxOperationPropose: public NTabletFlatExecutor::TTransacti
TProposeRequest::TPtr Request;
THolder<TProposeResponse> Response = nullptr;

TString PeerName;
TString UserSID;
TString SanitizedToken;

Expand Down Expand Up @@ -378,10 +379,12 @@ struct TSchemeShard::TTxOperationPropose: public NTabletFlatExecutor::TTransacti
UserSID = userToken->GetUserSID();
SanitizedToken = userToken->GetSanitizedToken();
}
PeerName = Request->Get()->Record.GetPeerName();

TMemoryChanges memChanges;
TStorageChanges dbChanges;
TOperationContext context{Self, txc, ctx, OnComplete, memChanges, dbChanges, std::move(userToken)};
context.PeerName = PeerName;

//NOTE: Successful IgniteOperation will leave created operation in Self->Operations and accumulated changes in the context.
// Unsuccessful IgniteOperation will leave no operation and context will also be clean.
Expand Down Expand Up @@ -444,7 +447,7 @@ struct TSchemeShard::TTxOperationPropose: public NTabletFlatExecutor::TTransacti
<< ", response: " << Response->Record.ShortDebugString()
<< ", at schemeshard: " << Self->TabletID());

AuditLogModifySchemeTransaction(record, Response->Record, Self, UserSID, SanitizedToken);
AuditLogModifySchemeTransaction(record, Response->Record, Self, PeerName, UserSID, SanitizedToken);

//NOTE: Double audit output into the common log as a way to ease
// transition to a new auditlog stream.
Expand Down
56 changes: 55 additions & 1 deletion ydb/core/tx/schemeshard/schemeshard__operation_alter_login.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "schemeshard_audit_log.h"
#include "schemeshard__operation_part.h"
#include "schemeshard__operation_common.h"
#include "schemeshard_impl.h"
Expand All @@ -16,19 +17,24 @@ class TAlterLogin: public TSubOperationBase {
THolder<TProposeResponse> Propose(const TString&, TOperationContext& context) override {
NIceDb::TNiceDb db(context.GetTxc().DB); // do not track is there are direct writes happen
TTabletId ssId = context.SS->SelfTabletId();
auto result = MakeHolder<TProposeResponse>(OperationId.GetTxId(), ssId);
const auto txId = OperationId.GetTxId();
auto result = MakeHolder<TProposeResponse>(txId, ssId);
if (!AppData()->AuthConfig.GetEnableLoginAuthentication()) {
result->SetStatus(NKikimrScheme::StatusPreconditionFailed, "Login authentication is disabled");
} else if (Transaction.GetWorkingDir() != context.SS->LoginProvider.Audience) {
result->SetStatus(NKikimrScheme::StatusPreconditionFailed, "Wrong working dir");
} else {
const NKikimrConfig::TSecurityConfig& securityConfig = context.SS->GetSecurityConfig();
const NKikimrSchemeOp::TAlterLogin& alterLogin = Transaction.GetAlterLogin();

TParts additionalParts;

switch (alterLogin.GetAlterCase()) {
case NKikimrSchemeOp::TAlterLogin::kCreateUser: {
const auto& createUser = alterLogin.GetCreateUser();
auto response = context.SS->LoginProvider.CreateUser(
{.User = createUser.GetUser(), .Password = createUser.GetPassword()});

if (response.Error) {
result->SetStatus(NKikimrScheme::StatusPreconditionFailed, response.Error);
} else {
Expand All @@ -46,6 +52,8 @@ class TAlterLogin: public TSubOperationBase {
}
}
result->SetStatus(NKikimrScheme::StatusSuccess);

AddIsUserAdmin(createUser.GetUser(), context.SS->LoginProvider, additionalParts);
}
break;
}
Expand All @@ -58,16 +66,27 @@ class TAlterLogin: public TSubOperationBase {
auto& sid = context.SS->LoginProvider.Sids[modifyUser.GetUser()];
db.Table<Schema::LoginSids>().Key(sid.Name).Update<Schema::LoginSids::SidType, Schema::LoginSids::SidHash>(sid.Type, sid.Hash);
result->SetStatus(NKikimrScheme::StatusSuccess);

AddIsUserAdmin(modifyUser.GetUser(), context.SS->LoginProvider, additionalParts);
AddLastSuccessfulLogin(sid, additionalParts);
}
break;
}
case NKikimrSchemeOp::TAlterLogin::kRemoveUser: {
const auto& removeUser = alterLogin.GetRemoveUser();

auto sid = context.SS->LoginProvider.Sids.find(removeUser.GetUser());
if (context.SS->LoginProvider.Sids.end() != sid) {
AddLastSuccessfulLogin(sid->second, additionalParts);
}

auto response = RemoveUser(context, removeUser, db);
if (response.Error) {
result->SetStatus(NKikimrScheme::StatusPreconditionFailed, response.Error);
} else {
result->SetStatus(NKikimrScheme::StatusSuccess);

AddIsUserAdmin(removeUser.GetUser(), context.SS->LoginProvider, additionalParts);
}
break;
}
Expand Down Expand Up @@ -162,6 +181,15 @@ class TAlterLogin: public TSubOperationBase {
break;
}
}

TString userSID, sanitizedToken;
if (context.UserToken) {
userSID = context.UserToken->GetUserSID();
sanitizedToken = context.UserToken->GetSanitizedToken();
}
const auto status = result->Record.GetStatus();
const auto reason = result->Record.HasReason() ? result->Record.GetReason() : TString();
AuditLogModifySchemeOperation(Transaction, status, reason, context.SS, context.PeerName, userSID, sanitizedToken, ui64(txId), additionalParts);
}

if (result->Record.GetStatus() == NKikimrScheme::StatusSuccess) {
Expand Down Expand Up @@ -246,6 +274,32 @@ class TAlterLogin: public TSubOperationBase {

return {}; // success
}

void AddIsUserAdmin(const TString& user, NLogin::TLoginProvider& loginProvider, TParts& additionalParts) {
const auto& adminSids = AppData()->AdministrationAllowedSIDs;
bool isAdmin = adminSids.empty();
if (!isAdmin) {
const auto providerGroups = loginProvider.GetGroupsMembership(user);
const TVector<NACLib::TSID> groups(providerGroups.begin(), providerGroups.end());
const auto userToken = NACLib::TUserToken(user, groups);
auto hasSid = [&userToken](const TString& sid) -> bool {
return userToken.IsExist(sid);
};
isAdmin = std::find_if(adminSids.begin(), adminSids.end(), hasSid) != adminSids.end();
}

if (isAdmin) {
additionalParts.emplace_back("login_user_level", "admin");
}
}

void AddLastSuccessfulLogin(NLogin::TLoginProvider::TSidRecord& sid, TParts& additionalParts) {
const auto duration = sid.LastSuccessfulLogin.time_since_epoch();
const auto time = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
if (time) {
additionalParts.emplace_back("last_login", TInstant::MicroSeconds(time).ToString());
}
}
};

}
Expand Down
1 change: 1 addition & 0 deletions ydb/core/tx/schemeshard/schemeshard__operation_part.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ struct TOperationContext {
TStorageChanges& DbChanges;

TMaybe<NACLib::TUserToken> UserToken;
TString PeerName;
bool IsAllowedPrivateTables = false;

private:
Expand Down
118 changes: 69 additions & 49 deletions ydb/core/tx/schemeshard/schemeshard_audit_log.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,57 +81,77 @@ TPath DatabasePathFromWorkingDir(TSchemeShard* SS, const TString &opWorkingDir)

} // anonymous namespace

void AuditLogModifySchemeTransaction(const NKikimrScheme::TEvModifySchemeTransaction& request, const NKikimrScheme::TEvModifySchemeTransactionResult& response, TSchemeShard* SS, const TString& userSID, const TString& sanitizedToken) {
void AuditLogModifySchemeOperation(const NKikimrSchemeOp::TModifyScheme& operation,
NKikimrScheme::EStatus status, const TString& reason, TSchemeShard* SS,
const TString& peerName, const TString& userSID, const TString& sanitizedToken,
ui64 txId, const TParts& additionalParts) {
auto logEntry = MakeAuditLogFragment(operation);

TPath databasePath = DatabasePathFromWorkingDir(SS, operation.GetWorkingDir());
auto [cloud_id, folder_id, database_id] = GetDatabaseCloudIds(databasePath);
auto address = NKikimr::NAddressClassifier::ExtractAddress(peerName);

AUDIT_LOG(
AUDIT_PART("component", SchemeshardComponentName)
AUDIT_PART("tx_id", std::to_string(txId))
AUDIT_PART("remote_address", (!address.empty() ? address : EmptyValue))
AUDIT_PART("subject", (!userSID.empty() ? userSID : EmptyValue))
AUDIT_PART("sanitized_token", (!sanitizedToken.empty() ? sanitizedToken : EmptyValue))
AUDIT_PART("database", (!databasePath.IsEmpty() ? databasePath.GetDomainPathString() : EmptyValue))
AUDIT_PART("operation", logEntry.Operation)
AUDIT_PART("paths", RenderList(logEntry.Paths), !logEntry.Paths.empty())
AUDIT_PART("status", GeneralStatus(status))
AUDIT_PART("detailed_status", NKikimrScheme::EStatus_Name(status))
AUDIT_PART("reason", reason, !reason.empty())

for (const auto& [name, value] : additionalParts) {
AUDIT_PART(name, (!value.empty() ? value : EmptyValue))
}

AUDIT_PART("cloud_id", cloud_id, !cloud_id.empty());
AUDIT_PART("folder_id", folder_id, !folder_id.empty());
AUDIT_PART("resource_id", database_id, !database_id.empty());

// Additionally:

// ModifyACL.
// Technically, non-empty ModifyACL field could come with any ModifyScheme operation.
// In practice, ModifyACL will get processed only by:
// 1. explicit operation ESchemeOpModifyACL -- to modify ACL on a path
// 2. ESchemeOpMkDir or ESchemeOpCreate* operations -- to set rights to newly created paths/entities
// 3. ESchemeOpCopyTable -- to be checked against acl size limit, not to be applied in any way
AUDIT_PART("new_owner", logEntry.NewOwner, !logEntry.NewOwner.empty());
AUDIT_PART("acl_add", RenderList(logEntry.ACLAdd), !logEntry.ACLAdd.empty());
AUDIT_PART("acl_remove", RenderList(logEntry.ACLRemove), !logEntry.ACLRemove.empty());

// AlterUserAttributes.
// 1. explicit operation ESchemeOpAlterUserAttributes -- to modify user attributes on a path
// 2. ESchemeOpMkDir or some ESchemeOpCreate* operations -- to set user attributes for newly created paths/entities
AUDIT_PART("user_attrs_add", RenderList(logEntry.UserAttrsAdd), !logEntry.UserAttrsAdd.empty());
AUDIT_PART("user_attrs_remove", RenderList(logEntry.UserAttrsRemove), !logEntry.UserAttrsRemove.empty());

// AlterLogin.
// explicit operation ESchemeOpAlterLogin -- to modify user and groups
AUDIT_PART("login_user", logEntry.LoginUser);
AUDIT_PART("login_group", logEntry.LoginGroup);
AUDIT_PART("login_member", logEntry.LoginMember);
);
}

void AuditLogModifySchemeTransaction(const NKikimrScheme::TEvModifySchemeTransaction& request,
const NKikimrScheme::TEvModifySchemeTransactionResult& response, TSchemeShard* SS,
const TString& peerName, const TString& userSID, const TString& sanitizedToken) {
// Each TEvModifySchemeTransaction.Transaction is a self sufficient operation and should be logged independently
// (even if it was packed into a single TxProxy transaction with some other operations).
const auto txId = request.GetTxId();
const auto status = response.GetStatus();
const auto reason = response.HasReason() ? response.GetReason() : TString();
for (const auto& operation : request.GetTransaction()) {
auto logEntry = MakeAuditLogFragment(operation);

TPath databasePath = DatabasePathFromWorkingDir(SS, operation.GetWorkingDir());
auto [cloud_id, folder_id, database_id] = GetDatabaseCloudIds(databasePath);
auto peerName = NKikimr::NAddressClassifier::ExtractAddress(request.GetPeerName());

AUDIT_LOG(
AUDIT_PART("component", SchemeshardComponentName)
AUDIT_PART("tx_id", std::to_string(request.GetTxId()))
AUDIT_PART("remote_address", (!peerName.empty() ? peerName : EmptyValue))
AUDIT_PART("subject", (!userSID.empty() ? userSID : EmptyValue))
AUDIT_PART("sanitized_token", (!sanitizedToken.empty() ? sanitizedToken : EmptyValue))
AUDIT_PART("database", (!databasePath.IsEmpty() ? databasePath.GetDomainPathString() : EmptyValue))
AUDIT_PART("operation", logEntry.Operation)
AUDIT_PART("paths", RenderList(logEntry.Paths), !logEntry.Paths.empty())
AUDIT_PART("status", GeneralStatus(response.GetStatus()))
AUDIT_PART("detailed_status", NKikimrScheme::EStatus_Name(response.GetStatus()))
AUDIT_PART("reason", response.GetReason(), response.HasReason())

AUDIT_PART("cloud_id", cloud_id, !cloud_id.empty());
AUDIT_PART("folder_id", folder_id, !folder_id.empty());
AUDIT_PART("resource_id", database_id, !database_id.empty());

// Additionally:

// ModifyACL.
// Technically, non-empty ModifyACL field could come with any ModifyScheme operation.
// In practice, ModifyACL will get processed only by:
// 1. explicit operation ESchemeOpModifyACL -- to modify ACL on a path
// 2. ESchemeOpMkDir or ESchemeOpCreate* operations -- to set rights to newly created paths/entities
// 3. ESchemeOpCopyTable -- to be checked against acl size limit, not to be applied in any way
AUDIT_PART("new_owner", logEntry.NewOwner, !logEntry.NewOwner.empty());
AUDIT_PART("acl_add", RenderList(logEntry.ACLAdd), !logEntry.ACLAdd.empty());
AUDIT_PART("acl_remove", RenderList(logEntry.ACLRemove), !logEntry.ACLRemove.empty());

// AlterUserAttributes.
// 1. explicit operation ESchemeOpAlterUserAttributes -- to modify user attributes on a path
// 2. ESchemeOpMkDir or some ESchemeOpCreate* operations -- to set user attributes for newly created paths/entities
AUDIT_PART("user_attrs_add", RenderList(logEntry.UserAttrsAdd), !logEntry.UserAttrsAdd.empty());
AUDIT_PART("user_attrs_remove", RenderList(logEntry.UserAttrsRemove), !logEntry.UserAttrsRemove.empty());

// AlterLogin.
// explicit operation ESchemeOpAlterLogin -- to modify user and groups
AUDIT_PART("login_user", logEntry.LoginUser);
AUDIT_PART("login_group", logEntry.LoginGroup);
AUDIT_PART("login_member", logEntry.LoginMember);
);
const auto type = operation.GetOperationType();
if (NKikimrSchemeOp::EOperationType::ESchemeOpAlterLogin == type) {
continue;
}
AuditLogModifySchemeOperation(operation, status, reason, SS, peerName, userSID, sanitizedToken, txId, TParts());
}
}

Expand Down Expand Up @@ -194,7 +214,7 @@ struct TXxportRecord {
TString Status;
Ydb::StatusIds::StatusCode DetailedStatus;
TString Reason;
TVector<std::pair<TString, TString>> AdditionalParts;
TParts AdditionalParts;
TString StartTime;
TString EndTime;
TString CloudId;
Expand Down
17 changes: 16 additions & 1 deletion ydb/core/tx/schemeshard/schemeshard_audit_log.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <util/generic/string.h>

namespace NKikimrScheme {
enum EStatus : int;

class TEvModifySchemeTransaction;
class TEvModifySchemeTransactionResult;

Expand All @@ -24,13 +26,26 @@ namespace NHttp {
class THttpIncomingRequest;
}

namespace NKikimrSchemeOp {
class TModifyScheme;
}

namespace NKikimr::NSchemeShard {

class TSchemeShard;
struct TExportInfo;
struct TImportInfo;

void AuditLogModifySchemeTransaction(const NKikimrScheme::TEvModifySchemeTransaction& request, const NKikimrScheme::TEvModifySchemeTransactionResult& response, TSchemeShard* SS, const TString& userSID, const TString& sanitizedToken);
using TParts = TVector<std::pair<TString, TString>>;

void AuditLogModifySchemeOperation(const NKikimrSchemeOp::TModifyScheme& operation,
NKikimrScheme::EStatus status, const TString& reason, TSchemeShard* SS,
const TString& peerName, const TString& userSID, const TString& sanitizedToken,
ui64 txId, const TParts& additionalParts);

void AuditLogModifySchemeTransaction(const NKikimrScheme::TEvModifySchemeTransaction& request,
const NKikimrScheme::TEvModifySchemeTransactionResult& response, TSchemeShard* SS,
const TString& peerName, const TString& userSID, const TString& sanitizedToken);
void AuditLogModifySchemeTransactionDeprecated(const NKikimrScheme::TEvModifySchemeTransaction& request, const NKikimrScheme::TEvModifySchemeTransactionResult& response, TSchemeShard* SS, const TString& userSID);

void AuditLogExportStart(const NKikimrExport::TEvCreateExportRequest& request, const NKikimrExport::TEvCreateExportResponse& response, TSchemeShard* SS);
Expand Down
Loading