Skip to content

Commit 1ea1662

Browse files
committed
auditlog: add logouts
Add audit logging for web logout operation. Web logout now require proper authentication as audit record without name of the logged out user is meaningless. Previously web lougout was anonymous. KIKIMR-21764
1 parent 7a36d05 commit 1ea1662

File tree

6 files changed

+228
-10
lines changed

6 files changed

+228
-10
lines changed

ydb/core/security/login_page.cpp

Lines changed: 57 additions & 2 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

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

ydb/library/login/login.cpp

Lines changed: 8 additions & 7 deletions
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)