diff --git a/Makefile b/Makefile index 0c9df3fb62..022731d8be 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,8 @@ SMALL_OPTS = #### Debug settings # You can enable debugging on specific code parts by setting DEBUG=-DDEBUG_xxx. # Currently defined DEBUG macros include DEBUG_FULL, DEBUG_MEMORY, DEBUG_FSM, -# and DEBUG_HASH. Please check sources for exact meaning or do not use at all. +# DEBUG_HASH and DEBUG_AUTH. Please check sources for exact meaning or do not +# use at all. DEBUG = #### Additional include and library dirs @@ -170,6 +171,7 @@ ifeq ($(TARGET),linux22) USE_GETSOCKNAME = implicit USE_POLL = implicit USE_TPROXY = implicit + USE_LIBCRYPT = implicit else ifeq ($(TARGET),linux24) # This is for standard Linux 2.4 with netfilter but without epoll() @@ -177,6 +179,7 @@ ifeq ($(TARGET),linux24) USE_NETFILTER = implicit USE_POLL = implicit USE_TPROXY = implicit + USE_LIBCRYPT = implicit else ifeq ($(TARGET),linux24e) # This is for enhanced Linux 2.4 with netfilter and epoll() patch > 0.21 @@ -187,6 +190,7 @@ ifeq ($(TARGET),linux24e) USE_SEPOLL = implicit USE_MY_EPOLL = implicit USE_TPROXY = implicit + USE_LIBCRYPT = implicit else ifeq ($(TARGET),linux26) # This is for standard Linux 2.6 with netfilter and standard epoll() @@ -196,6 +200,7 @@ ifeq ($(TARGET),linux26) USE_EPOLL = implicit USE_SEPOLL = implicit USE_TPROXY = implicit + USE_LIBCRYPT = implicit else ifeq ($(TARGET),solaris) # This is for Solaris 8 @@ -209,6 +214,7 @@ ifeq ($(TARGET),freebsd) USE_POLL = implicit USE_KQUEUE = implicit USE_TPROXY = implicit + USE_LIBCRYPT = implicit else ifeq ($(TARGET),openbsd) # This is for OpenBSD >= 3.0 @@ -324,6 +330,12 @@ OPTIONS_CFLAGS += -DCONFIG_HAP_LINUX_TPROXY BUILD_OPTIONS += $(call ignore_implicit,USE_LINUX_TPROXY) endif +ifneq ($(USE_LIBCRYPT),) +OPTIONS_CFLAGS += -DCONFIG_HAP_CRYPT +BUILD_OPTIONS += $(call ignore_implicit,USE_LIBCRYPT) +OPTIONS_LDFLAGS += -lcrypt +endif + ifneq ($(USE_POLL),) OPTIONS_CFLAGS += -DENABLE_POLL OPTIONS_OBJS += src/ev_poll.o @@ -464,7 +476,7 @@ OBJS = src/haproxy.o src/sessionhash.o src/base64.o src/protocols.o \ src/lb_chash.o src/lb_fwlc.o src/lb_fwrr.o src/lb_map.o \ src/stream_interface.o src/dumpstats.o src/proto_tcp.o \ src/session.o src/hdr_idx.o src/ev_select.o src/signal.o \ - src/acl.o src/pattern.o src/memory.o src/freq_ctr.o + src/acl.o src/pattern.o src/memory.o src/freq_ctr.o src/auth.o EBTREE_OBJS = $(EBTREE_DIR)/ebtree.o \ $(EBTREE_DIR)/eb32tree.o $(EBTREE_DIR)/eb64tree.o \ diff --git a/include/common/cfgparse.h b/include/common/cfgparse.h index 3b376a09f3..a67f0d1cf0 100644 --- a/include/common/cfgparse.h +++ b/include/common/cfgparse.h @@ -32,6 +32,7 @@ #define CFG_NONE 0 #define CFG_GLOBAL 1 #define CFG_LISTEN 2 +#define CFG_USERLIST 3 struct cfg_keyword { int section; /* section type for this keyword */ diff --git a/include/common/uri_auth.h b/include/common/uri_auth.h index 64f818b89e..b4c297c775 100644 --- a/include/common/uri_auth.h +++ b/include/common/uri_auth.h @@ -15,6 +15,8 @@ #include +#include + /* here we find a very basic list of base64-encoded 'user:passwd' strings */ struct user_auth { struct user_auth *next; /* next entry, NULL if none */ @@ -46,6 +48,7 @@ struct uri_auth { int flags; /* some flags describing the statistics page */ struct user_auth *users; /* linked list of valid user:passwd couples */ struct stat_scope *scope; /* linked list of authorized proxies */ + struct list req_acl; /* */ struct uri_auth *next; /* Used at deinit() to build a list of unique elements */ }; diff --git a/include/proto/auth.h b/include/proto/auth.h new file mode 100644 index 0000000000..980862168a --- /dev/null +++ b/include/proto/auth.h @@ -0,0 +1,36 @@ +/* + * User authentication & authorization. + * + * Copyright 2010 Krzysztof Piotr Oledzki + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version + * 2 of the License, or (at your option) any later version. + * + */ + +#ifndef _PROTO_AUTH_H +#define _PROTO_AUTH_H + +#include +#include + +extern struct userlist *userlist; + +struct userlist *auth_find_userlist(char *name); +unsigned int auth_resolve_groups(struct userlist *l, char *groups); +struct req_acl_rule *parse_auth_cond(const char **args, const char *file, int linenum, struct list *known_acl, int *acl_requires); +void userlist_free(struct userlist *ul); +void req_acl_free(struct list *r); +int acl_match_auth(struct acl_test *test, struct acl_pattern *pattern); + +#endif /* _PROTO_AUTH_H */ + +/* + * Local variables: + * c-indent-level: 8 + * c-basic-offset: 8 + * End: + */ + diff --git a/include/types/auth.h b/include/types/auth.h new file mode 100644 index 0000000000..d278de6b89 --- /dev/null +++ b/include/types/auth.h @@ -0,0 +1,73 @@ +/* + * User authentication & authorization. + * + * Copyright 2010 Krzysztof Piotr Oledzki + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version + * 2 of the License, or (at your option) any later version. + * + */ + +#ifndef _TYPES_AUTH_H +#define _TYPES_AUTH_H + +#include +#include + +#include + +#define MAX_AUTH_GROUPS (unsigned int)(sizeof(int)*8) + +#define AU_O_INSECURE 0x00000001 /* insecure, unencrypted password */ + +enum { + PR_REQ_ACL_ACT_UNKNOWN = 0, + PR_REQ_ACL_ACT_ALLOW, + PR_REQ_ACL_ACT_DENY, + PR_REQ_ACL_ACT_HTTP_AUTH, + + PR_REQ_ACL_ACT_MAX +}; + + +struct req_acl_rule { + struct list list; + struct acl_cond *cond; /* acl condition to meet */ + unsigned int action; + union { + struct { + char *realm; + } http_auth; + }; +}; + +struct auth_users { + struct auth_users *next; + unsigned int flags; + char *user, *pass; + union { + char *groups; + unsigned int group_mask; + }; +}; + +struct userlist { + struct userlist *next; + char *name; + struct auth_users *users; + int grpcnt; + char *groups[MAX_AUTH_GROUPS]; + char **groupusers; +}; + +#endif /* _TYPES_AUTH_H */ + +/* + * Local variables: + * c-indent-level: 8 + * c-basic-offset: 8 + * End: + */ + diff --git a/src/auth.c b/src/auth.c new file mode 100644 index 0000000000..58fc5bea8a --- /dev/null +++ b/src/auth.c @@ -0,0 +1,241 @@ +/* + * User authentication & authorization + * + * Copyright 2010 Krzysztof Piotr Oledzki + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version + * 2 of the License, or (at your option) any later version. + * + */ + +#define _XOPEN_SOURCE 500 + +#include +#include +#include +#include + +#include + +#include +#include + +#include + +struct userlist *userlist = NULL; /* list of all existing userlists */ + +/* find targets for selected gropus. The function returns pointer to + * the userlist struct ot NULL if name is NULL/empty or unresolvable. + */ + +struct userlist * +auth_find_userlist(char *name) +{ + struct userlist *l; + + if (!name || !*name) + return NULL; + + for (l = userlist; l; l = l->next) + if (!strcmp(l->name, name)) + return l; + + return NULL; +} + +/* find group_mask for selected gropus. The function returns 1 if OK or nothing to do, + * 0 if case of unresolved groupname. + * WARING: the function destroys the list (strtok), so it can only be used once. + */ + +unsigned int +auth_resolve_groups(struct userlist *l, char *groups) +{ + + char *group = NULL; + unsigned int g, group_mask = 0; + + if (!groups || !*groups) + return 0; + + while ((group = strtok(group?NULL:groups," "))) { + for (g = 0; g < l->grpcnt; g++) + if (!strcmp(l->groups[g], group)) + break; + + if (g == l->grpcnt) { + Alert("No such group '%s' in userlist '%s'.\n", + group, l->name); + return 0; + } + + group_mask |= (1 << g); + } + + return group_mask; +} + +struct req_acl_rule * +parse_auth_cond(const char **args, const char *file, int linenum, struct list *known_acl, int *acl_requires) +{ + struct req_acl_rule *req_acl; + int cur_arg; + + req_acl = (struct req_acl_rule*)calloc(1, sizeof(struct req_acl_rule)); + if (!req_acl) { + Alert("parsing [%s:%d]: out of memory.\n", file, linenum); + return NULL; + } + + if (!*args[0]) { + goto req_error_parsing; + } else if (!strcmp(args[0], "allow")) { + req_acl->action = PR_REQ_ACL_ACT_ALLOW; + cur_arg = 1; + } else if (!strcmp(args[0], "deny")) { + req_acl->action = PR_REQ_ACL_ACT_DENY; + cur_arg = 1; + } else if (!strcmp(args[0], "auth")) { + req_acl->action = PR_REQ_ACL_ACT_HTTP_AUTH; + cur_arg = 1; + + while(*args[cur_arg]) { + if (!strcmp(args[cur_arg], "realm")) { + req_acl->http_auth.realm = strdup(args[cur_arg + 1]); + cur_arg+=2; + continue; + } else + break; + } + } else { +req_error_parsing: + Alert("parsing [%s:%d]: %s '%s', expects 'allow', 'deny', 'auth'.\n", + file, linenum, *args[1]?"unknown parameter":"missing keyword in", args[*args[1]?1:0]); + return NULL; + } + + if (*args[cur_arg]) { + int pol = ACL_COND_NONE; + struct acl_cond *cond; + + if (!strcmp(args[cur_arg], "if")) + pol = ACL_COND_IF; + else if (!strcmp(args[cur_arg], "unless")) + pol = ACL_COND_UNLESS; + else { + Alert("parsing [%s:%d]: '%s' expects 'realm' for 'auth' or" + " either 'if' or 'unless' followed by a condition but found '%s'.\n", + file, linenum, args[0], args[cur_arg]); + return NULL; + } + + if ((cond = parse_acl_cond((const char **)args + cur_arg + 1, known_acl, pol)) == NULL) { + Alert("parsing [%s:%d]: error detected while parsing 'req' condition.\n", + file, linenum); + return NULL; + } + + cond->file = file; + cond->line = linenum; + *acl_requires |= cond->requires; + req_acl->cond = cond; + } + + return req_acl; +} + +void +userlist_free(struct userlist *ul) +{ + struct userlist *tul; + struct auth_users *au, *tau; + int i; + + while (ul) { + au = ul->users; + while (au) { + tau = au; + au = au->next; + free(tau->user); + free(tau->pass); + free(tau); + } + + tul = ul; + ul = ul->next; + + for (i = 0; i < tul->grpcnt; i++) + free(tul->groups[i]); + + free(tul->name); + free(tul); + }; +} + +void +req_acl_free(struct list *r) { + struct req_acl_rule *tr, *pr; + + list_for_each_entry_safe(pr, tr, r, list) { + LIST_DEL(&pr->list); + if (pr->action == PR_REQ_ACL_ACT_HTTP_AUTH) + free(pr->http_auth.realm); + + free(pr); + } +} + +/* + * Authenticate and authorize user; return 1 if OK, 0 if case of error. + */ +int +check_user(struct userlist *ul, unsigned int group_mask, const char *user, const char *pass) +{ + + struct auth_users *u; + const char *ep; + +#ifdef DEBUG_AUTH + fprintf(stderr, "req: userlist=%s, user=%s, pass=%s, group_mask=%u\n", + ul->name, user, pass, group_mask); +#endif + + for (u = ul->users; u; u = u->next) + if (!strcmp(user, u->user)) + break; + + if (!u) + return 0; + +#ifdef DEBUG_AUTH + fprintf(stderr, "cfg: user=%s, pass=%s, group_mask=%u, flags=%X", + u->user, u->pass, u->group_mask, u->flags); +#endif + + /* + * if user matches but group does not, + * it makes no sens to check passwords + */ + if (group_mask && !(group_mask & u->group_mask)) + return 0; + + if (!(u->flags & AU_O_INSECURE)) { +#ifdef CONFIG_HAP_CRYPT + ep = crypt(pass, u->pass); +#else + return 0; +#endif + } else + ep = pass; + +#ifdef DEBUG_AUTH + fprintf(stderr, ", crypt=%s\n", ep); +#endif + + if (!strcmp(ep, u->pass)) + return 1; + else + return 0; +} diff --git a/src/cfgparse.c b/src/cfgparse.c index 77e61d864d..5c181f16c4 100644 --- a/src/cfgparse.c +++ b/src/cfgparse.c @@ -37,6 +37,7 @@ #include #include +#include #include #include #include @@ -4008,6 +4009,175 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm) return err_code; } +int +cfg_parse_users(const char *file, int linenum, char **args, int kwm) +{ + + int err_code = 0; + const char *err; + + if (!strcmp(args[0], "userlist")) { /* new userlist */ + struct userlist *newul; + + if (!*args[1]) { + Alert("parsing [%s:%d]: '%s' expects as arguments.\n", + file, linenum, args[0]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + + err = invalid_char(args[1]); + if (err) { + Alert("parsing [%s:%d]: character '%c' is not permitted in '%s' name '%s'.\n", + file, linenum, *err, args[0], args[1]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + + for (newul = userlist; newul; newul = newul->next) + if (!strcmp(newul->name, args[1])) { + Warning("parsing [%s:%d]: ignoring duplicated userlist '%s'.\n", + file, linenum, args[1]); + err_code |= ERR_WARN; + goto out; + } + + newul = (struct userlist *)calloc(1, sizeof(struct userlist)); + if (!newul) { + Alert("parsing [%s:%d]: out of memory.\n", file, linenum); + err_code |= ERR_ALERT | ERR_ABORT; + goto out; + } + + newul->groupusers = calloc(MAX_AUTH_GROUPS, sizeof(char *)); + newul->name = strdup(args[1]); + + if (!newul->groupusers | !newul->name) { + Alert("parsing [%s:%d]: out of memory.\n", file, linenum); + err_code |= ERR_ALERT | ERR_ABORT; + goto out; + } + + newul->next = userlist; + userlist = newul; + + } else if (!strcmp(args[0], "group")) { /* new group */ + int cur_arg, i; + const char *err; + + if (!*args[1]) { + Alert("parsing [%s:%d]: '%s' expects as arguments.\n", + file, linenum, args[0]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + + err = invalid_char(args[1]); + if (err) { + Alert("parsing [%s:%d]: character '%c' is not permitted in '%s' name '%s'.\n", + file, linenum, *err, args[0], args[1]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + + for(i = 0; i < userlist->grpcnt; i++) + if (!strcmp(userlist->groups[i], args[1])) { + Warning("parsing [%s:%d]: ignoring duplicated group '%s' in userlist '%s'.\n", + file, linenum, args[1], userlist->name); + err_code |= ERR_ALERT; + goto out; + } + + if (userlist->grpcnt >= MAX_AUTH_GROUPS) { + Alert("parsing [%s:%d]: too many groups (%u) in in userlist '%s' while adding group '%s'.\n", + file, linenum, MAX_AUTH_GROUPS, userlist->name, args[1]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + + cur_arg = 2; + + while (*args[cur_arg]) { + if (!strcmp(args[cur_arg], "users")) { + userlist->groupusers[userlist->grpcnt] = strdup(args[cur_arg + 1]); + cur_arg += 2; + continue; + } else { + Alert("parsing [%s:%d]: '%s' only supports 'users' option.\n", + file, linenum, args[0]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + } + + userlist->groups[userlist->grpcnt++] = strdup(args[1]); + } else if (!strcmp(args[0], "user")) { /* new user */ + struct auth_users *newuser; + int cur_arg; + + if (!*args[1]) { + Alert("parsing [%s:%d]: '%s' expects as arguments.\n", + file, linenum, args[0]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + + for (newuser = userlist->users; newuser; newuser = newuser->next) + if (!strcmp(newuser->user, args[1])) { + Warning("parsing [%s:%d]: ignoring duplicated user '%s' in userlist '%s'.\n", + file, linenum, args[1], userlist->name); + err_code |= ERR_ALERT; + goto out; + } + + newuser = (struct auth_users *)calloc(1, sizeof(struct auth_users)); + if (!newuser) { + Alert("parsing [%s:%d]: out of memory.\n", file, linenum); + err_code |= ERR_ALERT | ERR_ABORT; + goto out; + } + + newuser->user = strdup(args[1]); + + newuser->next = userlist->users; + userlist->users = newuser; + + cur_arg = 2; + + while (*args[cur_arg]) { + if (!strcmp(args[cur_arg], "password")) { +#ifndef CONFIG_HAP_CRYPT + Warning("parsing [%s:%d]: no crypt(3) support compiled, encrypted passwords will not work.\n", + file, linenum); + err_code |= ERR_ALERT; +#endif + newuser->pass = strdup(args[cur_arg + 1]); + cur_arg += 2; + continue; + } else if (!strcmp(args[cur_arg], "insecure-password")) { + newuser->pass = strdup(args[cur_arg + 1]); + newuser->flags |= AU_O_INSECURE; + cur_arg += 2; + continue; + } else if (!strcmp(args[cur_arg], "groups")) { + newuser->groups = strdup(args[cur_arg + 1]); + cur_arg += 2; + continue; + } else { + Alert("parsing [%s:%d]: '%s' only supports 'password', 'insecure-password' and 'groups' options.\n", + file, linenum, args[0]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + } + } else { + Alert("parsing [%s:%d]: unknown keyword '%s' in '%s' section\n", file, linenum, args[0], "users"); + err_code |= ERR_ALERT | ERR_FATAL; + } + +out: + return err_code; +} /* * This function reads and parses the configuration file given in the argument. @@ -4172,6 +4342,10 @@ int readcfgfile(const char *file) confsect = CFG_GLOBAL; free(cursection); cursection = strdup(args[0]); + } else if (!strcmp(args[0], "userlist")) { + confsect = CFG_USERLIST; + free(cursection); + cursection = strdup(args[0]); } /* else it's a section keyword */ @@ -4182,8 +4356,11 @@ int readcfgfile(const char *file) case CFG_GLOBAL: err_code |= cfg_parse_global(file, linenum, args, kwm); break; + case CFG_USERLIST: + err_code |= cfg_parse_users(file, linenum, args, kwm); + break; default: - Alert("parsing [%s:%d] : unknown keyword '%s' out of section.\n", file, linenum, args[0]); + Alert("parsing [%s:%d]: unknown keyword '%s' out of section.\n", file, linenum, args[0]); err_code |= ERR_ALERT | ERR_FATAL; } @@ -4210,6 +4387,7 @@ int check_config_validity() int cfgerr = 0; struct proxy *curproxy = NULL; struct server *newsrv = NULL; + struct userlist *curuserlist = NULL; int err_code = 0; unsigned int next_pxid = 1; @@ -4817,6 +4995,78 @@ int check_config_validity() curproxy = curproxy->next; } + for (curuserlist = userlist; curuserlist; curuserlist = curuserlist->next) { + struct auth_users *curuser; + int g; + + for (curuser = curuserlist->users; curuser; curuser = curuser->next) { + unsigned int group_mask = 0; + char *group = NULL; + + if (!curuser->groups) + continue; + + while ((group = strtok(group?NULL:curuser->groups, ","))) { + + for (g = 0; g < curuserlist->grpcnt; g++) + if (!strcmp(curuserlist->groups[g], group)) + break; + + if (g == curuserlist->grpcnt) { + Alert("userlist '%s': no such group '%s' specified in user '%s'\n", + curuserlist->name, group, curuser->user); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + + group_mask |= (1 << g); + } + + free(curuser->groups); + curuser->group_mask = group_mask; + } + + for (g = 0; g < curuserlist->grpcnt; g++) { + char *user = NULL; + + if (!curuserlist->groupusers[g]) + continue; + + while ((user = strtok(user?NULL:curuserlist->groupusers[g], ","))) { + for (curuser = curuserlist->users; curuser; curuser = curuser->next) + if (!strcmp(curuser->user, user)) + break; + + if (!curuser) { + Alert("userlist '%s': no such user '%s' specified in group '%s'\n", + curuserlist->name, user, curuserlist->groups[g]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + + curuser->group_mask |= (1 << g); + } + + free(curuserlist->groupusers[g]); + } + + free(curuserlist->groupusers); + +#ifdef DEBUG_AUTH + for (g = 0; g < curuserlist->grpcnt; g++) { + fprintf(stderr, "group %s, id %d, mask %08X, users:", curuserlist->groups[g], g , 1 << g); + + for (curuser = curuserlist->users; curuser; curuser = curuser->next) { + if (curuser->group_mask & (1 << g)) + fprintf(stderr, " %s", curuser->user); + } + + fprintf(stderr, "\n"); + } +#endif + + } + /* * Recount currently required checks. */ diff --git a/src/haproxy.c b/src/haproxy.c index 2fdde61414..0393ebfee1 100644 --- a/src/haproxy.c +++ b/src/haproxy.c @@ -67,6 +67,7 @@ #include #include +#include #include #include #include @@ -177,6 +178,16 @@ void display_build_opts() "\n\n", DEFAULT_MAXCONN, BUFSIZE, MAXREWRITE, MAX_POLL_EVENTS); + printf("Encrypted password support via crypt(3): " +#ifdef CONFIG_HAP_CRYPT + "yes" +#else + "no" +#endif + "\n"); + + putchar('\n'); + list_pollers(stdout); putchar('\n'); } @@ -851,6 +862,7 @@ void deinit(void) pool_destroy2(p->req_cap_pool); pool_destroy2(p->rsp_cap_pool); pool_destroy2(p->hdr_idx_pool); + p0 = p; p = p->next; free(p0); @@ -874,6 +886,8 @@ void deinit(void) free(uap); } + userlist_free(userlist); + protocol_unbind_all(); free(global.chroot); global.chroot = NULL;