/* Copyright (C) 2011 the GSS-PROXY contributors, see COPYING for license */ #include "config.h" #include #include #include #include #include #include "gp_proxy.h" #include "gp_config.h" #include "gp_selinux.h" #include #include struct gp_flag_def { const char *name; uint32_t value; }; struct gp_flag_def flag_names[] = { { "DELEGATE", GSS_C_DELEG_FLAG }, { "MUTUAL_AUTH", GSS_C_MUTUAL_FLAG }, { "REPLAY_DETECT", GSS_C_REPLAY_FLAG }, { "SEQUENCE", GSS_C_SEQUENCE_FLAG }, { "CONFIDENTIALITY", GSS_C_CONF_FLAG }, { "INTEGRITIY", GSS_C_INTEG_FLAG }, { "ANONYMOUS", GSS_C_ANON_FLAG }, { NULL, 0 } }; #define DEFAULT_FILTERED_FLAGS GSS_C_DELEG_FLAG #define DEFAULT_ENFORCED_FLAGS 0 static void free_str_array(const char ***a, int *count) { const char **array; int i; if (!a) { return; } array = *a; if (count) { for (i = 0; i < *count; i++) { safefree(array[i]); } } else { for (i = 0; array[i]; i++) { safefree(array[i]); } } safefree(*a); } void free_cred_store_elements(gss_key_value_set_desc *cs) { if (!cs->elements) return; for (unsigned i = 0; i < cs->count; i++) { safefree(cs->elements[i].key); safefree(cs->elements[i].value); } safefree(cs->elements); cs->count = 0; } static void gp_service_free(struct gp_service *svc) { free(svc->name); if (svc->mechs & GP_CRED_KRB5) { free(svc->krb5.principal); free_cred_store_elements(&svc->krb5.store); gp_free_creds_handle(&svc->krb5.creds_handle); } free(svc->socket); free(svc->program); SELINUX_context_free(svc->selinux_ctx); memset(svc, 0, sizeof(struct gp_service)); } static int setup_krb5_creds_handle(struct gp_service *svc) { uint32_t ret_maj, ret_min; const char *keytab = NULL; for (unsigned i = 0; i < svc->krb5.store.count; i++) { if (strcmp(svc->krb5.store.elements[i].key, "keytab") == 0) { keytab = svc->krb5.store.elements[i].value; break; } } ret_maj = gp_init_creds_handle(&ret_min, svc->name, keytab, &svc->krb5.creds_handle); if (ret_maj) { return ret_min; } return 0; } static int get_krb5_mech_cfg(struct gp_service *svc, struct gp_ini_context *ctx, const char *secname) { struct { const char *a; const char *b; } deprecated_vals[] = { {"krb5_keytab", "keytab" }, {"krb5_ccache", "ccache" }, {"krb5_client_keytab", "client_keytab" } }; const char *value; const char **strings = NULL; int count = 0; int i; int ret; ret = gp_config_get_string(ctx, secname, "krb5_principal", &value); if (ret == 0) { svc->krb5.principal = strdup(value); if (!svc->krb5.principal) { return ENOMEM; } } else if (ret != ENOENT) { return ret; } /* check for deprecated options */ for (i = 0; i < 3; i++) { ret = gp_config_get_string(ctx, secname, deprecated_vals[i].a, &value); if (ret == 0) { GPERROR("\"%s = %s\" is deprecated, " "please use \"cred_store = %s:%s\"\n", deprecated_vals[i].a, value, deprecated_vals[i].b, value); return EINVAL; } else if (ret != ENOENT) { return ret; } } /* instead look for the cred_store parameter */ ret = gp_config_get_string_array(ctx, secname, "cred_store", &count, &strings); if (ret == 0) { const char *p; ssize_t len; char *key; svc->krb5.store.elements = calloc(count, sizeof(gss_key_value_element_desc)); if (!svc->krb5.store.elements) { ret = ENOMEM; goto done; } svc->krb5.store.count = count; for (int c = 0; c < count; c++) { p = strchr(strings[c], ':'); if (!p) { GPERROR("Invalid cred_store value, no ':' separator found in" " [%s].\n", strings[c]); ret = EINVAL; goto done; } len = asprintf(&key, "%.*s", (int)(p - strings[c]), strings[c]); if (len == -1) { ret = ENOMEM; goto done; } svc->krb5.store.elements[c].key = key; svc->krb5.store.elements[c].value = strdup(p + 1); if (!svc->krb5.store.elements[c].value) { ret = ENOMEM; goto done; } } } else if (ret == ENOENT) { /* when not there we ignore */ ret = 0; } if (ret == 0) { ret = setup_krb5_creds_handle(svc); } done: free_str_array(&strings, &count); return ret; } static int parse_flags(const char *value, uint32_t *storage) { char *handle; char *token; char *str; bool add; unsigned long int conv; uint32_t flagval; int i; str = strdup(value); if (!str) { return ENOMEM; } for (token = strtok_r(str, ", ", &handle); token != NULL; token = strtok_r(NULL, ", ", &handle)) { switch (token[0]) { case '+': add = true; break; case '-': add = false; break; default: GPERROR("Ignoring flag [%s], missing +/- qualifier.\n", token); continue; } token++; for (i = 0; flag_names[i].name != NULL; i++) { if (strcasecmp(token, flag_names[i].name) == 0) { flagval = flag_names[i].value; break; } } if (flag_names[i].name == NULL) { conv = strtoul(token, &handle, 0); if (conv == 0 || conv == ULONG_MAX || *handle != '\0') { GPERROR("Ignoring flag [%s], unrecognized value.\n", token); continue; } flagval = conv; } GPDEBUG("%s Flag %s (%u).\n", add?"Add":"Remove", token, flagval); if (add) *storage |= flagval; else *storage &= ~flagval; } safefree(str); return 0; } static int check_services(const struct gp_config *cfg) { int i, j; struct gp_service *isvc, *jsvc; const char *isock, *jsock; int ret = 0; /* [gssproxy] section does not get placed in svcs */ for (i = 0; i < cfg->num_svcs; i++) { isvc = cfg->svcs[i]; isock = isvc->socket; if (!isock) { isock = GP_SOCKET_NAME; } if (isvc->program) { if (isvc->program[0] != '/') { ret = 1; GPERROR("Program paths must be absolute!\n"); } else if (strchr(isvc->program, '|')) { ret = 1; GPERROR("The character '|' is invalid in program paths!\n"); } } for (j = 0; j < i; j++) { jsvc = cfg->svcs[j]; jsock = jsvc->socket; if (!jsock) { jsock = GP_SOCKET_NAME; } if (!gp_same(isock, jsock) || !gp_same(isvc->program, jsvc->program) || !gp_selinux_ctx_equal(isvc->selinux_ctx, jsvc->selinux_ctx)) { continue; } if (jsvc->any_uid) { ret = 1; GPERROR("%s sets allow_any_uid with the same socket, " "selinux_context, and program as %s!\n", jsvc->name, isvc->name); } else if (jsvc->euid == isvc->euid) { ret = 1; GPERROR("socket, selinux_context, euid, and program for " "%s and %s should not match!\n", isvc->name, jsvc->name); } } } return ret; } static int load_services(struct gp_config *cfg, struct gp_ini_context *ctx) { int num_sec; char *secname = NULL; const char *value; char *vcopy; char *token; char *handle; int valnum; int ret; int i, n; num_sec = gp_config_get_nsec(ctx); /* allocate enough space for num_sec services, * we won't waste too much space by overallocating */ cfg->svcs = calloc(num_sec, sizeof(struct gp_service *)); if (!cfg->svcs) { ret = ENOMEM; goto done; } for (i = 0; i < num_sec; i++) { secname = gp_config_get_secname(ctx, i); ret = strncmp(secname, "service/", 8); if (ret == 0) { n = cfg->num_svcs; cfg->svcs[n] = calloc(1, sizeof(struct gp_service)); if (!cfg->svcs[n]) { ret = ENOMEM; goto done; } cfg->num_svcs++; /* by default allow both */ cfg->svcs[n]->cred_usage = GSS_C_BOTH; cfg->svcs[n]->name = strdup(secname + 8); if (!cfg->svcs[n]->name) { ret = ENOMEM; goto done; } /* euid can be a string or an int */ ret = gp_config_get_int(ctx, secname, "euid", &valnum); if (ret != 0) { ret = gp_config_get_string(ctx, secname, "euid", &value); if (ret == 0) { struct passwd *eu_passwd; /* static; do not free */ errno = 0; /* needs to be 0; otherwise it won't be set */ eu_passwd = getpwnam(value); if (!eu_passwd) { ret = errno; if (ret == 0) { /* not that it gets set anyway... */ ret = ENOENT; } } else { valnum = eu_passwd->pw_uid; } } if (ret != 0) { /* if euid is missing or there is an error retrieving it * return an error and end. This is a fatal condition. */ if (ret == ENOENT) { GPERROR("Option 'euid' is missing from [%s].\n", secname); ret = EINVAL; } gp_service_free(cfg->svcs[n]); cfg->num_svcs--; safefree(secname); goto done; } } cfg->svcs[n]->euid = valnum; ret = gp_config_get_string(ctx, secname, "allow_any_uid", &value); if (ret == 0) { if (gp_boolean_is_true(value)) { cfg->svcs[n]->any_uid = true; } } ret = gp_config_get_string(ctx, secname, "allow_protocol_transition", &value); if (ret == 0) { if (gp_boolean_is_true(value)) { cfg->svcs[n]->allow_proto_trans = true; } } ret = gp_config_get_string(ctx, secname, "allow_constrained_delegation", &value); if (ret == 0) { if (gp_boolean_is_true(value)) { cfg->svcs[n]->allow_const_deleg = true; } } ret = gp_config_get_string(ctx, secname, "allow_client_ccache_sync", &value); if (ret == 0) { if (gp_boolean_is_true(value)) { cfg->svcs[n]->allow_cc_sync = true; } } ret = gp_config_get_string(ctx, secname, "trusted", &value); if (ret == 0) { if (gp_boolean_is_true(value)) { cfg->svcs[n]->trusted = true; } } ret = gp_config_get_string(ctx, secname, "kernel_nfsd", &value); if (ret == 0) { if (gp_boolean_is_true(value)) { cfg->svcs[n]->kernel_nfsd = true; } } ret = gp_config_get_string(ctx, secname, "impersonate", &value); if (ret == 0) { if (gp_boolean_is_true(value)) { cfg->svcs[n]->impersonate = true; } } ret = gp_config_get_string(ctx, secname, "socket", &value); if (ret == 0) { cfg->svcs[n]->socket = strdup(value); if (!cfg->svcs[n]->socket) { ret = ENOMEM; goto done; } } ret = gp_config_get_string(ctx, secname, "mechs", &value); if (ret != 0) { /* if mechs is missing or there is an error retrieving it * return an error and end. This is a fatal condition. */ if (ret == ENOENT) { GPERROR("Option 'mechs' is missing from [%s].\n", secname); ret = EINVAL; } gp_service_free(cfg->svcs[n]); cfg->num_svcs--; safefree(secname); goto done; } vcopy = strdup(value); if (!vcopy) { ret = ENOMEM; goto done; } token = strtok_r(vcopy, ", ", &handle); do { ret = strcmp(value, "krb5"); if (ret == 0) { ret = get_krb5_mech_cfg(cfg->svcs[n], ctx, secname); if (ret == 0) { cfg->svcs[n]->mechs |= GP_CRED_KRB5; } else { GPERROR("Failed to read krb5 config for %s.\n", secname); safefree(vcopy); return ret; } } else { GPERROR("Unknown mech: %s in [%s], ignoring.\n", token, secname); } token = strtok_r(NULL, ", ", &handle); } while (token != NULL); safefree(vcopy); if (cfg->svcs[n]->mechs == 0) { GPDEBUG("No mechs found for [%s], ignoring.\n", secname); gp_service_free(cfg->svcs[n]); cfg->num_svcs--; safefree(secname); continue; } ret = gp_config_get_string(ctx, secname, "selinux_context", &value); if (ret == 0) { GPDEBUG( "selinux_ctx is deprecated; use euid/socket instead.\n"); cfg->svcs[n]->selinux_ctx = SELINUX_context_new(value); if (!cfg->svcs[n]->selinux_ctx) { ret = EINVAL; goto done; } } ret = gp_config_get_string(ctx, secname, "cred_usage", &value); if (ret == 0) { if (strcasecmp(value, "initiate") == 0) { cfg->svcs[n]->cred_usage = GSS_C_INITIATE; } else if (strcasecmp(value, "accept") == 0) { cfg->svcs[n]->cred_usage = GSS_C_ACCEPT; } else if (strcasecmp(value, "both") == 0) { cfg->svcs[n]->cred_usage = GSS_C_BOTH; } else { GPDEBUG("Invalid value '%s' for cred_usage in [%s].\n", value, secname); ret = EINVAL; goto done; } } cfg->svcs[n]->filter_flags = DEFAULT_FILTERED_FLAGS; ret = gp_config_get_string(ctx, secname, "filter_flags", &value); if (ret == 0) { parse_flags(value, &cfg->svcs[n]->filter_flags); } cfg->svcs[n]->enforce_flags = DEFAULT_ENFORCED_FLAGS; ret = gp_config_get_string(ctx, secname, "enforce_flags", &value); if (ret == 0) { ret = parse_flags(value, &cfg->svcs[n]->enforce_flags); if (ret) goto done; } ret = gp_config_get_string(ctx, secname, "program", &value); if (ret == 0) { cfg->svcs[n]->program = strdup(value); if (!cfg->svcs[n]->program) { ret = ENOMEM; goto done; } } } safefree(secname); } if (cfg->num_svcs == 0) { GPERROR("No service sections configured!\n"); return ENOENT; } ret = check_services(cfg); done: safefree(secname); return ret; } static int gp_init_ini_context(const char *config_file, const char *config_dir, struct gp_ini_context **ctxp) { struct gp_ini_context *ctx; int ret; if (!ctxp) { return EINVAL; } ctx = calloc(1, sizeof(struct gp_ini_context)); if (!ctx) { return ENOENT; } ret = gp_config_init(config_file, config_dir, ctx); if (ret) { free(ctx); } else { *ctxp = ctx; } return ret; } int load_config(struct gp_config *cfg) { struct gp_ini_context *ctx; const char *tmpstr; int tmp_dbg_lvl = 0; int tmpint = 0; int ret; ret = gp_init_ini_context(cfg->config_file, cfg->config_dir, &ctx); if (ret) { return ret; } ret = gp_config_get_string(ctx, "gssproxy", "debug", &tmpstr); if (ret == 0) { if (gp_boolean_is_true(tmpstr)) { if (tmp_dbg_lvl == 0) { tmp_dbg_lvl = 1; } } } else if (ret != ENOENT) { goto done; } ret = gp_config_get_int(ctx, "gssproxy", "debug_level", &tmpint); if (ret == 0) { tmp_dbg_lvl = tmpint; } else if (ret != ENOENT) { goto done; } ret = gp_config_get_string(ctx, "gssproxy", "run_as_user", &tmpstr); if (ret == 0) { cfg->proxy_user = strdup(tmpstr); if (!cfg->proxy_user) { ret = ENOMEM; goto done; } } else if (ret != ENOENT) { goto done; } ret = gp_config_get_int(ctx, "gssproxy", "worker threads", &cfg->num_workers); if (ret != 0 && ret != ENOENT) { goto done; } ret = load_services(cfg, ctx); done: if (ret != 0) { GPERROR("Error reading configuration %d: %s", ret, gp_strerror(ret)); } gp_debug_toggle(tmp_dbg_lvl); gp_config_close(ctx); safefree(ctx); return ret; } struct gp_config *read_config(char *config_file, char *config_dir, char *socket_name, int opt_daemonize) { const char *socket = GP_SOCKET_NAME; const char *dir = NULL; struct gp_config *cfg; int ret; cfg = calloc(1, sizeof(struct gp_config)); if (!cfg) { return NULL; } if (config_file) { cfg->config_file = strdup(config_file); if (!cfg->config_file) { ret = ENOMEM; goto done; } } else { ret = asprintf(&cfg->config_file, "%s/gssproxy.conf", PUBCONF_PATH); if (ret == -1) { goto done; } } if (config_dir) { dir = config_dir; } else if (!config_file) { dir = PUBCONF_PATH; } if (dir) { cfg->config_dir = strdup(dir); if (!cfg->config_dir) { ret = ENOMEM; goto done; } } if (socket_name) socket = socket_name; cfg->socket_name = strdup(socket); if (cfg->socket_name == NULL) { ret = ENOMEM; goto done; } switch (opt_daemonize) { case 0: /* daemonize by default */ case 1: cfg->daemonize = true; break; case 2: cfg->daemonize = false; break; } ret = load_config(cfg); if (ret) { GPDEBUG("Config file(s) not found!\n"); } done: if (ret) { /* recursively frees cfg */ free_config(&cfg); return NULL; } return cfg; } struct gp_creds_handle *gp_service_get_creds_handle(struct gp_service *svc) { return svc->krb5.creds_handle; } void free_config(struct gp_config **cfg) { struct gp_config *config = *cfg; if (!config) { return; } free(config->config_file); free(config->config_dir); free(config->socket_name); free(config->proxy_user); for (int i = 0; i < config->num_svcs; i++) { gp_service_free(config->svcs[i]); safefree(config->svcs[i]); } free(config->svcs); free(config); *cfg = NULL; } static int gp_config_from_file(const char *config_file, struct ini_cfgobj *ini_config, const uint32_t collision_flags) { struct ini_cfgfile *file_ctx = NULL; int ret; ret = ini_config_file_open(config_file, 0, /* metadata_flags, FIXME */ &file_ctx); if (ret) { GPERROR("Failed to open config file: %d (%s)\n", ret, gp_strerror(ret)); ini_config_destroy(ini_config); return ret; } ret = ini_config_parse(file_ctx, INI_STOP_ON_ANY, /* error_level */ collision_flags, INI_PARSE_NOWRAP, /* parse_flags */ ini_config); if (ret) { char **errors = NULL; /* we had a parsing failure */ GPERROR("Failed to parse config file: %d (%s)\n", ret, gp_strerror(ret)); if (ini_config_error_count(ini_config)) { ini_config_get_errors(ini_config, &errors); if (errors) { ini_config_print_errors(stderr, errors); ini_config_free_errors(errors); } } ini_config_file_destroy(file_ctx); ini_config_destroy(ini_config); return ret; } ini_config_file_destroy(file_ctx); return 0; } static int gp_config_from_dir(const char *config_dir, struct ini_cfgobj **ini_config, const uint32_t collision_flags) { struct ini_cfgobj *result_cfg = NULL; struct ref_array *error_list = NULL; int ret; const char *patterns[] = { /* match only files starting with "##-" and ending in ".conf" */ "^[0-9]\\{2\\}-.\\{1,\\}\\.conf$", NULL, }; const char *sections[] = { /* match either "gssproxy" or sections that start with "service/" */ "^gssproxy$", "^service/.*$", NULL, }; /* Permission check failures silently skip the file, so they are not * useful to us. */ ret = ini_config_augment(*ini_config, config_dir, patterns, sections, NULL, /* check_perm */ INI_STOP_ON_ANY, /* error_level */ collision_flags, INI_PARSE_NOWRAP, /* allow sections with the same name in * different files, but log warnings */ INI_MS_DETECT | INI_MS_PRESERVE, &result_cfg, &error_list, NULL); if (error_list) { uint32_t len; len = ref_array_len(error_list); for (uint32_t i = 0; i < len; i++) { /* libini has an unfixable bug where error strings are (char **) */ GPAUDIT("Error when reading config directory: %s\n", *(char **)ref_array_get(error_list, i, NULL)); } ref_array_destroy(error_list); } if (ret && ret != EEXIST) { GPERROR("Error when reading config directory number: %d\n", ret); ref_array_destroy(error_list); return ret; } /* if we read no new files, result_cfg will be NULL */ if (result_cfg) { ini_config_destroy(*ini_config); *ini_config = result_cfg; } return 0; } int gp_config_init(const char *config_file, const char *config_dir, struct gp_ini_context *ctx) { struct ini_cfgobj *ini_config = NULL; int ret; /* Within a single file, merge all collisions */ const uint32_t collision_flags = INI_MS_MERGE | INI_MV1S_ALLOW | INI_MV2S_ALLOW; if (!ctx) { return EINVAL; } ret = ini_config_create(&ini_config); if (ret) { return ENOENT; } if (config_file) { ret = gp_config_from_file(config_file, ini_config, collision_flags); if (ret) { GPDEBUG("Error when trying to read config file %s.\n", config_file); return ret; } } if (config_dir) { ret = gp_config_from_dir(config_dir, &ini_config, collision_flags); if (ret) { GPDEBUG("Error when trying to read config directory %s.\n", config_dir); return ret; } } ctx->private_data = ini_config; return 0; } int gp_config_get_string(struct gp_ini_context *ctx, const char *secname, const char *keyname, const char **value) { struct ini_cfgobj *ini_config = (struct ini_cfgobj *)ctx->private_data; struct value_obj *vo = NULL; int ret; const char *val; if (!value) { return -1; } *value = NULL; ret = ini_get_config_valueobj(secname, keyname, ini_config, INI_GET_FIRST_VALUE, &vo); if (ret) { return ret; } if (!vo) { return ENOENT; } val = ini_get_const_string_config_value(vo, &ret); if (ret) { return ret; } *value = val; return 0; } int gp_config_get_string_array(struct gp_ini_context *ctx, const char *secname, const char *keyname, int *num_values, const char ***values) { struct ini_cfgobj *ini_config = (struct ini_cfgobj *)ctx->private_data; struct value_obj *vo = NULL; const char *value; int ret; int i, count = 0; const char **array = NULL; const char **t_array; if (!values || !num_values) { return EINVAL; } *num_values = 0; *values = NULL; ret = ini_get_config_valueobj(secname, keyname, ini_config, INI_GET_FIRST_VALUE, &vo); if (ret) { return ret; } if (!vo) { return ENOENT; } value = ini_get_const_string_config_value(vo, &ret); if (ret) { return ret; } array = calloc(1, sizeof(char *)); if (array == NULL) { ret = ENOMEM; goto done; } array[count] = strdup(value); if (array[count] == NULL) { ret = ENOMEM; goto done; } count++; do { ret = ini_get_config_valueobj(secname, keyname, ini_config, INI_GET_NEXT_VALUE, &vo); if (ret) { goto done; } if (!vo) { break; } value = ini_get_const_string_config_value(vo, &ret); if (ret) { goto done; } t_array = realloc(array, (count+1) * sizeof(char *)); if (t_array == NULL) { ret = ENOMEM; goto done; } array = t_array; array[count] = strdup(value); if (array[count] == NULL) { ret = ENOMEM; goto done; } count++; } while (1); *num_values = count; *values = array; ret = 0; done: if (ret && array) { for (i = 0; i < count; i++) { safefree(array[i]); } safefree(array); } return ret; } int gp_config_get_int(struct gp_ini_context *ctx, const char *secname, const char *keyname, int *value) { struct ini_cfgobj *ini_config = (struct ini_cfgobj *)ctx->private_data; struct value_obj *vo = NULL; int ret; int val; if (!value) { return EINVAL; } *value = -1; ret = ini_get_config_valueobj(secname, keyname, ini_config, INI_GET_FIRST_VALUE, &vo); if (ret) { return ret; } if (!vo) { return ENOENT; } val = ini_get_int_config_value(vo, 0, /* strict */ 0, /* default */ &ret); if (ret) { return ret; } *value = val; return 0; } int gp_config_get_nsec(struct gp_ini_context *ctx) { struct ini_cfgobj *ini_config = (struct ini_cfgobj *)ctx->private_data; char **list = NULL; int count; int error; list = ini_get_section_list(ini_config, &count, &error); if (error) { return 0; } ini_free_section_list(list); return count; } char *gp_config_get_secname(struct gp_ini_context *ctx, int i) { struct ini_cfgobj *ini_config = (struct ini_cfgobj *)ctx->private_data; char **list = NULL; int count; int error; char *secname; list = ini_get_section_list(ini_config, &count, &error); if (error) { return NULL; } if (i >= count) { return NULL; } secname = strdup(list[i]); ini_free_section_list(list); if (!secname) { return NULL; } return secname; } int gp_config_close(struct gp_ini_context *ctx) { struct ini_cfgobj *ini_config = NULL; if (!ctx) { return 0; } ini_config = (struct ini_cfgobj *)ctx->private_data; ini_config_destroy(ini_config); return 0; }