diff --git a/README.rst b/README.rst
index 1e76e3668..962c93929 100644
--- a/README.rst
+++ b/README.rst
@@ -157,6 +157,14 @@ Quick-start using keystone::
     [...]
     >>> nt.keypairs.list()
     [...]
+    
+    # if you want to use the keystone api to modify users/tenants:
+    >>> from novaclient import client
+    >>> conn = client.HTTPClient(USER, PASS, TENANT, KEYSTONE_URL)
+    >>> from novaclient import keystone
+    >>> kc = keystone.Client(conn.client)
+    >>> kc.tenants.list()
+    [...]
 
 What's new?
 -----------
diff --git a/novaclient/base.py b/novaclient/base.py
index 7928f8d5c..d2e18fe3e 100644
--- a/novaclient/base.py
+++ b/novaclient/base.py
@@ -67,8 +67,12 @@ class Manager(object):
 
         if obj_class is None:
             obj_class = self.resource_class
-        return [obj_class(self, res)
-                for res in body[response_key] if res]
+        data = body[response_key]
+        # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
+        #           unlike other services which just return the list...
+        if type(data) is dict:
+            data = data['values']
+        return [obj_class(self, res) for res in data if res]
 
     def _get(self, url, response_key):
         resp, body = self.api.client.get(url)
diff --git a/novaclient/keystone/__init__.py b/novaclient/keystone/__init__.py
new file mode 100644
index 000000000..10105dd65
--- /dev/null
+++ b/novaclient/keystone/__init__.py
@@ -0,0 +1,2 @@
+from novaclient.keystone.client import Client
+
diff --git a/novaclient/keystone/client.py b/novaclient/keystone/client.py
new file mode 100644
index 000000000..ce776e3db
--- /dev/null
+++ b/novaclient/keystone/client.py
@@ -0,0 +1,65 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+#    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 copy
+from novaclient.keystone import tenants
+from novaclient.keystone import users
+
+
+class Client(object):
+    """
+    Top-level object to access the OpenStack Keystone API.
+
+    Create an instance with your creds::
+
+        >>> from novaclient import client
+        >>> conn = client.HTTPClient(USER, PASS, TENANT, KEYSTONE_URL)
+        >>> from novaclient import keystone
+        >>> kc = keystone.Client(conn)
+
+    Then call methods on its managers::
+
+        >>> kc.tenants.list()
+        ...
+        >>> kc.users.list()
+        ...
+
+    """
+
+    def __init__(self, client):
+        # FIXME(ja): managers work by making calls against self.client
+        #            which assumes management_url is set for the service.
+        #            with keystone you get a token/endpoints for multiple
+        #            services - so we have to clone and override the endpoint
+        # NOTE(ja): need endpoint from service catalog...  no lazy auth
+        client.authenticate()
+        self.client = copy.copy(client)
+        endpoint = client.service_catalog.url_for('identity', 'admin')
+        self.client.management_url = endpoint
+
+        self.tenants = tenants.TenantManager(self)
+        self.users = users.UserManager(self)
+
+    def authenticate(self):
+        """
+        Authenticate against the server.
+
+        Normally this is called automatically when you first access the API,
+        but you can call this method to force authentication right now.
+
+        Returns on success; raises :exc:`exceptions.Unauthorized` if the
+        credentials are wrong.
+        """
+        self.client.authenticate()
diff --git a/novaclient/keystone/tenants.py b/novaclient/keystone/tenants.py
new file mode 100644
index 000000000..5ef1469b0
--- /dev/null
+++ b/novaclient/keystone/tenants.py
@@ -0,0 +1,93 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+#    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.
+
+from novaclient import base
+
+
+class RoleRefs(base.Resource):
+    def __repr__(self):
+        return "<Roleref %s>" % self._info
+
+
+class Tenant(base.Resource):
+    def __repr__(self):
+        return "<Tenant %s>" % self._info
+
+    def delete(self):
+        self.manager.delete(self)
+
+    def update(self, description=None, enabled=None):
+        # FIXME(ja): set the attributes in this object if successful
+        self.manager.update(self.id, description, enabled)
+
+    def add_user(self, user):
+        self.manager.add_user_to_tenant(self.id, base.getid(user))
+
+
+class TenantManager(base.ManagerWithFind):
+    resource_class = Tenant
+
+    def get(self, tenant_id):
+        return self._get("/tenants/%s" % tenant_id, "tenant")
+
+    # FIXME(ja): finialize roles once finalized in keystone
+    #            right now the only way to add/remove a tenant is to
+    #            give them a role within a project
+    def get_user_role_refs(self, user_id):
+        return self._get("/users/%s/roleRefs" % user_id, "roleRefs")
+
+    def add_user_to_tenant(self, tenant_id, user_id):
+        params = {"roleRef": {"tenantId": tenant_id, "roleId": "Member"}}
+        return self._create("/users/%s/roleRefs" % user_id, params, "roleRef")
+
+    def remove_user_from_tenant(self, tenant_id, user_id):
+        params = {"roleRef": {"tenantId": tenant_id, "roleId": "Member"}}
+        # FIXME(ja): we have to get the roleref?  what is 5?
+        return self._delete("/users/%s/roleRefs/5" % user_id)
+
+    def create(self, tenant_id, description=None, enabled=True):
+        """
+        Create a new tenant.
+        """
+        params = {"tenant": {"id": tenant_id,
+                             "description": description,
+                             "enabled": enabled}}
+
+        return self._create('/tenants', params, "tenant")
+
+    def list(self):
+        """
+        Get a list of tenants.
+        :rtype: list of :class:`Tenant`
+        """
+        return self._list("/tenants", "tenants")
+
+    def update(self, tenant_id, description=None, enabled=None):
+        """
+        update a tenant with a new name and description
+        """
+        body = {"tenant": {'id': tenant_id }}
+        if enabled is not None:
+            body['tenant']['enabled'] = enabled
+        if description:
+            body['tenant']['description'] = description
+
+        self._update("/tenants/%s" % tenant_id, body)
+
+    def delete(self, tenant):
+        """
+        Delete a tenant.
+        """
+        self._delete("/tenants/%s" % (base.getid(tenant)))
diff --git a/novaclient/keystone/users.py b/novaclient/keystone/users.py
new file mode 100644
index 000000000..d7ae617f9
--- /dev/null
+++ b/novaclient/keystone/users.py
@@ -0,0 +1,105 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+#    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.
+
+from novaclient import base
+
+
+class User(base.Resource):
+    def __repr__(self):
+        return "<User %s>" % self._info
+
+    def delete(self):
+        self.manager.delete(self)
+
+
+class UserManager(base.ManagerWithFind):
+    resource_class = User
+
+    def get(self, user):
+        return self._get("/users/%s" % base.getid(user), "user")
+
+    def update_email(self, user, email):
+        """
+        Update email
+        """
+        # FIXME(ja): why do we have to send id in params and url?
+        params = {"user": {"id": base.getid(user),
+                           "email": email }}
+
+        self._update("/users/%s" % base.getid(user), params)
+
+    def update_enabled(self, user, enabled):
+        """
+        Update enabled-ness
+        """
+        params = {"user": {"id": base.getid(user),
+                           "enabled": enabled }}
+
+        self._update("/users/%s/enabled" % base.getid(user), params)
+
+    def update_password(self, user, password):
+        """
+        Update password
+        """
+        params = {"user": {"id": base.getid(user),
+                           "password": password }}
+
+        self._update("/users/%s/password" % base.getid(user), params)
+
+    def update_tenant(self, user, tenant):
+        """
+        Update default tenant.
+        """
+        params = {"user": {"id": base.getid(user),
+                           "tenantId": base.getid(tenant) }}
+
+        # FIXME(ja): seems like a bad url - default tenant is an attribute
+        #            not a subresource!???
+        self._update("/users/%s/tenant" % base.getid(user), params)
+
+    def create(self, user_id, password, email, tenant_id=None, enabled=True):
+        """
+        Create a user.
+        """
+        # FIXME(ja): email should be optional but keystone currently requires it
+        params = {"user": {"id": user_id,
+                           "password": password,
+                           "tenantId": tenant_id,
+                           "email": email,
+                           "enabled": enabled}}
+        return self._create('/users', params, "user")
+
+    def _create(self, url, body, response_key):
+        # NOTE(ja): since we post the id, we have to use a PUT instead of POST
+        resp, body = self.api.client.put(url, body=body)
+        return self.resource_class(self, body[response_key])
+
+    def delete(self, user):
+        """
+        Delete a user.
+        """
+        self._delete("/users/%s" % base.getid(user))
+
+    def list(self, tenant_id=None):
+        """
+        Get a list of users (optionally limited to a tenant)
+
+        :rtype: list of :class:`User`
+        """
+
+        if not tenant_id:
+            return self._list("/users", "users")
+        else:
+            return self._list("/tenants/%s/users" % tenant_id, "users")