|
1 | 1 | #include <util/string/join.h>
|
2 | 2 |
|
| 3 | +#include <ydb/library/login/login.h> |
| 4 | +#include <ydb/library/actors/http/http_proxy.h> |
3 | 5 | #include <ydb/core/tx/schemeshard/ut_helpers/helpers.h>
|
4 | 6 | #include <ydb/core/tx/schemeshard/ut_helpers/auditlog_helpers.h>
|
5 |
| -#include <ydb/library/login/login.h> |
6 | 7 | #include <ydb/core/protos/auth.pb.h>
|
| 8 | +#include <ydb/core/security/ticket_parser.h> |
| 9 | +#include <ydb/core/security/login_page.h> |
7 | 10 |
|
8 | 11 | using namespace NKikimr;
|
9 | 12 | using namespace NSchemeShard;
|
@@ -126,3 +129,152 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) {
|
126 | 129 | UNIT_ASSERT_STRING_CONTAINS(last, "login_auth_domain={none}");
|
127 | 130 | }
|
128 | 131 | }
|
| 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 | +} |
0 commit comments