[PATCHv3 0/8] LDAP support

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Royce Lv (8): Add configuration of LDAP Split PAM and LDAP authentication Add LDAP authentication Fix test cases for authentication Split users and groups for permission query Move validation to authorizaiton change vm permission tag Update test model to fix user/group listing contrib/DEBIAN/control.in | 1 + contrib/kimchi.spec.fedora.in | 1 + contrib/kimchi.spec.suse.in | 1 + docs/README.md | 36 +++++----- src/kimchi.conf.in | 17 +++++ src/kimchi/auth.py | 157 +++++++++++++++++++++++++++++++----------- src/kimchi/config.py.in | 5 ++ src/kimchi/control/auth.py | 42 +++++++++++ src/kimchi/control/host.py | 14 ---- src/kimchi/i18n.py | 1 + src/kimchi/model/auth.py | 136 ++++++++++++++++++++++++++++++++++++ src/kimchi/model/host.py | 19 ----- src/kimchi/model/vms.py | 42 ++++++----- tests/test_model.py | 3 +- tests/test_rest.py | 4 +- tests/utils.py | 43 ++++++++---- 16 files changed, 398 insertions(+), 124 deletions(-) create mode 100644 src/kimchi/control/auth.py create mode 100644 src/kimchi/model/auth.py -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Add LDAP configuration to specify LDAP server, search base and filter for query user. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi.conf.in | 14 ++++++++++++++ src/kimchi/config.py.in | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/src/kimchi.conf.in b/src/kimchi.conf.in index ea39292..62eb40b 100644 --- a/src/kimchi.conf.in +++ b/src/kimchi.conf.in @@ -43,3 +43,17 @@ [display] # Port for websocket proxy to listen on #display_proxy_port = 64667 + +[authentication] +# Authentication method, available option: pam, ldap. +# method = pam + +# If specified method to ldap, following fields need to be specified. +# ldap server domain name used to authenticate. +# ldap_server = "localhost" + +# Search tree base in ldap +# ldap_search_base = "ou=People, dc=kimchi, dc=org" + +# User id filter +# ldap_search_filter = "uid=%(username)s" diff --git a/src/kimchi/config.py.in b/src/kimchi/config.py.in index 06765a2..88de1aa 100644 --- a/src/kimchi/config.py.in +++ b/src/kimchi/config.py.in @@ -285,6 +285,11 @@ def _get_config(): config.set("server", "environment", "production") config.set("server", "federation", "off") config.set('server', 'max_body_size', '4*1024*1024') + config.add_section("authentication") + config.set("authentication", "method", "pam") + config.set("authentication", "ldap_server", "") + config.set("authentication", "ldap_search_base", "") + config.set("authentication", "ldap_search_filter", "") config.add_section("logging") config.set("logging", "log_dir", paths.log_dir) config.set("logging", "log_level", DEFAULT_LOG_LEVEL) -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Split PAM authentication implementation and abstract a common class. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/auth.py | 113 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/src/kimchi/auth.py b/src/kimchi/auth.py index c8801a5..46593a4 100644 --- a/src/kimchi/auth.py +++ b/src/kimchi/auth.py @@ -31,6 +31,7 @@ import urllib2 from kimchi import template +from kimchi.config import config from kimchi.exception import InvalidOperation, OperationFailed from kimchi.utils import get_all_tabs, run_command @@ -39,6 +40,7 @@ USER_NAME = 'username' USER_GROUPS = 'groups' USER_ROLES = 'roles' REFRESH = 'robot-refresh' +AUTH = 'auth_method' tabs = get_all_tabs() @@ -59,6 +61,22 @@ def debug(msg): class User(object): + @classmethod + def get(cls, auth_args): + auth_type = auth_args.pop('auth_type') + for klass in cls.__subclasses__(): + if auth_type == klass.auth_type: + try: + if not klass.authenticate(**auth_args): + debug("cannot verify user with the given password") + return None + except OperationFailed: + raise + return klass(auth_args['username']) + + +class PAMUser(User): + auth_type = "pam" def __init__(self, username): self.user = {} @@ -124,40 +142,54 @@ class User(object): def get_user(self): return self.user + @staticmethod + def authenticate(username, password, service="passwd"): + '''Returns True if authenticate is OK via PAM.''' + def _pam_conv(auth, query_list, userData=None): + resp = [] + for i in range(len(query_list)): + query, qtype = query_list[i] + if qtype == PAM.PAM_PROMPT_ECHO_ON: + resp.append((username, 0)) + elif qtype == PAM.PAM_PROMPT_ECHO_OFF: + resp.append((password, 0)) + elif qtype == PAM.PAM_PROMPT_ERROR_MSG: + cherrypy.log.error_log.error( + "PAM authenticate prompt error: %s" % query) + resp.append(('', 0)) + elif qtype == PAM.PAM_PROMPT_TEXT_INFO: + resp.append(('', 0)) + else: + return None + return resp + + auth = PAM.pam() + auth.start(service) + auth.set_item(PAM.PAM_USER, username) + auth.set_item(PAM.PAM_CONV, _pam_conv) + try: + auth.authenticate() + except PAM.error, (resp, code): + msg_args = {'username': username, 'code': code} + raise OperationFailed("KCHAUTH0001E", msg_args) -def authenticate(username, password, service="passwd"): - '''Returns True if authenticate is OK via PAM.''' - def _pam_conv(auth, query_list, userData=None): - resp = [] - for i in range(len(query_list)): - query, qtype = query_list[i] - if qtype == PAM.PAM_PROMPT_ECHO_ON: - resp.append((username, 0)) - elif qtype == PAM.PAM_PROMPT_ECHO_OFF: - resp.append((password, 0)) - elif qtype == PAM.PAM_PROMPT_ERROR_MSG: - cherrypy.log.error_log.error("PAM authenticate prompt error " - "message: %s" % query) - resp.append(('', 0)) - elif qtype == PAM.PAM_PROMPT_TEXT_INFO: - resp.append(('', 0)) - else: - return None - return resp - - auth = PAM.pam() - auth.start(service) - auth.set_item(PAM.PAM_USER, username) - auth.set_item(PAM.PAM_CONV, _pam_conv) - - try: - auth.authenticate() - except PAM.error: - raise - - return True + return True +class LDAPUser(User): + auth_type = "ldap" + def __init__(self, username): + self.user = {} + self.user[USER_NAME] = username + self.user[USER_GROUPS] = None + # FIXME: user roles will be changed according roles assignment after + # objstore is integrated + self.user[USER_ROLES] = dict.fromkeys(tabs, 'user') + + @staticmethod + def authenticate(username, password): + return False + def from_browser(): # Enable Basic Authentication for REST tools. # Ajax request sent from jQuery in browser will have "X-Requested-With" @@ -216,21 +248,22 @@ def check_auth_httpba(): def login(username, password, **kwargs): - try: - if not authenticate(username, password): - debug("User cannot be verified with the supplied password") - return None - except PAM.error, (resp, code): - msg_args = {'username': username, 'code': code} - raise OperationFailed("KCHAUTH0001E", msg_args) - - user = User(username) + auth_args = {'auth_type': config.get("authentication", "method"), + 'username': username, + 'password': password} + + user = User.get(auth_args) + if not user: + debug("User cannot be verified with the supplied password") + return None + debug("User verified, establishing session") cherrypy.session.acquire_lock() cherrypy.session.regenerate() cherrypy.session[USER_NAME] = username cherrypy.session[USER_GROUPS] = user.get_groups() cherrypy.session[USER_ROLES] = user.get_roles() + cherrypy.session[AUTH] = config.get("authentication", "method") cherrypy.session[REFRESH] = time.time() cherrypy.session.release_lock() return user.get_user() -- 1.8.3.2

On 11/10/2014 05:09 AM, lvroyce@linux.vnet.ibm.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Split PAM authentication implementation and abstract a common class.
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/auth.py | 113 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 40 deletions(-)
diff --git a/src/kimchi/auth.py b/src/kimchi/auth.py index c8801a5..46593a4 100644 --- a/src/kimchi/auth.py +++ b/src/kimchi/auth.py @@ -31,6 +31,7 @@ import urllib2
from kimchi import template +from kimchi.config import config from kimchi.exception import InvalidOperation, OperationFailed from kimchi.utils import get_all_tabs, run_command
@@ -39,6 +40,7 @@ USER_NAME = 'username' USER_GROUPS = 'groups' USER_ROLES = 'roles' REFRESH = 'robot-refresh' +AUTH = 'auth_method'
tabs = get_all_tabs()
@@ -59,6 +61,22 @@ def debug(msg):
class User(object): + @classmethod + def get(cls, auth_args): + auth_type = auth_args.pop('auth_type') + for klass in cls.__subclasses__(): + if auth_type == klass.auth_type: + try: + if not klass.authenticate(**auth_args): + debug("cannot verify user with the given password") + return None + except OperationFailed: + raise + return klass(auth_args['username']) + + +class PAMUser(User): + auth_type = "pam"
def __init__(self, username): self.user = {} @@ -124,40 +142,54 @@ class User(object): def get_user(self): return self.user
+ @staticmethod + def authenticate(username, password, service="passwd"): + '''Returns True if authenticate is OK via PAM.''' + def _pam_conv(auth, query_list, userData=None): + resp = [] + for i in range(len(query_list)): + query, qtype = query_list[i] + if qtype == PAM.PAM_PROMPT_ECHO_ON: + resp.append((username, 0)) + elif qtype == PAM.PAM_PROMPT_ECHO_OFF: + resp.append((password, 0)) + elif qtype == PAM.PAM_PROMPT_ERROR_MSG: + cherrypy.log.error_log.error( + "PAM authenticate prompt error: %s" % query) + resp.append(('', 0)) + elif qtype == PAM.PAM_PROMPT_TEXT_INFO: + resp.append(('', 0)) + else: + return None + return resp + + auth = PAM.pam() + auth.start(service) + auth.set_item(PAM.PAM_USER, username) + auth.set_item(PAM.PAM_CONV, _pam_conv) + try: + auth.authenticate() + except PAM.error, (resp, code): + msg_args = {'username': username, 'code': code} + raise OperationFailed("KCHAUTH0001E", msg_args)
-def authenticate(username, password, service="passwd"): - '''Returns True if authenticate is OK via PAM.''' - def _pam_conv(auth, query_list, userData=None): - resp = [] - for i in range(len(query_list)): - query, qtype = query_list[i] - if qtype == PAM.PAM_PROMPT_ECHO_ON: - resp.append((username, 0)) - elif qtype == PAM.PAM_PROMPT_ECHO_OFF: - resp.append((password, 0)) - elif qtype == PAM.PAM_PROMPT_ERROR_MSG: - cherrypy.log.error_log.error("PAM authenticate prompt error " - "message: %s" % query) - resp.append(('', 0)) - elif qtype == PAM.PAM_PROMPT_TEXT_INFO: - resp.append(('', 0)) - else: - return None - return resp - - auth = PAM.pam() - auth.start(service) - auth.set_item(PAM.PAM_USER, username) - auth.set_item(PAM.PAM_CONV, _pam_conv) - - try: - auth.authenticate() - except PAM.error: - raise - - return True + return True
+class LDAPUser(User): + auth_type = "ldap" + def __init__(self, username): + self.user = {} + self.user[USER_NAME] = username + self.user[USER_GROUPS] = None + # FIXME: user roles will be changed according roles assignment after + # objstore is integrated + self.user[USER_ROLES] = dict.fromkeys(tabs, 'user') + + @staticmethod + def authenticate(username, password): + return False + def from_browser(): # Enable Basic Authentication for REST tools. # Ajax request sent from jQuery in browser will have "X-Requested-With" @@ -216,21 +248,22 @@ def check_auth_httpba():
def login(username, password, **kwargs): - try: - if not authenticate(username, password): - debug("User cannot be verified with the supplied password") - return None - except PAM.error, (resp, code): - msg_args = {'username': username, 'code': code} - raise OperationFailed("KCHAUTH0001E", msg_args) - - user = User(username) + auth_args = {'auth_type': config.get("authentication", "method"), + 'username': username, + 'password': password} + + user = User.get(auth_args) + if not user: + debug("User cannot be verified with the supplied password") + return None + debug("User verified, establishing session") cherrypy.session.acquire_lock() cherrypy.session.regenerate() cherrypy.session[USER_NAME] = username cherrypy.session[USER_GROUPS] = user.get_groups() cherrypy.session[USER_ROLES] = user.get_roles()
+ cherrypy.session[AUTH] = config.get("authentication", "method")
Would not it be moved to /config/capabilities?
cherrypy.session[REFRESH] = time.time() cherrypy.session.release_lock() return user.get_user()

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Add LDAP authentication, also deals with invalid user, LDAP search base configure error and other LDAP errors. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- contrib/DEBIAN/control.in | 1 + contrib/kimchi.spec.fedora.in | 1 + contrib/kimchi.spec.suse.in | 1 + docs/README.md | 36 +++++++++++++++++--------------- src/kimchi.conf.in | 3 +++ src/kimchi/auth.py | 48 +++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 72 insertions(+), 18 deletions(-) diff --git a/contrib/DEBIAN/control.in b/contrib/DEBIAN/control.in index ecddc71..7e0ed8d 100644 --- a/contrib/DEBIAN/control.in +++ b/contrib/DEBIAN/control.in @@ -26,6 +26,7 @@ Depends: python-cherrypy3 (>= 3.2.0), firewalld, nginx, python-guestfs, + python-ldap, libguestfs-tools, spice-html5 Build-Depends: libxslt, diff --git a/contrib/kimchi.spec.fedora.in b/contrib/kimchi.spec.fedora.in index 236c862..c1929f8 100644 --- a/contrib/kimchi.spec.fedora.in +++ b/contrib/kimchi.spec.fedora.in @@ -28,6 +28,7 @@ Requires: nfs-utils Requires: nginx Requires: iscsi-initiator-utils Requires: policycoreutils-python +Requires: python-ldap Requires: python-libguestfs Requires: libguestfs-tools BuildRequires: libxslt diff --git a/contrib/kimchi.spec.suse.in b/contrib/kimchi.spec.suse.in index 96d3b4c..be75590 100644 --- a/contrib/kimchi.spec.suse.in +++ b/contrib/kimchi.spec.suse.in @@ -22,6 +22,7 @@ Requires: python-psutil >= 0.6.0 Requires: python-jsonschema >= 1.3.0 Requires: python-ethtool Requires: python-ipaddr +Requires: python-ldap Requires: python-lxml Requires: python-xml Requires: nfs-client diff --git a/docs/README.md b/docs/README.md index d18a946..de6db91 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,11 +49,12 @@ Install Dependencies $ sudo yum install gcc make autoconf automake gettext-devel git \ python-cherrypy python-cheetah libvirt-python \ - libvirt python-imaging PyPAM m2crypto \ - python-jsonschema rpm-build qemu-kvm python-psutil \ - python-ethtool sos python-ipaddr python-lxml \ - nfs-utils iscsi-initiator-utils libxslt pyparted \ - nginx policycoreutils-python python-libguestfs \ + libvirt python-imaging \ + PyPAM m2crypto python-jsonschema rpm-build \ + qemu-kvm python-psutil python-ethtool sos \ + python-ipaddr python-ldap python-lxml nfs-utils \ + iscsi-initiator-utils libxslt pyparted nginx \ + policycoreutils-python python-libguestfs \ libguestfs-tools python-requests python-websockify \ novnc @@ -77,11 +78,12 @@ for more information on how to configure your system to access this repository. $ sudo apt-get install gcc make autoconf automake gettext git \ python-cherrypy3 python-cheetah python-libvirt \ - libvirt-bin python-imaging python-pam \ - python-m2crypto python-jsonschema qemu-kvm \ - libtool python-psutil python-ethtool \ - sosreport python-ipaddr python-lxml nfs-common \ - open-iscsi lvm2 xsltproc python-parted nginx \ + libvirt-bin python-imaging \ + python-pam python-m2crypto python-jsonschema \ + qemu-kvm libtool python-psutil python-ethtool \ + sosreport python-ipaddr python-ldap \ + python-lxml nfs-common open-iscsi lvm2 xsltproc \ + python-parted nginx \ firewalld python-guestfs libguestfs-tools \ python-requests websockify novnc spice-html5 @@ -93,12 +95,14 @@ for more information on how to configure your system to access this repository. $ sudo zypper install gcc make autoconf automake gettext-tools git \ python-CherryPy python-Cheetah libvirt-python \ - libvirt python-imaging python-pam python-M2Crypto \ - python-jsonschema rpm-build kvm python-psutil \ - python-ethtool python-ipaddr python-lxml \ - nfs-client open-iscsi libxslt-tools python-xml \ - python-parted nginx python-libguestfs \ - guestfs-tools python-requests python-websockify novnc + libvirt python-imaging \ + python-pam python-M2Crypto python-jsonschema \ + rpm-build kvm python-psutil python-ethtool \ + python-ipaddr python-ldap python-lxml nfs-client \ + open-iscsi libxslt-tools python-xml \ + python-parted nginx \ + python-libguestfs guestfs-tools python-requests \ + python-websockify novnc Packages version requirement: python-psutil >= 0.6.0 diff --git a/src/kimchi.conf.in b/src/kimchi.conf.in index 62eb40b..003ce4a 100644 --- a/src/kimchi.conf.in +++ b/src/kimchi.conf.in @@ -57,3 +57,6 @@ # User id filter # ldap_search_filter = "uid=%(username)s" + +# User IDs regarded as kimchi admin +# ldap_admin_id = "foo@foo.com, bar@bar.com" diff --git a/src/kimchi/auth.py b/src/kimchi/auth.py index 46593a4..bc9ce08 100644 --- a/src/kimchi/auth.py +++ b/src/kimchi/auth.py @@ -20,6 +20,7 @@ import base64 import cherrypy import fcntl +import ldap import multiprocessing import os import PAM @@ -178,17 +179,60 @@ class PAMUser(User): class LDAPUser(User): auth_type = "ldap" + def __init__(self, username): self.user = {} self.user[USER_NAME] = username - self.user[USER_GROUPS] = None + self.user[USER_GROUPS] = list() # FIXME: user roles will be changed according roles assignment after # objstore is integrated self.user[USER_ROLES] = dict.fromkeys(tabs, 'user') @staticmethod def authenticate(username, password): - return False + ldap_server = config.get("authentication", "ldap_server").strip('"') + ldap_search_base = config.get( + "authentication", "ldap_search_base").strip('"') + ldap_search_filter = config.get( + "authentication", "ldap_search_filter", + vars={"username": username.encode("utf-8")}).strip('"') + + connect = ldap.open(ldap_server) + try: + try: + result = connect.search_s( + ldap_search_base, ldap.SCOPE_SUBTREE, ldap_search_filter) + if len(result) == 0: + entity = ldap_search_filter % {'username': username} + raise ldap.LDAPError("Invalid ldap entity:%s" % entity) + except ldap.NO_SUCH_OBJECT: + # ldap search base specified wrongly. + raise ldap.LDAPError( + "invalid ldap search base %s" % ldap_search_base) + + try: + connect.bind_s(result[0][0], password) + except ldap.INVALID_CREDENTIALS: + # invalid user password + raise ldap.LDAPError("invalid user/passwd") + connect.unbind_s() + return True + except ldap.LDAPError, e: + arg = {"username": username, "code": e.message} + raise OperationFailed("KCHAUTH0001E", arg) + + def get_groups(self): + return self.user[USER_GROUPS] + + def get_roles(self): + admin_id = config.get("authentication", "ldap_admin_id").strip('"') + if self.user[USER_NAME] in admin_id.split(','): + self.user[USER_ROLES] = dict.fromkeys(tabs, 'admin') + return self.user[USER_ROLES] + + def get_user(self): + return self.user + def from_browser(): # Enable Basic Authentication for REST tools. -- 1.8.3.2

On 10-11-2014 05:09, lvroyce@linux.vnet.ibm.com wrote:
+ try: + try: + result = connect.search_s( + ldap_search_base, ldap.SCOPE_SUBTREE, ldap_search_filter) + if len(result) == 0: + entity = ldap_search_filter % {'username': username} + raise ldap.LDAPError("Invalid ldap entity:%s" % entity) + except ldap.NO_SUCH_OBJECT: + # ldap search base specified wrongly. + raise ldap.LDAPError( + "invalid ldap search base %s" % ldap_search_base) + + try: + connect.bind_s(result[0][0], password) + except ldap.INVALID_CREDENTIALS: + # invalid user password + raise ldap.LDAPError("invalid user/passwd") + connect.unbind_s() + return True + except ldap.LDAPError, e: + arg = {"username": username, "code": e.message} + raise OperationFailed("KCHAUTH0001E", arg)
I think the code would look better without the external try/except block.

On 2014年11月12日 23:19, Crístian Viana wrote:
On 10-11-2014 05:09, lvroyce@linux.vnet.ibm.com wrote:
+ try: + try: + result = connect.search_s( + ldap_search_base, ldap.SCOPE_SUBTREE, ldap_search_filter) + if len(result) == 0: + entity = ldap_search_filter % {'username': username} + raise ldap.LDAPError("Invalid ldap entity:%s" % entity) + except ldap.NO_SUCH_OBJECT: + # ldap search base specified wrongly. + raise ldap.LDAPError( + "invalid ldap search base %s" % ldap_search_base) + + try: + connect.bind_s(result[0][0], password) + except ldap.INVALID_CREDENTIALS: + # invalid user password + raise ldap.LDAPError("invalid user/passwd") + connect.unbind_s() + return True + except ldap.LDAPError, e: + arg = {"username": username, "code": e.message} + raise OperationFailed("KCHAUTH0001E", arg)
I think the code would look better without the external try/except block. Do you mean get them handled all by kimchi exception? I've considered that, but like: connect.search_s() connect.bind_s() connect.unbind_s() all throw ldap.LDAPError, they also throw exception like: ldap.INVALD_CREDENTIALS. so if delete the external try/except, I need to add 3 excepts to each operation. You can see I do this because ldap lib does not handle all exception with ldap.LDAPError.

On 13-11-2014 05:44, Royce Lv wrote:
I think the code would look better without the external try/except block. Do you mean get them handled all by kimchi exception? I've considered that, but like: connect.search_s() connect.bind_s() connect.unbind_s() all throw ldap.LDAPError, they also throw exception like: ldap.INVALD_CREDENTIALS. so if delete the external try/except, I need to add 3 excepts to each operation. You can see I do this because ldap lib does not handle all exception with ldap.LDAPError.
Wouldn't something like this work? try: result = connect.search_s( ldap_search_base, ldap.SCOPE_SUBTREE, ldap_search_filter) if len(result) == 0: entity = ldap_search_filter % {'username': username} raise ldap.LDAPError("Invalid ldap entity:%s" % entity) connect.bind_s(result[0][0], password) connect.unbind_s() return True except ldap.INVALID_CREDENTIALS: # invalid user password arg = {"username": username, "code": "invalid user/passwd"} raise OperationFailed("KCHAUTH0001E", arg) except ldap.NO_SUCH_OBJECT: # ldap search base specified wrongly. arg = {"username": username, "code": "invalid ldap search base %s" % ldap_search_base)} raise OperationFailed("KCHAUTH0001E", arg) except ldap.LDAPError, e: arg = {"username": username, "code": e.message} raise OperationFailed("KCHAUTH0001E", arg) Maybe you could even have different messages ID instead of passing "code" like that. Keep in mind that those "codes" won't be translated... But that's just code preference, I just think this looks better than using a nested block.

On 2014年11月14日 03:04, Crístian Viana wrote:
On 13-11-2014 05:44, Royce Lv wrote:
I think the code would look better without the external try/except block. Do you mean get them handled all by kimchi exception? I've considered that, but like: connect.search_s() connect.bind_s() connect.unbind_s() all throw ldap.LDAPError, they also throw exception like: ldap.INVALD_CREDENTIALS. so if delete the external try/except, I need to add 3 excepts to each operation. You can see I do this because ldap lib does not handle all exception with ldap.LDAPError.
Wouldn't something like this work?
try: result = connect.search_s( ldap_search_base, ldap.SCOPE_SUBTREE, ldap_search_filter) if len(result) == 0: entity = ldap_search_filter % {'username': username} raise ldap.LDAPError("Invalid ldap entity:%s" % entity)
connect.bind_s(result[0][0], password) connect.unbind_s() return True except ldap.INVALID_CREDENTIALS: # invalid user password arg = {"username": username, "code": "invalid user/passwd"} raise OperationFailed("KCHAUTH0001E", arg) except ldap.NO_SUCH_OBJECT: # ldap search base specified wrongly. arg = {"username": username, "code": "invalid ldap search base %s" % ldap_search_base)} raise OperationFailed("KCHAUTH0001E", arg) except ldap.LDAPError, e: arg = {"username": username, "code": e.message} raise OperationFailed("KCHAUTH0001E", arg)
Maybe you could even have different messages ID instead of passing "code" like that. Keep in mind that those "codes" won't be translated...
But that's just code preference, I just think this looks better than using a nested block. ACK, I was too used to catch things where they throw.

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Authentication function moved to class, so build a fake user class to cover test. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- tests/utils.py | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 9133904..f846d48 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -35,7 +35,8 @@ from lxml import etree import kimchi.mockmodel import kimchi.server -from kimchi.config import paths +from kimchi.config import config, paths +from kimchi.auth import User, USER_NAME, USER_GROUPS, USER_ROLES, tabs from kimchi.exception import OperationFailed from kimchi.utils import kimchi_log @@ -169,29 +170,43 @@ def get_remote_iso_path(): return remote_path -def patch_auth(sudo=True): - """ - Override the authenticate function with a simple test against an - internal dict of users and passwords. - """ +class FakeUser(User): + auth_type = "fake" + sudo = True + + def __init__(self, username): + self.user = {} + self.user[USER_NAME] = username + self.user[USER_GROUPS] = None + self.user[USER_ROLES] = dict.fromkeys(tabs, 'user') - def _get_groups(self): + def get_groups(self): return ['groupA', 'groupB', 'wheel'] - def _has_sudo(self, result): - result.value = sudo + def get_roles(self): + if self.sudo: + self.user[USER_ROLES] = dict.fromkeys(tabs, 'admin') + return self.user[USER_ROLES] + + def get_user(self): + return self.user - def _authenticate(username, password, service="passwd"): + @staticmethod + def authenticate(username, password, service="passwd"): try: return kimchi.mockmodel.fake_user[username] == password except KeyError, e: raise OperationFailed("KCHAUTH0001E", {'username': 'username', 'code': e.message}) - import kimchi.auth - kimchi.auth.authenticate = _authenticate - kimchi.auth.User.get_groups = _get_groups - kimchi.auth.User._has_sudo = _has_sudo + +def patch_auth(sudo=True): + """ + Override the authenticate function with a simple test against an + internal dict of users and passwords. + """ + config.set("authentication", "method", "fake") + FakeUser.sudo = sudo def normalize_xml(xml_str): -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Put ldap validation in a single function to resue in authorization. Tested: 1. LDAP: GET /users?_user_id=a_valid_user_id GET /users?_user_id=invalid_user_id GET /groups 2. PAM: GET /users GET /groups Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/control/auth.py | 42 ++++++++++++++ src/kimchi/control/host.py | 14 ----- src/kimchi/i18n.py | 1 + src/kimchi/model/auth.py | 136 +++++++++++++++++++++++++++++++++++++++++++++ src/kimchi/model/host.py | 19 ------- src/kimchi/model/vms.py | 4 +- tests/test_rest.py | 4 +- 7 files changed, 183 insertions(+), 37 deletions(-) create mode 100644 src/kimchi/control/auth.py create mode 100644 src/kimchi/model/auth.py diff --git a/src/kimchi/control/auth.py b/src/kimchi/control/auth.py new file mode 100644 index 0000000..ebc019e --- /dev/null +++ b/src/kimchi/control/auth.py @@ -0,0 +1,42 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013-2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.control.base import SimpleCollection +from kimchi.control.utils import get_class_name, model_fn, UrlSubNode +from kimchi.template import render + + +@UrlSubNode('users', True) +class Users(SimpleCollection): + def __init__(self, model): + super(Users, self).__init__(model) + self.role_key = 'guests' + + def get(self, filter_params): + res_list = [] + get_list = getattr(self.model, model_fn(self, 'get_list')) + res_list = get_list(*self.model_args, **filter_params) + return render(get_class_name(self), res_list) + + +@UrlSubNode('groups', True) +class Groups(SimpleCollection): + def __init__(self, model): + super(Groups, self).__init__(model) + self.role_key = 'guests' diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 4362da7..5a185e3 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -36,8 +36,6 @@ class Host(Resource): self.devices = Devices(self.model) self.packagesupdate = PackagesUpdate(self.model) self.repositories = Repositories(self.model) - self.users = Users(self.model) - self.groups = Groups(self.model) self.swupdate = self.generate_action_handler_task('swupdate') @property @@ -159,15 +157,3 @@ class Repository(Resource): @property def data(self): return self.info - - -class Users(SimpleCollection): - def __init__(self, model): - super(Users, self).__init__(model) - self.role_key = 'guests' - - -class Groups(SimpleCollection): - def __init__(self, model): - super(Groups, self).__init__(model) - self.role_key = 'guests' diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index f789259..4e5a59b 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -39,6 +39,7 @@ messages = { "KCHAUTH0001E": _("Authentication failed for user '%(username)s'. [Error code: %(code)s]"), "KCHAUTH0002E": _("You are not authorized to access Kimchi"), "KCHAUTH0003E": _("Specify %(item)s to login into Kimchi"), + "KCHAUTH0004E": _("User %(user_id)s not found with given LDAP settings."), "KCHDEVS0001E": _('Unknown "_cap" specified'), "KCHDEVS0002E": _('"_passthrough" should be "true" or "false"'), diff --git a/src/kimchi/model/auth.py b/src/kimchi/model/auth.py new file mode 100644 index 0000000..d52c919 --- /dev/null +++ b/src/kimchi/model/auth.py @@ -0,0 +1,136 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import grp +import ldap +import pwd + +from kimchi.config import config +from kimchi.exception import NotFoundError + + +class UsersModel(object): + def __init__(self, **args): + auth_type = config.get("authentication", "method") + for klass in UsersModel.__subclasses__(): + if auth_type == klass.auth_type: + self.user = klass(**args) + + def get_list(self, **args): + return self.user._get_list(**args) + + def validate(self, user): + return self.user._validate(user) + + +class PAMUsersModel(UsersModel): + auth_type = 'pam' + + def __init__(self, **kargs): + pass + + def _get_list(self): + return [user.pw_name for user in pwd.getpwall() + if user.pw_shell.rsplit("/")[-1] not in ["nologin", "false"]] + + def _validate(self, user): + try: + return user in self._get_list() + except: + return False + + +class LDAPUsersModel(UsersModel): + auth_type = 'ldap' + + def __init__(self, **kargs): + pass + + def _get_list(self, _user_id=''): + return self._get_user(_user_id) + + def _validate(self, user): + try: + self._get_user(user) + return True + except NotFoundError: + return False + + def _get_user(self, _user_id): + ldap_server = config.get("authentication", "ldap_server").strip('"') + ldap_search_base = config.get( + "authentication", "ldap_search_base").strip('"') + ldap_search_filter = config.get( + "authentication", "ldap_search_filter", + vars={"username": _user_id.encode("utf-8")}).strip('"') + + connect = ldap.open(ldap_server) + try: + result = connect.search_s( + ldap_search_base, ldap.SCOPE_SUBTREE, ldap_search_filter) + if len(result) == 0: + raise NotFoundError("KCHAUTH0004E", {'user_id': _user_id}) + return result[0][1] + except ldap.NO_SUCH_OBJECT: + raise NotFoundError("KCHAUTH0004E", {'user_id': _user_id}) + + +class GroupsModel(object): + def __init__(self, **args): + auth_type = config.get("authentication", "method") + for klass in GroupsModel.__subclasses__(): + if auth_type == klass.auth_type: + self.grp = klass(**args) + + + def get_list(self, **args): + if hasattr(self.grp, '_get_list'): + return self.grp._get_list(**args) + else: + return list() + + def validate(self, gid): + return self.grp._validate(gid) + + +class PAMGroupsModel(GroupsModel): + auth_type = 'pam' + + def __init__(self, **kargs): + pass + + def _get_list(self): + return [group.gr_name for group in grp.getgrall()] + + def _validate(self, gid): + try: + grp.getgrnam(gid) + except KeyError: + return False + return True + + +class LDAPGroupsModel(GroupsModel): + auth_type = 'ldap' + + def __init__(self, **kargs): + pass + + def _validate(self, gid): + return False diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index 8cddcdc..009f448 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -17,12 +17,10 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import grp import libvirt import os import time import platform -import pwd from collections import defaultdict import psutil @@ -457,20 +455,3 @@ class RepositoryModel(object): raise InvalidOperation('KCHREPOS0014E') return self._repositories.removeRepository(repo_id) - - -class UsersModel(object): - def __init__(self, **kargs): - pass - - def get_list(self): - return [user.pw_name for user in pwd.getpwall() - if user.pw_shell.rsplit("/")[-1] not in ["nologin", "false"]] - - -class GroupsModel(object): - def __init__(self, **kargs): - pass - - def get_list(self): - return [group.gr_name for group in grp.getgrall()] diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index f2e4ae3..8f14f9e 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -249,8 +249,8 @@ class VMModel(object): self.conn = kargs['conn'] self.objstore = kargs['objstore'] self.vmscreenshot = VMScreenshotModel(**kargs) - self.users = import_class('kimchi.model.host.UsersModel')(**kargs) - self.groups = import_class('kimchi.model.host.GroupsModel')(**kargs) + self.users = import_class('kimchi.model.auth.UsersModel')(**kargs) + self.groups = import_class('kimchi.model.auth.GroupsModel')(**kargs) def update(self, name, params): dom = self.get_vm(name, self.conn) diff --git a/tests/test_rest.py b/tests/test_rest.py index 9bc930f..420bfd0 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -277,7 +277,7 @@ class RestTests(unittest.TestCase): self.assertEquals(params[key], vm[key]) # change only VM users - groups are not changed (default is empty) - resp = self.request('/host/users', '{}', 'GET') + resp = self.request('/users', '{}', 'GET') users = json.loads(resp.read()) req = json.dumps({'users': users}) resp = self.request('/vms/∨м-црdαtеd', req, 'PUT') @@ -286,7 +286,7 @@ class RestTests(unittest.TestCase): self.assertEquals(users, info['users']) # change only VM groups - users are not changed (default is empty) - resp = self.request('/host/groups', '{}', 'GET') + resp = self.request('/groups', '{}', 'GET') groups = json.loads(resp.read()) req = json.dumps({'groups': groups}) resp = self.request('/vms/∨м-црdαtеd', req, 'PUT') -- 1.8.3.2

On 11/10/2014 05:09 AM, lvroyce@linux.vnet.ibm.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Put ldap validation in a single function to resue in authorization. Tested: 1. LDAP: GET /users?_user_id=a_valid_user_id GET /users?_user_id=invalid_user_id GET /groups 2. PAM: GET /users GET /groups
You need to adjust the docs/API.md to reflect those changes.
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/control/auth.py | 42 ++++++++++++++ src/kimchi/control/host.py | 14 ----- src/kimchi/i18n.py | 1 + src/kimchi/model/auth.py | 136 +++++++++++++++++++++++++++++++++++++++++++++ src/kimchi/model/host.py | 19 ------- src/kimchi/model/vms.py | 4 +- tests/test_rest.py | 4 +- 7 files changed, 183 insertions(+), 37 deletions(-) create mode 100644 src/kimchi/control/auth.py create mode 100644 src/kimchi/model/auth.py
diff --git a/src/kimchi/control/auth.py b/src/kimchi/control/auth.py new file mode 100644 index 0000000..ebc019e --- /dev/null +++ b/src/kimchi/control/auth.py
Name it as users.py and groups.py
@@ -0,0 +1,42 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013-2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.control.base import SimpleCollection +from kimchi.control.utils import get_class_name, model_fn, UrlSubNode +from kimchi.template import render +
+ +@UrlSubNode('users', True) +class Users(SimpleCollection): + def __init__(self, model): + super(Users, self).__init__(model) + self.role_key = 'guests' + + def get(self, filter_params): + res_list = [] + get_list = getattr(self.model, model_fn(self, 'get_list')) + res_list = get_list(*self.model_args, **filter_params) + return render(get_class_name(self), res_list) + + +@UrlSubNode('groups', True) +class Groups(SimpleCollection): + def __init__(self, model): + super(Groups, self).__init__(model) + self.role_key = 'guests'
Split it in 2 files: users.py and groups.py as we have done one URI per file to make maintenance easier.
diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 4362da7..5a185e3 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -36,8 +36,6 @@ class Host(Resource): self.devices = Devices(self.model) self.packagesupdate = PackagesUpdate(self.model) self.repositories = Repositories(self.model) - self.users = Users(self.model) - self.groups = Groups(self.model) self.swupdate = self.generate_action_handler_task('swupdate')
@property @@ -159,15 +157,3 @@ class Repository(Resource): @property def data(self): return self.info - - -class Users(SimpleCollection): - def __init__(self, model): - super(Users, self).__init__(model) - self.role_key = 'guests' - - -class Groups(SimpleCollection): - def __init__(self, model): - super(Groups, self).__init__(model) - self.role_key = 'guests' diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index f789259..4e5a59b 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -39,6 +39,7 @@ messages = { "KCHAUTH0001E": _("Authentication failed for user '%(username)s'. [Error code: %(code)s]"), "KCHAUTH0002E": _("You are not authorized to access Kimchi"), "KCHAUTH0003E": _("Specify %(item)s to login into Kimchi"), + "KCHAUTH0004E": _("User %(user_id)s not found with given LDAP settings."),
"KCHDEVS0001E": _('Unknown "_cap" specified'), "KCHDEVS0002E": _('"_passthrough" should be "true" or "false"'), diff --git a/src/kimchi/model/auth.py b/src/kimchi/model/auth.py new file mode 100644 index 0000000..d52c919 --- /dev/null +++ b/src/kimchi/model/auth.py @@ -0,0 +1,136 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import grp +import ldap +import pwd + +from kimchi.config import config +from kimchi.exception import NotFoundError + + +class UsersModel(object): + def __init__(self, **args): + auth_type = config.get("authentication", "method") + for klass in UsersModel.__subclasses__(): + if auth_type == klass.auth_type: + self.user = klass(**args) + + def get_list(self, **args): + return self.user._get_list(**args) + + def validate(self, user): + return self.user._validate(user) + + +class PAMUsersModel(UsersModel): + auth_type = 'pam' + + def __init__(self, **kargs): + pass + + def _get_list(self): + return [user.pw_name for user in pwd.getpwall() + if user.pw_shell.rsplit("/")[-1] not in ["nologin", "false"]] + + def _validate(self, user): + try: + return user in self._get_list() + except: + return False + + +class LDAPUsersModel(UsersModel): + auth_type = 'ldap' + + def __init__(self, **kargs): + pass + + def _get_list(self, _user_id=''): + return self._get_user(_user_id) + + def _validate(self, user): + try: + self._get_user(user) + return True + except NotFoundError: + return False + + def _get_user(self, _user_id): + ldap_server = config.get("authentication", "ldap_server").strip('"') + ldap_search_base = config.get( + "authentication", "ldap_search_base").strip('"') + ldap_search_filter = config.get( + "authentication", "ldap_search_filter", + vars={"username": _user_id.encode("utf-8")}).strip('"') + + connect = ldap.open(ldap_server) + try: + result = connect.search_s( + ldap_search_base, ldap.SCOPE_SUBTREE, ldap_search_filter) + if len(result) == 0: + raise NotFoundError("KCHAUTH0004E", {'user_id': _user_id}) + return result[0][1] + except ldap.NO_SUCH_OBJECT: + raise NotFoundError("KCHAUTH0004E", {'user_id': _user_id}) + +
Same here. One URI per file.
+class GroupsModel(object): + def __init__(self, **args): + auth_type = config.get("authentication", "method") + for klass in GroupsModel.__subclasses__(): + if auth_type == klass.auth_type: + self.grp = klass(**args) + + + def get_list(self, **args): + if hasattr(self.grp, '_get_list'): + return self.grp._get_list(**args) + else: + return list() + + def validate(self, gid): + return self.grp._validate(gid) + + +class PAMGroupsModel(GroupsModel): + auth_type = 'pam' + + def __init__(self, **kargs): + pass + + def _get_list(self): + return [group.gr_name for group in grp.getgrall()] + + def _validate(self, gid): + try: + grp.getgrnam(gid) + except KeyError: + return False + return True + + +class LDAPGroupsModel(GroupsModel): + auth_type = 'ldap' + + def __init__(self, **kargs): + pass + + def _validate(self, gid): + return False diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index 8cddcdc..009f448 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -17,12 +17,10 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-import grp import libvirt import os import time import platform -import pwd from collections import defaultdict
import psutil @@ -457,20 +455,3 @@ class RepositoryModel(object): raise InvalidOperation('KCHREPOS0014E')
return self._repositories.removeRepository(repo_id) - - -class UsersModel(object): - def __init__(self, **kargs): - pass - - def get_list(self): - return [user.pw_name for user in pwd.getpwall() - if user.pw_shell.rsplit("/")[-1] not in ["nologin", "false"]] - - -class GroupsModel(object): - def __init__(self, **kargs): - pass - - def get_list(self): - return [group.gr_name for group in grp.getgrall()] diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index f2e4ae3..8f14f9e 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -249,8 +249,8 @@ class VMModel(object): self.conn = kargs['conn'] self.objstore = kargs['objstore'] self.vmscreenshot = VMScreenshotModel(**kargs) - self.users = import_class('kimchi.model.host.UsersModel')(**kargs) - self.groups = import_class('kimchi.model.host.GroupsModel')(**kargs) + self.users = import_class('kimchi.model.auth.UsersModel')(**kargs) + self.groups = import_class('kimchi.model.auth.GroupsModel')(**kargs)
def update(self, name, params): dom = self.get_vm(name, self.conn) diff --git a/tests/test_rest.py b/tests/test_rest.py index 9bc930f..420bfd0 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -277,7 +277,7 @@ class RestTests(unittest.TestCase): self.assertEquals(params[key], vm[key])
# change only VM users - groups are not changed (default is empty) - resp = self.request('/host/users', '{}', 'GET') + resp = self.request('/users', '{}', 'GET') users = json.loads(resp.read()) req = json.dumps({'users': users}) resp = self.request('/vms/∨м-црdαtеd', req, 'PUT') @@ -286,7 +286,7 @@ class RestTests(unittest.TestCase): self.assertEquals(users, info['users'])
# change only VM groups - users are not changed (default is empty) - resp = self.request('/host/groups', '{}', 'GET') + resp = self.request('/groups', '{}', 'GET') groups = json.loads(resp.read()) req = json.dumps({'groups': groups}) resp = self.request('/vms/∨м-црdαtеd', req, 'PUT')

On 2014年11月12日 19:53, Aline Manera wrote:
On 11/10/2014 05:09 AM, lvroyce@linux.vnet.ibm.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Put ldap validation in a single function to resue in authorization. Tested: 1. LDAP: GET /users?_user_id=a_valid_user_id GET /users?_user_id=invalid_user_id GET /groups 2. PAM: GET /users GET /groups
You need to adjust the docs/API.md to reflect those changes.
ACK
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/control/auth.py | 42 ++++++++++++++ src/kimchi/control/host.py | 14 ----- src/kimchi/i18n.py | 1 + src/kimchi/model/auth.py | 136 +++++++++++++++++++++++++++++++++++++++++++++ src/kimchi/model/host.py | 19 ------- src/kimchi/model/vms.py | 4 +- tests/test_rest.py | 4 +- 7 files changed, 183 insertions(+), 37 deletions(-) create mode 100644 src/kimchi/control/auth.py create mode 100644 src/kimchi/model/auth.py
diff --git a/src/kimchi/control/auth.py b/src/kimchi/control/auth.py new file mode 100644 index 0000000..ebc019e --- /dev/null +++ b/src/kimchi/control/auth.py
Name it as users.py and groups.py
@@ -0,0 +1,42 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013-2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.control.base import SimpleCollection +from kimchi.control.utils import get_class_name, model_fn, UrlSubNode +from kimchi.template import render +
+ +@UrlSubNode('users', True) +class Users(SimpleCollection): + def __init__(self, model): + super(Users, self).__init__(model) + self.role_key = 'guests' + + def get(self, filter_params): + res_list = [] + get_list = getattr(self.model, model_fn(self, 'get_list')) + res_list = get_list(*self.model_args, **filter_params) + return render(get_class_name(self), res_list) + + +@UrlSubNode('groups', True) +class Groups(SimpleCollection): + def __init__(self, model): + super(Groups, self).__init__(model) + self.role_key = 'guests'
Split it in 2 files: users.py and groups.py as we have done one URI per file to make maintenance easier.
ACK
diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 4362da7..5a185e3 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -36,8 +36,6 @@ class Host(Resource): self.devices = Devices(self.model) self.packagesupdate = PackagesUpdate(self.model) self.repositories = Repositories(self.model) - self.users = Users(self.model) - self.groups = Groups(self.model) self.swupdate = self.generate_action_handler_task('swupdate') @property @@ -159,15 +157,3 @@ class Repository(Resource): @property def data(self): return self.info - - -class Users(SimpleCollection): - def __init__(self, model): - super(Users, self).__init__(model) - self.role_key = 'guests' - - -class Groups(SimpleCollection): - def __init__(self, model): - super(Groups, self).__init__(model) - self.role_key = 'guests' diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index f789259..4e5a59b 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -39,6 +39,7 @@ messages = { "KCHAUTH0001E": _("Authentication failed for user '%(username)s'. [Error code: %(code)s]"), "KCHAUTH0002E": _("You are not authorized to access Kimchi"), "KCHAUTH0003E": _("Specify %(item)s to login into Kimchi"), + "KCHAUTH0004E": _("User %(user_id)s not found with given LDAP settings."), "KCHDEVS0001E": _('Unknown "_cap" specified'), "KCHDEVS0002E": _('"_passthrough" should be "true" or "false"'), diff --git a/src/kimchi/model/auth.py b/src/kimchi/model/auth.py new file mode 100644 index 0000000..d52c919 --- /dev/null +++ b/src/kimchi/model/auth.py @@ -0,0 +1,136 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import grp +import ldap +import pwd + +from kimchi.config import config +from kimchi.exception import NotFoundError + + +class UsersModel(object): + def __init__(self, **args): + auth_type = config.get("authentication", "method") + for klass in UsersModel.__subclasses__(): + if auth_type == klass.auth_type: + self.user = klass(**args) + + def get_list(self, **args): + return self.user._get_list(**args) + + def validate(self, user): + return self.user._validate(user) + + +class PAMUsersModel(UsersModel): + auth_type = 'pam' + + def __init__(self, **kargs): + pass + + def _get_list(self): + return [user.pw_name for user in pwd.getpwall() + if user.pw_shell.rsplit("/")[-1] not in ["nologin", "false"]] + + def _validate(self, user): + try: + return user in self._get_list() + except: + return False + + +class LDAPUsersModel(UsersModel): + auth_type = 'ldap' + + def __init__(self, **kargs): + pass + + def _get_list(self, _user_id=''): + return self._get_user(_user_id) + + def _validate(self, user): + try: + self._get_user(user) + return True + except NotFoundError: + return False + + def _get_user(self, _user_id): + ldap_server = config.get("authentication", "ldap_server").strip('"') + ldap_search_base = config.get( + "authentication", "ldap_search_base").strip('"') + ldap_search_filter = config.get( + "authentication", "ldap_search_filter", + vars={"username": _user_id.encode("utf-8")}).strip('"') + + connect = ldap.open(ldap_server) + try: + result = connect.search_s( + ldap_search_base, ldap.SCOPE_SUBTREE, ldap_search_filter) + if len(result) == 0: + raise NotFoundError("KCHAUTH0004E", {'user_id': _user_id}) + return result[0][1] + except ldap.NO_SUCH_OBJECT: + raise NotFoundError("KCHAUTH0004E", {'user_id': _user_id}) + +
Same here. One URI per file.
ACK
+class GroupsModel(object): + def __init__(self, **args): + auth_type = config.get("authentication", "method") + for klass in GroupsModel.__subclasses__(): + if auth_type == klass.auth_type: + self.grp = klass(**args) + + + def get_list(self, **args): + if hasattr(self.grp, '_get_list'): + return self.grp._get_list(**args) + else: + return list() + + def validate(self, gid): + return self.grp._validate(gid) + + +class PAMGroupsModel(GroupsModel): + auth_type = 'pam' + + def __init__(self, **kargs): + pass + + def _get_list(self): + return [group.gr_name for group in grp.getgrall()] + + def _validate(self, gid): + try: + grp.getgrnam(gid) + except KeyError: + return False + return True + + +class LDAPGroupsModel(GroupsModel): + auth_type = 'ldap' + + def __init__(self, **kargs): + pass + + def _validate(self, gid): + return False diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index 8cddcdc..009f448 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -17,12 +17,10 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import grp import libvirt import os import time import platform -import pwd from collections import defaultdict import psutil @@ -457,20 +455,3 @@ class RepositoryModel(object): raise InvalidOperation('KCHREPOS0014E') return self._repositories.removeRepository(repo_id) - - -class UsersModel(object): - def __init__(self, **kargs): - pass - - def get_list(self): - return [user.pw_name for user in pwd.getpwall() - if user.pw_shell.rsplit("/")[-1] not in ["nologin", "false"]] - - -class GroupsModel(object): - def __init__(self, **kargs): - pass - - def get_list(self): - return [group.gr_name for group in grp.getgrall()] diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index f2e4ae3..8f14f9e 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -249,8 +249,8 @@ class VMModel(object): self.conn = kargs['conn'] self.objstore = kargs['objstore'] self.vmscreenshot = VMScreenshotModel(**kargs) - self.users = import_class('kimchi.model.host.UsersModel')(**kargs) - self.groups = import_class('kimchi.model.host.GroupsModel')(**kargs) + self.users = import_class('kimchi.model.auth.UsersModel')(**kargs) + self.groups = import_class('kimchi.model.auth.GroupsModel')(**kargs) def update(self, name, params): dom = self.get_vm(name, self.conn) diff --git a/tests/test_rest.py b/tests/test_rest.py index 9bc930f..420bfd0 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -277,7 +277,7 @@ class RestTests(unittest.TestCase): self.assertEquals(params[key], vm[key]) # change only VM users - groups are not changed (default is empty) - resp = self.request('/host/users', '{}', 'GET') + resp = self.request('/users', '{}', 'GET') users = json.loads(resp.read()) req = json.dumps({'users': users}) resp = self.request('/vms/∨м-црdαtеd', req, 'PUT') @@ -286,7 +286,7 @@ class RestTests(unittest.TestCase): self.assertEquals(users, info['users']) # change only VM groups - users are not changed (default is empty) - resp = self.request('/host/groups', '{}', 'GET') + resp = self.request('/groups', '{}', 'GET') groups = json.loads(resp.read()) req = json.dumps({'groups': groups}) resp = self.request('/vms/∨м-црdαtеd', req, 'PUT')

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Put validation in user and group class instead of validate in metadata update, so that different type of authorization can use their own authentication to validate input value. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/model/vms.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index 8f14f9e..0fee07d 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -272,16 +272,16 @@ class VMModel(object): users = groups = None if "users" in params: users = params["users"] - invalid_users = set(users) - set(self.users.get_list()) - if len(invalid_users) != 0: - raise InvalidParameter("KCHVM0027E", - {'users': ", ".join(invalid_users)}) + for user in users: + if not self.users.validate(user): + raise InvalidParameter("KCHVM0027E", + {'users': user}) if "groups" in params: groups = params["groups"] - invalid_groups = set(groups) - set(self.groups.get_list()) - if len(invalid_groups) != 0: - raise InvalidParameter("KCHVM0028E", - {'groups': ", ".join(invalid_groups)}) + for group in groups: + if not self.groups.validate(group): + raise InvalidParameter("KCHVM0028E", + {'groups': group}) if users is None and groups is None: return -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Add authorization type to vm tag, and update set/retrieve access tag accordingly. So that we can switch between different types of authentication. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/model/vms.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index 0fee07d..cb057c1 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -31,7 +31,7 @@ import libvirt from cherrypy.process.plugins import BackgroundTask from kimchi import vnc -from kimchi.config import READONLY_POOL_TYPE +from kimchi.config import READONLY_POOL_TYPE, config from kimchi.exception import InvalidOperation, InvalidParameter from kimchi.exception import NotFoundError, OperationFailed from kimchi.model.config import CapabilitiesModel @@ -259,13 +259,16 @@ class VMModel(object): return dom.name().decode('utf-8') def _build_access_elem(self, users, groups): - access = E.access() + auth = config.get("authentication", "method") + auth_elem = E.auth(type=auth) for user in users: - access.append(E.user(user)) + auth_elem.append(E.user(user)) for group in groups: - access.append(E.group(group)) + auth_elem.append(E.group(group)) + access = E.access() + access.append(auth_elem) return access def _vm_update_access_metadata(self, dom, params): @@ -288,8 +291,9 @@ class VMModel(object): access_xml = (get_metadata_node(dom, "access") or """<access></access>""") - old_users = xpath_get_text(access_xml, "/access/user") - old_groups = xpath_get_text(access_xml, "/access/group") + auth = config.get("authentication", "method") + old_users = xpath_get_text(access_xml, "/access/auth[@type='%s']/user" % auth) + old_groups = xpath_get_text(access_xml, "/access/auth[@type='%s']/group" % auth) users = old_users if users is None else users groups = old_groups if groups is None else groups @@ -425,8 +429,10 @@ class VMModel(object): access_xml = (get_metadata_node(dom, "access") or """<access></access>""") - users = xpath_get_text(access_xml, "/access/user") - groups = xpath_get_text(access_xml, "/access/group") + + auth = config.get("authentication", "method") + users = xpath_get_text(access_xml, "/access/auth[@type='%s']/user" % auth) + groups = xpath_get_text(access_xml, "/access/auth[@type='%s']/group" % auth) return {'name': name, 'state': state, -- 1.8.3.2

What about the users/groups tags in the existing VMs? Are they going to be ignored after this patch? On 10-11-2014 05:09, lvroyce@linux.vnet.ibm.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Add authorization type to vm tag, and update set/retrieve access tag accordingly. So that we can switch between different types of authentication.
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com>

On 2014年11月12日 23:24, Crístian Viana wrote:
What about the users/groups tags in the existing VMs? Are they going to be ignored after this patch? Do you mean back compatibility? Good point. I will consider a better way to deal with this.
On 10-11-2014 05:09, lvroyce@linux.vnet.ibm.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Add authorization type to vm tag, and update set/retrieve access tag accordingly. So that we can switch between different types of authentication.
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com>

On 13-11-2014 05:44, Royce Lv wrote:
On 2014年11月12日 23:24, Crístian Viana wrote:
What about the users/groups tags in the existing VMs? Are they going to be ignored after this patch? Do you mean back compatibility? Good point.
Yes.
I will consider a better way to deal with this.
Good :-) It'd be very frustrating for the user if they updated Kimchi and all the users/groups they spent time setting up were suddenly ignored and they needed to do it again...

From: Royce Lv <lvroyce@linux.vnet.ibm.com> As test model includes test against pam user/group listing, using pam authentication instead of fake authentication. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- tests/test_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_model.py b/tests/test_model.py index 3177fd4..40c19b5 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -38,7 +38,7 @@ import iso_gen import kimchi.objectstore import utils from kimchi import netinfo -from kimchi.config import paths +from kimchi.config import config, paths from kimchi.exception import ImageFormatError, InvalidOperation from kimchi.exception import InvalidParameter, NotFoundError, OperationFailed from kimchi.iscsi import TargetClient @@ -840,6 +840,7 @@ class ModelTests(unittest.TestCase): 'new-test', params) def test_vm_edit(self): + config.set("authentication", "method", "pam") inst = model.Model(None, objstore_loc=self.tmp_store) -- 1.8.3.2
participants (4)
-
Aline Manera
-
Crístian Viana
-
lvroyce@linux.vnet.ibm.com
-
Royce Lv