1/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2/*
3 *     Copyright 2016 Couchbase, Inc
4 *
5 *   Licensed under the Apache License, Version 2.0 (the "License");
6 *   you may not use this file except in compliance with the License.
7 *   You may obtain a copy of the License at
8 *
9 *       http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *   Unless required by applicable law or agreed to in writing, software
12 *   distributed under the License is distributed on an "AS IS" BASIS,
13 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *   See the License for the specific language governing permissions and
15 *   limitations under the License.
16 */
17
18/*
19 * Tests that check certain events make it into the audit log.
20 */
21
22#include "testapp.h"
23
24#include <platform/dirutils.h>
25#include "testapp_client_test.h"
26#include "memcached_audit_events.h"
27#include "auditd/auditd_audit_events.h"
28
29#include <string>
30#include <fstream>
31
32
33class AuditTest : public TestappClientTest {
34public:
35    void SetUp() override {
36        TestappClientTest::SetUp();
37        reconfigure_client_cert_auth("disable", "", "", "");
38        auto& logdir = mcd_env->getAuditLogDir();
39        EXPECT_NO_THROW(cb::io::rmrf(logdir));
40        cb::io::mkdirp(logdir);
41        setEnabled(true);
42    }
43
44    void TearDown() override {
45        reconfigure_client_cert_auth("disable", "", "", "");
46        setEnabled(false);
47        auto& logdir = mcd_env->getAuditLogDir();
48        EXPECT_NO_THROW(cb::io::rmrf(mcd_env->getAuditLogDir()));
49        cb::io::mkdirp(logdir);
50        TestappClientTest::TearDown();
51    }
52
53    void setEnabled(bool mode) {
54        auto* json = mcd_env->getAuditConfig();
55        try {
56            cJSON_ReplaceItemInObject(json, "auditd_enabled",
57                                      mode ? cJSON_CreateTrue() :
58                                             cJSON_CreateFalse());
59            mcd_env->rewriteAuditConfig();
60        } catch (std::exception& e) {
61            FAIL() << "Failed to toggle audit state: " << e.what();
62        }
63
64        auto& connection = getConnection();
65        connection.authenticate("@admin", "password", "PLAIN");
66        connection.reloadAuditConfiguration();
67        connection.reconnect();
68    }
69
70    std::vector<unique_cJSON_ptr> readAuditData();
71
72    bool searchAuditLogForID(int id, const std::string& username = "");
73};
74
75INSTANTIATE_TEST_CASE_P(TransportProtocols,
76                        AuditTest,
77                        ::testing::Values(TransportProtocols::McbpPlain),
78                        ::testing::PrintToStringParamName());
79
80
81std::vector<unique_cJSON_ptr> AuditTest::readAuditData() {
82    std::vector<unique_cJSON_ptr> rval;
83    auto files = cb::io::findFilesContaining(
84        mcd_env->getAuditLogDir(),
85        "audit.log");
86    for (auto file : files) {
87        std::ifstream auditData(file, std::ifstream::in);
88        while (auditData.good()) {
89            std::string line;
90            std::getline(auditData, line);
91            unique_cJSON_ptr jsonPtr(cJSON_Parse(line.c_str()));
92            if (jsonPtr.get() != nullptr) {
93                rval.push_back(std::move(jsonPtr));
94            }
95        }
96    }
97    return rval;
98}
99
100bool AuditTest::searchAuditLogForID(int id, const std::string& username) {
101    // @todo loop up to 5 sec trying to get it..
102    auto timeout = time(NULL) + 5;
103
104    do {
105        auto auditEntries = readAuditData();
106        for (auto& entry : auditEntries) {
107            cJSON* idEntry = cJSON_GetObjectItem(entry.get(), "id");
108            if (idEntry && idEntry->valueint == id) {
109                if (!username.empty()) {
110                    auto* ue = cJSON_GetObjectItem(entry.get(), "real_userid");
111                    if (ue == nullptr) {
112                        return false;
113                    }
114                    auto* user = cJSON_GetObjectItem(ue, "user");
115                    if (user != nullptr && username != user->valuestring) {
116                        // We found another user (needed to test authentication
117                        // success ;)
118                        continue;
119                    }
120                    return (user != nullptr && username == user->valuestring);
121                }
122
123                return true;
124            }
125        }
126
127        // Avoid busy-loop by backing off for 500 µsec
128        usleep(500);
129    } while (time(nullptr) < timeout);
130
131    return false;
132}
133
134/**
135 * Validate that a rejected illegal packet is audit logged.
136 */
137TEST_P(AuditTest, AuditIllegalPacket) {
138    union {
139        protocol_binary_request_no_extras request;
140        protocol_binary_response_no_extras response;
141        char bytes[1024];
142    } send, receive;
143    uint64_t value = 0xdeadbeefdeadcafe;
144    std::string key("AuditTest::AuditIllegalPacket");
145    size_t len = mcbp_storage_command(send.bytes, sizeof(send.bytes),
146                                      PROTOCOL_BINARY_CMD_SET,
147                                      key.c_str(), key.size(),
148                                      &value, sizeof(value),
149                                      0, 0);
150
151    // Now make packet illegal.
152    auto diff = ntohl(send.request.message.header.request.bodylen) - 1;
153    send.request.message.header.request.bodylen = htonl(1);
154    safe_send(send.bytes, len - diff, false);
155
156    safe_recv_packet(receive.bytes, sizeof(receive.bytes));
157    mcbp_validate_response_header(&receive.response, PROTOCOL_BINARY_CMD_SET,
158                                  PROTOCOL_BINARY_RESPONSE_EINVAL);
159
160    ASSERT_TRUE(searchAuditLogForID(MEMCACHED_AUDIT_INVALID_PACKET));
161}
162
163/**
164 * Validate that we log when we reconfigure
165 */
166TEST_P(AuditTest, AuditStartedStopped) {
167    ASSERT_TRUE(searchAuditLogForID(AUDITD_AUDIT_CONFIGURED_AUDIT_DAEMON));
168}
169
170/**
171 * Validate that a failed SASL auth is audit logged.
172 */
173TEST_P(AuditTest, AuditFailedAuth) {
174    union {
175        protocol_binary_request_no_extras request;
176        protocol_binary_response_no_extras response;
177        char bytes[1024];
178    } buffer;
179
180    // use a plain auth with an unknown user, easy failure to force.
181    const char* chosenmech = "PLAIN";
182    const char* data = "\0nouser\0nopassword";
183
184    size_t plen = mcbp_raw_command(buffer.bytes, sizeof(buffer.bytes),
185                                   PROTOCOL_BINARY_CMD_SASL_AUTH,
186                                   chosenmech, strlen(chosenmech),
187                                   data, sizeof(data));
188
189    safe_send(buffer.bytes, plen, false);
190    safe_recv_packet(&buffer, sizeof(buffer));
191    mcbp_validate_response_header(&buffer.response, PROTOCOL_BINARY_CMD_SASL_AUTH,
192                                  PROTOCOL_BINARY_RESPONSE_AUTH_ERROR);
193
194    ASSERT_TRUE(searchAuditLogForID(MEMCACHED_AUDIT_AUTHENTICATION_FAILED,
195                                    "nouser"));
196}
197
198TEST_P(AuditTest, AuditX509SuccessfulAuth) {
199    reconfigure_client_cert_auth("enable", "subject.cn", "", " ");
200    MemcachedConnection connection("127.0.0.1", ssl_port, AF_INET, true);
201    setClientCertData(connection);
202
203    // The certificate will be accepted, so the connection is established
204    // but the server will disconnect the client immediately
205    connection.connect();
206
207    ASSERT_TRUE(searchAuditLogForID(MEMCACHED_AUDIT_AUTHENTICATION_SUCCEEDED,
208                                    "Trond"));
209}
210
211TEST_P(AuditTest, AuditX509FailedAuth) {
212    reconfigure_client_cert_auth("mandatory", "subject.cn", "Tr", "");
213    MemcachedConnection connection("127.0.0.1", ssl_port, AF_INET, true);
214    setClientCertData(connection);
215
216    // The certificate will be accepted, so the connection is established
217    // but the server will disconnect the client immediately
218    connection.connect();
219    ASSERT_TRUE(searchAuditLogForID(MEMCACHED_AUDIT_AUTHENTICATION_FAILED,
220                                    "[unknown]"));
221}
222