From 419c2cb95f5ce0c515fd8636d90065dfcf784c8c Mon Sep 17 00:00:00 2001 From: termie Date: Mon, 20 Jun 2011 17:56:20 -0700 Subject: [PATCH] initial --- README.rst | 31 +++++++++++++ keystonelight/__init__.py | 0 keystonelight/backends/__init__.py | 0 keystonelight/backends/pam.py | 21 +++++++++ keystonelight/identity.py | 28 ++++++++++++ keystonelight/keystone_compat.py | 38 ++++++++++++++++ keystonelight/service.py | 70 ++++++++++++++++++++++++++++++ keystonelight/token.py | 16 +++++++ keystonelight/utils.py | 39 +++++++++++++++++ tools/pip-requires | 2 + 10 files changed, 245 insertions(+) create mode 100644 README.rst create mode 100644 keystonelight/__init__.py create mode 100644 keystonelight/backends/__init__.py create mode 100644 keystonelight/backends/pam.py create mode 100644 keystonelight/identity.py create mode 100644 keystonelight/keystone_compat.py create mode 100644 keystonelight/service.py create mode 100644 keystonelight/token.py create mode 100644 keystonelight/utils.py create mode 100644 tools/pip-requires diff --git a/README.rst b/README.rst new file mode 100644 index 0000000000..c6b57552f1 --- /dev/null +++ b/README.rst @@ -0,0 +1,31 @@ + + +General expected data model: + + ( tenants >--< users ) --< roles + + Tenants and Users have a many-to-many relationship. + A given Tenant-User pair can have many Roles. + + +Tenant Schema: + id: something big and unique + name: something displayable + .. created_at: datetime + .. deleted_at: datetime + +User Schema: + id: something big and unique + name: something displayable + .. created_at: datetime + .. deleted_at: datetime + + +General service model: + + (1) a web service with an API + (2) a variety of backend storage schemes for tenant-user pairs. + (3) a simple token datastore + + + diff --git a/keystonelight/__init__.py b/keystonelight/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystonelight/backends/__init__.py b/keystonelight/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystonelight/backends/pam.py b/keystonelight/backends/pam.py new file mode 100644 index 0000000000..8896c93049 --- /dev/null +++ b/keystonelight/backends/pam.py @@ -0,0 +1,21 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +from __future__ import absolute_imports + +import pam + + +class PamIdentity(object): + """Very basic identity based on PAM. + + Tenant is always the same as User, root user has admin role. + """ + + def authenticate(self, username, password): + if pam.authenticate(username, password): + extras = {} + if username == 'root': + extras['is_admin'] == True + # NOTE(termie): (tenant, user, extras) + return (username, username, extras) + diff --git a/keystonelight/identity.py b/keystonelight/identity.py new file mode 100644 index 0000000000..ee2345aa50 --- /dev/null +++ b/keystonelight/identity.py @@ -0,0 +1,28 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# these will be the basic data types for tenants and users +# backends will make use of them to return something that conforms to their apis + + +import hflags as flags + +from keystonelight import utils + + +FLAGS = flags.FLAGS +flags.DEFINE_string('identity_driver', + 'keystonelight.backends.dummy.DummyIdentity', + 'identity driver to handle identity requests') + + +class IdentityManager(object): + def __init__(self): + self.driver = utils.import_object(FLAGS.identity_driver) + + + def authenticate(self, context, **kwargs): + """Passthru authentication to the identity driver. + + This call will basically just result in getting a token. + """ + return self.driver.authenticate(**kwargs) diff --git a/keystonelight/keystone_compat.py b/keystonelight/keystone_compat.py new file mode 100644 index 0000000000..680c9d93f9 --- /dev/null +++ b/keystonelight/keystone_compat.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# this is the web service frontend that emulates keystone +from keystonelight import service + +def _token_to_keystone(token): + return {'id': token['id'], + 'expires': token.get('expires', '') + + +class KeystoneIdentityController(service.IdentityController): + def authenticate(self, context, **kwargs): + token = super(KeystoneIdentityController, self).authenticate( + context, **kwargs) + return {'auth': {'token': _token_to_keystone(token), + 'serviceCatalog': SERVICE_CATALOG}} + + +class KeystoneTokenController(service.TokenController): + def validate_token(self, context, token_id): + token = super(KeystoneTokenController, self).validate_token( + context, token_id) + # TODO(termie): munge into keystone format + + tenants = [{'tenantId': token['tenant']['id'], + 'name': token['tenant']['name']}] + roles = [] + if token['extras'].get('is_admin'): + roles.append({ + 'id': 1, + 'href': 'https://.openstack.org/identity/v2.0/roles/admin', + 'tenantId': token['tenant']['id']}) + + return {'auth': {'token': _token_to_keystone(token), + 'user': {'groups': {'group': tenants}, + 'roleRefs': {'roleRef': roles} + 'username': token['user']['name'], + 'tenantId': token['tenant']['id']}}} diff --git a/keystonelight/service.py b/keystonelight/service.py new file mode 100644 index 0000000000..e436cebc53 --- /dev/null +++ b/keystonelight/service.py @@ -0,0 +1,70 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# this is the web service frontend + +import hflags as flags + +from keystonelight import wsgi + + +FLAGS = flags.FLAGS + + +class TokenController(wsgi.Controller): + """Validate and pass through calls to TokenManager.""" + + def __init__(self): + self.token_api = token.Manager() + + def validate_token(self, context, token_id): + token = self.validate_token(context, token_id) + return token + + +class IdentityController(wsgi.Controller): + """Validate and pass calls through to IdentityManager. + + IdentityManager will also pretty much just pass calls through to + a specific driver. + """ + + def __init__(self): + self.identity_api = identity.Manager() + self.token_api = token.Manager() + + def authenticate(self, context, **kwargs): + tenant, user, extras = self.identity_api.authenticate(context, **kwargs) + token = self.token_api.create_token(context, + tenant=tenant, + user=user, + extras=extras) + return token + + + +class Router(object): + def __init__(self): + token_controller = TokenController() + identity_controller = IdentityController() + + mapper.connect('/v2.0/token', controller=identity_controller, + action='authenticate') + mapper.connect('/v2.0/token/{token_id}', controller=token_controller, + action='revoke_token', + conditions=dict(method=['DELETE'])) + + +class AdminRouter(object): + def __init__(self): + token_controller = TokenController() + identity_controller = IdentityController() + + mapper.connect('/v2.0/token', controller=identity_controller, + action='authenticate') + mapper.connect('/v2.0/token/{token_id}', controller=token_controller, + action='validate_token', + conditions=dict(method=['GET'])) + mapper.connect('/v2.0/token/{token_id}', controller=token_controller, + action='revoke_token', + conditions=dict(method=['DELETE'])) + diff --git a/keystonelight/token.py b/keystonelight/token.py new file mode 100644 index 0000000000..ffdd4c4c54 --- /dev/null +++ b/keystonelight/token.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# the token interfaces + +from keystonelight import identity + +class TokenManager(object): + def create_token(self, context, data): + pass + + def validate_token(self, context, token_id): + """Return info for a token if it is valid.""" + pass + + def revoke_token(self, context, token_id): + pass diff --git a/keystonelight/utils.py b/keystonelight/utils.py new file mode 100644 index 0000000000..406ad19e15 --- /dev/null +++ b/keystonelight/utils.py @@ -0,0 +1,39 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Justin Santa Barbara +# 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. + + +def import_class(import_str): + """Returns a class from a string including module and class.""" + mod_str, _sep, class_str = import_str.rpartition('.') + try: + __import__(mod_str) + return getattr(sys.modules[mod_str], class_str) + except (ImportError, ValueError, AttributeError), exc: + LOG.debug(_('Inner Exception: %s'), exc) + raise exception.ClassNotFound(class_name=class_str) + + +def import_object(import_str): + """Returns an object including a module or module and class.""" + try: + __import__(import_str) + return sys.modules[import_str] + except ImportError: + cls = import_class(import_str) + return cls() diff --git a/tools/pip-requires b/tools/pip-requires new file mode 100644 index 0000000000..14d62412c0 --- /dev/null +++ b/tools/pip-requires @@ -0,0 +1,2 @@ +hflags +pam==0.1.4