]> granicus.if.org Git - pgbouncer/commitdiff
Support pg_hba.conf-style syntax
authorMarko Kreen <markokr@gmail.com>
Mon, 3 Aug 2015 18:58:23 +0000 (21:58 +0300)
committerMarko Kreen <markokr@gmail.com>
Mon, 3 Aug 2015 20:20:11 +0000 (23:20 +0300)
Also add peer auth.

Main reason to have it is that unix and tcp connections may
want different auth and configuring it in plain .ini is pain.

As a bonus it provides ip-based filtering too.

No username mapping yet though.

12 files changed:
Makefile
include/bouncer.h
include/hba.h [new file with mode: 0644]
include/system.h
src/client.c
src/hba.c [new file with mode: 0644]
src/main.c
src/system.c
test/Makefile
test/hba_test.c [new file with mode: 0644]
test/hba_test.eval [new file with mode: 0644]
test/hba_test.rules [new file with mode: 0644]

index b9825b766d967842d5f91fc945f2a3be6ed88076..6058c98da99d28a17a8c85b71d4679713b9c4e56 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -7,6 +7,7 @@ pgbouncer_SOURCES = \
        src/admin.c \
        src/client.c \
        src/dnslookup.c \
+       src/hba.c \
        src/janitor.c \
        src/loader.c \
        src/main.c \
index 084fce7f4bf10c59b80a141e2a315761de1669a8..d409227f4c62b2c24b75ec278f9001aa35945ce8 100644 (file)
@@ -108,6 +108,7 @@ extern int cf_sbuf_len;
 #include "stats.h"
 #include "takeover.h"
 #include "janitor.h"
+#include "hba.h"
 
 /* to avoid allocations will use static buffers */
 #define MAX_DBNAME     64
@@ -122,6 +123,9 @@ extern int cf_sbuf_len;
 #define AUTH_MD5       5
 #define AUTH_CREDS     6
 #define AUTH_CERT      7
+#define AUTH_PEER      8
+#define AUTH_HBA       9
+#define AUTH_REJECT    10
 
 /* type codes for weird pkts */
 #define PKT_STARTUP_V2  0x20000
@@ -415,6 +419,7 @@ extern usec_t cf_dns_zone_check_period;
 extern int cf_auth_type;
 extern char *cf_auth_file;
 extern char *cf_auth_query;
+extern char *cf_auth_hba_file;
 
 extern char *cf_pidfile;
 
@@ -464,6 +469,7 @@ extern const struct CfLookup pool_mode_map[];
 extern usec_t g_suspend_start;
 
 extern struct DNSContext *adns;
+extern struct HBA *parsed_hba;
 
 static inline PgSocket * _MUSTCHECK
 pop_socket(struct StatList *slist)
diff --git a/include/hba.h b/include/hba.h
new file mode 100644 (file)
index 0000000..8e6f1eb
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * Host-Based-Access-control file support.
+ *
+ * Copyright (c) 2015 Marko Kreen
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+struct HBA;
+
+struct HBA *hba_load_rules(const char *fn);
+void hba_free(struct HBA *hba);
+int hba_eval(struct HBA *hba, PgAddr *addr, bool is_tls, const char *dbname, const char *username);
+
index cb531ae6ca1fcaf656df96d04fc29dcdb6669556..ab8e0aa57d03eab35e7394eb803771f9c52dc346 100644 (file)
@@ -67,6 +67,8 @@ static inline char *crypt(const char *p, const char *s) { return NULL; }
 static inline int lstat(const char *path, struct stat *st) { return stat(path, st); }
 #endif
 
+bool check_unix_peer_name(int fd, const char *username);
+
 void change_user(const char *user);
 
 void change_file_mode(const char *fn, mode_t mode, const char *user, const char *group);
index 70205b80bcfdfde8a4bed5c111f6f719a74c4537..1bc549a559a7e6cad2ebba623dd75670c043ebe6 100644 (file)
@@ -171,10 +171,23 @@ fail:
        return false;
 }
 
+static bool login_as_unix_peer(PgSocket *client)
+{
+       if (!pga_is_unix(&client->remote_addr))
+               goto fail;
+       if (!check_unix_peer_name(sbuf_socket(&client->sbuf), client->auth_user->name))
+               goto fail;
+       return finish_client_login(client);
+fail:
+       disconnect_client(client, true, "unix socket login rejected");
+       return false;
+}
+
 static bool finish_set_pool(PgSocket *client, bool takeover)
 {
        PgUser *user = client->auth_user;
        bool ok = false;
+       int auth;
 
        /* pool user may be forced */
        if (client->db->forced_user) {
@@ -212,7 +225,13 @@ static bool finish_set_pool(PgSocket *client, bool takeover)
        if (client->own_user)
                return finish_client_login(client);
 
-       switch (cf_auth_type) {
+       auth = cf_auth_type;
+       if (auth == AUTH_HBA) {
+               auth = hba_eval(parsed_hba, &client->remote_addr, !!client->sbuf.tls,
+                               client->db->name, client->auth_user->name);
+       }
+
+       switch (auth) {
        case AUTH_ANY:
        case AUTH_TRUST:
                ok = finish_client_login(client);
@@ -225,6 +244,9 @@ static bool finish_set_pool(PgSocket *client, bool takeover)
        case AUTH_CERT:
                ok = login_via_cert(client);
                break;
+       case AUTH_PEER:
+               ok = login_as_unix_peer(client);
+               break;
        default:
                disconnect_client(client, true, "login rejected");
                ok = false;
diff --git a/src/hba.c b/src/hba.c
new file mode 100644 (file)
index 0000000..7402bc6
--- /dev/null
+++ b/src/hba.c
@@ -0,0 +1,735 @@
+/*
+ * Host-Based-Access-control file support.
+ *
+ * Copyright (c) 2015 Marko Kreen
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "bouncer.h"
+
+#include <usual/cxextra.h>
+#include <usual/cbtree.h>
+
+enum RuleType {
+       RULE_LOCAL,
+       RULE_HOST,
+       RULE_HOSTSSL,
+       RULE_HOSTNOSSL,
+};
+
+#define NAME_ALL       1
+#define NAME_SAMEUSER  2
+
+struct NameSlot {
+       size_t strlen;
+       char str[];
+};
+
+struct HBAName {
+       unsigned int flags;
+       struct StrSet *name_set;
+};
+
+struct HBARule {
+       struct List node;
+       enum RuleType rule_type;
+       int rule_method;
+       int rule_af;
+       uint8_t rule_addr[16];
+       uint8_t rule_mask[16];
+       struct HBAName db_name;
+       struct HBAName user_name;
+};
+
+struct HBA {
+       struct List rules;
+};
+
+/*
+ * StrSet
+ */
+
+struct StrSetNode {
+       unsigned int s_len;
+       char s_val[FLEX_ARRAY];
+};
+
+struct StrSet {
+       CxMem *pool;
+       unsigned count;
+       unsigned alloc;
+       struct StrSetNode **nodes;
+       struct CBTree *cbtree;
+};
+
+struct StrSet *strset_new(CxMem *cx);
+void strset_free(struct StrSet *set);
+bool strset_add(struct StrSet *set, const char *str, unsigned int len);
+bool strset_contains(struct StrSet *set, const char *str, unsigned int len);
+
+struct StrSet *strset_new(CxMem *cx)
+{
+       struct StrSet *set;
+       CxMem *pool;
+
+       pool = cx_new_pool(cx, 1024, 0);
+       if (!pool)
+               return NULL;
+       set = cx_alloc(pool, sizeof *set);
+       if (!set)
+               return NULL;
+       set->pool = pool;
+       set->cbtree = NULL;
+       set->count = 0;
+       set->alloc = 10;
+       set->nodes = cx_alloc0(pool, set->alloc * sizeof(struct StrSet *));
+       if (!set->nodes) {
+               cx_destroy(pool);
+               return NULL;
+       }
+       return set;
+}
+
+static size_t strset_node_key(void *ctx, void *obj, const void **ptr_p)
+{
+       struct StrSetNode *node = obj;
+       *ptr_p = node->s_val;
+       return node->s_len;
+}
+
+bool strset_add(struct StrSet *set, const char *str, unsigned int len)
+{
+       struct StrSetNode *node;
+       unsigned int i;
+       bool ok;
+
+       if (strset_contains(set, str, len))
+               return true;
+
+       node = cx_alloc(set->pool, offsetof(struct StrSetNode, s_val) + len + 1);
+       if (!node)
+               return false;
+       node->s_len = len;
+       memcpy(node->s_val, str, len);
+       node->s_val[len] = 0;
+
+       if (set->count < set->alloc) {
+               set->nodes[set->count++] = node;
+               return true;
+       }
+       
+       if (!set->cbtree) {
+               set->cbtree = cbtree_create(strset_node_key, NULL, set, set->pool);
+               if (!set->cbtree)
+                       return false;
+               for (i = 0; i < set->count; i++) {
+                       ok = cbtree_insert(set->cbtree, set->nodes[i]);
+                       if (!ok)
+                               return false;
+               }
+       }
+       ok = cbtree_insert(set->cbtree, node);
+       if (!ok)
+               return false;
+       set->count++;
+       return true;
+}
+
+bool strset_contains(struct StrSet *set, const char *str, unsigned int len)
+{
+       unsigned int i;
+       struct StrSetNode *node;
+       if (set->cbtree)
+               return cbtree_lookup(set->cbtree, str, len) != NULL;
+       for (i = 0; i < set->count; i++) {
+               node = set->nodes[i];
+               if (node->s_len != len)
+                       continue;
+               if (memcmp(node->s_val, str, len) == 0)
+                       return true;
+       }
+       return false;
+}
+
+void strset_free(struct StrSet *set)
+{
+       if (set)
+               cx_destroy(set->pool);
+}
+
+/*
+ * Parse HBA tokens.
+ */
+
+enum TokType {
+       TOK_STRING,
+       TOK_IDENT,
+       TOK_COMMA,
+       TOK_FAIL,
+       TOK_EOL
+};
+
+struct TokParser {
+       const char *pos;
+       enum TokType cur_tok;
+       char *cur_tok_str;
+
+       char *buf;
+       size_t buflen;
+};
+
+static bool tok_buf_check(struct TokParser *p, size_t len)
+{
+       size_t tmplen;
+       char *tmp;
+       if (p->buflen >= len)
+               return true;
+       tmplen = len*2;
+       tmp = realloc(p->buf, tmplen);
+       if (!tmp)
+               return false;
+       p->buf = tmp;
+       p->buflen = tmplen;
+       return true;
+}
+
+static enum TokType next_token(struct TokParser *p)
+{
+       const char *s, *s2;
+       char *dst;
+       if (p->cur_tok == TOK_EOL)
+               return TOK_EOL;
+       p->cur_tok_str = NULL;
+       p->cur_tok = TOK_FAIL;
+
+       while (p->pos[0] && isspace((unsigned char)p->pos[0]))
+               p->pos++;
+
+       if (p->pos[0] == '#' || p->pos[0] == '\0') {
+               p->cur_tok = TOK_EOL;
+               p->pos = NULL;
+       } else if (p->pos[0] == ',') {
+               p->cur_tok = TOK_COMMA;
+               p->pos++;
+       } else if (p->pos[0] == '"') {
+               for (s = p->pos+1; s[0]; s++) {
+                       if (s[0] == '"') {
+                               if (s[1] == '"')
+                                       s++;
+                               else
+                                       break;
+                       }
+               }
+               if (s[0] != '"' || !tok_buf_check(p, s - p->pos))
+                       return TOK_FAIL;
+               dst = p->buf;
+               for (s2 = p->pos+1; s2 < s; s2++) {
+                       *dst++ = *s2;
+                       if (*s2 == '"') s2++;
+               }
+               *dst = 0;
+               p->pos = s + 1;
+               p->cur_tok = TOK_STRING;
+               p->cur_tok_str = p->buf;
+       } else {
+               for (s = p->pos + 1; *s; s++) {
+                       if (*s == ',' || *s == '#' || *s == '"')
+                               break;
+                       if (isspace((unsigned char)*s))
+                               break;
+               }
+               if (!tok_buf_check(p, s - p->pos))
+                       return TOK_FAIL;
+               memcpy(p->buf, p->pos, s - p->pos);
+               p->buf[s - p->pos] = 0;
+               p->pos = s;
+               p->cur_tok = TOK_IDENT;
+               p->cur_tok_str = p->buf;
+
+       }
+       return p->cur_tok;
+}
+
+static bool eat(struct TokParser *p, enum TokType ttype)
+{
+       if (p->cur_tok == ttype) {
+               next_token(p);
+               return true;
+       }
+       return false;
+}
+
+static bool eat_kw(struct TokParser *p, const char *kw)
+{
+       if (p->cur_tok == TOK_IDENT && strcmp(kw, p->cur_tok_str) == 0) {
+               next_token(p);
+               return true;
+       }
+       return false;
+}
+
+static bool expect(struct TokParser *tp, enum TokType ttype, const char **str_p)
+{
+       if (tp->cur_tok == ttype) {
+               *str_p = tp->buf;
+               return true;
+       }
+       return false;
+}
+
+static char *path_join(const char *p1, const char *p2)
+{
+       size_t len1, len2;
+       char *res = NULL, *pos;
+
+       if (p2[0] == '/' || p1[0] == 0 || !memcmp(p1, ".", 2))
+               return strdup(p2);
+       len1 = strlen(p1);
+       len2 = strlen(p2);
+       res = malloc(len1 + len2 + 2 + 1);
+       if (res) {
+               memcpy(res, p1, len1);
+               pos = res + len1;
+               if (pos[-1] != '/')
+                       *pos++ = '/';
+               memcpy(pos, p2, len2 + 1);
+       }
+       return res;
+}
+
+static char *path_join_dirname(const char *parent, const char *fn)
+{
+       char *tmp, *res;
+       const char *basedir;
+       if (fn[0] == '/')
+               return strdup(fn);
+       tmp = strdup(parent);
+       if (!tmp)
+               return NULL;
+       basedir = dirname(tmp);
+       res = path_join(basedir, fn);
+       free(tmp);
+       return res;
+}
+
+static void init_parser(struct TokParser *p)
+{
+       memset(p, 0, sizeof(*p));
+}
+
+static void parse_from_string(struct TokParser *p, const char *str)
+{
+       p->pos = str;
+       p->cur_tok = TOK_COMMA;
+       p->cur_tok_str = NULL;
+       next_token(p);
+}
+
+static void free_parser(struct TokParser *p)
+{
+       free(p->buf);
+       p->buf = NULL;
+}
+
+static bool parse_names(struct HBAName *hname, struct TokParser *p, bool is_db, const char *parent_filename);
+
+static bool parse_namefile(struct HBAName *hname, const char *fn, bool is_db)
+{
+       FILE *f;
+       ssize_t len;
+       char *ln = NULL;
+       size_t buflen = 0;
+       int linenr;
+       bool ok = false;
+       struct TokParser tp;
+
+       init_parser(&tp);
+
+       f = fopen(fn, "r");
+       if (!f) {
+               free(fn);
+               return false;
+       }
+       for (linenr = 1; ; linenr++) {
+               len = getline(&ln, &buflen, f);
+               if (len < 0) {
+                       ok = true;
+                       break;
+               }
+               parse_from_string(&tp, ln);
+               if (!parse_names(hname, &tp, is_db, fn))
+                       break;
+       }
+       free_parser(&tp);
+       free(fn);
+       free(ln);
+       fclose(f);
+       return ok;
+}
+
+static bool parse_names(struct HBAName *hname, struct TokParser *tp, bool is_db, const char *parent_filename)
+{
+       const char *tok;
+       while (1) {
+               if (eat_kw(tp, "all")) {
+                       hname->flags |= NAME_ALL;
+                       goto eat_comma;
+               }
+               if (is_db) {
+                       if (eat_kw(tp, "sameuser")) {
+                               hname->flags |= NAME_SAMEUSER;
+                               goto eat_comma;
+                       }
+                       if (eat_kw(tp, "samerole")) {
+                               return false;
+                       }
+                       if (eat_kw(tp, "samegroup")) {
+                               return false;
+                       }
+                       if (eat_kw(tp, "replication")) {
+                               return false;
+                       }
+               }
+
+               if (expect(tp, TOK_IDENT, &tok)) {
+                       if (tok[0] == '+') {
+                               return false;
+                       }
+
+                       if (tok[0] == '@') {
+                               bool ok;
+                               const char *fn;
+                               fn = path_join_dirname(parent_filename, tok + 1);
+                               if (!fn)
+                                       return false;
+                               ok = parse_namefile(hname, fn, is_db);
+                               free(fn);
+                               if (!ok)
+                                       return false;
+                               goto eat_comma;
+                       }
+                       /* fallthrough */
+               } else if (expect(tp, TOK_STRING, &tok)) {
+                       /* fallthrough */
+               } else {
+                       return false;
+               }
+
+               /*
+                * TOK_IDENT or TOK_STRING as plain name.
+                */
+
+               if (!hname->name_set) {
+                       hname->name_set = strset_new(NULL);
+                       if (!hname->name_set)
+                               return false;
+               }
+               if (!strset_add(hname->name_set, tok, strlen(tok)))
+                       return false;
+               next_token(tp);
+eat_comma:
+               if (!eat(tp, TOK_COMMA))
+                       break;
+       }
+       return true;
+}
+
+static void rule_free(struct HBARule *rule)
+{
+       free(rule);
+}
+
+static bool parse_addr(struct HBARule *rule, const char *addr)
+{
+       if (inet_pton(AF_INET6, addr, rule->rule_addr)) {
+               rule->rule_af = AF_INET6;
+       } else if (inet_pton(AF_INET, addr, rule->rule_addr)) {
+               rule->rule_af = AF_INET;
+       } else {
+               return false;
+       }
+       return true;
+}
+
+static bool parse_nmask(struct HBARule *rule, const char *nmask)
+{
+       char *end = NULL;
+       unsigned long bits;
+       unsigned int i;
+       errno = 0;
+       bits = strtoul(nmask, &end, 10);
+       if (errno || *end) {
+               return false;
+       }
+       if (rule->rule_af == AF_INET && bits > 32) {
+               return false;
+       }
+       if (rule->rule_af == AF_INET6 && bits > 128) {
+               return false;
+       }
+       for (i = 0; i < bits/8; i++)
+               rule->rule_mask[i] = 255;
+       if (bits % 8)
+               rule->rule_mask[i] = 255 << (8 - (bits % 8));
+       return true;
+}
+
+static bool bad_mask(struct HBARule *rule)
+{
+       int i, bytes = rule->rule_af == AF_INET ? 4 : 16;
+       uint8_t res = 0;
+       for (i = 0; i < bytes; i++)
+               res |= rule->rule_addr[i] & (255 ^ rule->rule_mask[i]);
+       return !!res;
+}
+
+static bool parse_line(struct HBA *hba, struct TokParser *tp, int linenr, const char *parent_filename)
+{
+       const char *addr = NULL, *mask = NULL;
+       enum RuleType rtype;
+       char *nmask = NULL;
+       struct HBARule *rule = NULL;
+
+       if (eat_kw(tp, "local")) {
+               rtype = RULE_LOCAL;
+       } else if (eat_kw(tp, "host")) {
+               rtype = RULE_HOST;
+       } else if (eat_kw(tp, "hostssl")) {
+               rtype = RULE_HOSTSSL;
+       } else if (eat_kw(tp, "hostnossl")) {
+               rtype = RULE_HOSTNOSSL;
+       } else if (eat(tp, TOK_EOL)) {
+               return true;
+       } else {
+               log_warning("hba line %d: unknown type", linenr);
+               return false;
+       }
+
+       rule = calloc(sizeof *rule, 1);
+       if (!rule) {
+               log_warning("hba: no mem for rule");
+               goto failed;
+       }
+       rule->rule_type = rtype;
+
+       if (!parse_names(&rule->db_name, tp, true, parent_filename))
+               goto failed;
+       if (!parse_names(&rule->user_name, tp, true, parent_filename))
+               goto failed;
+
+       if (rtype == RULE_LOCAL) {
+               rule->rule_af = AF_UNIX;
+       } else {
+               if (!expect(tp, TOK_IDENT, &addr)) {
+                       log_warning("hba line %d: did not find address - %d - '%s'", linenr, tp->cur_tok, tp->buf);
+                       goto failed;
+               }
+               nmask = strchr(addr, '/');
+               if (nmask) {
+                       *nmask++ = 0;
+               }
+
+               if (!parse_addr(rule, addr)) {
+                       log_warning("hba line %d: failed to parse address - %s", linenr, addr);
+                       goto failed;
+               }
+               
+               if (nmask) {
+                       if (!parse_nmask(rule, nmask)) {
+                               log_warning("hba line %d: invalid mask", linenr);
+                               goto failed;
+                       }
+                       next_token(tp);
+               } else {
+                       next_token(tp);
+                       if (!expect(tp, TOK_IDENT, &mask)) {
+                               log_warning("hba line %d: did not find mask", linenr);
+                               goto failed;
+                       }
+                       if (!inet_pton(rule->rule_af, mask, rule->rule_mask)) {
+                               log_warning("hba line %d: failed to parse mask: %s", linenr, mask);
+                               goto failed;
+                       }
+                       next_token(tp);
+               }
+               if (bad_mask(rule)) {
+                       char buf1[128], buf2[128];
+                       log_warning("Addres does not match mask in %s line #%d: %s / %s", parent_filename, linenr,
+                                   inet_ntop(rule->rule_af, rule->rule_addr, buf1, sizeof buf1),
+                                   inet_ntop(rule->rule_af, rule->rule_mask, buf2, sizeof buf2));
+               }
+       }
+
+       if (eat_kw(tp, "trust")) {
+               rule->rule_method = AUTH_TRUST;
+       } else if (eat_kw(tp, "reject")) {
+               rule->rule_method = AUTH_REJECT;
+       } else if (eat_kw(tp, "md5")) {
+               rule->rule_method = AUTH_MD5;
+       } else if (eat_kw(tp, "password")) {
+               rule->rule_method = AUTH_PLAIN;
+       } else if (eat_kw(tp, "peer")) {
+               rule->rule_method = AUTH_PEER;
+       } else if (eat_kw(tp, "cert")) {
+               rule->rule_method = AUTH_CERT;
+       } else {
+               log_warning("hba line %d: unsupported method: buf=%s", linenr, tp->buf);
+               goto failed;
+       }
+
+       if (!eat(tp, TOK_EOL)) {
+               log_warning("hba line %d: unsupported parameters", linenr);
+               goto failed;
+       }
+
+       list_append(&hba->rules, &rule->node);
+       return true;
+failed:
+       rule_free(rule);
+       return false;
+}
+
+struct HBA *hba_load_rules(const char *fn)
+{
+       struct HBA *hba = NULL;
+       FILE *f = NULL;
+       char *ln = NULL;
+       size_t lnbuf = 0;
+       ssize_t len;
+       int linenr;
+       struct TokParser tp;
+
+       init_parser(&tp);
+
+       hba = malloc(sizeof *hba);
+       if (!hba)
+               goto out;
+
+       f = fopen(fn, "r");
+       if (!f)
+               goto out;
+
+       list_init(&hba->rules);
+       for (linenr = 1; ; linenr++) {
+               len = getline(&ln, &lnbuf, f);
+               if (len < 0)
+                       break;
+               parse_from_string(&tp, ln);
+               if (!parse_line(hba, &tp, linenr, fn)) {
+                       free(hba);
+                       hba = NULL;
+                       break;
+               }
+       }
+out:
+       free_parser(&tp);
+       free(ln);
+       if (f)
+               fclose(f);
+       return hba;
+}
+
+void hba_free(struct HBA *hba)
+{
+       struct List *el, *tmp;
+       struct HBARule *rule;
+       if (!hba)
+               return;
+       list_for_each_safe(el, &hba->rules, tmp) {
+               rule = container_of(el, struct HBARule, node);
+               list_del(&rule->node);
+               rule_free(rule);
+       }
+       free(hba);
+}
+
+static bool name_match(struct HBAName *hname, const char *name, unsigned int namelen, const char *pair)
+{
+       if (hname->flags & NAME_ALL)
+               return true;
+       if ((hname->flags & NAME_SAMEUSER) && strcmp(name, pair) == 0)
+               return true;
+       if (hname->name_set)
+               return strset_contains(hname->name_set, name, namelen);
+       return false;
+}
+
+static bool match_inet4(const struct HBARule *rule, PgAddr *addr)
+{
+       const uint32_t *src, *base, *mask;
+       if (pga_family(addr) != AF_INET)
+               return false;
+       src = &addr->sin.sin_addr.s_addr;
+       base = (uint32_t *)rule->rule_addr;
+       mask = (uint32_t *)rule->rule_mask;
+       return (src[0] & mask[0]) == base[0];
+}
+
+static bool match_inet6(const struct HBARule *rule, PgAddr *addr)
+{
+       const uint32_t *src, *base, *mask;
+       if (pga_family(addr) != AF_INET6)
+               return false;
+       src = (uint32_t *)addr->sin6.sin6_addr.s6_addr;
+       base = (uint32_t *)rule->rule_addr;
+       mask = (uint32_t *)rule->rule_mask;
+       return (src[0] & mask[0]) == base[0] && (src[1] & mask[1]) == base[1] &&
+               (src[2] & mask[2]) == base[2] && (src[3] & mask[3]) == base[3];
+}
+
+int hba_eval(struct HBA *hba, PgAddr *addr, bool is_tls, const char *dbname, const char *username)
+{
+       struct List *el;
+       struct HBARule *rule;
+       unsigned int dbnamelen = strlen(dbname);
+       unsigned int unamelen = strlen(username);
+
+       if (!hba)
+               return AUTH_REJECT;
+
+       list_for_each(el, &hba->rules) {
+               rule = container_of(el, struct HBARule, node);
+
+               /* match address */
+               if (pga_is_unix(addr)) {
+                       if (rule->rule_type != RULE_LOCAL)
+                               continue;
+               } else if (rule->rule_type == RULE_LOCAL) {
+                       continue;
+               } else if (rule->rule_type == RULE_HOSTSSL && !is_tls) {
+                       continue;
+               } else if (rule->rule_type == RULE_HOSTNOSSL && is_tls) {
+                       continue;
+               } else if (rule->rule_af == AF_INET) {
+                       if (!match_inet4(rule, addr))
+                               continue;
+               } else if (rule->rule_af == AF_INET6) {
+                       if (!match_inet6(rule, addr))
+                               continue;
+               } else {
+                       continue;
+               }
+
+               /* match db & user */
+               if (!name_match(&rule->db_name, dbname, dbnamelen, username))
+                       continue;
+               if (!name_match(&rule->user_name, username, unamelen, dbname))
+                       continue;
+
+               /* rule matches */
+               return rule->rule_method;
+       }
+       return AUTH_REJECT;
+}
+
index a083e2901b2190df8eac6694bcbaa3d8593fccb3..9495573a0e15e0c53f6fc5bf135cdb8a572973a1 100644 (file)
@@ -55,6 +55,8 @@ static void usage(int err, char *exe)
 /* async dns handler */
 struct DNSContext *adns;
 
+struct HBA *parsed_hba;
+
 /*
  * configuration storage
  */
@@ -91,6 +93,7 @@ int cf_tcp_keepintvl;
 
 int cf_auth_type = AUTH_MD5;
 char *cf_auth_file;
+char *cf_auth_hba_file;
 char *cf_auth_query;
 
 int cf_max_client_conn;
@@ -174,6 +177,7 @@ static const struct CfLookup auth_type_map[] = {
 #endif
        { "md5", AUTH_MD5 },
        { "cert", AUTH_CERT },
+       { "hba", AUTH_HBA },
        { NULL }
 };
 
@@ -212,6 +216,7 @@ CF_ABS("unix_socket_group", CF_STR, cf_unix_socket_group, CF_NO_RELOAD, ""),
 #endif
 CF_ABS("auth_type", CF_LOOKUP(auth_type_map), cf_auth_type, 0, "md5"),
 CF_ABS("auth_file", CF_STR, cf_auth_file, 0, "unconfigured_file"),
+CF_ABS("auth_hba_file", CF_STR, cf_auth_hba_file, 0, ""),
 CF_ABS("auth_query", CF_STR, cf_auth_query, 0, "SELECT usename, passwd FROM pg_shadow WHERE usename=$1"),
 CF_ABS("pool_mode", CF_LOOKUP(pool_mode_map), cf_pool_mode, 0, "session"),
 CF_ABS("max_client_conn", CF_INT, cf_max_client_conn, 0, "100"),
@@ -374,6 +379,15 @@ void load_config(void)
                set_dbs_dead(false);
        }
 
+       if (cf_auth_type == AUTH_HBA) {
+               struct HBA *hba = hba_load_rules(cf_auth_hba_file);
+               if (hba) {
+                       if (parsed_hba)
+                               hba_free(parsed_hba);
+                       parsed_hba = hba;
+               }
+       }
+
        /* reset pool_size, kill dbs */
        config_postprocess();
 
index c7ed15c26712416c39c959abff49b67701033de5..c9d69c4c6bb58022c0f2be616246c4790d51734f 100644 (file)
@@ -122,3 +122,23 @@ void change_file_mode(const char *fn, mode_t mode,
        }
 }
 
+/*
+ * UNIX socket helper.
+ */
+
+bool check_unix_peer_name(int fd, const char *username)
+{
+       int res;
+       uid_t peer_uid = -1;
+       gid_t peer_gid = -1;
+       struct passwd *pw;
+
+       res = getpeereid(fd, &peer_uid, &peer_gid);
+       if (res < 0)
+               return false;
+       pw = getpwuid(peer_uid);
+       if (!pw)
+               return false;
+       return strcmp(pw->pw_name, username) == 0;
+}
+
index c9bfbc7c42350b7147fd79a359ba1350731f0681..368b5982e992ddc9437ba362cea3f2aa37cf538c 100644 (file)
@@ -1,22 +1,28 @@
 
-PGINC = -I$(shell pg_config --includedir)
-PGLIB = -L$(shell pg_config --libdir)
+#PGINC = -I$(shell pg_config --includedir)
+#PGLIB = -L$(shell pg_config --libdir)
+#CPPFLAGS += -I../include -I../lib $(PGINC)
+#LDFLAGS += $(PGLIB)
+#LIBS := -lpq $(LIBS)
+#ifeq ($(PORTNAME),win32)
+#CPPFLAGS += -I../win32
+#endif
 
-include ../config.mak
+USUAL_DIR = ../lib
 
-CPPFLAGS += -I../include $(PGINC)
-LDFLAGS += $(PGLIB)
-LIBS := -lpq $(LIBS)
+noinst_PROGRAMS = hba_test
+hba_test_CPPFLAGS = -I../include
+hba_test_CFLAGS = -O0
+hba_test_SOURCES = hba_test.c ../src/hba.c ../src/util.c
+hba_test_EMBED_LIBUSUAL = 1
 
-ifeq ($(PORTNAME),win32)
-CPPFLAGS += -I../win32
-endif
+AM_FEATURES = libusual
 
-all: asynctest
+include ../config.mak
+include ../lib/mk/antimake.mk
 
-asynctest: asynctest.c
-       $(CC) -o $@ $< $(DEFS) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $(LIBS)
+all: run_test
 
-clean:
-       rm -f asynctest
+run_test: hba_test
+       ./hba_test
 
diff --git a/test/hba_test.c b/test/hba_test.c
new file mode 100644 (file)
index 0000000..776c951
--- /dev/null
@@ -0,0 +1,122 @@
+
+#include "bouncer.h"
+
+#include <usual/logging.h>
+#include <usual/string.h>
+#include <usual/time.h>
+#include <usual/logging.h>
+#include <usual/mbuf.h>
+#include <usual/socket.h>
+#include <usual/err.h>
+
+int cf_tcp_keepcnt;
+int cf_tcp_keepintvl;
+int cf_tcp_keepidle;
+int cf_tcp_keepalive;
+int cf_tcp_socket_buffer;
+int cf_listen_port;
+
+static const char *method2string[] = {
+       "trust",
+       "x1",
+       "x2",
+       "password",
+       "crypt",
+       "md5",
+       "creds",
+       "cert",
+       "peer",
+       "hba",
+       "reject",
+};
+
+static char *get_token(char **ln_p)
+{
+       char *ln = *ln_p, *tok, *end;
+
+       while (*ln && *ln == '\t') ln++;
+       tok = ln;
+       while (*ln && *ln != '\t') ln++;
+       end = ln;
+       while (*ln && *ln == '\t') ln++;
+
+       *ln_p = ln;
+       if (tok == end)
+               return NULL;
+       *end = 0;
+       return tok;
+}
+
+static int hba_test_eval(struct HBA *hba, char *ln, int linenr)
+{
+       const char *addr=NULL, *user=NULL, *db=NULL, *tls=NULL, *exp=NULL;
+       PgAddr pgaddr;
+       int res;
+
+       if (ln[0] == '#')
+               return 0;
+       exp = get_token(&ln);
+       db = get_token(&ln);
+       user = get_token(&ln);
+       addr = get_token(&ln);
+       tls = get_token(&ln);
+       if (!exp)
+               return 0;
+       if (!db || !user)
+               die("hbatest: invalid line #%d", linenr);
+
+       if (!pga_pton(&pgaddr, addr, 9999))
+               die("hbatest: invalid addr on line #%d", linenr);
+
+       res = hba_eval(hba, &pgaddr, !!tls, db, user);
+       if (strcmp(method2string[res], exp) == 0) {
+               res = 0;
+       } else {
+               log_warning("FAIL on line %d: expected '%s' got '%s' - user=%s db=%s addr=%s",
+                           linenr, exp, method2string[res], user, db, addr);
+               res = 1;
+       }
+       return res;
+}
+
+static void hba_test(void)
+{
+       struct HBA *hba;
+       FILE *f;
+       char *ln = NULL;
+       size_t lnbuf = 0;
+       ssize_t len;
+       int linenr;
+       int nfailed = 0;
+
+       hba = hba_load_rules("hba_test.rules");
+       if (!hba)
+               die("hbatest: did not find config");
+
+       f = fopen("hba_test.eval", "r");
+       if (!f)
+               die("hbatest: cannot open eval");
+
+       for (linenr = 1; ; linenr++) {
+               len = getline(&ln, &lnbuf, f);
+               if (len < 0)
+                       break;
+               if (len && ln[len-1] == '\n')
+                       ln[len-1] = 0;
+               nfailed += hba_test_eval(hba, ln, linenr);
+       }
+       free(ln);
+       fclose(f);
+       hba_free(hba);
+       if (nfailed)
+               errx(1, "HBA test failures: %d", nfailed);
+       else
+               printf("HBA test OK\n");
+}
+
+int main(void)
+{
+       hba_test();
+       return 0;
+}
+
diff --git a/test/hba_test.eval b/test/hba_test.eval
new file mode 100644 (file)
index 0000000..eecb4fa
--- /dev/null
@@ -0,0 +1,82 @@
+
+# peer
+md5            db      user            unix
+peer           dbp     user            unix
+password       db      userp           unix
+trust          dbz     userz           unix
+
+# hostssl
+cert           db      user            10.1.1.1        tls
+reject         db      user            10.1.1.1
+reject         db      user            13.1.1.1
+
+# hostnossl
+reject         db      user            11.1.1.1        tls
+md5            db      user            11.1.1.1
+reject         db      user            13.1.1.1
+
+# host
+password       db      user            127.0.0.2       tls
+password       db      user            127.0.0.3
+reject         db      user            127.0.1.4
+
+# db1 filt
+reject         db1x    user            127.0.1.4
+md5            db1     user            127.0.1.4
+
+# user1 filt
+md5            db1z    user1           15.0.0.1
+reject         db1z    user2           15.0.0.1
+
+# someusers
+reject         db2     user            16.0.0.1
+md5            db2     user1           16.0.0.1
+md5            db2     user2           16.0.0.1
+md5            db2     user3           16.0.0.1
+reject         db2     user4           16.0.0.1
+
+# manyusers
+md5            db2     u1              17.0.0.1
+md5            db2     u2              17.0.0.1
+md5            db2     u3              17.0.0.1
+md5            db2     u4              17.0.0.1
+md5            db2     u5              17.0.0.1
+md5            db2     u6              17.0.0.1
+md5            db2     u7              17.0.0.1
+md5            db2     u8              17.0.0.1
+md5            db2     u9              17.0.0.1
+md5            db2     u10             17.0.0.1
+md5            db2     u11             17.0.0.1
+
+# manydbs
+reject         d1      user            18.0.0.2
+trust          d1      t18user         18.0.0.2
+trust          d2      t18user         18.0.0.2
+trust          d3      t18user         18.0.0.2
+trust          d4      t18user         18.0.0.2
+trust          d5      t18user         18.0.0.2
+trust          d6      t18user         18.0.0.2
+trust          d7      t18user         18.0.0.2
+trust          d8      t18user         18.0.0.2
+trust          d9      t18user         18.0.0.2
+trust          d10     t18user         18.0.0.2
+trust          d11     t18user         18.0.0.2
+
+# quoting
+reject         db      t19user         19.0.0.2
+cert           all     all             19.0.0.2
+cert           q1"q2   a , b           19.0.0.2
+
+# bitmask
+cert           mdb     muser           199.199.199.199
+reject         mdb     muser           199.199.199.198
+reject         mdb     muser           199.199.199.200
+
+cert           mdb2    muser           254.1.1.1
+
+# ipv6
+md5            mdb     muser           ff11:2::1
+md5            mdb     muser           ff22:3::1
+trust          mdb     muser           ::1
+reject         mdb     muser           ::2
+
diff --git a/test/hba_test.rules b/test/hba_test.rules
new file mode 100644 (file)
index 0000000..51d3459
--- /dev/null
@@ -0,0 +1,51 @@
+
+# METHOD: trust, reject, md5, password, peer, cert
+
+# local      DATABASE  USER  METHOD  [OPTIONS]
+# host       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+
+# ws
+                       
+                       # z
+
+# testing
+
+local          dbp             all                                     peer
+local          all             userp                                   password
+local          dbz             userz                                   trust
+local          all             all                                     md5
+
+hostssl                all             all     10.0.0.0/8                      cert
+hostnossl      all             all     11.0.0.0/8                      md5
+host           all             all     127.0.0.0  255.255.255.0        password
+
+host           db1             all     0.0.0.0/0                       md5
+host           all             user1   15.0.0.0/8                      md5
+
+host           tmp1,all        user1,user2 , user3                     16.0.0.0/8      md5
+host           tmp2,all        u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11,u2   17.0.0.0/8      md5
+host           d1,d2,d3,d4,d5,d6,d7,d8,d9,d10,d11      t18user         18.0.0.0/8      trust # comment
+
+host           "all"           "all"           19.0.0.0/8      cert
+host           "q1""q2"        "a , b"         19.0.0.0/8      cert
+
+# mask
+host           mdb     muser           199.199.199.199/32      cert
+
+host           mdb2    muser           128.0.0.0/9             trust
+host           mdb2    muser           128.0.0.0/8             md5
+host           mdb2    muser           128.0.0.0/7             cert
+host           mdb2    muser           128.0.0.0/6             password
+host           mdb2    muser           128.0.0.0/5             cert
+host           mdb2    muser           128.0.0.0/4             trust
+host           mdb2    muser           128.0.0.0/3             md5
+host           mdb2    muser           128.0.0.0/2             password
+host           mdb2    muser           128.0.0.0/1             cert
+
+# ipv6
+host           mdb     muser           ff11::0/16              md5
+host           mdb     muser           ff20::/12               md5
+host           mdb     muser           ::1/128                 trust
+