[Patch] Using Windows groups for SSPI authentication
I have some code that I've been using that supports adding and
authenticating Windows groups via the pg_ident file. This is useful for
sysadmins as it lets them control database access outside the database
using Windows groups. It has a new
indicator (+), that signifies the identifier is a Windows group, as in the
following example:
# MAPNAME SYSTEM-USERNAME PG-USERNAME
"Users" "+User group" postgres
A new function was added to test if a user token is in the windows group:
/*
* Check if the user (sspiToken) is a member of the specified group
*/
static BOOL
sspi_user_is_in_group(HANDLE sspiToken, LPCTSTR groupName)
Attached is the patch.
thanks,
Russell Foster
Attachments:
0001-Add-support-for-Windows-groups-in-SSPI-authenticatio.patchapplication/octet-stream; name=0001-Add-support-for-Windows-groups-in-SSPI-authenticatio.patchDownload
From 8875db29dc6e1504efbe14d7cdf010ec441b3c53 Mon Sep 17 00:00:00 2001
From: Russell Foster <russell.foster.coding@gmail.com>
Date: Tue, 13 Oct 2020 09:02:47 -0400
Subject: [PATCH] * Add support for Windows groups in SSPI authentication
---
src/backend/libpq/auth.c | 85 ++++++++++++++++------------------
src/backend/libpq/hba.c | 118 +++++++++++++++++++++++++++++++++++++++++++++--
src/include/libpq/hba.h | 2 +-
3 files changed, 156 insertions(+), 49 deletions(-)
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 36565df4fc..562c2ec278 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1281,7 +1281,7 @@ pg_GSS_checkauth(Port *port)
}
ret = check_usermap(port->hba->usermap, port->user_name, gbuf.value,
- pg_krb_caseins_users);
+ pg_krb_caseins_users, NULL);
gss_release_buffer(&lmin_s, &gbuf);
@@ -1321,6 +1321,7 @@ pg_SSPI_error(int severity, const char *errmsg, SECURITY_STATUS r)
static int
pg_SSPI_recvauth(Port *port)
{
+ int retval = STATUS_ERROR;
int mtype;
StringInfoData buf;
SECURITY_STATUS r;
@@ -1398,7 +1399,7 @@ pg_SSPI_recvauth(Port *port)
(errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("expected SSPI response, got message type %d",
mtype)));
- return STATUS_ERROR;
+ return retval;
}
/* Get the actual SSPI token */
@@ -1413,7 +1414,7 @@ pg_SSPI_recvauth(Port *port)
free(sspictx);
}
FreeCredentialsHandle(&sspicred);
- return STATUS_ERROR;
+ return retval;
}
/* Map to SSPI style buffer */
@@ -1563,8 +1564,6 @@ pg_SSPI_recvauth(Port *port)
(errmsg_internal("could not get token information: error code %lu",
GetLastError())));
- CloseHandle(token);
-
if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
domainname, &domainnamesize, &accountnameuse))
ereport(ERROR,
@@ -1573,51 +1572,47 @@ pg_SSPI_recvauth(Port *port)
free(tokenuser);
- if (!port->hba->compat_realm)
- {
- int status = pg_SSPI_make_upn(accountname, sizeof(accountname),
- domainname, sizeof(domainname),
- port->hba->upn_username);
-
- if (status != STATUS_OK)
- /* Error already reported from pg_SSPI_make_upn */
- return status;
- }
-
- /*
- * Compare realm/domain if requested. In SSPI, always compare case
- * insensitive.
- */
- if (port->hba->krb_realm && strlen(port->hba->krb_realm))
+ if (port->hba->compat_realm ||
+ (pg_SSPI_make_upn(accountname, sizeof(accountname), domainname,
+ sizeof(domainname), port->hba->upn_username) == STATUS_OK))
{
- if (pg_strcasecmp(port->hba->krb_realm, domainname) != 0)
+ /*
+ * Compare realm/domain if requested. In SSPI, always compare case
+ * insensitive.
+ */
+ if (port->hba->krb_realm && strlen(port->hba->krb_realm) &&
+ (pg_strcasecmp(port->hba->krb_realm, domainname) != 0))
{
elog(DEBUG2,
- "SSPI domain (%s) and configured domain (%s) don't match",
- domainname, port->hba->krb_realm);
+ "SSPI domain (%s) and configured domain (%s) don't match",
+ domainname, port->hba->krb_realm);
+ }
+ else
+ {
+ /*
+ * We have the username (without domain/realm) in accountname, compare to
+ * the supplied value. In SSPI, always compare case insensitive.
+ *
+ * If set to include realm, append it in <username>@<realm> format.
+ */
+ if (port->hba->include_realm)
+ {
+ char *namebuf;
- return STATUS_ERROR;
+ namebuf = psprintf("%s@%s", accountname, domainname);
+ retval = check_usermap(port->hba->usermap, port->user_name, namebuf, true, token);
+ pfree(namebuf);
+ }
+ else
+ {
+ retval = check_usermap(port->hba->usermap, port->user_name, accountname, true, token);
+ }
}
}
- /*
- * We have the username (without domain/realm) in accountname, compare to
- * the supplied value. In SSPI, always compare case insensitive.
- *
- * If set to include realm, append it in <username>@<realm> format.
- */
- if (port->hba->include_realm)
- {
- char *namebuf;
- int retval;
+ CloseHandle(token);
- namebuf = psprintf("%s@%s", accountname, domainname);
- retval = check_usermap(port->hba->usermap, port->user_name, namebuf, true);
- pfree(namebuf);
- return retval;
- }
- else
- return check_usermap(port->hba->usermap, port->user_name, accountname, true);
+ return retval;
}
/*
@@ -1972,7 +1967,7 @@ ident_inet_done:
if (ident_return)
/* Success! Check the usermap */
- return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+ return check_usermap(port->hba->usermap, port->user_name, ident_user, false, NULL);
return STATUS_ERROR;
}
@@ -2031,7 +2026,7 @@ auth_peer(hbaPort *port)
/* Make a copy of static getpw*() result area. */
peer_user = pstrdup(pw->pw_name);
- ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+ ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false, NULL);
pfree(peer_user);
@@ -2883,7 +2878,7 @@ CheckCertAuth(Port *port)
}
/* Just pass the certificate cn to the usermap check */
- status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
+ status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false, NULL);
if (status_check_usermap != STATUS_OK)
{
/*
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 4c86fb6087..b5a38cf26e 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -2797,6 +2797,101 @@ parse_ident_line(TokenizedLine *tok_line)
return parsedline;
}
+#ifdef ENABLE_SSPI
+
+/*
+ * Get the sid for an account name
+ */
+static PSID
+lookup_account_name(LPCTSTR accountName, LPDWORD accountSidSize, LPDWORD domainNameCharCount, LPDWORD lastError)
+{
+ PSID accountSid;
+ LPCTSTR domainName;
+ SID_NAME_USE sidType;
+ BOOL lookupResult;
+
+ accountSid = malloc(*accountSidSize);
+
+ if (accountSid == NULL)
+ {
+ ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("out of memory")));
+ }
+
+ domainName = malloc(*domainNameCharCount * sizeof(TCHAR));
+
+ if (domainName == NULL)
+ {
+ ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("out of memory")));
+ }
+
+ lookupResult = LookupAccountName(NULL, accountName, accountSid, accountSidSize, domainName, domainNameCharCount, &sidType);
+ *lastError = GetLastError();
+
+ free(domainName);
+
+ if (!lookupResult && (accountSid != NULL))
+ {
+ free(accountSid);
+
+ accountSid = NULL;
+ }
+
+ return accountSid;
+}
+
+/*
+ * Check if the user (sspiToken) is a member of the specified group
+ */
+static bool
+sspi_user_is_in_group(HANDLE sspiToken, LPCTSTR groupName)
+{
+ BOOL isMember = FALSE;
+ DWORD groupSidSize = 1024;
+ DWORD domainNameCharCount = 1024;
+ PSID groupSid;
+ DWORD lastError;
+
+ // try a default buffer size. if this doesn't work, use the returned buffer size
+ groupSid = lookup_account_name(groupName, &groupSidSize, &domainNameCharCount, &lastError);
+
+ if (groupSid == NULL)
+ {
+ if (lastError == 122)
+ {
+ elog(DEBUG2, "larger buffer required to get sid, groupSidSize=%lu, domainNameCharCount=%lu", groupSidSize, domainNameCharCount);
+
+ groupSid = lookup_account_name(groupName, &groupSidSize, &domainNameCharCount, &lastError);
+
+ if (groupSid == NULL)
+ {
+ elog(DEBUG2, "could not get sid on second attempt: error=%lu", lastError);
+ }
+ }
+ else
+ {
+ elog(DEBUG2, "could not get sid on first attempt: error=%lu", lastError);
+ }
+ }
+
+ if (groupSid != NULL)
+ {
+ if (CheckTokenMembership(sspiToken, groupSid, &isMember))
+ {
+ elog(DEBUG4, "check group membership groupName=%s, isMember=%i", groupName, isMember);
+ }
+ else
+ {
+ elog(DEBUG2, "could not check group membership: error=%lu", lastError);
+ }
+
+ free(groupSid);
+ }
+
+ return (isMember == TRUE);
+}
+
+#endif /* ENABLE_SSPI */
+
/*
* Process one line from the parsed ident config lines.
*
@@ -2806,7 +2901,8 @@ parse_ident_line(TokenizedLine *tok_line)
static void
check_ident_usermap(IdentLine *identLine, const char *usermap_name,
const char *pg_role, const char *ident_user,
- bool case_insensitive, bool *found_p, bool *error_p)
+ bool case_insensitive, bool *found_p, bool *error_p,
+ void *sspi_token)
{
*found_p = false;
*error_p = false;
@@ -2906,6 +3002,21 @@ check_ident_usermap(IdentLine *identLine, const char *usermap_name,
return;
}
+#ifdef ENABLE_SSPI
+ else if (identLine->ident_user[0] == '+')
+ {
+ if (case_insensitive)
+ {
+ if (pg_strcasecmp(identLine->pg_role, pg_role) == 0)
+ *found_p = sspi_user_is_in_group(sspi_token, identLine->ident_user + 1);
+ }
+ else
+ {
+ if (strcmp(identLine->pg_role, pg_role) == 0)
+ *found_p = sspi_user_is_in_group(sspi_token, identLine->ident_user + 1);
+ }
+ }
+#endif /* ENABLE_SSPI */
else
{
/* Not regular expression, so make complete match */
@@ -2942,7 +3053,8 @@ int
check_usermap(const char *usermap_name,
const char *pg_role,
const char *auth_user,
- bool case_insensitive)
+ bool case_insensitive,
+ void *sspi_token)
{
bool found_entry = false,
error = false;
@@ -2972,7 +3084,7 @@ check_usermap(const char *usermap_name,
{
check_ident_usermap(lfirst(line_cell), usermap_name,
pg_role, auth_user, case_insensitive,
- &found_entry, &error);
+ &found_entry, &error, sspi_token);
if (found_entry || error)
break;
}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index d638479d88..5faf4deca7 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -128,7 +128,7 @@ extern bool load_ident(void);
extern void hba_getauthmethod(hbaPort *port);
extern int check_usermap(const char *usermap_name,
const char *pg_role, const char *auth_user,
- bool case_sensitive);
+ bool case_sensitive, void *sspi_token);
extern bool pg_isblank(const char c);
#endif /* HBA_H */
--
2.16.1.windows.4
Russell Foster <russell.foster.coding@gmail.com> writes:
I have some code that I've been using that supports adding and
authenticating Windows groups via the pg_ident file. This is useful for
sysadmins as it lets them control database access outside the database
using Windows groups. It has a new
indicator (+), that signifies the identifier is a Windows group, as in the
following example:
# MAPNAME SYSTEM-USERNAME PG-USERNAME
"Users" "+User group" postgres
While I don't object to adding functionality to access Windows groups,
I do object to using syntax that makes random assumptions about what a
user name can or can't be.
There was a prior discussion of this in the context of some other patch
that had a similar idea. [ digs in archives... ] Ah, here it is:
/messages/by-id/4ba3ad54-bb32-98c6-033a-ccca7058fc2f@2ndquadrant.com
It doesn't look like we arrived at any firm consensus about what to
do instead, but maybe you can find some ideas there.
regards, tom lane
Going to take a guess at what you mean by:
I do object to using syntax that makes random assumptions about what a
user name can or can't be.
Are you referring to the "+" syntax in the ident file? I chose that because
somewhere else (hba?) using the same syntax for groups. The quotes are just
there to make the group name case sensitive.
On Tue, Oct 13, 2020 at 1:15 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Show quoted text
Russell Foster <russell.foster.coding@gmail.com> writes:
I have some code that I've been using that supports adding and
authenticating Windows groups via the pg_ident file. This is useful for
sysadmins as it lets them control database access outside the database
using Windows groups. It has a new
indicator (+), that signifies the identifier is a Windows group, as inthe
following example:
# MAPNAME SYSTEM-USERNAME PG-USERNAME
"Users" "+User group" postgresWhile I don't object to adding functionality to access Windows groups,
I do object to using syntax that makes random assumptions about what a
user name can or can't be.There was a prior discussion of this in the context of some other patch
that had a similar idea. [ digs in archives... ] Ah, here it is:/messages/by-id/4ba3ad54-bb32-98c6-033a-ccca7058fc2f@2ndquadrant.com
It doesn't look like we arrived at any firm consensus about what to
do instead, but maybe you can find some ideas there.regards, tom lane
Russell Foster <russell.foster.coding@gmail.com> writes:
Going to take a guess at what you mean by:
I do object to using syntax that makes random assumptions about what a
user name can or can't be.
Are you referring to the "+" syntax in the ident file? I chose that because
somewhere else (hba?) using the same syntax for groups. The quotes are just
there to make the group name case sensitive.
If this were a Postgres group name, I'd say yeah we already broke
the case of spelling group names with a leading "+". (Which I'm
not very happy about either, but the precedent is there.)
However, this isn't. Unless I'm totally confused, the field you're
talking about is normally an external, operating-system-defined name.
I do not think it's wise to make any assumptions about what those
can be.
By the same token, the idea of using a "pg_" prefix as discussed
in the other thread will not work here :-(.
After a few minutes' thought, the best I can can come up with is
to extend the syntax of identmap files with an "options" field,
so that your example becomes something like
# MAPNAME SYSTEM-USERNAME PG-USERNAME OPTIONS
"Users" "User group" postgres windows-group
I'm envisioning OPTIONS as allowing a comma- or space-separated
list of keywords, which would give room to grow for other special
features we might want later.
regards, tom lane
Russell Foster <russell.foster.coding@gmail.com> writes:
I understand your concerns overall, and the solution you propose seems
reasonable. But are we just using "windows-group" because the code is not
there today to check for a user in another OS group?
It's not clear to me whether Windows groups have exact equivalents in
other OSes. If we think the concept is generic, I'd be okay with
spelling the keyword system-group or the like. The patch you
proposed looked pretty Windows-specific though. Somebody with more
SSPI knowledge than me would have to opine on whether "sspi-group"
is a reasonable name.
regards, tom lane
Import Notes
Reply to msg id not found: CA+VXQbLP4T-mRam72eynLUsyxMrfcax4u92QBGkJsQ2aFw@mail.gmail.com
Right after I sent that I realized that sspi-group was a bad idea, not sure
if that's even a thing. Tried to cancel as it was still in moderation, but
it made it through anyways! You are right, it is very windows specific. I
can make it windows-group as you said, and resubmit.
On Tue, Oct 13, 2020 at 4:32 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Show quoted text
Russell Foster <russell.foster.coding@gmail.com> writes:
I understand your concerns overall, and the solution you propose seems
reasonable. But are we just using "windows-group" because the code is not
there today to check for a user in another OS group?It's not clear to me whether Windows groups have exact equivalents in
other OSes. If we think the concept is generic, I'd be okay with
spelling the keyword system-group or the like. The patch you
proposed looked pretty Windows-specific though. Somebody with more
SSPI knowledge than me would have to opine on whether "sspi-group"
is a reasonable name.regards, tom lane
Greetings,
* Russell Foster (russell.foster.coding@gmail.com) wrote:
Right after I sent that I realized that sspi-group was a bad idea, not sure
if that's even a thing. Tried to cancel as it was still in moderation, but
it made it through anyways! You are right, it is very windows specific. I
can make it windows-group as you said, and resubmit.
Please don't top-post on these lists..
On Tue, Oct 13, 2020 at 4:32 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Russell Foster <russell.foster.coding@gmail.com> writes:
I understand your concerns overall, and the solution you propose seems
reasonable. But are we just using "windows-group" because the code is not
there today to check for a user in another OS group?It's not clear to me whether Windows groups have exact equivalents in
other OSes. If we think the concept is generic, I'd be okay with
spelling the keyword system-group or the like. The patch you
proposed looked pretty Windows-specific though. Somebody with more
SSPI knowledge than me would have to opine on whether "sspi-group"
is a reasonable name.
While not exactly the same, of course, they are more-or-less equivilant
to Unix groups (it's even possible using NSS to get Unix groups to be
backed by Windows groups) and so calling it 'system-group' does seem
like it'd make sense, rather than calling it "Windows groups" or
similar.
One unfortunate thing regarding this is that, unless things have
changed, this won't end up working with GSS (unless we add the unix
group support and that's then backed by AD as I described above) since
the ability to check group membership using SSPI is an extension to the
Kerberos protocol, which never included group membership information in
it, and therefore while this would work for Windows clients connecting
to Windows servers, it won't work for Windows clients connecting to Unix
servers with GSSAPI authentication.
The direction I had been thinking of addressing that was to add an
option to pg_hba.conf's 'gss' auth method which would allow reaching out
to check group membership against an AD server. In a similar vein, we
could add an option to the 'sspi' auth method to check the group
membership, rather than having this done in pg_ident.conf, which is
really intended to allow mapping between system usernames and PG
usernames which are different, not really for controlling authentication
based on group membership when the username is the same.
Russell, thoughts on that..?
Thanks,
Stephen
On Thu, Oct 15, 2020 at 11:31 AM Stephen Frost <sfrost@snowman.net> wrote:
Please don't top-post on these lists..
Didn't even know what that was, had to look it up. Hopefully it is
resolved. Gmail does too many things for you!
While not exactly the same, of course, they are more-or-less equivilant
to Unix groups (it's even possible using NSS to get Unix groups to be
backed by Windows groups) and so calling it 'system-group' does seem
like it'd make sense, rather than calling it "Windows groups" or
similar.One unfortunate thing regarding this is that, unless things have
changed, this won't end up working with GSS (unless we add the unix
group support and that's then backed by AD as I described above) since
the ability to check group membership using SSPI is an extension to the
Kerberos protocol, which never included group membership information in
it, and therefore while this would work for Windows clients connecting
to Windows servers, it won't work for Windows clients connecting to Unix
servers with GSSAPI authentication.The direction I had been thinking of addressing that was to add an
option to pg_hba.conf's 'gss' auth method which would allow reaching out
to check group membership against an AD server. In a similar vein, we
could add an option to the 'sspi' auth method to check the group
membership, rather than having this done in pg_ident.conf, which is
really intended to allow mapping between system usernames and PG
usernames which are different, not really for controlling authentication
based on group membership when the username is the same.Russell, thoughts on that..?
So are you saying something like this where its an option to the sspi method?
# TYPE DATABASE USER ADDRESS MASK METHOD
hostssl all some_user 0.0.0.0 0.0.0.0 sspi group="Windows Group"
I guess the code wouldn't change much, unless you mean for it to do a
more generic ldap query. Seems OK to me, but I guess the hba could
become more verbose. The map is nice as it allows your HBA to be very
precise in how your connections and database users are represented,
and the ident map file is there to group those external identities. I
can't say I have a strong opinion either way though.
Greetings,
* Russell Foster (russell.foster.coding@gmail.com) wrote:
On Thu, Oct 15, 2020 at 11:31 AM Stephen Frost <sfrost@snowman.net> wrote:
Please don't top-post on these lists..
Didn't even know what that was, had to look it up. Hopefully it is
resolved. Gmail does too many things for you!
Indeed! This looks much better, thanks!
While not exactly the same, of course, they are more-or-less equivilant
to Unix groups (it's even possible using NSS to get Unix groups to be
backed by Windows groups) and so calling it 'system-group' does seem
like it'd make sense, rather than calling it "Windows groups" or
similar.One unfortunate thing regarding this is that, unless things have
changed, this won't end up working with GSS (unless we add the unix
group support and that's then backed by AD as I described above) since
the ability to check group membership using SSPI is an extension to the
Kerberos protocol, which never included group membership information in
it, and therefore while this would work for Windows clients connecting
to Windows servers, it won't work for Windows clients connecting to Unix
servers with GSSAPI authentication.The direction I had been thinking of addressing that was to add an
option to pg_hba.conf's 'gss' auth method which would allow reaching out
to check group membership against an AD server. In a similar vein, we
could add an option to the 'sspi' auth method to check the group
membership, rather than having this done in pg_ident.conf, which is
really intended to allow mapping between system usernames and PG
usernames which are different, not really for controlling authentication
based on group membership when the username is the same.Russell, thoughts on that..?
So are you saying something like this where its an option to the sspi method?
# TYPE DATABASE USER ADDRESS MASK METHOD
hostssl all some_user 0.0.0.0 0.0.0.0 sspi group="Windows Group"
Yes, something along those lines.
I guess the code wouldn't change much, unless you mean for it to do a
more generic ldap query. Seems OK to me, but I guess the hba could
become more verbose. The map is nice as it allows your HBA to be very
precise in how your connections and database users are represented,
and the ident map file is there to group those external identities. I
can't say I have a strong opinion either way though.
No, no, not suggesting you need to rewrite it as a generic LDAP query-
that would be a patch that I'd like to see but is a different feature
from this and wouldn't even be applicable to SSPI (it'd be for GSS..
and perhaps some other methods, but with SSPI we should use the SSPI
methods- I can't think of a reason to go to an LDAP query when the group
membership is directly available from SSPI, can you?).
The pg_ident is specifically intended to be a mapping from external user
identities to PG users. Reading back through the thread, in the end it
seems like it really depends on what we're trying to solve here and
perhaps it's my fault for misunderstanding your original goal, but maybe
we get two features out of this in the end, and for not much more code.
Based on your example pg_ident.conf (which I took as more of a "this is
what using this would look like" and not as literally as I think you
meant it, now that I read back through it), there's a use-case of:
"Allow anyone in this group to log in as this *specific* PG user"
The other use-case is:
"Allow users in this group to be able to log into this PG server"
(The latter use-case potentially being further extended to
"automatically create the PG user if it doesn't already exist",
something which has been discussed elsewhere previously and is what
folks coming from other database systems may be used to).
The former would be more appropriate in pg_ident.conf, the latter would
fit into pg_hba.conf, both are useful.
To the prior discussion around pg_ident.conf, I do think having the
keyword being 'system-group' would fit well, but something we need to
think about is that multiple auth methods work with pg_ident and we need
to either implement the functionality for each of them, or make it clear
that it doesn't work- in particular, if you have 'system-group' as an
option in pg_ident.conf and you're using 'peer' auth on a Unix system,
we either need to make it work (which should be pretty easy..?), or
refuse to accept that map for that auth-method if it's not going to
work.
As it relates to pg_hba.conf- if you don't think it'd be much additional
code and you'd be up for it, I do think it'd be awesome to address that
use-case as well, but I do agree it's a separate feature and probably
committed independently.
Or, if I've managed to misunderstand again, please let me know. :)
Thanks!
Stephen