RADIUS fallback servers
In a discussion at
/messages/by-id/55D51B54.4050902@joh.to we talked
about adding RADIUS fallback servers. It never got to the point of it being
done.
PFA a patch that implements this.
It supports multiple RADIUS servers. For all other parameters (secret,
port, identifier) one can specify either the exact same number of entries,
in which case each server gets it's own, or exactly one entry in which case
that entry will apply to all servers. (Or zero entries for everything
except secret, which will make it the default).
Each server is tried in order. If it responds positive, auth is OK. If it
responds negative, auth is rejected. If it does not respond at all, we move
on to the next one.
I'm wondering if in doing this we should also make the RADIUS timeout a
configurable as HBA option, since it might become more important now?
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
Attachments:
radius_fallback.patchtext/x-patch; charset=US-ASCII; name=radius_fallback.patchDownload
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 231fc40..f1ecf95 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1589,23 +1589,35 @@ host ... ldap ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub"
</para>
<para>
+ Multiple RADIUS servers can be specified, in which case they will
+ be tried sequentially. If a negative response is received from
+ a server, the authentication will fail. If no response is received,
+ the next server in the list will be tried. To specify multiple
+ servers, put the names within quotes and separate the server names
+ with a comma. If multiple servers are specified, all other RADIUS
+ options can also be given as a comma separate list, to apply
+ individual values to each server. They can also be specified as
+ a single value, in which case this value will apply to all servers.
+ </para>
+
+ <para>
The following configuration options are supported for RADIUS:
<variablelist>
<varlistentry>
- <term><literal>radiusserver</literal></term>
+ <term><literal>radiusservers</literal></term>
<listitem>
<para>
- The name or IP address of the RADIUS server to connect to.
+ The name or IP addresses of the RADIUS servers to connect to.
This parameter is required.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term><literal>radiussecret</literal></term>
+ <term><literal>radiussecrets</literal></term>
<listitem>
<para>
- The shared secret used when talking securely to the RADIUS
+ The shared secrets used when talking securely to the RADIUS
server. This must have exactly the same value on the PostgreSQL
and RADIUS servers. It is recommended that this be a string of
at least 16 characters. This parameter is required.
@@ -1623,17 +1635,17 @@ host ... ldap ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub"
</varlistentry>
<varlistentry>
- <term><literal>radiusport</literal></term>
+ <term><literal>radiusports</literal></term>
<listitem>
<para>
- The port number on the RADIUS server to connect to. If no port
+ The port number on the RADIUS servers to connect to. If no port
is specified, the default port <literal>1812</> will be used.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term><literal>radiusidentifier</literal></term>
+ <term><literal>radiusidentifiers</literal></term>
<listitem>
<para>
The string used as <literal>NAS Identifier</> in the RADIUS
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 824e408..e033b24 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -195,6 +195,7 @@ static int pg_SSPI_make_upn(char *accountname,
*----------------------------------------------------------------
*/
static int CheckRADIUSAuth(Port *port);
+static int PerformRadiusTransaction(char *server, char *secret, char *portstr, char *identifier, char *user_name, char *passwd);
/*
@@ -2455,7 +2456,97 @@ static int
CheckRADIUSAuth(Port *port)
{
char *passwd;
- char *identifier = "postgresql";
+ ListCell *server,
+ *secrets,
+ *radiusports,
+ *identifiers;
+
+ /* Make sure struct alignment is correct */
+ Assert(offsetof(radius_packet, vector) == 4);
+
+ /* Verify parameters */
+ if (list_length(port->hba->radiusservers) < 1)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS server not specified")));
+ return STATUS_ERROR;
+ }
+
+ if (list_length(port->hba->radiussecrets) < 1)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS secret not specified")));
+ return STATUS_ERROR;
+ }
+
+ /* Send regular password request to client, and get the response */
+ sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
+
+ passwd = recv_password_packet(port);
+ if (passwd == NULL)
+ return STATUS_EOF; /* client wouldn't send password */
+
+ if (strlen(passwd) == 0)
+ {
+ ereport(LOG,
+ (errmsg("empty password returned by client")));
+ return STATUS_ERROR;
+ }
+
+ if (strlen(passwd) > RADIUS_MAX_PASSWORD_LENGTH)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS authentication does not support passwords longer than %d characters", RADIUS_MAX_PASSWORD_LENGTH)));
+ return STATUS_ERROR;
+ }
+
+ /*
+ * Loop over and try each server in order.
+ */
+ secrets = list_head(port->hba->radiussecrets);
+ radiusports = list_head(port->hba->radiusports);
+ identifiers = list_head(port->hba->radiusidentifiers);
+ foreach(server, port->hba->radiusservers)
+ {
+ int ret = PerformRadiusTransaction(lfirst(server),
+ lfirst(secrets),
+ radiusports ? lfirst(radiusports) : NULL,
+ identifiers ? lfirst(identifiers) : NULL,
+ port->user_name,
+ passwd);
+
+ /*------
+ * STATUS_OK = Login OK
+ * STATUS_ERROR = Login not OK, but try next server
+ * STATUS_EOF = Login not OK, and don't try next server
+ *------
+ */
+ if (ret == STATUS_OK)
+ return STATUS_OK;
+ else if (ret == STATUS_EOF)
+ return STATUS_ERROR;
+
+ /*
+ * secret, port and identifiers either have length 0 (use default),
+ * length 1 (use the same everywhere) or the same length as servers.
+ * So if the length is >1, we advance one step. In other cases, we
+ * don't and will then reuse the correct value.
+ */
+ if (list_length(port->hba->radiussecrets) > 1)
+ secrets = lnext(secrets);
+ if (list_length(port->hba->radiusports) > 1)
+ radiusports = lnext(radiusports);
+ if (list_length(port->hba->radiusidentifiers) > 1)
+ identifiers = lnext(identifiers);
+ }
+
+ /* No servers left to try, so give up */
+ return STATUS_ERROR;
+}
+
+static int
+PerformRadiusTransaction(char *server, char *secret, char *portstr, char *identifier, char *user_name, char *passwd)
+{
char radius_buffer[RADIUS_BUFFER_SIZE];
char receive_buffer[RADIUS_BUFFER_SIZE];
radius_packet *packet = (radius_packet *) radius_buffer;
@@ -2477,7 +2568,7 @@ CheckRADIUSAuth(Port *port)
#endif
struct addrinfo hint;
struct addrinfo *serveraddrs;
- char portstr[128];
+ int port;
ACCEPT_TYPE_ARG3 addrsize;
fd_set fdset;
struct timeval endtime;
@@ -2485,69 +2576,29 @@ CheckRADIUSAuth(Port *port)
j,
r;
- /* Make sure struct alignment is correct */
- Assert(offsetof(radius_packet, vector) == 4);
-
- /* Verify parameters */
- if (!port->hba->radiusserver || port->hba->radiusserver[0] == '\0')
- {
- ereport(LOG,
- (errmsg("RADIUS server not specified")));
- return STATUS_ERROR;
- }
-
- if (!port->hba->radiussecret || port->hba->radiussecret[0] == '\0')
- {
- ereport(LOG,
- (errmsg("RADIUS secret not specified")));
- return STATUS_ERROR;
- }
-
- if (port->hba->radiusport == 0)
- port->hba->radiusport = 1812;
+ /* Assign default values */
+ if (portstr == NULL)
+ portstr = "1812";
+ if (identifier == NULL)
+ identifier = "postgresql";
MemSet(&hint, 0, sizeof(hint));
hint.ai_socktype = SOCK_DGRAM;
hint.ai_family = AF_UNSPEC;
- snprintf(portstr, sizeof(portstr), "%d", port->hba->radiusport);
+ port = atoi(portstr);
- r = pg_getaddrinfo_all(port->hba->radiusserver, portstr, &hint, &serveraddrs);
+ r = pg_getaddrinfo_all(server, portstr, &hint, &serveraddrs);
if (r || !serveraddrs)
{
ereport(LOG,
(errmsg("could not translate RADIUS server name \"%s\" to address: %s",
- port->hba->radiusserver, gai_strerror(r))));
+ server, gai_strerror(r))));
if (serveraddrs)
pg_freeaddrinfo_all(hint.ai_family, serveraddrs);
return STATUS_ERROR;
}
/* XXX: add support for multiple returned addresses? */
- if (port->hba->radiusidentifier && port->hba->radiusidentifier[0])
- identifier = port->hba->radiusidentifier;
-
- /* Send regular password request to client, and get the response */
- sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
-
- passwd = recv_password_packet(port);
- if (passwd == NULL)
- return STATUS_EOF; /* client wouldn't send password */
-
- if (strlen(passwd) == 0)
- {
- ereport(LOG,
- (errmsg("empty password returned by client")));
- return STATUS_ERROR;
- }
-
- if (strlen(passwd) > RADIUS_MAX_PASSWORD_LENGTH)
- {
- ereport(LOG,
- (errmsg("RADIUS authentication does not support passwords longer than %d characters", RADIUS_MAX_PASSWORD_LENGTH)));
- return STATUS_ERROR;
- }
-
-
/* Construct RADIUS packet */
packet->code = RADIUS_ACCESS_REQUEST;
packet->length = RADIUS_HEADER_LENGTH;
@@ -2559,7 +2610,7 @@ CheckRADIUSAuth(Port *port)
}
packet->id = packet->vector[0];
radius_add_attribute(packet, RADIUS_SERVICE_TYPE, (unsigned char *) &service, sizeof(service));
- radius_add_attribute(packet, RADIUS_USER_NAME, (unsigned char *) port->user_name, strlen(port->user_name));
+ radius_add_attribute(packet, RADIUS_USER_NAME, (unsigned char *) user_name, strlen(user_name));
radius_add_attribute(packet, RADIUS_NAS_IDENTIFIER, (unsigned char *) identifier, strlen(identifier));
/*
@@ -2569,14 +2620,14 @@ CheckRADIUSAuth(Port *port)
* (if necessary)
*/
encryptedpasswordlen = ((strlen(passwd) + RADIUS_VECTOR_LENGTH - 1) / RADIUS_VECTOR_LENGTH) * RADIUS_VECTOR_LENGTH;
- cryptvector = palloc(strlen(port->hba->radiussecret) + RADIUS_VECTOR_LENGTH);
- memcpy(cryptvector, port->hba->radiussecret, strlen(port->hba->radiussecret));
+ cryptvector = palloc(strlen(secret) + RADIUS_VECTOR_LENGTH);
+ memcpy(cryptvector, secret, strlen(secret));
/* for the first iteration, we use the Request Authenticator vector */
md5trailer = packet->vector;
for (i = 0; i < encryptedpasswordlen; i += RADIUS_VECTOR_LENGTH)
{
- memcpy(cryptvector + strlen(port->hba->radiussecret), md5trailer, RADIUS_VECTOR_LENGTH);
+ memcpy(cryptvector + strlen(secret), md5trailer, RADIUS_VECTOR_LENGTH);
/*
* .. and for subsequent iterations the result of the previous XOR
@@ -2584,7 +2635,7 @@ CheckRADIUSAuth(Port *port)
*/
md5trailer = encryptedpassword + i;
- if (!pg_md5_binary(cryptvector, strlen(port->hba->radiussecret) + RADIUS_VECTOR_LENGTH, encryptedpassword + i))
+ if (!pg_md5_binary(cryptvector, strlen(secret) + RADIUS_VECTOR_LENGTH, encryptedpassword + i))
{
ereport(LOG,
(errmsg("could not perform MD5 encryption of password")));
@@ -2676,7 +2727,8 @@ CheckRADIUSAuth(Port *port)
if (timeoutval <= 0)
{
ereport(LOG,
- (errmsg("timeout waiting for RADIUS response")));
+ (errmsg("timeout waiting for RADIUS response from %s",
+ server)));
closesocket(sock);
return STATUS_ERROR;
}
@@ -2701,7 +2753,8 @@ CheckRADIUSAuth(Port *port)
if (r == 0)
{
ereport(LOG,
- (errmsg("timeout waiting for RADIUS response")));
+ (errmsg("timeout waiting for RADIUS response from %s",
+ server)));
closesocket(sock);
return STATUS_ERROR;
}
@@ -2728,19 +2781,19 @@ CheckRADIUSAuth(Port *port)
}
#ifdef HAVE_IPV6
- if (remoteaddr.sin6_port != htons(port->hba->radiusport))
+ if (remoteaddr.sin6_port != htons(port))
#else
- if (remoteaddr.sin_port != htons(port->hba->radiusport))
+ if (remoteaddr.sin_port != htons(port))
#endif
{
#ifdef HAVE_IPV6
ereport(LOG,
- (errmsg("RADIUS response was sent from incorrect port: %d",
- ntohs(remoteaddr.sin6_port))));
+ (errmsg("RADIUS response from %s was sent from incorrect port: %d",
+ server, ntohs(remoteaddr.sin6_port))));
#else
ereport(LOG,
- (errmsg("RADIUS response was sent from incorrect port: %d",
- ntohs(remoteaddr.sin_port))));
+ (errmsg("RADIUS response from %s was sent from incorrect port: %d",
+ server, ntohs(remoteaddr.sin_port))));
#endif
continue;
}
@@ -2748,23 +2801,23 @@ CheckRADIUSAuth(Port *port)
if (packetlength < RADIUS_HEADER_LENGTH)
{
ereport(LOG,
- (errmsg("RADIUS response too short: %d", packetlength)));
+ (errmsg("RADIUS response from %s too short: %d", server, packetlength)));
continue;
}
if (packetlength != ntohs(receivepacket->length))
{
ereport(LOG,
- (errmsg("RADIUS response has corrupt length: %d (actual length %d)",
- ntohs(receivepacket->length), packetlength)));
+ (errmsg("RADIUS response from %s has corrupt length: %d (actual length %d)",
+ server, ntohs(receivepacket->length), packetlength)));
continue;
}
if (packet->id != receivepacket->id)
{
ereport(LOG,
- (errmsg("RADIUS response is to a different request: %d (should be %d)",
- receivepacket->id, packet->id)));
+ (errmsg("RADIUS response from %s is to a different request: %d (should be %d)",
+ server, receivepacket->id, packet->id)));
continue;
}
@@ -2772,7 +2825,7 @@ CheckRADIUSAuth(Port *port)
* Verify the response authenticator, which is calculated as
* MD5(Code+ID+Length+RequestAuthenticator+Attributes+Secret)
*/
- cryptvector = palloc(packetlength + strlen(port->hba->radiussecret));
+ cryptvector = palloc(packetlength + strlen(secret));
memcpy(cryptvector, receivepacket, 4); /* code+id+length */
memcpy(cryptvector + 4, packet->vector, RADIUS_VECTOR_LENGTH); /* request
@@ -2781,10 +2834,10 @@ CheckRADIUSAuth(Port *port)
if (packetlength > RADIUS_HEADER_LENGTH) /* there may be no
* attributes at all */
memcpy(cryptvector + RADIUS_HEADER_LENGTH, receive_buffer + RADIUS_HEADER_LENGTH, packetlength - RADIUS_HEADER_LENGTH);
- memcpy(cryptvector + packetlength, port->hba->radiussecret, strlen(port->hba->radiussecret));
+ memcpy(cryptvector + packetlength, secret, strlen(secret));
if (!pg_md5_binary(cryptvector,
- packetlength + strlen(port->hba->radiussecret),
+ packetlength + strlen(secret),
encryptedpassword))
{
ereport(LOG,
@@ -2797,7 +2850,8 @@ CheckRADIUSAuth(Port *port)
if (memcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0)
{
ereport(LOG,
- (errmsg("RADIUS response has incorrect MD5 signature")));
+ (errmsg("RADIUS response from %s has incorrect MD5 signature",
+ server)));
continue;
}
@@ -2809,13 +2863,13 @@ CheckRADIUSAuth(Port *port)
else if (receivepacket->code == RADIUS_ACCESS_REJECT)
{
closesocket(sock);
- return STATUS_ERROR;
+ return STATUS_EOF;
}
else
{
ereport(LOG,
- (errmsg("RADIUS response has invalid code (%d) for user \"%s\"",
- receivepacket->code, port->user_name)));
+ (errmsg("RADIUS response from %s has invalid code (%d) for user \"%s\"",
+ server, receivepacket->code, user_name)));
continue;
}
} /* while (true) */
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index be63a4b..9e81099 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -39,6 +39,7 @@
#include "storage/fd.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/varlena.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
@@ -145,6 +146,8 @@ static List *tokenize_inc_file(List *tokens, const char *outer_filename,
const char *inc_filename, int elevel, char **err_msg);
static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
int elevel, char **err_msg);
+static bool verify_option_list_length(List *options, char *optionname,
+ List *masters, char *mastername, int line_num);
static ArrayType *gethba_options(HbaLine *hba);
static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
int lineno, HbaLine *hba, const char *err_msg);
@@ -1532,8 +1535,52 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
if (parsedline->auth_method == uaRADIUS)
{
- MANDATORY_AUTH_ARG(parsedline->radiusserver, "radiusserver", "radius");
- MANDATORY_AUTH_ARG(parsedline->radiussecret, "radiussecret", "radius");
+ MANDATORY_AUTH_ARG(parsedline->radiusservers, "radiusservers", "radius");
+ MANDATORY_AUTH_ARG(parsedline->radiussecrets, "radiussecrets", "radius");
+
+ if (list_length(parsedline->radiusservers) < 1)
+ {
+ ereport(LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("list of RADIUS servers cannot be empty"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ return NULL;
+ }
+
+ if (list_length(parsedline->radiussecrets) < 1)
+ {
+ ereport(LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("list of RADIUS secrets cannot be empty"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ return NULL;
+ }
+
+ /*
+ * Verify length of option lists - each can be 0 (except for secrets,
+ * but that's already checked above), 1 (use the same value
+ * everywhere) or the same as the number of servers.
+ */
+ if (!verify_option_list_length(parsedline->radiussecrets,
+ "RADIUS secrets",
+ parsedline->radiusservers,
+ "RADIUS servers",
+ line_num))
+ return NULL;
+ if (!verify_option_list_length(parsedline->radiusports,
+ "RADIUS ports",
+ parsedline->radiusservers,
+ "RADIUS servers",
+ line_num))
+ return NULL;
+ if (!verify_option_list_length(parsedline->radiusidentifiers,
+ "RADIUS identifiers",
+ parsedline->radiusservers,
+ "RADIUS servers",
+ line_num))
+ return NULL;
}
/*
@@ -1547,6 +1594,28 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
return parsedline;
}
+
+static bool
+verify_option_list_length(List *options, char *optionname, List *masters, char *mastername, int line_num)
+{
+ if (list_length(options) == 0 ||
+ list_length(options) == 1 ||
+ list_length(options) == list_length(masters))
+ return true;
+
+ ereport(LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("the number of %s (%i) must be 1 or the same as the number of %s (%i)",
+ optionname,
+ list_length(options),
+ mastername,
+ list_length(masters)
+ ),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ return false;
+}
+
/*
* Parse one name-value pair as an authentication option into the given
* HbaLine. Return true if we successfully parse the option, false if we
@@ -1766,60 +1835,137 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
else
hbaline->upn_username = false;
}
- else if (strcmp(name, "radiusserver") == 0)
+ else if (strcmp(name, "radiusservers") == 0)
{
struct addrinfo *gai_result;
struct addrinfo hints;
int ret;
+ List *parsed_servers;
+ ListCell *l;
+ char *dupval = pstrdup(val);
- REQUIRE_AUTH_OPTION(uaRADIUS, "radiusserver", "radius");
-
- MemSet(&hints, 0, sizeof(hints));
- hints.ai_socktype = SOCK_DGRAM;
- hints.ai_family = AF_UNSPEC;
+ REQUIRE_AUTH_OPTION(uaRADIUS, "radiusservers", "radius");
- ret = pg_getaddrinfo_all(val, NULL, &hints, &gai_result);
- if (ret || !gai_result)
+ if (!SplitIdentifierString(dupval, ',', &parsed_servers))
{
+ /* syntax error in list */
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("could not translate RADIUS server name \"%s\" to address: %s",
- val, gai_strerror(ret)),
+ errmsg("could not parse RADIUS server list \"%s\"",
+ val),
errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName)));
- *err_msg = psprintf("could not translate RADIUS server name \"%s\" to address: %s",
- val, gai_strerror(ret));
- if (gai_result)
- pg_freeaddrinfo_all(hints.ai_family, gai_result);
return false;
}
- pg_freeaddrinfo_all(hints.ai_family, gai_result);
- hbaline->radiusserver = pstrdup(val);
+
+ /* For each entry in the list, translate it */
+ foreach(l, parsed_servers)
+ {
+ MemSet(&hints, 0, sizeof(hints));
+ hints.ai_socktype = SOCK_DGRAM;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all((char *) lfirst(l), NULL, &hints, &gai_result);
+ if (ret || !gai_result)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not translate RADIUS server name \"%s\" to address: %s",
+ (char *) lfirst(l), gai_strerror(ret)),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ if (gai_result)
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ list_free(parsed_servers);
+ return false;
+ }
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+ }
+
+ /* All entries are OK, so store them */
+ hbaline->radiusservers = parsed_servers;
+ hbaline->radiusservers_s = pstrdup(val);
}
- else if (strcmp(name, "radiusport") == 0)
+ else if (strcmp(name, "radiusports") == 0)
{
- REQUIRE_AUTH_OPTION(uaRADIUS, "radiusport", "radius");
- hbaline->radiusport = atoi(val);
- if (hbaline->radiusport == 0)
+ List *parsed_ports;
+ ListCell *l;
+ char *dupval = pstrdup(val);
+
+ REQUIRE_AUTH_OPTION(uaRADIUS, "radiusports", "radius");
+
+ if (!SplitIdentifierString(dupval, ',', &parsed_ports))
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("invalid RADIUS port number: \"%s\"", val),
+ errmsg("could not parse RADIUS port list \"%s\"",
+ val),
errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName)));
*err_msg = psprintf("invalid RADIUS port number: \"%s\"", val);
return false;
}
+
+ foreach(l, parsed_ports)
+ {
+ if (atoi(lfirst(l)) == 0)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("invalid RADIUS port number: \"%s\"", val),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+
+ return false;
+ }
+ }
+ hbaline->radiusports = parsed_ports;
+ hbaline->radiusports_s = pstrdup(val);
}
- else if (strcmp(name, "radiussecret") == 0)
+ else if (strcmp(name, "radiussecrets") == 0)
{
- REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecret", "radius");
- hbaline->radiussecret = pstrdup(val);
+ List *parsed_secrets;
+ char *dupval = pstrdup(val);
+
+ REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecrets", "radius");
+
+ if (!SplitIdentifierString(dupval, ',', &parsed_secrets))
+ {
+ /* syntax error in list */
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not parse RADIUS secret list \"%s\"",
+ val),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ return false;
+ }
+
+ hbaline->radiussecrets = parsed_secrets;
+ hbaline->radiussecrets_s = pstrdup(val);
}
- else if (strcmp(name, "radiusidentifier") == 0)
+ else if (strcmp(name, "radiusidentifiers") == 0)
{
- REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifier", "radius");
- hbaline->radiusidentifier = pstrdup(val);
+ List *parsed_identifiers;
+ char *dupval = pstrdup(val);
+
+ REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifiers", "radius");
+
+ if (!SplitIdentifierString(dupval, ',', &parsed_identifiers))
+ {
+ /* syntax error in list */
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not parse RADIUS identifiers list \"%s\"",
+ val),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ return false;
+ }
+
+ hbaline->radiusidentifiers = parsed_identifiers;
+ hbaline->radiusidentifiers_s = pstrdup(val);
}
else
{
@@ -2124,21 +2270,21 @@ gethba_options(HbaLine *hba)
if (hba->auth_method == uaRADIUS)
{
- if (hba->radiusserver)
+ if (hba->radiusservers_s)
options[noptions++] =
- CStringGetTextDatum(psprintf("radiusserver=%s", hba->radiusserver));
+ CStringGetTextDatum(psprintf("radiusservers=%s", hba->radiusservers_s));
- if (hba->radiussecret)
+ if (hba->radiussecrets_s)
options[noptions++] =
- CStringGetTextDatum(psprintf("radiussecret=%s", hba->radiussecret));
+ CStringGetTextDatum(psprintf("radiussecrets=%s", hba->radiussecrets_s));
- if (hba->radiusidentifier)
+ if (hba->radiusidentifiers_s)
options[noptions++] =
- CStringGetTextDatum(psprintf("radiusidentifier=%s", hba->radiusidentifier));
+ CStringGetTextDatum(psprintf("radiusidentifiers=%s", hba->radiusidentifiers_s));
- if (hba->radiusport)
+ if (hba->radiusports_s)
options[noptions++] =
- CStringGetTextDatum(psprintf("radiusport=%d", hba->radiusport));
+ CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s));
}
Assert(noptions <= MAX_HBA_OPTIONS);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 748a072..13706ff 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -88,10 +88,14 @@ typedef struct HbaLine
bool include_realm;
bool compat_realm;
bool upn_username;
- char *radiusserver;
- char *radiussecret;
- char *radiusidentifier;
- int radiusport;
+ List *radiusservers;
+ char *radiusservers_s;
+ List *radiussecrets;
+ char *radiussecrets_s;
+ List *radiusidentifiers;
+ char *radiusidentifiers_s;
+ List *radiusports;
+ char *radiusports_s;
} HbaLine;
typedef struct IdentLine
I've given an initial review of this patch. It applies cleanly and
compiles without issue as of 6da9759. I'm going to continue with
testing it against a set of RADIUS servers in the next few days. But
in the meantime, I have a few questions (below).
It supports multiple RADIUS servers. For all other parameters (secret, port,
identifier) one can specify either the exact same number of entries, in
which case each server gets it's own, or exactly one entry in which case
that entry will apply to all servers. (Or zero entries for everything except
secret, which will make it the default).
I wonder if removing the complexity of maintaining two separate lists
for the server and port would be a better/less complex approach. For
instance, why not go with a list of typical 'host:port' strings for
'radiusservers'? If no port is specified, then simply use the default
for that specific host. Therefore, we would not have to worry about
keeping the two lists in sync. Thoughts?
Each server is tried in order. If it responds positive, auth is OK. If it
responds negative, auth is rejected. If it does not respond at all, we move
on to the next one.I'm wondering if in doing this we should also make the RADIUS timeout a
configurable as HBA option, since it might become more important now?
Yes, I think this would make sense and would be a good idea.
-Adam
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Friday, March 3, 2017, Adam Brightwell <adam.brightwell@crunchydata.com>
wrote:
I've given an initial review of this patch. It applies cleanly and
compiles without issue as of 6da9759. I'm going to continue with
testing it against a set of RADIUS servers in the next few days. But
in the meantime, I have a few questions (below).It supports multiple RADIUS servers. For all other parameters (secret,
port,
identifier) one can specify either the exact same number of entries, in
which case each server gets it's own, or exactly one entry in which case
that entry will apply to all servers. (Or zero entries for everythingexcept
secret, which will make it the default).
I wonder if removing the complexity of maintaining two separate lists
for the server and port would be a better/less complex approach. For
instance, why not go with a list of typical 'host:port' strings for
'radiusservers'? If no port is specified, then simply use the default
for that specific host. Therefore, we would not have to worry about
keeping the two lists in sync. Thoughts?
If we do that we should do it for all the parameters, no? So not just
host:port, but something like host:port:secret:identifier? Mixing the two
ways of doing it would be quite confusing I think.
And I wonder if that format wouldn't get even more confusing if you for
example want to use default ports, but non-default secrets.
I can see how it would probably be easier in some of the simple cases, but
I wonder if it wouldn't make it worse in a lot of other cases.
Each server is tried in order. If it responds positive, auth is OK. If it
responds negative, auth is rejected. If it does not respond at all, wemove
on to the next one.
I'm wondering if in doing this we should also make the RADIUS timeout a
configurable as HBA option, since it might become more important now?Yes, I think this would make sense and would be a good idea.
//Magnus
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
I wonder if removing the complexity of maintaining two separate lists
for the server and port would be a better/less complex approach. For
instance, why not go with a list of typical 'host:port' strings for
'radiusservers'? If no port is specified, then simply use the default
for that specific host. Therefore, we would not have to worry about
keeping the two lists in sync. Thoughts?If we do that we should do it for all the parameters, no? So not just
host:port, but something like host:port:secret:identifier? Mixing the two
ways of doing it would be quite confusing I think.And I wonder if that format wouldn't get even more confusing if you for
example want to use default ports, but non-default secrets.
Yes, I agree. Such a format would be more confusing and I certainly
wouldn't be in favor of it.
I can see how it would probably be easier in some of the simple cases, but I
wonder if it wouldn't make it worse in a lot of other cases.
Ultimately, I think that it would be better off in a separate
configuration file. Something to the effect of each line representing
a server, something like:
'<server> <port> <secret> <identifier>'
With 'radiusservers' simply being the path to that file and
'radiusserver', etc. would remain as is. Where only one or the other
could be provided, but not both. Though, that's perhaps would be
beyond the scope of this patch.
At any rate, I'm going to continue moving forward with testing this patch as is.
-Adam
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Mar 6, 2017 at 10:24 AM, Adam Brightwell
<adam.brightwell@crunchydata.com> wrote:
I wonder if removing the complexity of maintaining two separate lists
for the server and port would be a better/less complex approach. For
instance, why not go with a list of typical 'host:port' strings for
'radiusservers'? If no port is specified, then simply use the default
for that specific host. Therefore, we would not have to worry about
keeping the two lists in sync. Thoughts?If we do that we should do it for all the parameters, no? So not just
host:port, but something like host:port:secret:identifier? Mixing the two
ways of doing it would be quite confusing I think.And I wonder if that format wouldn't get even more confusing if you for
example want to use default ports, but non-default secrets.Yes, I agree. Such a format would be more confusing and I certainly
wouldn't be in favor of it.I can see how it would probably be easier in some of the simple cases, but I
wonder if it wouldn't make it worse in a lot of other cases.Ultimately, I think that it would be better off in a separate
configuration file. Something to the effect of each line representing
a server, something like:'<server> <port> <secret> <identifier>'
With 'radiusservers' simply being the path to that file and
'radiusserver', etc. would remain as is. Where only one or the other
could be provided, but not both. Though, that's perhaps would be
beyond the scope of this patch.At any rate, I'm going to continue moving forward with testing this patch as is.
I have run through testing this patch against a small set of RADIUS
servers. This testing included both single server and multiple server
configurations. All seems to work as expected.
-Adam
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Mar 6, 2017 at 8:14 PM, Adam Brightwell <
adam.brightwell@crunchydata.com> wrote:
On Mon, Mar 6, 2017 at 10:24 AM, Adam Brightwell
<adam.brightwell@crunchydata.com> wrote:I wonder if removing the complexity of maintaining two separate lists
for the server and port would be a better/less complex approach. For
instance, why not go with a list of typical 'host:port' strings for
'radiusservers'? If no port is specified, then simply use the default
for that specific host. Therefore, we would not have to worry about
keeping the two lists in sync. Thoughts?If we do that we should do it for all the parameters, no? So not just
host:port, but something like host:port:secret:identifier? Mixing thetwo
ways of doing it would be quite confusing I think.
And I wonder if that format wouldn't get even more confusing if you for
example want to use default ports, but non-default secrets.Yes, I agree. Such a format would be more confusing and I certainly
wouldn't be in favor of it.I can see how it would probably be easier in some of the simple cases,
but I
wonder if it wouldn't make it worse in a lot of other cases.
Ultimately, I think that it would be better off in a separate
configuration file. Something to the effect of each line representing
a server, something like:'<server> <port> <secret> <identifier>'
With 'radiusservers' simply being the path to that file and
'radiusserver', etc. would remain as is. Where only one or the other
could be provided, but not both. Though, that's perhaps would be
beyond the scope of this patch.
I think it is. If we want to go there I don't think we should do that as a
RADIUS thing -- we should rethink it within the context of *all* the
different authentication methods we have.
At any rate, I'm going to continue moving forward with testing this
patch as is.
I have run through testing this patch against a small set of RADIUS
servers. This testing included both single server and multiple server
configurations. All seems to work as expected.
Seeps I forgot about this one.
Thanks for the review -- I've applied the patch.
I'll look into the timeout thing as a separate patch later.
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/