Merge lp:~hazmat/pyjuju/security-node-policy-def into lp:pyjuju

Proposed by Kapil Thangavelu
Status: Merged
Approved by: Benjamin Saller
Approved revision: 274
Merged at revision: 277
Proposed branch: lp:~hazmat/pyjuju/security-node-policy-def
Merge into: lp:pyjuju
Diff against target: 171 lines (+128/-4)
2 files modified
ensemble/state/security.py (+58/-2)
ensemble/state/tests/test_security.py (+70/-2)
To merge this branch: bzr merge lp:~hazmat/pyjuju/security-node-policy-def
Reviewer Review Type Date Requested Status
Benjamin Saller (community) Approve
Gustavo Niemeyer Approve
Review via email: mp+67972@code.launchpad.net

Description of the change

Zookeeper clients need to use ACLs for nodes created by them in order to leverage zookeeper security. The security policy class implemented in this branch enable these policy choices via a path based lookup for the corresponding ACLs. The policy defers to rules matched by the path to provide an ACL for the path.

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

This looks very nice.. tiny, organized, clear. Thanks!

[1]

33 +class SecurityPolicy(object):
34 + """The security policy generates ACLs for new nodes based on their path.
35 + """

This should be able to handle existing nodes as well, I suppose? Imagine a
relation.. what happens when we add a new service unit to it?

[2]

83 +def owner_ace(principal):
84 + return make_ace(principal.get_token(), all=True)

This function doesn't feel worth it. Besides being called a single time, it's
a single liner, so it's just obscuring logic at the call site.

[3]

132 + """If no rules match, the default acl gives authenticated users access.
133 + """

Might be nice to add an equivalent comment here saying we don't actually want this for real.

review: Approve
Revision history for this message
Benjamin Saller (bcsaller) wrote :

LGTM as well.

[4] Want to verify that Principals don't need a detach method because the connection is implicitly scoped to the secured session?

[5] The way `owner` is used I'm not sure you really need to track it at all. It only maps to an `all` token once. Similar to point [2]

[6] add_rule should make more clear the signature of rules given that they don't have a base class or imply an interface. docstring change.

review: Approve
Revision history for this message
Kapil Thangavelu (hazmat) wrote :

> This looks very nice.. tiny, organized, clear. Thanks!
>
> [1]
>
> 33 +class SecurityPolicy(object):
> 34 + """The security policy generates ACLs for new nodes based on
> their path.
> 35 + """
>
> This should be able to handle existing nodes as well, I suppose? Imagine a
> relation.. what happens when we add a new service unit to it?

I'm going to utilize group principals for services, so giving access to the group for the acl will suffice, else there are too many possible extant acls on nodes that will need cleanup/modification when a principal (like a unit agent) dis/appears.

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

> LGTM as well.
>
> [4] Want to verify that Principals don't need a detach method because the
> connection is implicitly scoped to the secured session?

There is no api to remove them from a connection/session available at the extension level.

> [5] The way `owner` is used I'm not sure you really need to track it at all.
> It only maps to an `all` token once. Similar to point [2]

sounds good, i'm still playing with with how this integrates with the connection, but your right, just tracking the owner ace value would suffice here.

>
> [6] add_rule should make more clear the signature of rules given that they
> don't have a base class or imply an interface. docstring change.

sounds good.

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

> > This looks very nice.. tiny, organized, clear. Thanks!
> >
> > [1]
> >
> > 33 +class SecurityPolicy(object):
> > 34 + """The security policy generates ACLs for new nodes based on
> > their path.
> > 35 + """
> >
> > This should be able to handle existing nodes as well, I suppose? Imagine a
> > relation.. what happens when we add a new service unit to it?
>
> I'm going to utilize group principals for services, so giving access to the
> group for the acl will suffice, else there are too many possible extant acls
> on nodes that will need cleanup/modification when a principal (like a unit
> agent) dis/appears.

Just to clarify some more re updates, afaics with group principals, they will be a rare enough case, that the code that needs them can handle them inline. The only case i've come across so far is the provisioning agent updating a machine state to grant access to the machine agent principal it just created.

275. By Kapil Thangavelu

Merged trunk-merge into security-node-policy-def.

276. By Kapil Thangavelu

address review comments, inline owner_ace func, additional doc comments on rule signature.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ensemble/state/security.py'
2--- ensemble/state/security.py 2011-07-11 18:16:42 +0000
3+++ ensemble/state/security.py 2011-07-19 15:43:29 +0000
4@@ -1,8 +1,12 @@
5 from twisted.internet.defer import inlineCallbacks, returnValue
6-from ensemble.state.auth import make_identity
7+
8+from ensemble.state.auth import make_identity, make_ace
9 from ensemble.state.utils import YAMLState
10
11
12+ZOO_OPEN_AUTH_ACL_UNSAFE = make_ace("auth", "world", all=True)
13+
14+
15 class Principal(object):
16 """An ensemble/zookeeper principal.
17 """
18@@ -24,7 +28,7 @@
19 return make_identity("%s:%s" % (self._name, self._password))
20
21 def attach(self, connection):
22- """A princpal can be attached to a connection."""
23+ """A principal can be attached to a connection."""
24 return connection.add_auth(
25 "digest", "%s:%s" % (self._name, self._password))
26
27@@ -60,3 +64,55 @@
28 if name in self._state:
29 del self._state[name]
30 yield self._state.write()
31+
32+
33+class SecurityPolicy(object):
34+ """The security policy generates ACLs for new nodes based on their path.
35+ """
36+ def __init__(self, client, token_db, rules=(), owner=None):
37+ self._client = client
38+ self._rules = list(rules)
39+ self._token_db = token_db
40+ self._owner = None
41+
42+ def set_owner(self, principal):
43+ """If an owner is set all nodes ACLs will grant access to the owner.
44+ """
45+ assert not self._owner, "Owner already assigned"
46+ self._owner = principal
47+
48+ def add_rule(self, rule):
49+ """Add a security rule to the policy.
50+
51+ A rule is a callable object accepting the policy and the path as
52+ arguments. The rule should return a list of ACL entries that apply
53+ to the node at the given path. Rules may return deferred values.
54+ """
55+ self._rules.append(rule)
56+
57+ @inlineCallbacks
58+ def __call__(self, path):
59+ """Given a node path, determine the ACL.
60+ """
61+ acl_entries = []
62+
63+ for rule in self._rules:
64+ entries = yield rule(self, path)
65+ if entries:
66+ acl_entries.extend(entries)
67+
68+ # XXX/TODO - Remove post security-integration
69+ # Allow incremental integration
70+ if not acl_entries:
71+ acl_entries.append(ZOO_OPEN_AUTH_ACL_UNSAFE)
72+
73+ # Give cli admin access by default
74+ admin_token = yield self._token_db.get("admin")
75+ acl_entries.append(make_ace(admin_token, all=True))
76+
77+ # Give owner access by default
78+ if self._owner:
79+ acl_entries.append(
80+ make_ace(self._owner.get_token(), all=True))
81+
82+ returnValue(acl_entries)
83
84=== modified file 'ensemble/state/tests/test_security.py'
85--- ensemble/state/tests/test_security.py 2011-07-11 18:16:42 +0000
86+++ ensemble/state/tests/test_security.py 2011-07-19 15:43:29 +0000
87@@ -1,10 +1,10 @@
88 import yaml
89 import zookeeper
90
91-from twisted.internet.defer import inlineCallbacks
92+from twisted.internet.defer import inlineCallbacks, succeed
93
94 from ensemble.state.auth import make_identity, make_ace
95-from ensemble.state.security import Principal, TokenDatabase
96+from ensemble.state.security import Principal, TokenDatabase, SecurityPolicy
97
98 from ensemble.lib.testing import TestCase
99 from txzookeeper.tests.utils import deleteTree
100@@ -93,3 +93,71 @@
101 yield self.db.add(principal)
102 token = yield self.db.get(principal.name)
103 self.assertEqual(token, principal.get_token())
104+
105+
106+class PolicyTest(TestCase):
107+
108+ @inlineCallbacks
109+ def setUp(self):
110+ zookeeper.set_debug_level(0)
111+ self.client = yield self.get_zookeeper_client().connect()
112+ self.tokens = TokenDatabase(self.client)
113+ yield self.tokens.add(Principal("admin", "admin"))
114+ self.policy = SecurityPolicy(self.client, self.tokens)
115+
116+ def tearDown(self):
117+ deleteTree(handle=self.client.handle)
118+ self.client.close()
119+
120+ @inlineCallbacks
121+ def test_default_no_owner_no_rules_gives_admin_access(self):
122+ """By default the policy setups a global access for the cli admins.
123+ """
124+ acl = yield self.policy("/random")
125+ self.assertIn(
126+ make_ace(Principal("admin", "admin").get_token(), all=True), acl)
127+
128+ @inlineCallbacks
129+ def test_default_no_rules_gives_global_authenticated_access(self):
130+ """If no rules match, the default acl gives authenticated users access.
131+
132+ XXX/TODO: This is intended as a temporary crutch for
133+ integration of the security machinery, not a long term
134+ solution.
135+ """
136+ acl = yield self.policy("/random")
137+ self.assertIn(make_ace("auth", "world", all=True), acl)
138+
139+ @inlineCallbacks
140+ def test_rule_match_suppress_open_access(self):
141+ """If a rule returns an acl, then no default access is given."""
142+ principal = Principal("foobar", "foobar")
143+ self.policy.add_rule(lambda policy, path: [
144+ make_ace(principal.get_token(), all=True)])
145+ acl = yield self.policy("/random")
146+ # Check for matched rule ACL
147+ self.assertIn(make_ace(principal.get_token(), all=True), acl)
148+ # Verify no default access
149+ self.assertNotIn(make_ace("auth", "world", all=True), acl)
150+
151+ @inlineCallbacks
152+ def test_rule_that_returns_deferred(self):
153+ """If a rule may do additional lookups, resulting in deferred values.
154+ """
155+ principal = Principal("foobar", "foobar")
156+ self.policy.add_rule(lambda policy, path: succeed([
157+ make_ace(principal.get_token(), all=True)]))
158+ acl = yield self.policy("/random")
159+ # Check for matched rule ACL
160+ self.assertIn(make_ace(principal.get_token(), all=True), acl)
161+ # Verify no default access
162+ self.assertNotIn(make_ace("auth", "world", all=True), acl)
163+
164+ @inlineCallbacks
165+ def test_owner_ace(self):
166+ """If an owner is set, all nodes ACLs will have an owner ACE.
167+ """
168+ owner = Principal("john", "doe")
169+ self.policy.set_owner(owner)
170+ acl = yield self.policy("/random")
171+ self.assertIn(make_ace(owner.get_token(), all=True), acl)

Subscribers

People subscribed via source and target branches

to status/vote changes: