summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames E. Blair <jeblair@openstack.org>2014-04-02 14:43:45 -0700
committerJames E. Blair <jeblair@openstack.org>2014-04-22 11:55:09 -0700
commit27e5d6366128cecb97e8c88a0fac89eb47b35eb8 (patch)
tree8a62ffff5a3ff9654cc3fa00f40c7ec2946b9b40
parenteee19fd998135b21ea9f175640cc07a91495a211 (diff)
downloadgear-27e5d6366128cecb97e8c88a0fac89eb47b35eb8.tar.gz
Add access control
The gear server is modified to check an access control list for certain actions if SSL and ACLs are enabled. Clients and workers can be granted permission to register and invoke only functions matching certain regexes, as well as the ability to grant ACLs to other connections. A connection can always revoke its own access. ACL entries are checked based on the subject name in the SSL certificate (which is trustworthy because only certificates signed by the supplied CA are accepted). The submitJob method is updated to give up sooner if it is unable to submit a job to any connected servers (older behavior would busy loop trying to submit it, but since there are now more scenarios where a functioning gear server might reject a job, that behavior seems less desirable). Docs and tests are updated for the new feature. Change-Id: I07edfb964bf716df08be86f844061bdad29b9728
-rw-r--r--doc/source/index.rst86
-rw-r--r--gear/__init__.py171
-rw-r--r--gear/acl.py289
-rw-r--r--gear/cmd/geard.py26
-rw-r--r--gear/tests/test_gear.py56
5 files changed, 620 insertions, 8 deletions
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 5b62114..c8fa5ec 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -158,6 +158,25 @@ Server Usage
------------
.. program-output:: geard --help
+The syntax of the optional ACL file consists of a number of sections
+identified by the SSL certificate Common Name Subject, and the
+arguments to the :py:class:`ACLEntry` constructor as key-value pairs::
+
+ [<subject>]
+ register=<regex>
+ invoke=<regex>
+ grant=<boolean>
+
+For example::
+
+ [my_worker]
+ register=.*
+
+ [my_client]
+ invoke=.*
+
+ [my_node_manager]
+ grant=True
Server Objects
^^^^^^^^^^^^^^
@@ -166,6 +185,73 @@ Server Objects
:inherited-members:
+Access Control
+--------------
+
+The gear server supports authorization via access control lists. When
+an :py:class:`ACL` object is supplied to the server (or a file on the
+command line), gear changes from the normal Gearman mode of
+allow-by-default to deny-by-default and only clients with ACL entries
+will be able to perform actions such as registering functions or
+submitting jobs. Authorization is based on the SSL certificate Common
+Name Subject associated with the connection. An :py:class:`ACL`
+object may be modified programatically at run-time.
+
+The administrative protocol supports modifying ACLs with the following
+commands:
+
+**acl list**
+ List the current acls::
+
+ acl list
+ client register=None invoke=.* grant=True
+ worker register=.* invoke=None grant=True
+ .
+
+**acl grant <verb> <subject> <pattern>**
+ Grant the `<verb>` action for functions matching `<pattern>` to
+ `<subject>`. Verbs can be one of ``register``, ``invoke``, or
+ ``grant``. This requires the current connection have the grant
+ permission. Example::
+
+ acl grant register worker .*
+ OK
+
+**acl revoke <verb> <subject>**
+ Revoke the `<verb>` action from `<subject>`. Verbs can be one of
+ ``register``, ``invoke``, ``grant``, or ``all`` to indicate all
+ permissions for the subject should be revoked. This requires the
+ grant permission, except that a subject may always revoke its own
+ permissions. Example::
+
+ acl revoke register worker
+ OK
+
+**acl self-revoke <verb>**
+ Revoke the `<verb>` action from connections associted with the
+ current certificate subject. Verbs can be one of ``register``,
+ ``invoke``, ``grant``, or ``all`` to indicate all permissions for
+ the subject should be revoked. This is similar to ``acl revoke``
+ but is a convenience method so that a subject does not need to know
+ its own common name. A subject always has permission to revoke its
+ own permissions. Example::
+
+ acl self-revoke register
+ OK
+
+ACL Objects
+^^^^^^^^^^^
+.. autoclass:: gear.ACL
+ :members:
+ :inherited-members:
+
+ACLEntry Objects
+^^^^^^^^^^^^^^^^
+.. autoclass:: gear.ACLEntry
+ :members:
+ :inherited-members:
+
+
Common
------
diff --git a/gear/__init__.py b/gear/__init__.py
index e26d698..dcdb15b 100644
--- a/gear/__init__.py
+++ b/gear/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2013 OpenStack Foundation
+# Copyright 2013-2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@@ -23,6 +23,7 @@ import time
import uuid as uuid_module
from gear import constants
+from gear.acl import ACLError, ACLEntry, ACL # noqa
try:
import Queue as queue
@@ -1292,10 +1293,14 @@ class Client(BaseClient):
else:
raise ConfigurationError("Invalid precedence value")
packet = Packet(constants.REQ, cmd, data)
+ attempted_connections = set()
while True:
+ if attempted_connections == set(self.active_connections):
+ break
conn = self.getConnection()
task = SubmitJobTask(job)
conn.pending_tasks.append(task)
+ attempted_connections.add(conn)
try:
self.sendPacket(packet, conn)
except Exception:
@@ -2132,6 +2137,12 @@ class ServerConnection(Connection):
self.client_id = None
self.functions = set()
self.related_jobs = {}
+ self.ssl_subject = None
+ if self.use_ssl:
+ for x in conn.getpeercert()['subject']:
+ if x[0][0] == 'commonName':
+ self.ssl_subject = x[0][1]
+ self.log.debug("SSL subject: %s" % self.ssl_subject)
self.changeState("INIT")
def _getAdminRequest(self):
@@ -2161,11 +2172,13 @@ class Server(BaseClientServer):
:arg str client_id: The ID associated with this server.
It will be appending to the name of the logger (e.g.,
gear.Server.server_id). Defaults to 'unknown'.
+ :arg ACL acl: An :py:class:`ACL` object if the server should apply
+ access control rules to its connections.
"""
def __init__(self, port=4730, ssl_key=None, ssl_cert=None, ssl_ca=None,
statsd_host=None, statsd_port=8125, statsd_prefix=None,
- server_id='unknown'):
+ server_id='unknown', acl=None):
self.port = port
self.ssl_key = ssl_key
self.ssl_cert = ssl_cert
@@ -2176,6 +2189,7 @@ class Server(BaseClientServer):
self.jobs = {}
self.functions = set()
self.max_handle = 0
+ self.acl = acl
self.connect_wake_read, self.connect_wake_write = os.pipe()
self.use_ssl = False
@@ -2322,6 +2336,29 @@ class Server(BaseClientServer):
self.handleStatus(request)
elif request.command.startswith(b'workers'):
self.handleWorkers(request)
+ elif request.command.startswith(b'acl list'):
+ self.handleACLList(request)
+ elif request.command.startswith(b'acl grant'):
+ self.handleACLGrant(request)
+ elif request.command.startswith(b'acl revoke'):
+ self.handleACLRevoke(request)
+ elif request.command.startswith(b'acl self-revoke'):
+ self.handleACLSelfRevoke(request)
+
+ def _cancelJob(self, request, job, queue):
+ if self.acl:
+ if not self.acl.canInvoke(request.connection.ssl_subject,
+ job.name):
+ self.log.info("Rejecting cancel job from %s for %s "
+ "due to ACL" %
+ (request.connection.ssl_subject, job.name))
+ request.connection.sendRaw(b'ERR PERMISSION_DENIED\n')
+ return
+ queue.remove(job)
+ del self.jobs[job.handle]
+ self._updateStats()
+ request.connection.sendRaw(b'OK\n')
+ return
def handleCancelJob(self, request):
words = request.command.split()
@@ -2331,13 +2368,111 @@ class Server(BaseClientServer):
for queue in [self.high_queue, self.normal_queue, self.low_queue]:
for job in queue:
if handle == job.handle:
- queue.remove(job)
- del self.jobs[handle]
- self._updateStats()
- request.connection.sendRaw(b'OK\n')
- return
+ return self._cancelJob(request, job, queue)
request.connection.sendRaw(b'ERR UNKNOWN_JOB\n')
+ def handleACLList(self, request):
+ if self.acl is None:
+ request.connection.sendRaw(b'ERR ACL_DISABLED\n')
+ return
+ for entry in self.acl.getEntries():
+ l = "%s\tregister=%s\tinvoke=%s\tgrant=%s\n" % (
+ entry.subject, entry.register, entry.invoke, entry.grant)
+ request.connection.sendRaw(l.encode('utf8'))
+ request.connection.sendRaw(b'.\n')
+
+ def handleACLGrant(self, request):
+ # acl grant register worker .*
+ words = request.command.split(None, 4)
+ verb = words[2]
+ subject = words[3]
+
+ if self.acl is None:
+ request.connection.sendRaw(b'ERR ACL_DISABLED\n')
+ return
+ if not self.acl.canGrant(request.connection.ssl_subject):
+ request.connection.sendRaw(b'ERR PERMISSION_DENIED\n')
+ return
+ try:
+ if verb == 'invoke':
+ self.acl.grantInvoke(subject, words[4])
+ elif verb == 'register':
+ self.acl.grantRegister(subject, words[4])
+ elif verb == 'grant':
+ self.acl.grantGrant(subject)
+ else:
+ request.connection.sendRaw(b'ERR UNKNOWN_ACL_VERB\n')
+ return
+ except ACLError, e:
+ self.log.info("Error in grant command: %s" % (e.message,))
+ request.connection.sendRaw(b'ERR UNABLE %s\n' % (e.message,))
+ return
+ request.connection.sendRaw(b'OK\n')
+
+ def handleACLRevoke(self, request):
+ # acl revoke register worker
+ words = request.command.split()
+ verb = words[2]
+ subject = words[3]
+
+ if self.acl is None:
+ request.connection.sendRaw(b'ERR ACL_DISABLED\n')
+ return
+ if subject != request.connection.ssl_subject:
+ if not self.acl.canGrant(request.connection.ssl_subject):
+ request.connection.sendRaw(b'ERR PERMISSION_DENIED\n')
+ return
+ try:
+ if verb == 'invoke':
+ self.acl.revokeInvoke(subject)
+ elif verb == 'register':
+ self.acl.revokeRegister(subject)
+ elif verb == 'grant':
+ self.acl.revokeGrant(subject)
+ elif verb == 'all':
+ try:
+ self.acl.remove(subject)
+ except ACLError:
+ pass
+ else:
+ request.connection.sendRaw(b'ERR UNKNOWN_ACL_VERB\n')
+ return
+ except ACLError, e:
+ self.log.info("Error in revoke command: %s" % (e.message,))
+ request.connection.sendRaw(b'ERR UNABLE %s\n' % (e.message,))
+ return
+ request.connection.sendRaw(b'OK\n')
+
+ def handleACLSelfRevoke(self, request):
+ # acl self-revoke register
+ words = request.command.split()
+ verb = words[2]
+
+ if self.acl is None:
+ request.connection.sendRaw(b'ERR ACL_DISABLED\n')
+ return
+ subject = request.connection.ssl_subject
+ try:
+ if verb == 'invoke':
+ self.acl.revokeInvoke(subject)
+ elif verb == 'register':
+ self.acl.revokeRegister(subject)
+ elif verb == 'grant':
+ self.acl.revokeGrant(subject)
+ elif verb == 'all':
+ try:
+ self.acl.remove(subject)
+ except ACLError:
+ pass
+ else:
+ request.connection.sendRaw(b'ERR UNKNOWN_ACL_VERB\n')
+ return
+ except ACLError, e:
+ self.log.info("Error in self-revoke command: %s" % (e.message,))
+ request.connection.sendRaw(b'ERR UNABLE %s\n' % (e.message,))
+ return
+ request.connection.sendRaw(b'OK\n')
+
def _getFunctionStats(self):
functions = {}
for function in self.functions:
@@ -2442,6 +2577,14 @@ class Server(BaseClientServer):
if not unique:
unique = None
arguments = packet.getArgument(2, True)
+ if self.acl:
+ if not self.acl.canInvoke(packet.connection.ssl_subject, name):
+ self.log.info("Rejecting SUBMIT_JOB from %s for %s "
+ "due to ACL" %
+ (packet.connection.ssl_subject, name))
+ self.sendError(packet.connection, 0,
+ 'Permission denied by ACL')
+ return
self.max_handle += 1
handle = ('H:%s:%s' % (packet.connection.host,
self.max_handle)).encode('utf8')
@@ -2546,8 +2689,22 @@ class Server(BaseClientServer):
name = packet.getArgument(0)
packet.connection.client_id = name
+ def sendError(self, connection, code, text):
+ data = (str(code).encode('utf8') + b'\x00' +
+ str(text).encode('utf8') + b'\x00')
+ p = Packet(constants.RES, constants.ERROR, data)
+ connection.sendPacket(p)
+
def handleCanDo(self, packet):
name = packet.getArgument(0)
+ if self.acl:
+ if not self.acl.canRegister(packet.connection.ssl_subject, name):
+ self.log.info("Ignoring CAN_DO from %s for %s due to ACL" %
+ (packet.connection.ssl_subject, name))
+ # CAN_DO normally does not merit a response so it is
+ # not clear that it is appropriate to send an ERROR
+ # response at this point.
+ return
self.log.debug("Adding function %s to %s" % (name, packet.connection))
packet.connection.functions.add(name)
self.functions.add(name)
diff --git a/gear/acl.py b/gear/acl.py
new file mode 100644
index 0000000..ea79a67
--- /dev/null
+++ b/gear/acl.py
@@ -0,0 +1,289 @@
+# Copyright 2014 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import re
+
+
+class ACLError(Exception):
+ pass
+
+
+class ACLEntry(object):
+ """An access control list entry.
+
+ :arg str subject: The SSL certificate Subject Common Name to which
+ the entry applies.
+
+ :arg str register: A regular expression that matches the jobs that
+ connections with this certificate are permitted to register.
+
+ :arg str invoke: A regular expression that matches the jobs that
+ connections with this certificate are permitted to invoke.
+ Also implies the permission to cancel the same set of jobs in
+ the queue.
+
+ :arg boolean grant: A flag indicating whether connections with
+ this certificate are permitted to grant access to other
+ connections. Also implies the permission to revoke access
+ from other connections. The ability to self-revoke access is
+ always implied.
+ """
+
+ def __init__(self, subject, register=None, invoke=None, grant=False):
+ self.subject = subject
+ self.setRegister(register)
+ self.setInvoke(invoke)
+ self.setGrant(grant)
+
+ def __repr__(self):
+ return ('<ACLEntry for %s register=%s invoke=%s grant=%s>' %
+ (self.subject, self.register, self.invoke, self.grant))
+
+ def isEmpty(self):
+ """Checks whether this entry grants any permissions at all.
+
+ :returns: False if any permission is granted, otherwise True.
+ """
+ if (self.register is None and
+ self.invoke is None and
+ self.grant is False):
+ return True
+ return False
+
+ def canRegister(self, name):
+ """Check whether this subject is permitted to register a function.
+
+ :arg str name: The function name to check.
+ :returns: A boolean indicating whether the action should be permitted.
+ """
+ if self.register is None:
+ return False
+ if not self._register.match(name):
+ return False
+ return True
+
+ def canInvoke(self, name):
+ """Check whether this subject is permitted to register a function.
+
+ :arg str name: The function name to check.
+ :returns: A boolean indicating whether the action should be permitted.
+ """
+ if self.invoke is None:
+ return False
+ if not self._invoke.match(name):
+ return False
+ return True
+
+ def setRegister(self, register):
+ """Sets the functions that this subject can register.
+
+ :arg str register: A regular expression that matches the jobs that
+ connections with this certificate are permitted to register.
+ """
+ self.register = register
+ if register:
+ try:
+ self._register = re.compile(register)
+ except re.error, e:
+ raise ACLError('Regular expression error: %s' % (e.message,))
+ else:
+ self._register = None
+
+ def setInvoke(self, invoke):
+ """Sets the functions that this subject can invoke.
+
+ :arg str invoke: A regular expression that matches the jobs that
+ connections with this certificate are permitted to invoke.
+ """
+ self.invoke = invoke
+ if invoke:
+ try:
+ self._invoke = re.compile(invoke)
+ except re.error, e:
+ raise ACLError('Regular expression error: %s' % (e.message,))
+ else:
+ self._invoke = None
+
+ def setGrant(self, grant):
+ """Sets whether this subject can grant ACLs to others.
+
+ :arg boolean grant: A flag indicating whether connections with
+ this certificate are permitted to grant access to other
+ connections. Also implies the permission to revoke access
+ from other connections. The ability to self-revoke access is
+ always implied.
+ """
+ self.grant = grant
+
+
+class ACL(object):
+ """An access control list.
+
+ ACLs are deny-by-default. The checked actions are only allowed if
+ there is an explicit rule in the ACL granting permission for a
+ given client (identified by SSL certificate Common Name Subject)
+ to perform that action.
+ """
+
+ def __init__(self):
+ self.subjects = {}
+
+ def add(self, entry):
+ """Add an ACL entry.
+
+ :arg Entry entry: The :py:class:`ACLEntry` to add.
+ :raises ACLError: If there is already an entry for the subject.
+ """
+ if entry.subject in self.subjects:
+ raise ACLError("An ACL entry for %s already exists" %
+ (entry.subject,))
+ self.subjects[entry.subject] = entry
+
+ def remove(self, subject):
+ """Remove an ACL entry.
+
+ :arg str subject: The SSL certificate Subject Common Name to
+ remove from the ACL.
+ :raises ACLError: If there is no entry for the subject.
+ """
+ if subject not in self.subjects:
+ raise ACLError("There is no ACL entry for %s" % (subject,))
+ del self.subjects[subject]
+
+ def getEntries(self):
+ """Return a list of current ACL entries.
+
+ :returns: A list of :py:class:`ACLEntry` objects.
+ """
+ items = self.subjects.items()
+ items.sort(lambda a, b: cmp(a[0], b[0]))
+ return [x[1] for x in items]
+
+ def canRegister(self, subject, name):
+ """Check whether a subject is permitted to register a function.
+
+ :arg str subject: The SSL certificate Subject Common Name to
+ check against.
+ :arg str name: The function name to check.
+ :returns: A boolean indicating whether the action should be permitted.
+ """
+ entry = self.subjects.get(subject)
+ if entry is None:
+ return False
+ return entry.canRegister(name)
+
+ def canInvoke(self, subject, name):
+ """Check whether a subject is permitted to invoke a function.
+
+ :arg str subject: The SSL certificate Subject Common Name to
+ check against.
+ :arg str name: The function name to check.
+ :returns: A boolean indicating whether the action should be permitted.
+ """
+ entry = self.subjects.get(subject)
+ if entry is None:
+ return False
+ return entry.canInvoke(name)
+
+ def canGrant(self, subject):
+ """Check whether a subject is permitted to grant access to others.
+
+ :arg str subject: The SSL certificate Subject Common Name to
+ check against.
+ :returns: A boolean indicating whether the action should be permitted.
+ """
+ entry = self.subjects.get(subject)
+ if entry is None:
+ return False
+ if not entry.grant:
+ return False
+ return True
+
+ def grantInvoke(self, subject, invoke):
+ """Grant permission to invoke certain functions.
+
+ :arg str subject: The SSL certificate Subject Common Name to which
+ the entry applies.
+ :arg str invoke: A regular expression that matches the jobs
+ that connections with this certificate are permitted to
+ invoke. Also implies the permission to cancel the same
+ set of jobs in the queue.
+ """
+ e = self.subjects.get(subject)
+ if not e:
+ e = ACLEntry(subject)
+ self.add(e)
+ e.setInvoke(invoke)
+
+ def grantRegister(self, subject, register):
+ """Grant permission to register certain functions.
+
+ :arg str subject: The SSL certificate Subject Common Name to which
+ the entry applies.
+ :arg str register: A regular expression that matches the jobs that
+ connections with this certificate are permitted to register.
+ """
+ e = self.subjects.get(subject)
+ if not e:
+ e = ACLEntry(subject)
+ self.add(e)
+ e.setRegister(register)
+
+ def grantGrant(self, subject):
+ """Grant permission to grant permissions to other connections.
+
+ :arg str subject: The SSL certificate Subject Common Name to which
+ the entry applies.
+ """
+ e = self.subjects.get(subject)
+ if not e:
+ e = ACLEntry(subject)
+ self.add(e)
+ e.setGrant(True)
+
+ def revokeInvoke(self, subject):
+ """Revoke permission to invoke all functions.
+
+ :arg str subject: The SSL certificate Subject Common Name to which
+ the entry applies.
+ """
+ e = self.subjects.get(subject)
+ if e:
+ e.setInvoke(None)
+ if e.isEmpty():
+ self.remove(subject)
+
+ def revokeRegister(self, subject):
+ """Revoke permission to register all functions.
+
+ :arg str subject: The SSL certificate Subject Common Name to which
+ the entry applies.
+ """
+ e = self.subjects.get(subject)
+ if e:
+ e.setRegister(None)
+ if e.isEmpty():
+ self.remove(subject)
+
+ def revokeGrant(self, subject):
+ """Revoke permission to grant permissions to other connections.
+
+ :arg str subject: The SSL certificate Subject Common Name to which
+ the entry applies.
+ """
+ e = self.subjects.get(subject)
+ if e:
+ e.setGrant(False)
+ if e.isEmpty():
+ self.remove(subject)
diff --git a/gear/cmd/geard.py b/gear/cmd/geard.py
index fa33216..227f1cd 100644
--- a/gear/cmd/geard.py
+++ b/gear/cmd/geard.py
@@ -14,6 +14,7 @@
# under the License.
import argparse
+import ConfigParser
import daemon
import extras
import gear
@@ -56,6 +57,8 @@ support.
help='path to SSL public certificate')
parser.add_argument('--ssl-key', dest='ssl_key', metavar='PATH',
help='path to SSL private key')
+ parser.add_argument('--acl', dest='acl', metavar='PATH',
+ help='path to ACL file')
parser.add_argument('--version', dest='version', action='store_true',
help='show version')
self.args = parser.parse_args()
@@ -78,13 +81,34 @@ support.
statsd_host = os.environ.get('STATSD_HOST')
statsd_port = int(os.environ.get('STATSD_PORT', 8125))
statsd_prefix = os.environ.get('STATSD_PREFIX')
+ acl = None
+ if self.args.acl:
+ aclf = ConfigParser.RawConfigParser()
+ aclf.read(self.args.acl)
+ acl = gear.ACL()
+ for section in aclf.sections():
+ if aclf.has_option(section, 'register'):
+ register = aclf.get(section, 'register')
+ else:
+ register = None
+ if aclf.has_option(section, 'invoke'):
+ invoke = aclf.get(section, 'invoke')
+ else:
+ invoke = None
+ if aclf.has_option(section, 'grant'):
+ grant = aclf.getboolean(section, 'grant')
+ else:
+ grant = None
+ entry = gear.ACLEntry(section, register, invoke, grant)
+ acl.add(entry)
self.server = gear.Server(self.args.port,
self.args.ssl_key,
self.args.ssl_cert,
self.args.ssl_ca,
statsd_host,
statsd_port,
- statsd_prefix)
+ statsd_prefix,
+ acl=acl)
signal.pause()
diff --git a/gear/tests/test_gear.py b/gear/tests/test_gear.py
index d56d923..31e0d9d 100644
--- a/gear/tests/test_gear.py
+++ b/gear/tests/test_gear.py
@@ -69,6 +69,62 @@ class TestClient(tests.BaseTestCase):
self.assertTrue(job.known)
self.assertFalse(job.running)
+ def test_ACL(self):
+ acl = gear.ACL()
+ acl.add(gear.ACLEntry('worker', register='foo.*'))
+ acl.add(gear.ACLEntry('client', invoke='foo.*'))
+ acl.add(gear.ACLEntry('manager', grant=True))
+ self.assertEqual(len(acl.getEntries()), 3)
+
+ self.assertTrue(acl.canRegister('worker', 'foo-bar'))
+ self.assertTrue(acl.canRegister('worker', 'foo'))
+ self.assertFalse(acl.canRegister('worker', 'bar-foo'))
+ self.assertFalse(acl.canRegister('worker', 'bar'))
+ self.assertFalse(acl.canInvoke('worker', 'foo'))
+ self.assertFalse(acl.canGrant('worker'))
+
+ self.assertTrue(acl.canInvoke('client', 'foo-bar'))
+ self.assertTrue(acl.canInvoke('client', 'foo'))
+ self.assertFalse(acl.canInvoke('client', 'bar-foo'))
+ self.assertFalse(acl.canInvoke('client', 'bar'))
+ self.assertFalse(acl.canRegister('client', 'foo'))
+ self.assertFalse(acl.canGrant('client'))
+
+ self.assertFalse(acl.canInvoke('manager', 'bar'))
+ self.assertFalse(acl.canRegister('manager', 'foo'))
+ self.assertTrue(acl.canGrant('manager'))
+
+ acl.remove('worker')
+ acl.remove('client')
+ acl.remove('manager')
+
+ self.assertFalse(acl.canRegister('worker', 'foo'))
+ self.assertFalse(acl.canInvoke('client', 'foo'))
+ self.assertFalse(acl.canGrant('manager'))
+
+ self.assertEqual(len(acl.getEntries()), 0)
+
+ def test_ACL_register(self):
+ acl = gear.ACL()
+ acl.grantRegister('worker', 'bar.*')
+ self.assertTrue(acl.canRegister('worker', 'bar'))
+ acl.revokeRegister('worker')
+ self.assertFalse(acl.canRegister('worker', 'bar'))
+
+ def test_ACL_invoke(self):
+ acl = gear.ACL()
+ acl.grantInvoke('client', 'bar.*')
+ self.assertTrue(acl.canInvoke('client', 'bar'))
+ acl.revokeInvoke('client')
+ self.assertFalse(acl.canInvoke('client', 'bar'))
+
+ def test_ACL_grant(self):
+ acl = gear.ACL()
+ acl.grantGrant('manager')
+ self.assertTrue(acl.canGrant('manager'))
+ acl.revokeGrant('manager')
+ self.assertFalse(acl.canGrant('manager'))
+
def load_tests(loader, in_tests, pattern):
return testscenarios.load_tests_apply_scenarios(loader, in_tests, pattern)