Skip to content

Commit 7da87a5

Browse files
authored
24-3: auditlog: add logouts (#9052)
Add audit logging for web logout operation. merged ... (#9050) from main Web logout now require proper authentication as audit record without name of the logged out user is meaningless. Previously web logout was anonymous. KIKIMR-21764
1 parent 163d6fb commit 7da87a5

File tree

6 files changed

+228
-10
lines changed

6 files changed

+228
-10
lines changed

ydb/core/security/login_page.cpp

+57-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include <library/cpp/json/json_reader.h>
77
#include <library/cpp/json/json_writer.h>
88

9+
#include <ydb/core/util/address_classifier.h>
10+
#include <ydb/core/audit/audit_log.h>
911
#include <ydb/core/base/tablet_pipe.h>
1012
#include <ydb/core/tx/scheme_cache/scheme_cache.h>
1113
#include <ydb/core/tx/schemeshard/schemeshard.h>
@@ -21,6 +23,24 @@ using namespace NKikimr;
2123
using namespace NSchemeShard;
2224
using namespace NMonitoring;
2325

26+
void AuditLogWebUILogout(const NHttp::THttpIncomingRequest& request, const TString& userSID) {
27+
static const TString WebLoginComponentName = "web-login";
28+
static const TString LogoutOperationName = "LOGOUT";
29+
static const TString EmptyValue = "{none}";
30+
31+
auto remoteAddress = NKikimr::NAddressClassifier::ExtractAddress(request.Address->ToString());
32+
33+
// NOTE: audit field set here must be in sync with ydb/core/tx/schemeshard/schemeshard_audit_log.h, AuditLogLogin()
34+
AUDIT_LOG(
35+
AUDIT_PART("component", WebLoginComponentName)
36+
AUDIT_PART("remote_address", (!remoteAddress.empty() ? remoteAddress : EmptyValue))
37+
AUDIT_PART("subject", (!userSID.empty() ? userSID : EmptyValue))
38+
//NOTE: no database specified as web logout considered cluster-wide
39+
AUDIT_PART("operation", LogoutOperationName)
40+
AUDIT_PART("status", TString("SUCCESS"))
41+
);
42+
}
43+
2444
using THttpResponsePtr = THolder<NMon::IEvHttpInfoRes>;
2545

2646
class TLoginRequest : public NActors::TActorBootstrapped<TLoginRequest> {
@@ -251,6 +271,8 @@ class TLogoutRequest : public NActors::TActorBootstrapped<TLogoutRequest> {
251271
STATEFN(StateWork) {
252272
switch (ev->GetTypeRewrite()) {
253273
hFunc(TEvents::TEvPoisonPill, HandlePoisonPill);
274+
hFunc(TEvTicketParser::TEvAuthorizeTicketResult, Handle);
275+
cFunc(TEvents::TSystem::Wakeup, HandleTimeout);
254276
}
255277
}
256278

@@ -265,13 +287,42 @@ class TLogoutRequest : public NActors::TActorBootstrapped<TLogoutRequest> {
265287
return ReplyErrorAndPassAway("400", "Bad Request", "Invalid method");
266288
}
267289

268-
ReplyDeleteCookieAndPassAway();
290+
NHttp::TCookies cookies(NHttp::THeaders(Request->Headers)["Cookie"]);
291+
TStringBuf ydbSessionId = cookies["ydb_session_id"];
292+
if (ydbSessionId.empty()) {
293+
return ReplyErrorAndPassAway("401", "Unauthorized", "No ydb_session_id cookie");
294+
}
295+
296+
Send(NKikimr::MakeTicketParserID(), new NKikimr::TEvTicketParser::TEvAuthorizeTicket({
297+
.Database = TString(),
298+
.Ticket = TString("Login ") + ydbSessionId,
299+
.PeerName = Request->Address->ToString(),
300+
}));
301+
302+
Become(&TThis::StateWork, Timeout, new TEvents::TEvWakeup());
303+
}
304+
305+
void Handle(TEvTicketParser::TEvAuthorizeTicketResult::TPtr& ev) {
306+
const TEvTicketParser::TEvAuthorizeTicketResult& result = *ev->Get();
307+
if (result.Error) {
308+
return ReplyErrorAndPassAway("403", "Forbidden", result.Error.Message);
309+
}
310+
if (result.Token == nullptr) {
311+
return ReplyErrorAndPassAway("403", "Forbidden", "Empty token");
312+
}
313+
314+
ReplyDeleteCookieAndPassAway(result.Token->GetUserSID());
269315
}
270316

271317
void HandlePoisonPill(TEvents::TEvPoisonPill::TPtr&) {
272318
PassAway();
273319
}
274320

321+
void HandleTimeout() {
322+
ALOG_ERROR(NActorsServices::HTTP, Request->Address << " " << Request->Method << " " << Request->URL << " timeout");
323+
ReplyErrorAndPassAway("504", "Gateway Timeout", "Timeout");
324+
}
325+
275326
void ReplyOptionsAndPassAway() {
276327
NHttp::THeadersBuilder headers;
277328
headers.Set("Allow", "OPTIONS, POST");
@@ -291,12 +342,15 @@ class TLogoutRequest : public NActors::TActorBootstrapped<TLogoutRequest> {
291342
headers.Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST");
292343
}
293344

294-
void ReplyDeleteCookieAndPassAway() {
345+
void ReplyDeleteCookieAndPassAway(const TString& userSID) {
295346
ALOG_DEBUG(NActorsServices::HTTP, "Logout success");
296347
NHttp::THeadersBuilder headers;
297348
SetCORS(headers);
298349
headers.Set("Set-Cookie", "ydb_session_id=; Max-Age=0");
299350
Send(Sender, new NHttp::TEvHttpProxy::TEvHttpOutgoingResponse(Request->CreateResponse("200", "OK", headers)));
351+
352+
AuditLogWebUILogout(*Request, userSID);
353+
300354
PassAway();
301355
}
302356

@@ -314,6 +368,7 @@ class TLogoutRequest : public NActors::TActorBootstrapped<TLogoutRequest> {
314368
protected:
315369
TActorId Sender;
316370
NHttp::THttpIncomingRequestPtr Request;
371+
TDuration Timeout = TDuration::Seconds(5);
317372
};
318373

319374
class TLoginService : public TActor<TLoginService> {

ydb/core/security/ya.make

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ PEERDIR(
1616
ydb/library/grpc/actor_client
1717
library/cpp/monlib/service/pages
1818
library/cpp/openssl/io
19+
ydb/core/audit
1920
ydb/core/base
2021
ydb/core/protos
2122
ydb/library/aclib

ydb/core/tx/schemeshard/schemeshard_audit_log.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include <ydb/public/api/protos/ydb_export.pb.h>
44
#include <ydb/public/api/protos/ydb_import.pb.h>
55

6+
#include <ydb/library/actors/http/http.h>
7+
68
#include <ydb/core/protos/flat_tx_scheme.pb.h>
79
#include <ydb/core/protos/export.pb.h>
810
#include <ydb/core/protos/import.pb.h>
@@ -383,6 +385,7 @@ void AuditLogLogin(const NKikimrScheme::TEvLogin& request, const NKikimrScheme::
383385
TPath databasePath = TPath::Root(SS);
384386
auto peerName = NKikimr::NAddressClassifier::ExtractAddress(request.GetPeerName());
385387

388+
// NOTE: audit field set here must be in sync with ydb/core/security/audit_log.h, AuditLogWebUILogout()
386389
AUDIT_LOG(
387390
AUDIT_PART("component", SchemeshardComponentName)
388391
AUDIT_PART("remote_address", (!peerName.empty() ? peerName : EmptyValue))

ydb/core/tx/schemeshard/schemeshard_audit_log.h

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class TEvCreateImportRequest;
2020
class TEvCreateImportResponse;
2121
}
2222

23+
namespace NHttp {
24+
class THttpIncomingRequest;
25+
}
26+
2327
namespace NKikimr::NSchemeShard {
2428

2529
class TSchemeShard;
@@ -36,4 +40,6 @@ void AuditLogImportStart(const NKikimrImport::TEvCreateImportRequest& request, c
3640
void AuditLogImportEnd(const TImportInfo& importInfo, TSchemeShard* SS);
3741

3842
void AuditLogLogin(const NKikimrScheme::TEvLogin& request, const NKikimrScheme::TEvLoginResult& response, TSchemeShard* SS);
43+
void AuditLogWebUILogout(const NHttp::THttpIncomingRequest& request, const TString& userSID);
44+
3945
}

ydb/core/tx/schemeshard/ut_login/ut_login.cpp

+153-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
#include <util/string/join.h>
22

3+
#include <ydb/library/login/login.h>
4+
#include <ydb/library/actors/http/http_proxy.h>
35
#include <ydb/core/tx/schemeshard/ut_helpers/helpers.h>
46
#include <ydb/core/tx/schemeshard/ut_helpers/auditlog_helpers.h>
5-
#include <ydb/library/login/login.h>
67
#include <ydb/core/protos/auth.pb.h>
8+
#include <ydb/core/security/ticket_parser.h>
9+
#include <ydb/core/security/login_page.h>
710

811
using namespace NKikimr;
912
using namespace NSchemeShard;
@@ -126,3 +129,152 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) {
126129
UNIT_ASSERT_STRING_CONTAINS(last, "login_auth_domain={none}");
127130
}
128131
}
132+
133+
namespace NSchemeShardUT_Private {
134+
135+
void EatWholeString(NHttp::THttpIncomingRequestPtr request, const TString& data) {
136+
request->EnsureEnoughSpaceAvailable(data.size());
137+
auto size = std::min(request->Avail(), data.size());
138+
memcpy(request->Pos(), data.data(), size);
139+
request->Advance(size);
140+
}
141+
142+
NHttp::THttpIncomingRequestPtr MakeLoginRequest(const TString& user, const TString& password) {
143+
TString payload = [](const auto& user, const auto& password) {
144+
NJson::TJsonValue value;
145+
value["user"] = user;
146+
value["password"] = password;
147+
return NJson::WriteJson(value, false);
148+
}(user, password);
149+
TStringBuilder text;
150+
text << "POST /login HTTP/1.1\r\n"
151+
<< "Host: test.ydb\r\n"
152+
<< "Content-Type: application/json\r\n"
153+
<< "Content-Length: " << payload.size() << "\r\n"
154+
<< "\r\n"
155+
<< payload;
156+
NHttp::THttpIncomingRequestPtr request = new NHttp::THttpIncomingRequest();
157+
EatWholeString(request, text);
158+
// WebLoginService will crash without address
159+
request->Address = std::make_shared<TSockAddrInet>("127.0.0.1", 0);
160+
// Cerr << "TEST: http login request: " << text << Endl;
161+
return request;
162+
}
163+
164+
NHttp::THttpIncomingRequestPtr MakeLogoutRequest(const TString& cookieName, const TString& cookieValue) {
165+
TStringBuilder text;
166+
text << "POST /logout HTTP/1.1\r\n"
167+
<< "Host: test.ydb\r\n"
168+
<< "Content-Type: text/plain\r\n"
169+
<< "Cookie: " << cookieName << "=" << cookieValue << "\r\n"
170+
<< "\r\n";
171+
NHttp::THttpIncomingRequestPtr request = new NHttp::THttpIncomingRequest();
172+
EatWholeString(request, text);
173+
// WebLoginService will crash without address
174+
request->Address = std::make_shared<TSockAddrInet>("127.0.0.1", 0);
175+
// Cerr << "TEST: http logout request: " << text << Endl;
176+
return request;
177+
}
178+
179+
}
180+
181+
Y_UNIT_TEST_SUITE(TWebLoginService) {
182+
183+
Y_UNIT_TEST(Logout) {
184+
TTestBasicRuntime runtime;
185+
std::vector<std::string> lines;
186+
runtime.AuditLogBackends = std::move(CreateTestAuditLogBackends(lines));
187+
TTestEnv env(runtime);
188+
189+
// Add ticket parser to the mix
190+
{
191+
NKikimrProto::TAuthConfig authConfig;
192+
authConfig.SetUseBlackBox(false);
193+
authConfig.SetUseLoginProvider(true);
194+
195+
IActor* ticketParser = NKikimr::CreateTicketParser({.AuthConfig = authConfig});
196+
TActorId ticketParserId = runtime.Register(ticketParser);
197+
runtime.RegisterService(NKikimr::MakeTicketParserID(), ticketParserId);
198+
}
199+
200+
UNIT_ASSERT_VALUES_EQUAL(lines.size(), 1); // alter root subdomain
201+
202+
ui64 txId = 100;
203+
204+
TestCreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1", {{NKikimrScheme::StatusSuccess}});
205+
UNIT_ASSERT_VALUES_EQUAL(lines.size(), 2); // +user creation
206+
207+
// test body
208+
const auto target = runtime.Register(CreateWebLoginService());
209+
const auto edge = runtime.AllocateEdgeActor();
210+
211+
TString ydbSessionId;
212+
{
213+
runtime.Send(new IEventHandle(target, edge, new NHttp::TEvHttpProxy::TEvHttpIncomingRequest(
214+
MakeLoginRequest("user1", "password1")
215+
)));
216+
217+
TAutoPtr<IEventHandle> handle;
218+
auto responseEv = runtime.GrabEdgeEvent<NHttp::TEvHttpProxy::TEvHttpOutgoingResponse>(handle);
219+
UNIT_ASSERT_STRINGS_EQUAL(responseEv->Response->Status, "200");
220+
NHttp::THeaders headers(responseEv->Response->Headers);
221+
NHttp::TCookies cookies(headers["Set-Cookie"]);
222+
ydbSessionId = cookies["ydb_session_id"];
223+
}
224+
UNIT_ASSERT_VALUES_EQUAL(lines.size(), 3); // +user login
225+
226+
// New security keys are created in the subdomain as a consequence of a login.
227+
// In real system they are transferred to the ticket parser by the grpc-proxy
228+
// on receiving subdomain update notification.
229+
// Here there are no grpc-proxy, so we should transfer keys to the ticket parser manually.
230+
{
231+
const auto describe = DescribePath(runtime, "/MyRoot");
232+
const auto& securityState = describe.GetPathDescription().GetDomainDescription().GetSecurityState();
233+
TActorId edge = runtime.AllocateEdgeActor();
234+
runtime.Send(new IEventHandle(MakeTicketParserID(), edge, new TEvTicketParser::TEvUpdateLoginSecurityState(securityState)), 0);
235+
}
236+
237+
// Then we are ready to test some authentication on /logout
238+
{ // no cookie
239+
runtime.Send(new IEventHandle(target, edge, new NHttp::TEvHttpProxy::TEvHttpIncomingRequest(
240+
MakeLogoutRequest("not-an-ydb_session_id", ydbSessionId)
241+
)));
242+
243+
TAutoPtr<IEventHandle> handle;
244+
auto responseEv = runtime.GrabEdgeEvent<NHttp::TEvHttpProxy::TEvHttpOutgoingResponse>(handle);
245+
UNIT_ASSERT_STRINGS_EQUAL(responseEv->Response->Status, "401");
246+
247+
// no audit record for actions without auth
248+
UNIT_ASSERT_VALUES_EQUAL(lines.size(), 3);
249+
}
250+
{ // bad cookie
251+
runtime.Send(new IEventHandle(target, edge, new NHttp::TEvHttpProxy::TEvHttpIncomingRequest(
252+
MakeLogoutRequest("ydb_session_id", "jklhagsfjhg")
253+
)));
254+
255+
TAutoPtr<IEventHandle> handle;
256+
auto responseEv = runtime.GrabEdgeEvent<NHttp::TEvHttpProxy::TEvHttpOutgoingResponse>(handle);
257+
UNIT_ASSERT_STRINGS_EQUAL(responseEv->Response->Status, "403");
258+
259+
// no audit record for actions without auth
260+
UNIT_ASSERT_VALUES_EQUAL(lines.size(), 3);
261+
}
262+
{ // good cookie
263+
runtime.Send(new IEventHandle(target, edge, new NHttp::TEvHttpProxy::TEvHttpIncomingRequest(
264+
MakeLogoutRequest("ydb_session_id", ydbSessionId)
265+
)));
266+
267+
TAutoPtr<IEventHandle> handle;
268+
auto responseEv = runtime.GrabEdgeEvent<NHttp::TEvHttpProxy::TEvHttpOutgoingResponse>(handle);
269+
UNIT_ASSERT_STRINGS_EQUAL(responseEv->Response->Status, "200");
270+
271+
UNIT_ASSERT_VALUES_EQUAL(lines.size(), 4); // +user web logout
272+
273+
auto last = FindAuditLine(lines, "operation=LOGOUT");
274+
UNIT_ASSERT_STRING_CONTAINS(last, "component=web-login");
275+
UNIT_ASSERT_STRING_CONTAINS(last, "remote_address="); // can't check the value
276+
UNIT_ASSERT_STRING_CONTAINS(last, "operation=LOGOUT");
277+
UNIT_ASSERT_STRING_CONTAINS(last, "status=SUCCESS");
278+
}
279+
}
280+
}

ydb/library/login/login.cpp

+8-7
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88
#include <openssl/pem.h>
99
#include <openssl/rand.h>
1010

11-
#include <util/generic/singleton.h>
1211
#include <util/string/builder.h>
13-
#include <util/string/cast.h>
14-
#include <util/string/hex.h>
1512

1613
#include <deque>
1714

@@ -339,7 +336,8 @@ TLoginProvider::TLoginUserResponse TLoginProvider::LoginUser(const TLoginUserReq
339336
.set_issued_at(now)
340337
.set_expires_at(expires_at);
341338
if (!Audience.empty()) {
342-
token.set_audience(Audience);
339+
// jwt.h require audience claim to be a set
340+
token.set_audience(std::set<std::string>{Audience});
343341
}
344342

345343
if (request.ExternalAuth) {
@@ -381,7 +379,7 @@ TLoginProvider::TValidateTokenResponse TLoginProvider::ValidateToken(const TVali
381379
try {
382380
jwt::decoded_jwt decoded_token = jwt::decode(request.Token);
383381
if (Audience) {
384-
// we check audience manually because we wan't this error instead of wrong key id in case of databases mismatch
382+
// we check audience manually because we want an explicit error instead of wrong key id in case of databases mismatch
385383
auto audience = decoded_token.get_audience();
386384
if (audience.empty() || TString(*audience.begin()) != Audience) {
387385
response.Error = "Wrong audience";
@@ -394,7 +392,8 @@ TLoginProvider::TValidateTokenResponse TLoginProvider::ValidateToken(const TVali
394392
auto verifier = jwt::verify()
395393
.allow_algorithm(jwt::algorithm::ps256(key->PublicKey));
396394
if (Audience) {
397-
verifier.with_audience(std::set<std::string>({Audience}));
395+
// jwt.h require audience claim to be a set
396+
verifier.with_audience(std::set<std::string>{Audience});
398397
}
399398
verifier.verify(decoded_token);
400399
response.User = decoded_token.get_subject();
@@ -435,8 +434,10 @@ TLoginProvider::TValidateTokenResponse TLoginProvider::ValidateToken(const TVali
435434
response.Error = "Key not found";
436435
}
437436
}
438-
} catch (const jwt::token_verification_exception& e) {
437+
} catch (const jwt::signature_verification_exception& e) {
439438
response.Error = e.what(); // invalid token signature
439+
} catch (const jwt::token_verification_exception& e) {
440+
response.Error = e.what(); // invalid token
440441
} catch (const std::invalid_argument& e) {
441442
response.Error = "Token is not in correct format";
442443
response.TokenUnrecognized = true;

0 commit comments

Comments
 (0)