diff options
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/credmonger.c | 491 |
2 files changed, 493 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 2dd2a80..8214567 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,2 +1,4 @@ +AM_CFLAGS = @KRB5_CFLAGS@ +LDFLAGS = @KRB5_LIBS@ sbin_PROGRAMS = credmonger credmonger_SOURCES = credmonger.c diff --git a/src/credmonger.c b/src/credmonger.c index 6ee3741..9af86bd 100644 --- a/src/credmonger.c +++ b/src/credmonger.c @@ -1,5 +1,496 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <sys/types.h> +#include <errno.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include <krb5.h> + +#define CONFIG_FILE "./credmongertab" +#define FCC_PREFIX "FILE:" +#define DEFAULT_RETRY_TIME 300 + +/* A ccache that we need to maintain. */ +static struct monger_entry { + uid_t uid; /* UID of ccache owner. */ + gid_t gid; /* GID of ccache owner. */ + char *keytab; /* Name of keytab. */ + char *principal_name; /* Client name. */ + char *fccache; /* Name of existing ccache, or NULL. */ + char *fccache_pattern; /* Pattern to use for naming ccache. */ + krb5_timestamp when; /* When the creds in the ccache expire. */ +} **entries, **cleanup; + +/* Time to exit. */ +static int quit = 0; + +/* Read the configuration and return a list of ccache names. */ +static struct monger_entry ** +entries_read(void) +{ + char buf[LINE_MAX], *p, *uids, *keytab, *principal, *fccache_pattern; + char fccache[PATH_MAX]; + struct monger_entry **list, **tmp, *entry; + struct passwd *pwd; + uid_t uid; + gid_t gid; + FILE *fp; + int n_entries; + + list = NULL; + n_entries = 0; + fp = fopen(CONFIG_FILE, "r"); + if (fp != NULL) { + while (fgets(buf, sizeof(buf), fp) != NULL) { + /* Trim end-of-line characters. */ + buf[strcspn(buf, "\r\n")] = '\0'; + /* Skip comments. */ + if (strchr("#:", buf[0]) != NULL) { + continue; + } + /* Trim end-of-line characters. */ + uids = buf; + /* Skip ahead to the keytab name and close the user + * name. */ + keytab = strchr(uids, ':'); + if (keytab == NULL) { + fprintf(stderr, "No keytab name field.\n"); + continue; + } + *keytab++ = '\0'; + /* Skip ahead to the client principal name and close + * the keytab name. */ + principal = strchr(keytab, ':'); + if (principal == NULL) { + fprintf(stderr, "No principal name field.\n"); + continue; + } + *principal++ = '\0'; + /* Skip ahead to the ccache name pattern and close the + * principal name. */ + fccache_pattern = strchr(principal, ':'); + if (fccache_pattern != NULL) { + *fccache_pattern++ = '\0'; + } + /* Now figure out the UID/GID. */ + uid = (uid_t) strtol(uids, &p, 10); + if ((p == NULL) || (*p != '\0')) { + /* Not a number, so treat it as a user name. */ + pwd = getpwnam(uids); + if (pwd != NULL) { + uid = pwd->pw_uid; + gid = pwd->pw_gid; + } else { + fprintf(stderr, + "Unknown user \"%s\".\n", uids); + continue; + } + } else { + /* Treat it as a number. */ + pwd = getpwuid(uid); + if (pwd != NULL) { + gid = pwd->pw_gid; + } else { + fprintf(stderr, + "Unknown user %lu.\n", + (unsigned long) uid); + continue; + } + } + /* Now go back and figure out the ccache name pattern. + * If it's empty, assume that we want a "session" file + * in the temporary directory. */ + if ((fccache_pattern == NULL) || + (strlen(fccache_pattern) == 0)) { + snprintf(fccache, sizeof(fccache), + "FILE:%s/krb5cc_%lu_XXXXXX", + getenv("TMPDIR") ?: "/tmp", + (unsigned long) uid); + } else { + /* If the user supplied us with a typed ccache + * name, just use it as-is. */ + if (strncmp(fccache_pattern, FCC_PREFIX, + strlen(FCC_PREFIX)) == 0) { + snprintf(fccache, sizeof(fccache), + "%s", fccache_pattern); + } else { + /* Mark it as a file-based ccache, and + * take the filename as-is. */ + snprintf(fccache, sizeof(fccache), + "FILE:%s", fccache_pattern); + } + } + fccache_pattern = fccache; + /* Make space for this entry in the list. */ + tmp = realloc(list, sizeof(*list) * (n_entries + 2)); + if (tmp == NULL) { + fprintf(stderr, "Out of memory.\n"); + break; + } + list = tmp; + /* Allocate this entry. */ + list[n_entries] = malloc(sizeof(**list)); + if (list[n_entries] == NULL) { + fprintf(stderr, "Out of memory.\n"); + break; + } + entry = list[n_entries]; + /* NULL-terminate the list. */ + n_entries++; + list[n_entries] = NULL; + /* Initialize this entry. */ + memset(entry, 0, sizeof(*entry)); + entry->uid = uid; + entry->gid = gid; + entry->keytab = strdup(keytab); + if (entry->keytab == NULL) { + fprintf(stderr, "Out of memory.\n"); + break; + } + entry->principal_name = strdup(principal); + if (entry->principal_name == NULL) { + fprintf(stderr, "Out of memory.\n"); + break; + } + entry->fccache_pattern = strdup(fccache_pattern); + if (entry->fccache_pattern == NULL) { + fprintf(stderr, "Out of memory.\n"); + break; + } + fprintf(stderr, "%ld:%s,%s,%s\n", + (unsigned long) entry->uid, + entry->keytab, + entry->principal_name, + entry->fccache_pattern); + } + fclose(fp); + } + + return list; +} + +/* Do the heavy lifting. */ +static void +entries_poll(void) +{ + int i, j, fd, len, need_rename; + krb5_error_code ret; + krb5_context ctx; + krb5_creds creds; + krb5_keytab keytab; + krb5_ccache ccache; + krb5_principal client; + krb5_get_init_creds_opt *gic_opts; + char host[LINE_MAX], fccache[PATH_MAX + strlen(FCC_PREFIX) + 1]; + char *principal_name, *oldfile; + + /* Figure out the client hostname. */ + memset(host, '\0', sizeof(host)); + if (gethostname(host, sizeof(host) - 1) != 0) { + fprintf(stderr, "Error determining hostname.\n"); + snprintf(host, sizeof(host), "localhost"); + } + + ctx = NULL; + gic_opts = NULL; + if (krb5_init_context(&ctx) != 0) { + fprintf(stderr, "Error initializing Kerberos.\n"); + } else { + /* Initialize the get_init_creds options. */ + if (krb5_get_init_creds_opt_alloc(ctx, &gic_opts) != 0) { + gic_opts = NULL; + } else { + krb5_get_init_creds_opt_set_forwardable(gic_opts, 0); + krb5_get_init_creds_opt_set_proxiable(gic_opts, 0); + } + /* Walk the list of entries. */ + for (i = 0; (entries != NULL) && (entries[i] != NULL); i++) { + fprintf(stderr, "%ld:%s,%s,%s\n", + (unsigned long) entries[i]->uid, + entries[i]->keytab, + entries[i]->principal_name, + entries[i]->fccache_pattern); + /* Open the keytab. */ + keytab = NULL; + if ((entries[i]->keytab == NULL) || + (strlen(entries[i]->keytab) == 0)) { + ret = krb5_kt_default(ctx, &keytab); + } else { + ret = krb5_kt_resolve(ctx, entries[i]->keytab, + &keytab); + } + if (ret != 0) { + fprintf(stderr, "Error resolving keytab: %s.\n", + error_message(ret)); + continue; + } + /* If the principal name ends with "/", append the + * local host name. Parse it. */ + client = NULL; + len = strlen(entries[i]->principal_name); + if ((len > 0) && + (entries[i]->principal_name[len - 1] == '/')) { + principal_name = malloc(len + strlen(host) + 1); + if (principal_name == NULL) { + fprintf(stderr, "Out of memory.\n"); + krb5_kt_close(ctx, keytab); + continue; + } + strcpy(principal_name, + entries[i]->principal_name); + strcat(principal_name, host); + } else { + principal_name = strdup(entries[i]->principal_name); + if (principal_name == NULL) { + fprintf(stderr, "Out of memory.\n"); + krb5_kt_close(ctx, keytab); + continue; + } + } + ret = krb5_parse_name(ctx, principal_name, &client); + if (ret != 0) { + fprintf(stderr, "Error parsing client: %s.\n", + error_message(ret)); + free(principal_name); + krb5_kt_close(ctx, keytab); + continue; + } + free(principal_name); + /* Now "unparse" it. */ + principal_name = NULL; + ret = krb5_unparse_name(ctx, client, &principal_name); + if (ret != 0) { + fprintf(stderr, "Error unparsing name: %s.\n", + error_message(ret)); + krb5_kt_close(ctx, keytab); + krb5_free_principal(ctx, client); + continue; + } + /* Okay, we have everything we need. Give it a shot. */ + memset(&creds, 0, sizeof(creds)); + ret = krb5_get_init_creds_keytab(ctx, &creds, client, + keytab, 0, NULL, + gic_opts); + if (ret != 0) { + fprintf(stderr, "Error getting creds for %s: " + "%s.\n", principal_name, + error_message(ret)); + krb5_kt_close(ctx, keytab); + krb5_free_unparsed_name(ctx, principal_name); + krb5_free_principal(ctx, client); + continue; + } + /* Create a temporary file to hold the creds. */ + snprintf(fccache, sizeof(fccache), "%s", + entries[i]->fccache_pattern); + need_rename = 0; + fd = mkstemp(fccache + strlen(FCC_PREFIX)); + /* If we got EINVAL, then it's not a pattern, so create + * a pattern and note that we'll need to rename it over + * the "real" name when we're done setting it up. */ + if ((fd == -1) && (errno == EINVAL)) { + need_rename++; + snprintf(fccache, sizeof(fccache), + "%sXXXXXX", + entries[i]->fccache_pattern); + fd = mkstemp(fccache + strlen(FCC_PREFIX)); + } + if (fd == -1) { + fprintf(stderr, "Error creating temporary " + "ccache.\n"); + krb5_free_cred_contents(ctx, &creds); + krb5_kt_close(ctx, keytab); + krb5_free_unparsed_name(ctx, principal_name); + krb5_free_principal(ctx, client); + continue; + } + close(fd); + /* Open the ccache. */ + ccache = NULL; + ret = krb5_cc_resolve(ctx, fccache, &ccache); + if (ret != 0) { + fprintf(stderr, "Error opening temporary " + "ccache: %s.\n", error_message(ret)); + unlink(fccache + strlen(FCC_PREFIX)); + krb5_free_cred_contents(ctx, &creds); + krb5_kt_close(ctx, keytab); + krb5_free_unparsed_name(ctx, principal_name); + krb5_free_principal(ctx, client); + continue; + } + /* Write the client's name to the ccache. */ + ret = krb5_cc_initialize(ctx, ccache, client); + if (ret != 0) { + fprintf(stderr, "Error initializing temporary " + "ccache: %s.\n", error_message(ret)); + krb5_cc_close(ctx, ccache); + unlink(fccache + strlen(FCC_PREFIX)); + krb5_free_cred_contents(ctx, &creds); + krb5_kt_close(ctx, keytab); + krb5_free_unparsed_name(ctx, principal_name); + krb5_free_principal(ctx, client); + continue; + } + /* Store the TGT. */ + ret = krb5_cc_store_cred(ctx, ccache, &creds); + if (ret != 0) { + fprintf(stderr, "Error storing creds: %s.\n", + error_message(ret)); + krb5_cc_close(ctx, ccache); + unlink(fccache + strlen(FCC_PREFIX)); + krb5_free_cred_contents(ctx, &creds); + krb5_kt_close(ctx, keytab); + krb5_free_unparsed_name(ctx, principal_name); + krb5_free_principal(ctx, client); + continue; + } + /* Done with the ccache structure. */ + krb5_cc_close(ctx, ccache); + /* Fixup permissions. */ + chown(fccache + strlen(FCC_PREFIX), + entries[i]->uid, entries[i]->gid); + /* If the configuration doesn't want a unique ccache + * name, overwrite the destination with the contents of + * this temporary file. */ + if (need_rename) { + rename(fccache + strlen(FCC_PREFIX), + entries[i]->fccache_pattern + + strlen(FCC_PREFIX)); + entries[i]->fccache = + entries[i]->fccache_pattern; + entries[i]->when = creds.times.endtime; + } else { + /* Remove the old file, and record this one. */ + oldfile = entries[i]->fccache; + entries[i]->fccache = strdup(fccache); + if (entries[i]->fccache == NULL) { + /* Better luck next time, which is + * soon. */ + entries[i]->fccache = oldfile; + entries[i]->when = DEFAULT_RETRY_TIME; + unlink(fccache + strlen(FCC_PREFIX)); + } else { + /* Get rid of a previously-initialized + * file. */ + if (oldfile != NULL) { + unlink(oldfile + strlen(FCC_PREFIX)); + free(oldfile); + } + /* Record the renewal time. */ + entries[i]->when = creds.times.endtime; + } + } + fprintf(stderr, "Saved creds for \"%s\" to \"%s\".\n", + principal_name, entries[i]->fccache); + krb5_free_cred_contents(ctx, &creds); + krb5_kt_close(ctx, keytab); + krb5_free_unparsed_name(ctx, principal_name); + krb5_free_principal(ctx, client); + } + krb5_get_init_creds_opt_free(ctx, gic_opts); + krb5_free_context(ctx); + } + + /* Walk the list of to-be-cleaned-up entries. */ + for (i = 0; (cleanup != NULL) && (cleanup[i] != NULL); i++) { + /* Remove the ccache, if there is one. */ + if (cleanup[i]->fccache != NULL) { + for (j = 0; + (entries != NULL) && (entries[j] != NULL); + j++) { + /* ... unless it's being used elsewhere. */ + if ((entries[j]->fccache != NULL) && + (strcmp(cleanup[i]->fccache, + entries[j]->fccache) == 0)) { + break; + } + } + if ((entries == NULL) || (entries[j] == NULL)) { + fprintf(stderr, "Removing \"%s\".\n", + cleanup[i]->fccache); + unlink(cleanup[i]->fccache + + strlen(FCC_PREFIX)); + } + } + free(cleanup[i]->fccache); + /* Free the pattern if it's not a plain filename. */ + if (cleanup[i]->fccache != cleanup[i]->fccache_pattern) { + free(cleanup[i]->fccache_pattern); + } + /* Finish up. */ + free(cleanup[i]->principal_name); + free(cleanup[i]->keytab); + free(cleanup[i]); + } + free(cleanup); + cleanup = NULL; +} + +static void +entries_reload(void) +{ + struct monger_entry **list, **cleanup; + list = entries_read(); + cleanup = entries; + entries = list; + entries_poll(); +} + +static void +entries_unload(void) +{ + cleanup = entries; + entries = NULL; + entries_poll(); +} + +static int +entries_waittime(void) +{ + int i; + krb5_timestamp last; + for (i = 0, last = 0; (entries != NULL) && (entries[i] != NULL); i++) { + if (entries[i]->fccache != NULL) { + if ((last == 0) || (entries[i]->when < last)) { + last = entries[i]->when; + } + } + } + if (last == 0) { + i = DEFAULT_RETRY_TIME; + } else { + i = (last - time(NULL)) * 9 / 10; + } + return i; +} + +static void +setquit(int signum) +{ + quit++; +} + int main(int argc, char **argv) { + time_t now, howlong; + signal(SIGINT, setquit); + signal(SIGQUIT, setquit); + while (quit == 0) { + entries_reload(); + now = time(NULL); + howlong = entries_waittime(); + fprintf(stderr, "Waiting for %ld seconds.\n", (long) howlong); + sleep(howlong); + }; + entries_unload(); return 0; } |