summaryrefslogtreecommitdiff
path: root/plugin/general.c
blob: b6bb6a174577e4dc15e1bd2b9f7903ddfd515f45 (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
316
317
318
319
320
/*
 * The public APIs of the password update kadmind plugin.
 *
 * Provides the public pwupdate_init, pwupdate_close,
 * pwupdate_precommit_password, and pwupdate_postcommit_password APIs for the
 * kadmind plugin.  These APIs can also be called by command-line utilities.
 *
 * Active Directory synchronization is done in precommit and AFS kaserver
 * synchronization is done in postcommit.  The implication is that if Active
 * Directory synchronization fails, the update fails, but if AFS kaserver
 * synchronization fails, everything else still succeeds.
 *
 * Written by Russ Allbery <eagle@eyrie.org>
 * Based on code developed by Derrick Brashear and Ken Hornstein of Sine
 *     Nomine Associates, on behalf of Stanford University.
 * Copyright 2006, 2007, 2010, 2013
 *     The Board of Trustees of the Leland Stanford Junior University
 *
 * See LICENSE for licensing terms.
 */

#include <config.h>
#include <portable/krb5.h>
#include <portable/system.h>

#include <errno.h>

#include <plugin/internal.h>
#include <util/macros.h>


/*
 * Initialize the module.  This consists solely of loading our configuration
 * options from krb5.conf into a newly allocated struct stored in the second
 * argument to this function.  Returns 0 on success, non-zero on failure.
 * This function returns failure only if it could not allocate memory.
 */
krb5_error_code
sync_init(krb5_context ctx, kadm5_hook_modinfo **result)
{
    kadm5_hook_modinfo *config;

    /* Allocate our internal data. */
    config = calloc(1, sizeof(*config));
    if (config == NULL)
        return sync_error_system(ctx, "cannot allocate memory");

    /* Get Active Directory connection information from krb5.conf. */
    sync_config_string(ctx, "ad_keytab", &config->ad_keytab);
    sync_config_string(ctx, "ad_principal", &config->ad_principal);
    sync_config_string(ctx, "ad_realm", &config->ad_realm);
    sync_config_string(ctx, "ad_admin_server", &config->ad_admin_server);
    sync_config_string(ctx, "ad_ldap_base", &config->ad_ldap_base);

    /* Get allowed instances from krb5.conf. */
    sync_config_list(ctx, "ad_instances", &config->ad_instances);

    /*
     * See if we're propagating an instance to the base account in AD.  This
     * option is not supported on MIT Kerberos and results in an error there,
     * since calling libkadm5srv functions from inside a plugin appears to
     * result in corruption with MIT Kerberos (at least in 1.10.1).
     */
    sync_config_string(ctx, "ad_base_instance", &config->ad_base_instance);
#if HAVE_KRB5_MIT
    if (config->ad_base_instance != NULL) {
        sync_close(ctx, config);
        return sync_error_config(ctx, "ad_base_instance not supported on MIT"
                                 " Kerberos");
    }
#endif

    /* See if we're forcing queuing of all changes. */
    sync_config_boolean(ctx, "ad_queue_only", &config->ad_queue_only);

    /* Get the directory for queued changes from krb5.conf. */
    sync_config_string(ctx, "queue_dir", &config->queue_dir);

    /* Whether to log informational and warning messages to syslog. */
    config->syslog = true;
    sync_config_boolean(ctx, "syslog", &config->syslog);

    /* Initialized.  Set data and return. */
    *result = config;
    return 0;
}


/*
 * Shut down the module.  This just means freeing our configuration struct,
 * since we don't store any other local state.
 */
void
sync_close(krb5_context ctx UNUSED, kadm5_hook_modinfo *config)
{
    free(config->ad_admin_server);
    free(config->ad_base_instance);
    sync_vector_free(config->ad_instances);
    free(config->ad_keytab);
    free(config->ad_ldap_base);
    free(config->ad_principal);
    free(config->ad_realm);
    free(config->queue_dir);
    free(config);
}


/*
 * Given the configuration and the instance of a principal, returns true if
 * that instance is allowed and false otherwise.
 */
static bool
instance_allowed(kadm5_hook_modinfo *config, const char *instance)
{
    size_t i;

    if (instance == NULL)
        return false;
    if (config->ad_base_instance)
        if (strcmp(config->ad_base_instance, instance) == 0)
            return true;
    if (config->ad_instances == NULL)
        return false;
    for (i = 0; i < config->ad_instances->count; i++)
        if (strcmp(config->ad_instances->strings[i], instance) == 0)
            return true;
    return false;
}


/*
 * Check the principal for which we're changing a password or the enable
 * status.  Takes a flag, which is true for a password change and false for
 * other types of changes, since password changes use ad_base_instance.
 *
 * If it contains a non-null instance, we don't want to propagate the change;
 * we only want to change passwords for regular users.
 *
 * If it is a single-part principal name, ad_base_instance is set, and the
 * equivalent principal with that instance also exists, we don't propagate
 * this change because the instance's password is propagated as the base
 * account in Active Directory instead.
 *
 * Sets the allowed flag based on whether we should proceed, and returns a
 * Kerberos status code for more serious errors.  If we shouldn't proceed,
 * logs a debug-level message to syslog.
 */
static krb5_error_code
principal_allowed(kadm5_hook_modinfo *config, krb5_context ctx,
                  krb5_principal principal, bool pwchange, bool *allowed)
{
    char *display;
    krb5_error_code code;
    int ncomp;
    bool exists = false;

    /* Default to propagating. */
    *allowed = true;

    /* Get the number of components. */
    ncomp = krb5_principal_get_num_comp(ctx, principal);

    /*
     * If the principal is single-part, check against ad_base_instance.
     * Otherwise, if the principal is multi-part, check the instance.
     */
    if (pwchange && ncomp == 1 && config->ad_base_instance != NULL) {
        code = sync_instance_exists(ctx, principal, config->ad_base_instance,
                                    &exists);
        if (code != 0)
            return code;
        if (exists) {
            code = krb5_unparse_name(ctx, principal, &display);
            if (code != 0)
                return code;
            sync_syslog_debug(config, "krb5-sync: ignoring principal \"%s\""
                              " because %s instance exists", display,
                              config->ad_base_instance);
            krb5_free_unparsed_name(ctx, display);
            *allowed = false;
        }
    } else if (ncomp > 1) {
        const char *instance;

        instance = krb5_principal_get_comp_string(ctx, principal, 1);
        if (!instance_allowed(config, instance)) {
            code = krb5_unparse_name(ctx, principal, &display);
            if (code != 0)
                return code;
            sync_syslog_debug(config, "krb5-sync: ignoring principal \"%s\""
                              " with non-null instance", display);
            krb5_free_unparsed_name(ctx, display);
            *allowed = false;
        }
    }
    return 0;
}


/*
 * Actions to take before the password is changed in the local database.
 *
 * Push the new password to Active Directory if we have the necessary
 * configuration information and return any error it returns, but skip any
 * principals with a non-NULL instance since those are kept separately in each
 * realm.
 *
 * If a password change is already queued for this usequeue this password
 * change as well.  If the password change fails for a reason that may mean
 * that the user doesn't already exist, also queue this change.
 *
 * If the new password is NULL, that means that the keys are being randomized.
 * Currently, we can't do anything in that case, so just skip it.
 */
krb5_error_code
sync_chpass(kadm5_hook_modinfo *config, krb5_context ctx,
            krb5_principal principal, const char *password)
{
    krb5_error_code code;
    const char *message;
    bool allowed = false;
    bool conflict = true;

    /* Do nothing if we don't have required configuration. */
    if (config->ad_realm == NULL)
        return 0;

    /* If there was no password, this is probably a key randomization. */
    if (password == NULL)
        return 0;

    /* Check if this principal should be synchronized. */
    code = principal_allowed(config, ctx, principal, true, &allowed);
    if (code != 0)
        return code;
    if (!allowed)
        return 0;

    /* Check if there was a queue conflict or if we always queue. */
    code = sync_queue_conflict(config, ctx, principal, "password", &conflict);
    if (code != 0)
        return code;
    if (conflict)
        goto queue;
    if (config->ad_queue_only)
        goto queue;

    /* Do the password change, and queue if it fails. */
    code = sync_ad_chpass(config, ctx, principal, password);
    if (code != 0) {
        message = krb5_get_error_message(ctx, code);
        sync_syslog_notice(config, "krb5-sync: AD password change failed,"
                           " queuing: %s", message);
        krb5_free_error_message(ctx, message);
        goto queue;
    }
    return 0;

queue:
    return sync_queue_write(config, ctx, principal, "password", password);
}


/*
 * Actions to take after the account status is changed in the local database.
 *
 * Push the new account status to Active Directory if so configured, but skip
 * principals with non-NULL instances.  Return any error that it returns.
 *
 * If a status change is already queued, or if making the status change fails,
 * queue it for later processing.
 */
krb5_error_code
sync_status(kadm5_hook_modinfo *config, krb5_context ctx,
            krb5_principal principal, bool enabled)
{
    krb5_error_code code;
    const char *message;
    bool allowed = false;
    bool conflict = true;

    /* Do nothing if we don't have the required configuration. */
    if (config->ad_admin_server == NULL
        || config->ad_keytab == NULL
        || config->ad_ldap_base == NULL
        || config->ad_principal == NULL
        || config->ad_realm == NULL)
        return 0;

    /* Check if this principal should be synchronized. */
    code = principal_allowed(config, ctx, principal, false, &allowed);
    if (code != 0)
        return code;
    if (!allowed)
        return 0;

    /* Check if there was a queue conflict or if we always queue. */
    code = sync_queue_conflict(config, ctx, principal, "enable", &conflict);
    if (code != 0)
        return code;
    if (conflict)
        goto queue;
    if (config->ad_queue_only)
        goto queue;

    /* Synchronize the status. */
    code = sync_ad_status(config, ctx, principal, enabled);
    if (code != 0) {
        message = krb5_get_error_message(ctx, code);
        sync_syslog_notice(config, "krb5-sync: AD status change failed,"
                           " queuing: %s", message);
        krb5_free_error_message(ctx, message);
        goto queue;
    }
    return 0;

queue:
    return sync_queue_write(config, ctx, principal,
                            enabled ? "enable" : "disable", NULL);
}