summaryrefslogtreecommitdiffstats
path: root/src/shared/pam-util.c
blob: 3cbe431531c1b7834032b035f87e9264cccaa87a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <security/pam_ext.h>
#include <syslog.h>
#include <stdlib.h>

#include "alloc-util.h"
#include "bus-internal.h"
#include "errno-util.h"
#include "format-util.h"
#include "macro.h"
#include "pam-util.h"
#include "process-util.h"
#include "stdio-util.h"
#include "string-util.h"

void pam_log_setup(void) {
        /* Make sure we don't leak the syslog fd we open by opening/closing the fd each time. */
        log_set_open_when_needed(true);

        /* pam logs to syslog so let's make our generic logging functions do the same thing. */
        log_set_target(LOG_TARGET_SYSLOG);
}

int pam_syslog_errno(pam_handle_t *handle, int level, int error, const char *format, ...) {
        va_list ap;

        LOCAL_ERRNO(error);

        va_start(ap, format);
        pam_vsyslog(handle, LOG_ERR, format, ap);
        va_end(ap);

        return error == -ENOMEM ? PAM_BUF_ERR : PAM_SERVICE_ERR;
}

int pam_syslog_pam_error(pam_handle_t *handle, int level, int error, const char *format, ...) {
        /* This wraps pam_syslog() but will replace @PAMERR@ with a string from pam_strerror().
         * @PAMERR@ must be at the very end. */

        va_list ap;
        va_start(ap, format);

        const char *p = endswith(format, "@PAMERR@");
        if (p) {
                const char *pamerr = pam_strerror(handle, error);
                if (strchr(pamerr, '%'))
                        pamerr = "n/a";  /* We cannot have any formatting chars */

                char buf[p - format + strlen(pamerr) + 1];
                xsprintf(buf, "%.*s%s", (int)(p - format), format, pamerr);

                DISABLE_WARNING_FORMAT_NONLITERAL;
                pam_vsyslog(handle, level, buf, ap);
                REENABLE_WARNING;
        } else
                pam_vsyslog(handle, level, format, ap);

        va_end(ap);

        return error;
}

/* A small structure we store inside the PAM session object, that allows us to reuse bus connections but pins
 * it to the process thoroughly. */
struct PamBusData {
        sd_bus *bus;
        pam_handle_t *pam_handle;
        char *cache_id;
};

static PamBusData *pam_bus_data_free(PamBusData *d) {
        /* The actual destructor */
        if (!d)
                return NULL;

        /* NB: PAM sessions usually involve forking off a child process, and thus the PAM context might be
         * duplicated in the child. This destructor might be called twice: both in the parent and in the
         * child. sd_bus_flush_close_unref() however is smart enough to be a NOP when invoked in any other
         * process than the one it was invoked from, hence we don't need to add any extra protection here to
         * ensure that destruction of the bus connection in the child affects the parent's connection
         * somehow. */
        sd_bus_flush_close_unref(d->bus);
        free(d->cache_id);

        /* Note: we don't destroy pam_handle here, because this object is pinned by the handle, and not vice versa! */

        return mfree(d);
}

DEFINE_TRIVIAL_CLEANUP_FUNC(PamBusData*, pam_bus_data_free);

static void pam_bus_data_destroy(pam_handle_t *handle, void *data, int error_status) {
        /* Destructor when called from PAM. Note that error_status is supposed to tell us via PAM_DATA_SILENT
         * whether we are called in a forked off child of the PAM session or in the original parent. We don't
         * bother with that however, and instead rely on the PID checks that sd_bus_flush_close_unref() does
         * internally anyway. That said, we still generate a warning message, since this really shouldn't
         * happen. */

        if (!data)
                return;

        PamBusData *d = data;
        if (FLAGS_SET(error_status, PAM_DATA_SILENT) &&
            d->bus && bus_origin_changed(d->bus))
                /* Please adjust test/units/end.sh when updating the log message. */
                pam_syslog(handle, LOG_DEBUG,
                           "Warning: cannot close sd-bus connection (%s) after fork when it was opened before the fork.",
                           strna(d->cache_id));

        pam_bus_data_free(data);
}

static char* pam_make_bus_cache_id(const char *module_name) {
        char *id;

        /* We want to cache bus connections between hooks. But we don't want to allow them to be reused in
         * child processes (because sd-bus doesn't support that). We also don't want them to be reused
         * between our own PAM modules, because they might be linked against different versions of our
         * utility functions and share different state. Hence include both a module ID and a PID in the data
         * field ID. */

        if (asprintf(&id, "system-bus-%s-" PID_FMT, ASSERT_PTR(module_name), getpid_cached()) < 0)
                return NULL;

        return id;
}

void pam_bus_data_disconnectp(PamBusData **_d) {
        PamBusData *d = *ASSERT_PTR(_d);
        pam_handle_t *handle;
        int r;

        /* Disconnects the connection explicitly (for use via _cleanup_()) when called */

        if (!d)
                return;

        handle = ASSERT_PTR(d->pam_handle); /* Keep a reference to the session even after 'd' might be invalidated */

        r = pam_set_data(handle, ASSERT_PTR(d->cache_id), NULL, NULL);
        if (r != PAM_SUCCESS)
                pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to release PAM user record data, ignoring: @PAMERR@");

        /* Note, the pam_set_data() call will invalidate 'd', don't access here anymore */
}

int pam_acquire_bus_connection(
                pam_handle_t *handle,
                const char *module_name,
                sd_bus **ret_bus,
                PamBusData **ret_pam_bus_data) {

        _cleanup_(pam_bus_data_freep) PamBusData *d = NULL;
        _cleanup_free_ char *cache_id = NULL;
        int r;

        assert(handle);
        assert(module_name);
        assert(ret_bus);

        cache_id = pam_make_bus_cache_id(module_name);
        if (!cache_id)
                return pam_log_oom(handle);

        /* We cache the bus connection so that we can share it between the session and the authentication hooks */
        r = pam_get_data(handle, cache_id, (const void**) &d);
        if (r == PAM_SUCCESS && d)
                goto success;
        if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA))
                return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get bus connection: @PAMERR@");

        d = new(PamBusData, 1);
        if (!d)
                return pam_log_oom(handle);

        *d = (PamBusData) {
                .cache_id = TAKE_PTR(cache_id),
                .pam_handle = handle,
        };

        r = sd_bus_open_system(&d->bus);
        if (r < 0)
                return pam_syslog_errno(handle, LOG_ERR, r, "Failed to connect to system bus: %m");

        r = pam_set_data(handle, d->cache_id, d, pam_bus_data_destroy);
        if (r != PAM_SUCCESS)
                return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set PAM bus data: @PAMERR@");

        pam_syslog(handle, LOG_DEBUG, "New sd-bus connection (%s) opened.", d->cache_id);

success:
        *ret_bus = sd_bus_ref(d->bus);

        if (ret_pam_bus_data)
                *ret_pam_bus_data = d;

        TAKE_PTR(d); /* don't auto-destroy anymore, it's installed now */

        return PAM_SUCCESS;
}

int pam_release_bus_connection(pam_handle_t *handle, const char *module_name) {
        _cleanup_free_ char *cache_id = NULL;
        int r;

        assert(module_name);

        cache_id = pam_make_bus_cache_id(module_name);
        if (!cache_id)
                return pam_log_oom(handle);

        r = pam_set_data(handle, cache_id, NULL, NULL);
        if (r != PAM_SUCCESS)
                return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to release PAM user record data: @PAMERR@");

        return PAM_SUCCESS;
}

int pam_get_bus_data(
                pam_handle_t *handle,
                const char *module_name,
                PamBusData **ret) {

        PamBusData *d = NULL;
        _cleanup_free_ char *cache_id = NULL;
        int r;

        assert(handle);
        assert(module_name);
        assert(ret);

        cache_id = pam_make_bus_cache_id(module_name);
        if (!cache_id)
                return pam_log_oom(handle);

        /* We cache the bus connection so that we can share it between the session and the authentication hooks */
        r = pam_get_data(handle, cache_id, (const void**) &d);
        if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA))
                return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get bus connection: @PAMERR@");

        *ret = d;
        return PAM_SUCCESS;
}

void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status) {
        /* A generic destructor for pam_set_data() that just frees the specified data */
        free(data);
}

int pam_get_item_many_internal(pam_handle_t *handle, ...) {
        va_list ap;
        int r;

        va_start(ap, handle);
        for (;;) {
                int item_type = va_arg(ap, int);

                if (item_type <= 0) {
                        r = PAM_SUCCESS;
                        break;
                }

                const void **value = ASSERT_PTR(va_arg(ap, const void **));

                r = pam_get_item(handle, item_type, value);
                if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS))
                        break;
        }
        va_end(ap);

        return r;
}

int pam_prompt_graceful(pam_handle_t *handle, int style, char **ret_response, const char *fmt, ...) {
        va_list args;
        int r;

        assert(handle);
        assert(fmt);

        /* This is just like pam_prompt(), but does not noisily (i.e. beyond LOG_DEBUG) log on its own, but leaves that to the caller */

        _cleanup_free_ char *msg = NULL;
        va_start(args, fmt);
        r = vasprintf(&msg, fmt, args);
        va_end(args);
        if (r < 0)
                return PAM_BUF_ERR;

        const struct pam_conv *conv = NULL;
        r = pam_get_item(handle, PAM_CONV, (const void**) &conv);
        if (!IN_SET(r, PAM_SUCCESS, PAM_BAD_ITEM))
                return pam_syslog_pam_error(handle, LOG_DEBUG, r, "Failed to get conversation function structure: @PAMERR@");
        if (!conv || !conv->conv) {
                pam_syslog(handle, LOG_DEBUG, "No conversation function.");
                return PAM_SYSTEM_ERR;
        }

        struct pam_message message = {
                .msg_style = style,
                .msg = msg,
        };
        const struct pam_message *pmessage = &message;
        _cleanup_free_ struct pam_response *response = NULL;
        r = conv->conv(1, &pmessage, &response, conv->appdata_ptr);
        _cleanup_(erase_and_freep) char *rr = response ? response->resp : NULL; /* make sure string is freed + erased */
        if (r != PAM_SUCCESS)
                return pam_syslog_pam_error(handle, LOG_DEBUG, r, "Conversation function failed: @PAMERR@");

        if (ret_response)
                *ret_response = TAKE_PTR(rr);

        return PAM_SUCCESS;
}