Re: New patch for Column-level privileges
* Stephen Frost (sfrost@snowman.net) wrote:
Please find attached an updated patch for column-level privileges
which incorporates Alvaro's suggested changes and is updated to the
latest CVS HEAD. Regression tests have been added as well as
documentation (though this could probably be improved). Currently,
column-level privileges are not honored when JOINs are involved (you
must have the necessary table-level privileges, as you do today). It
would really be great to have that working and mainly involves
modifying the rewriter to add on to the appropriate range table column
list entries the columns which are used in the joins and output from
joins.
Here it is gzip'd since I'm afraid the last might not have made it
through due to size..
Stephen
Attachments:
Import Notes
Reply to msg id not found: 20090102230310.GI26233@tamriel.snowman.netReference msg id not found: 20090102230310.GI26233@tamriel.snowman.net
On Fri, Jan 2, 2009 at 16:11, Stephen Frost <sfrost@snowman.net> wrote:
* Stephen Frost (sfrost@snowman.net) wrote:
Please find attached an updated patch for column-level privileges
which incorporates Alvaro's suggested changes and is updated to the
latest CVS HEAD.
Hi!
This gives me
aclchk.c: In function 'ExecuteGrantStmt':
aclchk.c:276: warning: 'errormsg_col' may be used uninitialized in this function
Now it looks bogos but why not just move errmsg_col down to where we
actually use it? Or am I missing something?
*** a/src/backend/catalog/aclchk.c
--- b/src/backend/catalog/aclchk.c
***************
*** 273,279 **** ExecuteGrantStmt(GrantStmt *stmt)
InternalGrant istmt;
ListCell *cell;
const char *errormsg;
- const char *errormsg_col;
AclMode all_privileges;
/*
--- 273,278 ----
***************
*** 324,330 **** ExecuteGrantStmt(GrantStmt *stmt)
case ACL_OBJECT_RELATION:
all_privileges = ACL_ALL_RIGHTS_RELATION | ACL_ALL_RIGHTS_SEQUENCE;
errormsg = gettext_noop("invalid privilege type %s for relation");
- errormsg_col = gettext_noop("invalid privilege type %s for column");
break;
case ACL_OBJECT_SEQUENCE:
all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
--- 323,328 ----
***************
*** 362,368 **** ExecuteGrantStmt(GrantStmt *stmt)
/* keep compiler quiet */
all_privileges = ACL_NO_RIGHTS;
errormsg = NULL;
- errormsg_col = NULL;
elog(ERROR, "unrecognized GrantStmt.objtype: %d",
(int) stmt->objtype);
}
--- 360,365 ----
***************
*** 404,409 **** ExecuteGrantStmt(GrantStmt *stmt)
--- 401,408 ----
{
/* Column-level privileges given. */
ListCell *cell_obj;
+ const char *errormsg_col = gettext_noop("invalid privilege type
%s for column");
+
/* Must be of objtype ACL_OBJECT_RELATION for column
* level privileges */
Attachments:
fixwarning.patchapplication/octet-stream; name=fixwarning.patchDownload
*** a/src/backend/catalog/aclchk.c
--- b/src/backend/catalog/aclchk.c
***************
*** 273,279 **** ExecuteGrantStmt(GrantStmt *stmt)
InternalGrant istmt;
ListCell *cell;
const char *errormsg;
- const char *errormsg_col;
AclMode all_privileges;
/*
--- 273,278 ----
***************
*** 324,330 **** ExecuteGrantStmt(GrantStmt *stmt)
case ACL_OBJECT_RELATION:
all_privileges = ACL_ALL_RIGHTS_RELATION | ACL_ALL_RIGHTS_SEQUENCE;
errormsg = gettext_noop("invalid privilege type %s for relation");
- errormsg_col = gettext_noop("invalid privilege type %s for column");
break;
case ACL_OBJECT_SEQUENCE:
all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
--- 323,328 ----
***************
*** 362,368 **** ExecuteGrantStmt(GrantStmt *stmt)
/* keep compiler quiet */
all_privileges = ACL_NO_RIGHTS;
errormsg = NULL;
- errormsg_col = NULL;
elog(ERROR, "unrecognized GrantStmt.objtype: %d",
(int) stmt->objtype);
}
--- 360,365 ----
***************
*** 404,409 **** ExecuteGrantStmt(GrantStmt *stmt)
--- 401,408 ----
{
/* Column-level privileges given. */
ListCell *cell_obj;
+ const char *errormsg_col = gettext_noop("invalid privilege type %s for column");
+
/* Must be of objtype ACL_OBJECT_RELATION for column
* level privileges */
Alex,
* Alex Hunsaker (badalex@gmail.com) wrote:
Now it looks bogos but why not just move errmsg_col down to where we
actually use it? Or am I missing something?
Honestly, I think at this point is makes the most sense to just remove
it entirely, which I've done in the attached patch.
Thanks,
Stephen
Attachments:
Hello Stephen,
Stephen Frost wrote:
..in the attached patch.
Thanks, I've reviewed this patch again.
Please find attached an updated patch for column-level privileges
which incorporates Alvaro's suggested changes and is updated to the
latest CVS HEAD.
Cool, applies cleanly and compiles without any error or warning on my
Debian box. Regression tests pass fine as well.
Regression tests have been added as well as
documentation (though this could probably be improved).
Sorry I didn't get around writing docu. Looks good so far. Some more
hints on its usage wouldn't hurt, especially because the error messages
aren't overly verbose ('permission denied..' doesn't tell you much).
Currently,
column-level privileges are not honored when JOINs are involved (you
must have the necessary table-level privileges, as you do today). It
would really be great to have that working and mainly involves
modifying the rewriter to add on to the appropriate range table column
list entries the columns which are used in the joins and output from
joins.
Understood. Do you plan to work on that? Or do you think the patch
should go into 8.4 even without support for JOINs?
Experimenting with relation vs column level privileges, I've discovered
a strange behavior:
test=# GRANT SELECT ON test TO joe;
GRANT
test=# GRANT SELECT(id) ON test TO joe;
GRANT
test=# REVOKE SELECT ON test FROM joe;
ERROR: tuple already updated by self
That's odd. Maybe you need to increment the command counter in between
two updates of that tuple?
Also note, that I'm unsure about what to expect from this REVOKE. Is it
intended to remove the column level privilege as well or not?
Removing the column level privilege first, then the relation level one
works:
test=# REVOKE SELECT(id) ON test FROM joe;
REVOKE
test=# REVOKE SELECT ON test FROM joe;
REVOKE
I've also tested column level privileges on hidden attributes (xmin,
xmax, ctid, tableoid, cmin), which works fine for SELECTs. However, you
might want to filter out INSERT and UPDATE privileges on those, as those
don't make much sense:
test=# GRANT UPDATE(xmin) ON test TO joe;
GRANT
test=# GRANT INSERT(xmin) ON test TO joe;
GRANT
[ Note that user joe can INSERT or UPDATE tuples of relation test even
without those column level privileges, as long as he is allowed to
INSERT or UPDATE the affected non-hidden columns. ]
Some minor nit-picks: some lines exceed 80 columns, multi-line comments
don't follow coding standards.
BTW: how are long constant strings expected to be formatted? Are those
allowed to exceed 80 columns, or are they expected to be split like so
(i.e. for errmsg):
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed "
"do eiusmod tempor incididunt ut labore et dolore magna aliqua."
Nice work! I'd like to see this shipping with 8.4. The above mentioned
bugs (the "updated by self" and the hidden columns) should be easy
enough to fix, I think. Respecting columns level privileges for JOINs is
probably going to be more work, but is required as well, IMO.
Regards
Markus Wanner
Markus,
* Markus Wanner (markus@bluegap.ch) wrote:
Stephen Frost wrote:
..in the attached patch.
Thanks, I've reviewed this patch again.
Thanks!
Currently,
column-level privileges are not honored when JOINs are involved (you
must have the necessary table-level privileges, as you do today). It
would really be great to have that working and mainly involves
modifying the rewriter to add on to the appropriate range table column
list entries the columns which are used in the joins and output from
joins.Understood. Do you plan to work on that? Or do you think the patch
should go into 8.4 even without support for JOINs?
I'm going to look into it but it's a bit complicated. I was hoping
someone who's more familiar with those parts would be able to look at
it.
Experimenting with relation vs column level privileges, I've discovered
a strange behavior:test=# GRANT SELECT ON test TO joe;
GRANT
test=# GRANT SELECT(id) ON test TO joe;
GRANT
test=# REVOKE SELECT ON test FROM joe;
ERROR: tuple already updated by selfThat's odd. Maybe you need to increment the command counter in between
two updates of that tuple?
I don't think that's the right approach, but I'll look into it. I ran
into a similiar issue though, and I don't believe it's too hard to fix
(the issue here is that the REVOKE needs to remove the column-level grant
as well). I'll try and look into it tonight or tomorrow.
Also note, that I'm unsure about what to expect from this REVOKE. Is it
intended to remove the column level privilege as well or not?
Yes, the SQL spec requires that a table-level REVOKE also revoke all
column-level grants as well.
Removing the column level privilege first, then the relation level one
works:test=# REVOKE SELECT(id) ON test FROM joe;
REVOKE
test=# REVOKE SELECT ON test FROM joe;
REVOKE
This is essentially what should happen automagically.
I've also tested column level privileges on hidden attributes (xmin,
xmax, ctid, tableoid, cmin), which works fine for SELECTs. However, you
might want to filter out INSERT and UPDATE privileges on those, as those
don't make much sense:test=# GRANT UPDATE(xmin) ON test TO joe;
GRANT
test=# GRANT INSERT(xmin) ON test TO joe;
GRANT
Hmm, ok, that's easy enough to fix.
[ Note that user joe can INSERT or UPDATE tuples of relation test even
without those column level privileges, as long as he is allowed to
INSERT or UPDATE the affected non-hidden columns. ]
Right, that's correct. You don't need table-level permissions so long
as you have permissions on the columns you're trying to select/modify.
Some minor nit-picks: some lines exceed 80 columns, multi-line comments
don't follow coding standards.
Hrmpf. I'll go back and review the coding standards.. I don't recall
that 80 column was a fixed limit.
BTW: how are long constant strings expected to be formatted? Are those
allowed to exceed 80 columns, or are they expected to be split like so
(i.e. for errmsg):"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed "
"do eiusmod tempor incididunt ut labore et dolore magna aliqua."
Honestly, I think I've seen both done. I can do it either way, of
course.
Nice work! I'd like to see this shipping with 8.4. The above mentioned
bugs (the "updated by self" and the hidden columns) should be easy
enough to fix, I think. Respecting columns level privileges for JOINs is
probably going to be more work, but is required as well, IMO.
Thanks for the review!
Stephen
Hello Stephen,
Stephen Frost wrote:
I'm going to look into it but it's a bit complicated. I was hoping
someone who's more familiar with those parts would be able to look at
it.
Good to hear. I've just been asking, because it remained unclear to me.
I don't think that's the right approach, but I'll look into it. I ran
into a similiar issue though, and I don't believe it's too hard to fix
(the issue here is that the REVOKE needs to remove the column-level grant
as well). I'll try and look into it tonight or tomorrow.
Cool, because that's the biggest issue, IMO.
test=# GRANT UPDATE(xmin) ON test TO joe;
GRANT
test=# GRANT INSERT(xmin) ON test TO joe;
GRANTHmm, ok, that's easy enough to fix.
[ Note that user joe can INSERT or UPDATE tuples of relation test even
without those column level privileges, as long as he is allowed to
INSERT or UPDATE the affected non-hidden columns. ]Right, that's correct. You don't need table-level permissions so long
as you have permissions on the columns you're trying to select/modify.
I was trying to check, if these privileges on hidden columns have any
effect. So far I didn't encounter any, except for SELECT.
Some minor nit-picks: some lines exceed 80 columns, multi-line comments
don't follow coding standards.Hrmpf. I'll go back and review the coding standards.. I don't recall
that 80 column was a fixed limit.
Hm.. sorry, looks like it's not mentioned in the docu. I'm pretty sure
pgindent strips lines to something below 80 columns, though. (And I'm
personally used to terminals with exactly 80 cols, so everything longer
than is not easy to my eyes, thus the complaint. Don't bother much).
BTW: how are long constant strings expected to be formatted? Are those
allowed to exceed 80 columns, or are they expected to be split like so
(i.e. for errmsg):"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed "
"do eiusmod tempor incididunt ut labore et dolore magna aliqua."Honestly, I think I've seen both done.
Yeah, that's why I'm asking.
Regards
Markus Wanner
Markus Wanner <markus@bluegap.ch> writes:
Stephen Frost wrote:
BTW: how are long constant strings expected to be formatted? Are those
allowed to exceed 80 columns, or are they expected to be split like soHonestly, I think I've seen both done.
Yeah, that's why I'm asking.
IMHO, the trouble with breaking up error strings like that is that it
can defeat attempts to grep the source for a particular error message.
(If you search for "foo bar baz", you won't find it if someone chose
to break the string between those words.) This isn't too harmful if
you get no hits, because you can try again with a substring --- but it
really sucks if some instances of the string are broken up differently
than others, because you might see only the wrong instances and come
to a mistaken conclusion about the possible sources of an error reported
from the field. So I tend not to like breaking up long strings at all,
and definitely look with disfavor on breaking them in random spots
rather than natural break points of the sentence.
Because of that, I tend not to worry about holding to the 80-column
window width when it comes to error message strings. Other than that
case, though, you should try to stay to less than 80 columns. If you
don't, pgindent will do it for you, and the end result will probably
be uglier than if you'd made the code fit manually.
regards, tom lane
Markus Wanner wrote:
Hello Stephen,
Stephen Frost wrote:
..in the attached patch.
Thanks, I've reviewed this patch again.
Please find attached an updated patch for column-level privileges
which incorporates Alvaro's suggested changes and is updated to the
latest CVS HEAD.Cool, applies cleanly and compiles without any error or warning on my
Debian box. Regression tests pass fine as well.Regression tests have been added as well as
documentation (though this could probably be improved).Sorry I didn't get around writing docu. Looks good so far. Some more
hints on its usage wouldn't hurt, especially because the error messages
aren't overly verbose ('permission denied..' doesn't tell you much).Currently,
column-level privileges are not honored when JOINs are involved (you
must have the necessary table-level privileges, as you do today). It
would really be great to have that working and mainly involves
modifying the rewriter to add on to the appropriate range table column
list entries the columns which are used in the joins and output from
joins.Understood. Do you plan to work on that? Or do you think the patch
should go into 8.4 even without support for JOINs?
I tried to check the latest Column-level privileges patch.
This trouble related to JOINs is come from inappropriate handling
of rte->cols_sel list, in my opinion.
The list is constructed at scanRTEForColumn() and expandRelation().
However, scanRTEForColumn() appends an appeared attribute number
even if given RangeTblEntry is RTE_JOIN, and expandRelation() appends
all the attribute number contained within the given relation.
I think your design is basically fine, so the issue is minor one.
But it is necessary to pick up carefully what columns are really
accessed.
Here is one proposition.
Is it possible to implement a walker function to pick up appeared
columns and to chain them on rte->cols_sel/cols_mod?
In this idea, columns in Query->targetList should be chained on
rte->cols_mod, and others should be chained on rte->cols_sel.
Here is an example to pick up all appeared relation:
http://code.google.com/p/sepgsql/source/browse/trunk/sepgsql/src/backend/security/rowacl/rowacl.c#30
If you don't have enough availability, I'll be able to do it within
a few days.
Thanks,
--
OSS Platform Development Division, NEC
KaiGai Kohei <kaigai@ak.jp.nec.com>
Currently,
column-level privileges are not honored when JOINs are involved (you
must have the necessary table-level privileges, as you do today). It
would really be great to have that working and mainly involves
modifying the rewriter to add on to the appropriate range table column
list entries the columns which are used in the joins and output from
joins.Understood. Do you plan to work on that? Or do you think the patch
should go into 8.4 even without support for JOINs?I tried to check the latest Column-level privileges patch.
This trouble related to JOINs is come from inappropriate handling
of rte->cols_sel list, in my opinion.
The list is constructed at scanRTEForColumn() and expandRelation().
However, scanRTEForColumn() appends an appeared attribute number
even if given RangeTblEntry is RTE_JOIN, and expandRelation() appends
all the attribute number contained within the given relation.I think your design is basically fine, so the issue is minor one.
But it is necessary to pick up carefully what columns are really
accessed.Here is one proposition.
Is it possible to implement a walker function to pick up appeared
columns and to chain them on rte->cols_sel/cols_mod?
In this idea, columns in Query->targetList should be chained on
rte->cols_mod, and others should be chained on rte->cols_sel.Here is an example to pick up all appeared relation:
http://code.google.com/p/sepgsql/source/browse/trunk/sepgsql/src/backend/security/rowacl/rowacl.c#30If you don't have enough availability, I'll be able to do it within
a few days.
The attached patch is a proof of the concept.
It walks on a given query tree to append accessed columns on
rte->cols_sel and rte->cols_mod.
When aliasvar of JOIN'ed relation is accesses, its source is
appended on the list.
This patch can be applied on the latest CVS HEAD with Stephen's
colprivs_wip.2009010201.diff.gz.
Any comment?
I strongly want the Column-level privileges to be get merged
as soon as possible, so I don't spare any possible assist
for his works.
Thanks,
---- example of the patched Column-level privileges ----
postgres=# CREATE TABLE t1 (a int, b text, c bool);
CREATE TABLE
postgres=# CREATE TABLE t2 (x int, y text, z bool);
CREATE TABLE
postgres=# GRANT SELECT(a,b) ON t1 TO ymj;
GRANT
postgres=# GRANT UPDATE(a,b) ON t1 TO ymj;
GRANT
postgres=# GRANT SELECT(x,y) ON t2 TO ymj;
GRANT
postgres=# INSERT INTO t1 VALUES (1, 'aaa', true), (2, 'bbb', null), (3, 'ccc', false);
INSERT 0 3
postgres=# INSERT INTO t2 VALUES (1, 'xxx', false), (2, 'yyy', null), (3, 'zzz', true);
INSERT 0 3
postgres=# \c - ymj
psql (8.4devel)
You are now connected to database "postgres" as user "ymj".
postgres=> SELECT * FROM t1;
NOTICE: pg_attribute_aclmask: t1.a required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.b required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.c required: 0002 allowed: 0000
ERROR: permission denied for relation t1
postgres=> SELECT a,b FROM t1;
NOTICE: pg_attribute_aclmask: t1.a required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.b required: 0002 allowed: 0002
a | b
---+-----
1 | aaa
2 | bbb
3 | ccc
(3 rows)
postgres=> SELECT x,y FROM t2;
NOTICE: pg_attribute_aclmask: t2.x required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t2.y required: 0002 allowed: 0002
x | y
---+-----
1 | xxx
2 | yyy
3 | zzz
(3 rows)
postgres=> SELECT b,y FROM t1 JOIN t2 ON a = x;
NOTICE: pg_attribute_aclmask: t1.b required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.a required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t2.y required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t2.x required: 0002 allowed: 0002
b | y
-----+-----
aaa | xxx
bbb | yyy
ccc | zzz
(3 rows)
postgres=> SELECT b,z FROM t1 JOIN t2 ON a = x;
NOTICE: pg_attribute_aclmask: t1.b required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.a required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t2.z required: 0002 allowed: 0000
ERROR: permission denied for relation t2
postgres=> UPDATE t1 SET a = 1;
NOTICE: pg_attribute_aclmask: t1.a required: 0004 allowed: 0004
UPDATE 3
postgres=> UPDATE t1 SET c = true;
NOTICE: pg_attribute_aclmask: t1.c required: 0004 allowed: 0000
ERROR: permission denied for relation t1
postgres=>
--
OSS Platform Development Division, NEC
KaiGai Kohei <kaigai@ak.jp.nec.com>
Attachments:
colprivs_fix_joins.20090107.difftext/x-diff; name=colprivs_fix_joins.20090107.diffDownload
Index: src/backend/parser/analyze.c
===================================================================
*** src/backend/parser/analyze.c (revision 1)
--- src/backend/parser/analyze.c (working copy)
***************
*** 27,32 ****
--- 27,33 ----
#include "catalog/pg_type.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
+ #include "optimizer/tlist.h"
#include "optimizer/var.h"
#include "parser/analyze.h"
#include "parser/parse_agg.h"
***************
*** 267,272 ****
--- 268,338 ----
}
/*
+ * applyColumnPrivs()
+ * construct rte->cols_sel and rte->cols_mod for Column-level
+ * privileges.
+ */
+ static bool
+ applyColumnPrivsWalker(Node *node, ParseState *pstate)
+ {
+ if (!node)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ RangeTblEntry *rte;
+ Var *v = (Var *) node;
+ int lv;
+
+ for (lv = v->varlevelsup; lv > 0; lv--)
+ {
+ Assert(pstate->parentParseState != NULL);
+ pstate = pstate->parentParseState;
+ }
+
+ rte = rt_fetch(v->varno, pstate->p_rtable);
+ Assert(IsA(rte, RangeTblEntry));
+
+ if (rte->rtekind == RTE_RELATION)
+ {
+ rte->cols_sel = lappend_int(rte->cols_sel, v->varattno);
+ }
+ else if (rte->rtekind == RTE_JOIN)
+ {
+ node = list_nth(rte->joinaliasvars, v->varattno - 1);
+
+ applyColumnPrivsWalker(node, pstate);
+ }
+ }
+ else if (IsA(node, SortGroupClause))
+ return false;
+
+ return expression_tree_walker(node, applyColumnPrivsWalker, (void *) pstate);
+ }
+
+ static void
+ applyColumnPrivs(Query *query, ParseState *pstate)
+ {
+ ListCell *l;
+
+ if (query->commandType != CMD_SELECT)
+ {
+ RangeTblEntry *rte
+ = rt_fetch(query->resultRelation, query->rtable);
+
+ foreach (l, query->targetList)
+ {
+ TargetEntry *tle = lfirst(l);
+
+ Assert(IsA(tle, TargetEntry));
+ rte->cols_mod = lappend_int(rte->cols_mod, tle->resno);
+ }
+ }
+ query_tree_walker(query, applyColumnPrivsWalker, (void *) pstate,
+ QTW_IGNORE_JOINALIASES);
+ }
+
+ /*
* transformDeleteStmt -
* transforms a Delete Statement
*/
***************
*** 683,688 ****
--- 749,756 ----
parser_errposition(pstate,
locate_windowfunc((Node *) qry))));
+ applyColumnPrivs(qry, pstate);
+
return qry;
}
***************
*** 876,881 ****
--- 944,951 ----
transformLockingClause(pstate, qry, (LockingClause *) lfirst(l));
}
+ applyColumnPrivs(qry, pstate);
+
return qry;
}
***************
*** 1716,1721 ****
--- 1786,1793 ----
if (origTargetList != NULL)
elog(ERROR, "UPDATE target count mismatch --- internal error");
+ applyColumnPrivs(qry, pstate);
+
return qry;
}
Index: src/backend/parser/parse_relation.c
===================================================================
*** src/backend/parser/parse_relation.c (revision 2)
--- src/backend/parser/parse_relation.c (working copy)
***************
*** 479,485 ****
result = (Node *) make_var(pstate, rte, attnum, location);
/* Require read access */
rte->requiredPerms |= ACL_SELECT;
- rte->cols_sel = lappend_int(rte->cols_sel,attnum);
}
}
--- 479,484 ----
***************
*** 508,514 ****
result = (Node *) make_var(pstate, rte, attnum, location);
/* Require read access */
rte->requiredPerms |= ACL_SELECT;
- rte->cols_sel = lappend_int(rte->cols_sel,attnum);
}
}
}
--- 507,512 ----
***************
*** 1749,1762 ****
expandTupleDesc(rel->rd_att, rte->eref, rtindex, sublevels_up,
location, include_dropped,
colnames, colvars);
-
- for (attrno = 0; attrno < rel->rd_att->natts; attrno++)
- {
- Form_pg_attribute attr = rel->rd_att->attrs[attrno];
-
- if (!attr->attisdropped)
- rte->cols_sel = lappend_int(rte->cols_sel, attrno+1);
- }
relation_close(rel, AccessShareLock);
}
--- 1747,1752 ----
Index: src/backend/parser/parse_target.c
===================================================================
*** src/backend/parser/parse_target.c (revision 2)
--- src/backend/parser/parse_target.c (working copy)
***************
*** 372,378 ****
attrtype = attnumTypeId(rd, attrno);
attrtypmod = rd->rd_att->attrs[attrno - 1]->atttypmod;
- pstate->p_target_rangetblentry->cols_mod = lappend_int(pstate->p_target_rangetblentry->cols_mod, attrno);
/*
* If the expression is a DEFAULT placeholder, insert the attribute's
--- 372,377 ----
***************
*** 785,791 ****
col->location = -1;
cols = lappend(cols, col);
*attrnos = lappend_int(*attrnos, i + 1);
- pstate->p_target_rangetblentry->cols_mod = lappend_int(pstate->p_target_rangetblentry->cols_mod, i + 1);
}
}
else
--- 784,789 ----
***************
*** 842,848 ****
}
*attrnos = lappend_int(*attrnos, attrno);
- pstate->p_target_rangetblentry->cols_mod = lappend_int(pstate->p_target_rangetblentry->cols_mod, attrno);
}
}
--- 840,845 ----
Index: src/backend/catalog/aclchk.c
===================================================================
*** src/backend/catalog/aclchk.c (revision 2)
--- src/backend/catalog/aclchk.c (working copy)
***************
*** 2291,2296 ****
--- 2291,2302 ----
pfree(acl);
+ elog(NOTICE, "%s: %s.%s required: %04x allowed: %04x",
+ __FUNCTION__,
+ NameStr(classForm->relname),
+ NameStr(((Form_pg_attribute) GETSTRUCT(attTuple))->attname),
+ mask, result);
+
/* if we have a detoasted copy, free it */
if (colacl && (Pointer) colacl != DatumGetPointer(colaclDatum))
pfree(colacl);
Hi,
KaiGai Kohei wrote:
The attached patch is a proof of the concept.
Awesome! I'll try to review during the day.
I strongly want the Column-level privileges to be get merged
as soon as possible, so I don't spare any possible assist
for his works.
+1
Can you quickly comment on CLP vs. SE-Postgres. Are these dependent or
completely orthogonal? I didn't follow the SE-Postgres thread very closely.
Regards
Markus Wanner
Markus Wanner wrote:
Hi,
KaiGai Kohei wrote:
The attached patch is a proof of the concept.
Awesome! I'll try to review during the day.
I strongly want the Column-level privileges to be get merged
as soon as possible, so I don't spare any possible assist
for his works.+1
Can you quickly comment on CLP vs. SE-Postgres. Are these dependent or
completely orthogonal? I didn't follow the SE-Postgres thread very closely.
These are completely orthogonal.
However, in previous discussion, we decided CLP is a prerequisite to
merge SE-PostgreSQL feature.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
KaiGai,
* KaiGai Kohei (kaigai@ak.jp.nec.com) wrote:
Is it possible to implement a walker function to pick up appeared
columns and to chain them on rte->cols_sel/cols_mod?
In this idea, columns in Query->targetList should be chained on
rte->cols_mod, and others should be chained on rte->cols_sel.
This sounds like a reasonable approach to me, but as I mentioned before,
I'm not very familiar with the analyzer and company.
The attached patch is a proof of the concept.
Excellent, I'll play around with it.
Any comment?
I'm generally not a huge fan of recursion simply because it's often
overrated and overused and implements a limit based on stack depth which
can cause unexpected failures. Can we be confident that the recursion
added here doesn't add a new limit on the size/complexity of queries
which, if hit, will cause a stack overflow? I notice that we do use
recursion in some other places, but we also occationally have checks to
prevent recursing too far.
I strongly want the Column-level privileges to be get merged
as soon as possible, so I don't spare any possible assist
for his works.
Thanks so much for your help! It's definitely appriciated. I'm going
to try and play with your patch today and probably add some additional
regression tests and make sure everything works as expected.
Thanks again!
Stephen
Stephen Frost wrote:
KaiGai,
* KaiGai Kohei (kaigai@ak.jp.nec.com) wrote:
Is it possible to implement a walker function to pick up appeared
columns and to chain them on rte->cols_sel/cols_mod?
In this idea, columns in Query->targetList should be chained on
rte->cols_mod, and others should be chained on rte->cols_sel.This sounds like a reasonable approach to me, but as I mentioned before,
I'm not very familiar with the analyzer and company.The attached patch is a proof of the concept.
Excellent, I'll play around with it.
Any comment?
I'm generally not a huge fan of recursion simply because it's often
overrated and overused and implements a limit based on stack depth which
can cause unexpected failures. Can we be confident that the recursion
added here doesn't add a new limit on the size/complexity of queries
which, if hit, will cause a stack overflow? I notice that we do use
recursion in some other places, but we also occationally have checks to
prevent recursing too far.
I think it does not give us any other new size/complexity limitation,
because we already have some of recursive walker functions which
consume stack more than applyColumnPrivsWalker() I proposed.
The applyColumnPrivsWalker() consumes its stack for a return address,
two pointers and an integer variables for each depth.
But, for example, existing check_ungrouped_columns_walker() consumes
it more.
In addition, please note that expression_tree_walker() invokes
check_stack_depth() to prevent unexpected stack overflow.
Thanks,
I strongly want the Column-level privileges to be get merged
as soon as possible, so I don't spare any possible assist
for his works.Thanks so much for your help! It's definitely appriciated. I'm going
to try and play with your patch today and probably add some additional
regression tests and make sure everything works as expected.Thanks again!
Stephen
--
OSS Platform Development Division, NEC
KaiGai Kohei <kaigai@ak.jp.nec.com>
KaiGai,
* KaiGai Kohei (kaigai@ak.jp.nec.com) wrote:
In addition, please note that expression_tree_walker() invokes
check_stack_depth() to prevent unexpected stack overflow.
Ah, that's the part I was looking for and somehow overlooked. As long
as we're checking at some point then I worry alot less. I'm currently
working out some dependency kinks (which is the real source of the
ERROR: tuple updated by self that Markus ran into). I'm planning to
post a new patch tomorrow which includes your patch, some additional
regression tests, and the dependency fix.
Thanks,
Stephen
On Wed, Jan 7, 2009 at 1:46 AM, KaiGai Kohei <kaigai@ak.jp.nec.com> wrote:
The attached patch is a proof of the concept.
It walks on a given query tree to append accessed columns on
rte->cols_sel and rte->cols_mod.
When aliasvar of JOIN'ed relation is accesses, its source is
appended on the list.
for my test i created to tables:
CREATE TABLE t1 (col1 int primary key, col2 int);
CREATE TABLE t2 (col1 int references t1);
and a role:
CREATE ROLE rol1;
then i granted all cols in the table to the role:
GRANT SELECT (col1) ON t1 TO rol1;
GRANT SELECT (col2) ON t1 TO rol1;
GRANT SELECT (col1) ON t2 TO rol1;
prueba=> \dp
Access privileges
Schema | Name | Type | Access privileges | Column Access privileges
--------+------+-------+---------------------------+--------------------------
public | t1 | table | postgres=arwdDxt/postgres | col1
: postgres=arw/postgres
: rol1=r/postgres
: col2
: postgres=arw/postgres
: rol1=r/postgres
public | t2 | table | postgres=arwdDxt/postgres | col1
: postgres=arw/postgres
: rol1=r/postgres
(2 rows)
then i execute:
prueba=> select t1.* from t1, t2 where t1.col1 = t2.col1;
NOTICE: pg_attribute_aclmask: t1.col1 required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.col2 required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.col1 required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t2.col1 required: 0002 allowed: 0002
col1 | col2
------+------
(0 rows)
good, but if i doesn't include filter conditions:
prueba=> select t1.* from t1, t2;
NOTICE: pg_attribute_aclmask: t1.col1 required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.col2 required: 0002 allowed: 0002
ERROR: permission denied for relation t2
is this intended?
--
Atentamente,
Jaime Casanova
Soporte y capacitación de PostgreSQL
Asesoría y desarrollo de sistemas
Guayaquil - Ecuador
Cel. +59387171157
Jaime Casanova wrote:
On Wed, Jan 7, 2009 at 1:46 AM, KaiGai Kohei <kaigai@ak.jp.nec.com> wrote:
The attached patch is a proof of the concept.
It walks on a given query tree to append accessed columns on
rte->cols_sel and rte->cols_mod.
When aliasvar of JOIN'ed relation is accesses, its source is
appended on the list.for my test i created to tables:
CREATE TABLE t1 (col1 int primary key, col2 int);
CREATE TABLE t2 (col1 int references t1);and a role:
CREATE ROLE rol1;
then i granted all cols in the table to the role:
GRANT SELECT (col1) ON t1 TO rol1;
GRANT SELECT (col2) ON t1 TO rol1;
GRANT SELECT (col1) ON t2 TO rol1;prueba=> \dp
Access privileges
Schema | Name | Type | Access privileges | Column Access privileges
--------+------+-------+---------------------------+--------------------------
public | t1 | table | postgres=arwdDxt/postgres | col1
: postgres=arw/postgres
: rol1=r/postgres
: col2
: postgres=arw/postgres
: rol1=r/postgres
public | t2 | table | postgres=arwdDxt/postgres | col1
: postgres=arw/postgres
: rol1=r/postgres
(2 rows)then i execute:
prueba=> select t1.* from t1, t2 where t1.col1 = t2.col1;
NOTICE: pg_attribute_aclmask: t1.col1 required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.col2 required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.col1 required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t2.col1 required: 0002 allowed: 0002
col1 | col2
------+------
(0 rows)good, but if i doesn't include filter conditions:
prueba=> select t1.* from t1, t2;
NOTICE: pg_attribute_aclmask: t1.col1 required: 0002 allowed: 0002
NOTICE: pg_attribute_aclmask: t1.col2 required: 0002 allowed: 0002
ERROR: permission denied for relation t2is this intended?
Basically, I want to wait for Stephen's opinion.
However, it seem's to me it is a correct behavior.
ExecCheckRTEPerms() checks user's privileges on columns, when he does
not have required privileges on the table. When he has proper privileges
on all the appeared columns within the table, it is allowed.
But, when no columns are used on the table, it applies result of checks
on the table.
In this example, "rol1" does not have any privileges on relation "t1"
and "t2", but he can select "t1.col1", "t1.col2" and "t2.col1".
Since he does not have any privs on relations, column's privs are
checked in both of queries.
In the first query, he uses "col1" and "col2" for "t1" and "col1" for
"t2", and all of them are allowed to select. So, he got succeeded.
In the other query, he uses "col1" and "col2" for "t1" but no columns
for "t2", so the result of checks on relation "t2" is applied.
Stephen, could you indicate us what behavior is proper in this case?
Thanks,
--
OSS Platform Development Division, NEC
KaiGai Kohei <kaigai@ak.jp.nec.com>
KaiGai Kohei <kaigai@ak.jp.nec.com> writes:
ExecCheckRTEPerms() checks user's privileges on columns, when he does
not have required privileges on the table. When he has proper privileges
on all the appeared columns within the table, it is allowed.
But, when no columns are used on the table, it applies result of checks
on the table.
Surely the SQL spec tells us what to do here (and I cannot believe this
is it...)
regards, tom lane
Tom, et al,
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
KaiGai Kohei <kaigai@ak.jp.nec.com> writes:
ExecCheckRTEPerms() checks user's privileges on columns, when he does
not have required privileges on the table. When he has proper privileges
on all the appeared columns within the table, it is allowed.
But, when no columns are used on the table, it applies result of checks
on the table.Surely the SQL spec tells us what to do here (and I cannot believe this
is it...)
Based on what I see in the SQL spec, we have to allow table references
like this when the user has SELECT rights on at least one column of the
table. If the column is referenced anywhere (SELECT clause, WHERE
clause, JOIN clause, through a NATURAL JOIN, etc) then the user must
have SELECT rights on the mentioned column(s).
I'm open to suggestions about how to handle this. My first thought
would be- add an entry to the cols_sel list for the RTE that is special
and indicates "any column", perhaps by using a '0' for the attrid, as is
done elsewhere. Then modify ExecCheckRTEPerms() to handle it.
Thanks,
Stephen
Stephen Frost <sfrost@snowman.net> writes:
I'm open to suggestions about how to handle this. My first thought
would be- add an entry to the cols_sel list for the RTE that is special
and indicates "any column", perhaps by using a '0' for the attrid, as is
done elsewhere. Then modify ExecCheckRTEPerms() to handle it.
Wouldn't it be sufficient to treat an empty cols_sel list that way?
regards, tom lane
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Stephen Frost <sfrost@snowman.net> writes:
I'm open to suggestions about how to handle this. My first thought
would be- add an entry to the cols_sel list for the RTE that is special
and indicates "any column", perhaps by using a '0' for the attrid, as is
done elsewhere. Then modify ExecCheckRTEPerms() to handle it.Wouldn't it be sufficient to treat an empty cols_sel list that way?
I've thought about it some, and yes, that sounds reasonable. I'll try
and implement it tonight and test it out.
Thanks!
Stephen
Stephen Frost wrote:
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Stephen Frost <sfrost@snowman.net> writes:
I'm open to suggestions about how to handle this. My first thought
would be- add an entry to the cols_sel list for the RTE that is special
and indicates "any column", perhaps by using a '0' for the attrid, as is
done elsewhere. Then modify ExecCheckRTEPerms() to handle it.Wouldn't it be sufficient to treat an empty cols_sel list that way?
I've thought about it some, and yes, that sounds reasonable. I'll try
and implement it tonight and test it out.
I also think it is a reasonable solution.
In addition, please note that the following case:
| postgres=# SELECT t1 FROM t1;
| t1
| ---------
| (1,aaa)
| (2,bbb)
| (3,ccc)
When we refer table-rowtype, analyzer handles its Var->varattno has
InvalidAttrNumber (=0). If SQL standard mention nothing, it is quite
natural to consider it refers whole of the user columns.
I think it can share the code to handle the above empty cols_sel cases.
Thanks,
--
OSS Platform Development Division, NEC
KaiGai Kohei <kaigai@ak.jp.nec.com>
KaiGai,
* KaiGai Kohei (kaigai@ak.jp.nec.com) wrote:
I've thought about it some, and yes, that sounds reasonable. I'll try
and implement it tonight and test it out.
I've implemented this and it appears to work well.
When we refer table-rowtype, analyzer handles its Var->varattno has
InvalidAttrNumber (=0). If SQL standard mention nothing, it is quite
natural to consider it refers whole of the user columns.
I've also updated the patch to handle this correctly.
I still have a dependency tracking issue that I'm trying to nail down.
I'll post an updated patch tonight regardless, however.
Thanks,
Stephen
KaiGai, Tom, all,
Attached is an updated patch which fixes Markus' dependency issue,
handles the other issues mentioned by KaiGai and Tom, has some
additional regressions tests to make sure all of that works, and has
been cleaned up to the 80-col goal (where it seemed to work well and
make sense..).
Please let me know if you have comments. Also, as is always the
case, testing by others is appreciated.
Thanks,
Stephen
Attachments:
On Thu, Jan 8, 2009 at 11:34 PM, Stephen Frost <sfrost@snowman.net> wrote:
Please let me know if you have comments.
a fast test seems to show that this documentation change is no longer
true ;) nice work!
--- 443,450 ----
</para>
<para>
! Currently, <productname>PostgreSQL</productname> does not recognize
! column-level SELECT privileges when a JOIN is involved.
One possible workaround is to create a view having just the desired
columns and then grant privileges to that view.
</para>
***************
--
Atentamente,
Jaime Casanova
Soporte y capacitación de PostgreSQL
Asesoría y desarrollo de sistemas
Guayaquil - Ecuador
Cel. +59387171157
Jamie, et al,
* Jaime Casanova (jcasanov@systemguards.com.ec) wrote:
<para>
! Currently, <productname>PostgreSQL</productname> does not recognize
! column-level SELECT privileges when a JOIN is involved.
One possible workaround is to create a view having just the desired
columns and then grant privileges to that view.
</para>
Remove this from the documentation, and:
- Other minor cleanups (thanks KaiGai)
- Added pg_dump support
- Added support for 'ALL(col1)' grant/revokes (helped with pg_dump)
- Added more regression tests
- Updated documentation accordingly
Please test, comment, etc.
Thanks,
Stephen
Attachments:
colprivs_wip.2009010901.diff.gzapplication/octet-streamDownload
�3�gI colprivs_wip.2009010901.diff �<�w�V�?����{6G�lc;��PL��b��;iG��Q#$* �������O �$��l�����~��;3w���n8����8����3�=7u��&���''������b?�Gl{G?q/M���$��t{~��l�G��}�4������$~��V������LX�c���Y����vV:d�t���!������uV��OI �V�;�y���+��������7��n�P��M{���O��o����t=?WO<L����$�^:�y0>�F?%��?>��[�����������a�{�M_�\����7Q�\����3/
��a��O��NU��Y�����S���l��I"�b>(i�2p��~��`R��8�`T� 0H�}&7�����82��m[2�9�
�A��j�Y��L|�px�#�q����
�K���?<�lH���o����B'tg�t~3�V�5o���-ZT�U�v�lj�&v�����,kCm����>2���Z��3���>`�����Qm_j���r�?M+�kN}�hxl��6xB��'�]�?��(��E�e���_�W��N{��8e���~�q���^w�����~�i3���q���������n���@��g�A�w�����C
�Y����a���w������������A��� � H�;x���J����������a�Q!�������n��k������W����.��k���������{v]�e����E[��M��������}������_�yL������+N�4��s7v����9���l�)�x[=2��M]�L���������pr�#���S2'S7�"a����t��C75��*e���n}�tX�^����@G
v�$��V��-cw>��n�i�-=/B/&��>��xZ�nx�po R�M��u�8Z:��*YM�����2�hy��?g>� l�H�P�gn�Y�����q��-�;0��EJ���p���b��������B�����X���e���_�f���z,�H�9���~����|�.���������z�b���b��=�1��5e��-�Y��
8`a���Q R�zOd���"p"(jH�������(7�52�{T#����-�����\3Z �����,��w�}=�d�JT��&�Zc�4����!��%N!g��������������� �� {f9���
�h���w���<�*���
���n���o��I�����\# (��{>�
��^�)��e��D� >8y��qb�p���]Cl��
.,��]���C���`$�1D��wH8�E��[7X�$��8{5 �A6��R�����S��#>v��� ��3��<��������?��g�h�[R�����Bk�P��y�!�o �7�����vfS�-��I|��Zs����3P���dU����Rw05J�����;6������I�6��t/�PpBk��>��.�`Yx_���Cx�'�a��ES��w���5�s^��"C�����p�^�1����x"��Y��-y������)�-��R�����4��q������(c�.�@�~;���7_��D���������������K�U��n�9�{��Q2g����*=�1��w���X�/�$���������v6�8z�)K�y��vY���Fo���������|��@x�u���e��`%����2���q� ~�D�%��d�!����OH�����5�����hc
��p����!0��"V��g[����������y��������F���R"A?���k���Ej����BY�pg��a���b(�v��&}�~����rf�p�
\<iW��������0�=`��-+��T��y5���d?���@�+���;�!L����r|�.��F
|��I��=0��c8Q�/��hU��s��{���[��9= �~�i�Y��Ef]����f�}�����:��'�`=�j����=��x.R�2-|���\���� Gf��B�N"�_"���T�
�|��"J���T;+�����;�jW��2s"���n(�.�$��cA.�L���C�g3�b5�"�F�l�`vN�b��1g��e�c�M$�����O�����
��\�HB��5�)��y �|P
�2��&~V/��G*�`tW��������^Q���`�J��?N��gc��Vq��=�f�s���RDW�X&"���`�������l|�W��\Y��&)���c������C�5�i�Q��x��*��� E�t
���Z/;&����"Ee8������#�M]��8&�R�C@/ G#Q���s��6 �8���������-��|��~�E�,���d
�v���fQ�5���]-*!�|b
A��7����0E0&��P[7��\��������?�� �i���@Q��[W���(��� ?�J������MB�Q�K>}�j�C���c��eM�T�D��u+��U�N��OVn��[�_hL8�S��<�4jp���8��4�#29 -j��r:�]*�E����tw�����6�-���/��l�="��>E���1c�T�����0I�+#b��������n���{sBv�1�v7Q7�2���R�Q���6ck�.TI>�(��j{�z����m����-��j������j����o����j�To5�MC�x�iFm�f�����b�H��Y��^b�n8����nR �u�M��]����B�C �� �)�L^$���'e�vA������PM$M�jy���e�tx3L��> ����7�6�'�s��4A`e[�-s��C��7���!m�<�� �]<9z�{����&�k��DF��4�3��&��@.�/vA/�-��� 5��C���#w,�Y�����e�&o�k@���~� ��/WG%+a!���*Z�i�T� ����@�Z������/I|Q�t���#s�������Zi.fIYw�$I���d�e���qO���i:�j����q�3@�)����I�[��r�����=��B���Z����Sh����s�p������`�_q1�L��n�97T�d:�S���"�WJ�c���,�#�,yX�4~���
�����#�-���px���\
����8/��Q�]X�<���v���_z|���<{��������#�y�cb�1P����_���:��.p�RQF��A���Y�$�A�X(�'��?
{Q_���h���j��n]};l����N�vzr71�\D�G�Q\�!�W���8�<��Xj�Z���Vy��So4����:��]��_�T�R��ZB&����$�#hEi<I4�*Z�q����9��_�q�c��N�P��x,�tK&A�W]$P=���~� B�7�F�;�f�)0� ������/�\!���NX�{���J�����������){����T5N5�0�������M l��@���a�i�����2`#�d�(��q*���R�.�$5^�8���
���)�>��%�m�93l���Q������i� �_B+�cl��8Q���p{Fj�@���*xR������l������.l�*7E� R� 9��D5]�x}B��Y�M6����"Wq��!O�w��9hXo��J���M��(J�m����Z�q:@ q��s�r@:���6�8\] ;2������q�3��eWT�9�xU���1�[�r��������y���8����Z��3��;}2����)���L��8�(���J�"����iI����T�S#�:��8�������{!pug�`�z���xD�C0�� +��
�YT�!l3�HA
�A�����9���u�+y�1}��~�}HP����e�F/���[�!�@��|b5Y[`r� G�I�z
T�u{S��d~����F����6&�7Q���v�p!�W��}�����j����?����s��#[�
<o{��X8R���[88���v��][�����W��A-ca���"�����SDP�Q2�!�e2���k�!��P������%m��NO! ?/�~(P4O$��-��������(��}�x3�J0Te����aQ^����SE uJ����"q�'t+jt
A�XqJ%
P�$����$ t�t}��%�H���*z)�P��Q��C�"��Pe2�M��-Id ��tbV�82CT�a�B�l���1����;�����Mf|o����,��D��kQ��*��A�[����M���d�2�3z�1�$�����Vk��P��&�Xx���X��wN����%���OR�w���xl2���aT�L���>H��~\��L���M!�D%���D�M;�)������g$B(=�z��������[�*��4<mD5�m'#n����L���{��>�8��%Q�hU���J\IH-�������8�K$�Ma ��������&��b�cIN4���-����i�8;����x���t8.�z&AZk��/��=�1���<��"(
���+������)e"���(`��$v�$�^���5���+B�����*�[�]���V|S.���-HH~ x�kr����7w�f]�2��9��!���Gt ��o�S>�y�����JCr
@e��P�R.�{-x���-�A��U&�F
V����
&�"*!�d�e4J����%}��.����S
Wd�������y�*��\����BC�qzFp��R�4�
Br~yo+����\����w������,���(��{��2Z;�IG�;B�TD �
|��G}E���*�j�jW��R��-Z)��&�&�91���r�K�P��m�����1���~�W���B�*!ub�
���V���
5�n&l��"��j���u�*F���M�;��f�����G��;/X$p����d ��Y� �D������p����/�Q�� mMo!6h�&��P��������Dl3
��������V�$y}`7R��jz,�/��� i��~���Y�$��}�R��G�~C�������0m��|��Myi�i�2��+I��{�L���@@��{����
��e�h/�g&��/��
h"@YF
������4�������x? <&}���>���Ui�<<pj;����J�*��J6�����%��'��{����B������SM���|��T�z� ��W��t0
U����Ly~����2�F��2
z��]��nH�#v~/�PF���X!�@�5��NSY_y�����$u�{F�]?�0�w��|��-�2J����h�&GJ#|�].����ae�|�dK� �
��rt�W%�:����X�0K1t�(if4\��Z�i������?�ZJ�`85�_*fX
��X ���>T�$������pT�s�Y�.E��4�9�^p���L&N��~+x�oi��P�G�~��4U��v��T�#~�d8�I:���_'�a�"~��:��F$�������\m�gY�j�@�T_�H��/L�X ��������`p�d�n���T'�P<�[*I+h]L-�8D����.g�p'.?����*�D�gm��T$�~%_4fX^R�U9���0\F������f���\������d���/`�K��� u";T���~5��-�>j ��A���+�&������8�^�����a�?�������|q�U��gf���t�`���7��^��������/��u����?$1������0Vv�r�{3"�d&~�'S�3X��t'Y#~�������LPIj���+������� J.��Z���uDS����@�����vK��Zd3X��~�������������%I����QT3����N����`V�y�/.�`
T���$���^ ����!?�������'�)-F-���|.r��N_��@�-�dD��J.���kZ�+�Y+�������N<�j� �rgj�+��R!Zw1��<p��������1F�iS+� ������}m�����hsN��c��[x���6����w�W?!
��������|�[U���f�`��K�zz��Q]���<�cGz���=�&�|�,-�.;w�NV?.�1��C8zsH(N�����|�w8��
����gD��<0� �^�7P����[���*�BU,�� M�<}
Z���< iJ��<J��M���y2���~�� �'��qXjz�(�tG���ItTaI�z����UY��Y��V��|����W�ol�K�`Xj*\�
\�|���P��� ��a���{$�����sd�@���O���6i�4�J�K%���R��a5<�l0)&�v�g<H~����^7��u��b�Y��Em�F���R��XmDD���H�E��MB�s��8'��s�b��2��y�@��T��,�lE����K�p*���<;�v�}�Le$0J������.=�a���5z?�m�g���B�?����r�U������I?L UQ�?�q���������A�E�$i�
�1��-����hY15�g��9� ����5ko���'��M��z��e��U�P[&����gl����TN���B������������
|�X2g1������z���y];}g�����_�����D�wE�����l����������Vfz����I�!�� =Q���� 6<����:�H=�aR/.�\g��S����0(���e�D� �S������� �4-���W�`g��C�u���X���Rsz7f����d�F���\^QO��D?�\&�U(^T:��u�q���� ��������LKC#[���}"R6�,�T��Q?���$���y��L��T�|.S�/���k7�������1����������m����&�.�U>�t$�9�=9@�������r�����p������*�M� N���D�K�4dk�]�����R/m��W����u�Vr����&wf�vVL�Y�����+���������"C6���������"�0�!)T�S��4���o��t�XV�? �#��-��H��})�I��j�W�h*���G���z.��{�����\5=|��u8�������IY����R��a�#��WuC�d�=��"4�m���l:����,wz�����V��WK�8A�����^�$6.���b��g�������[����#����&e�n|����g��i�4+�������Ss���5�i�Pb�F���0�����f������W_: p�tCeeH1��x7Tz�������� I�1�*W��@�[@I��d22��*���2'��! ��X�B0�<�u���U����44����|1�����c�~
��KKg�����"�S(8��F���z$gh�$�B�����`�Qp^�_� ������vPdg�L+^3�T+^A�6�S��*)�=�{#2��m^��u��q�s�����.���J���A�i���l����yT�'X��M���$+=��L�8�JO�F���W���9
z�*���
T@9f'��n
����VCR�D�u�/`�g��/� xs �4i��LE��
��a���k�#?��$�w
��0�� ���� >��_�^ �;L��0�}�w�O
^�79J����E;�I\�R�3�]s�W�����kv��$M��tuF���db�Kv �k2$e��� �$�ud"��Ry�Y'�q����^s5M=�qB����$��jNO_$�t��r|�fX�?�)u�kr ��������t�O������p���6�^���\�=���O���g�.f�w{O�2GL{�L�����[����-�m��<9�~8��O���.Q~9�\� �(�j�pk���?m0J`Y�[C���~.L��z�L�U���j����=X��?�]��n���� /�J3b>�����D���:��E�R��+��g`��:ep����^�?9'��������1\O�uZMd���[iY�L8L��B��}z3���������O{��$��rx��$��(�i7���n>��QF�d���������s�#��g���0N����@�����f��f�h?�`�f����?�����")c�$�$@+��:-�N�����q��l��l�n�l?�nt,{�L�S����5��=�g�������)���)t�e���]e�a��P���n������l������������������<��6������<�� �nw���9�Z�����q3D��|���_�0� �������J�{�����0���;��������l>_T7�`Y������
���`�K;6�D�y@gCSg�`��u�>�k\��j��p@s^�����(��� ��c�i�]}=c�\���]m%��$�)Jr��_�{j4=���4��P���s��t���G1�z�yyI�9�*7��Wf@�h�$��
5~k����66������aR�6��[�}��P���`�J�{x|����������@�NO�k.N=F�Pi���d"{���m=��t�?}���+�j��f���Y������F����_�������Nf����8��u���Z���T�����a��f��!� �
����C�9��'c+�/��o d`���8��V�st
�oA�S71>�5�/D��5.��HV�+5?ZH*[��h�h�h�s�s�v8�v�<������=d+���b�������R6Y%)�s���$%t ��6%E����^JR��gd>�X^D�S{|�L�yZ���y$��j��vt��3���\T�D�1�O��z�����C�P^�������N�}�&E ������R}�f����C��s~��scT��7�3�TIH�b;a2GX�L�t��,��T�x� ^��'d�0�+������3R��������F�����Z��Ef, K���x�+�B�F�W��9(����Z�:o����a ��T0���^N�\�mz&f#�|�$�� ;E}���j\%�M���x��D���{�LW�Ve*V�a���2=��n������#�ThO4��ovu?�%l(�0mB��fu9[���;�" �p�R��� �H-X���?��p�v����"�@���w���OA��Czwx�f�>�Q�����B ��d��4�m<�������g�W��.x�����t�y�/<�A�L����4��@��`y��A0 �BG���/x�����b�������>�qX�eA�|�F�(�*M�H�U���kFg�@�����{k?}<�;��/����B����R������>EU�z�Jt���:I�����d��1�I(M�B�4)�>z�8�*T�k��d�1J��z�(�������W�@�c�[|���%�B9�d�w:�����S� =��51.����>��`�_��*0 �?����P�cb-��6����E�E�(U�_����c��;e���&�����0:��X��e��4d`F�G�����Z��?���g��aI�3R�K�����sI}���b�NX'���tz�� �K���n��*?{�jj���m�c/�A���b�`$f������������/�C���t_���J�a�[���zH��&.)S��{�:���;��e:E��Z�C�h"M��Z 'x�����TZ�����gd:%�3 ��ZE<c�{B�@�?���wx5���c�5��$�Kxm6��ye3��h���2�M.��6��0��F&�cE��Y������ssx1��0d�0+��)��H����929����a���y���"nf&�3b6�p��+@3����L`�6<�'��wN }�.��C����7� R�oG������������g�S0{�ZYZ��'Z��:��d������{1K]���-`�i1����������=�aB{�W��;��iwiB�$�����=:v��HW""��X�X�V�9���6��O6�V!��4��&��3���{�CG����QF�����N�d�g�?��D�D=V��~b��$(#[����9�6cucr'���t=0�'���iCb�K�r�L���G����pJ�
�'�t�,)g�1�x�~pg�����;��,J�h��@�5d`Bh
�#=/N�I#"iJ��K�M���{��s�l���$�9=h�����@���#����qt��qt�A::�f���C�*}[j��tr�7��b~�X�
�W�.�*��*�v�k��R��or]������}��Y&�����v��*��Y�;�I*$�9>[��0�����S^M���%A.h��yWE��-&�^�u!YKC09��O�jI��w���m��4�8G���'F��W�M�&���C��7B/�����H>,�.�He���,�T_EO�^���+].��L�/|�>�v�N�L���� �$��fDY��F���g���(�F!����j�����7����������F���rs���9�^�^���J5}H�x#�����4T��I5�NEOx��z�%zW.�9������|k(�� ?d�r���m������p2�|8���fc�[�����;�i���>e��
3}�������N{���6V��
?�a����������%�����{.� /���(1�E
�����b8���������3z���?/N�����z�����S��7������7������U�pn_������tr;!�����^�'��������+�Z��w����F�0��[o�6��u
~*��J=F����s����:���>