[PATCH v2 0/4] Initial authorization support

From: Leonardo Garcia <lagarcia@br.ibm.com> Current, in Kimchi, no real authorization support is implemented. We do have authentication support, and, apart from that, no other kind of control is provided in order to authorize or not a Kimchi user to access its features. IOW, today, a user can access everything or nothing Kimchi provides. This patch series tries to implement an initial support for user authorization in Kimchi back-end. Some has already been discussed in the community about this feature [1, 2, 3]. The RFC proposed in [2] and the WIP sent in [3] seems to be diverging from the simple proposal first put in [1] and sustained in replies to [2]. So, the purpose of this patch series is to try to be, as much as possible, compliant to [1] and keep things as simple as possible. In summary, that means we will identify users as having sudo rights or not. This information will be passed to the UI by the /login REST API during logging in. With this information the UI will be able to decide which components (tabs, buttons, etc.) it will show to the user. Additionally, an infrastructure was also built in order to identify a REST API as one that needs sudo rights or not to be accessed. So, if the UI, for some reason, tries to access a REST API in a session whose user does not have sudo rights, the REST API call will return HTTP error 401. [1] https://github.com/kimchi-project/kimchi/wiki/authorization [2] http://lists.ovirt.org/pipermail/kimchi-devel/2014-January/001218.html [3] http://lists.ovirt.org/pipermail/kimchi-devel/2014-January/001898.html Leonardo Garcia (4): Code cleanup. Find out user groups and sudo status during login. Enhance UrlSubNode decorator and kimchiauth tool to check for sudo rights. Limit user access to REST API /host. src/kimchi/auth.py | 85 ++++++++++++++++++++++++++++++++++++--------- src/kimchi/control/host.py | 2 +- src/kimchi/control/utils.py | 9 ++++- src/kimchi/root.py | 4 +-- src/kimchi/server.py | 7 +++- 5 files changed, 86 insertions(+), 21 deletions(-) -- 1.8.5.3

From: Leonardo Garcia <lagarcia@br.ibm.com> Remove useless statements and improve debug message. Signed-off-by: Leonardo Garcia <lagarcia@br.ibm.com> --- src/kimchi/auth.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/kimchi/auth.py b/src/kimchi/auth.py index c5c6266..f2c2a3e 100644 --- a/src/kimchi/auth.py +++ b/src/kimchi/auth.py @@ -88,13 +88,11 @@ def check_auth_session(): for the user. """ try: - s = cherrypy.session[SESSION_USER] - user = cherrypy.request.login = cherrypy.session[SESSION_USER] - debug("Authenticated with session: %s, for user: %s" % (s, user)) + user = cherrypy.session[SESSION_USER] + debug("Session authenticated for user %s" % user) except KeyError: debug("Session not found") return False - debug("Session found for user %s" % user) return True @@ -136,8 +134,7 @@ def login(userid, password): def logout(): cherrypy.session.acquire_lock() - userid = cherrypy.session.get(SESSION_USER, None) - cherrypy.session[SESSION_USER] = cherrypy.request.login = None + cherrypy.session[SESSION_USER] = None cherrypy.session.release_lock() cherrypy.lib.sessions.expire() -- 1.8.5.3

From: Leonardo Garcia <lagarcia@br.ibm.com> When the /login REST API is called with a valid username and password, Kimchi will find out the user groups and sudo status, store these values in sessions variables, and return them in a JSON message with the following format: {"sudo": true | false, "userid": "<username>", "groups": [<list of groups>]} For example: {"sudo": false, "userid": "guest", "groups": ["guest", "foo"]} This JSON return message can be used by the UI to figure out which tabs/options should be shown in the web interface according to the user rights. It is important to notice that sudo configuration for a given user is not as simple as an on/off key. Sudo permits various levels of access controls. For instance, it is possible to specify which executables a user can run with sudo. For the purpose of this implementation, we only consider a user to have sudo rights if this user can run any command in the system with sudo (which is equivalent to have root rights). Signed-off-by: Leonardo Garcia <lagarcia@br.ibm.com> --- src/kimchi/auth.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++-------- src/kimchi/root.py | 4 ++-- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/kimchi/auth.py b/src/kimchi/auth.py index f2c2a3e..e8deb40 100644 --- a/src/kimchi/auth.py +++ b/src/kimchi/auth.py @@ -24,15 +24,19 @@ import base64 import cherrypy +import grp import PAM import re from kimchi import template from kimchi.exception import InvalidOperation, OperationFailed +from kimchi.utils import run_command -SESSION_USER = 'userid' +USER_ID = 'userid' +USER_GROUPS = 'groups' +USER_SUDO = 'sudo' def debug(msg): @@ -40,6 +44,46 @@ def debug(msg): # cherrypy.log.error(msg) +class User(object): + + def __init__(self, userid): + self.user = {} + self.user[USER_ID] = userid + self.user[USER_GROUPS] = None + self.user[USER_SUDO] = False + + def get_groups(self): + self.user[USER_GROUPS] = [g.gr_name for g in grp.getgrall() + if self.user[USER_ID] in g.gr_mem] + return self.user[USER_GROUPS] + + def has_sudo(self): + out, err, exit = run_command(['sudo', '-l', '-U', self.user[USER_ID], + 'sudo']) + if exit == 0: + debug("User %s is allowed to run sudo" % self.user[USER_ID]) + # sudo allows a wide range of configurations, such as controlling + # which binaries the user can execute with sudo. + # For now, we will just check whether the user is allowed to run + # any command with sudo. + out, err, exit = run_command(['sudo', '-l', '-U', + self.user[USER_ID]]) + for line in out.split('\n'): + if line and re.search("(ALL)", line): + self.user[USER_SUDO] = True + debug("User %s can run any command with sudo" % + self.user[USER_ID]) + return self.user[USER_SUDO] + debug("User %s can only run some commands with sudo" % + self.user[USER_ID]) + else: + debug("User %s is not allowed to run sudo" % self.user[USER_ID]) + return self.user[USER_SUDO] + + def get_user(self): + return self.user + + def authenticate(username, password, service="passwd"): '''Returns True if authenticate is OK via PAM.''' def _pam_conv(auth, query_list, userData=None): @@ -88,12 +132,14 @@ def check_auth_session(): for the user. """ try: - user = cherrypy.session[SESSION_USER] - debug("Session authenticated for user %s" % user) + if cherrypy.session[USER_ID]: + debug("Session authenticated for user %s" % + cherrypy.session[USER_ID]) + return True except KeyError: - debug("Session not found") - return False - return True + pass + debug("Session not found") + return False def check_auth_httpba(): @@ -123,18 +169,21 @@ def check_auth_httpba(): def login(userid, password): if not authenticate(userid, password): debug("User cannot be verified with the supplied password") - return False + return None + user = User(userid) debug("User verified, establishing session") cherrypy.session.acquire_lock() cherrypy.session.regenerate() - cherrypy.session[SESSION_USER] = cherrypy.request.login = userid + cherrypy.session[USER_ID] = userid + cherrypy.session[USER_GROUPS] = user.get_groups() + cherrypy.session[USER_SUDO] = user.has_sudo() cherrypy.session.release_lock() - return True + return user.get_user() def logout(): cherrypy.session.acquire_lock() - cherrypy.session[SESSION_USER] = None + cherrypy.session[USER_ID] = None cherrypy.session.release_lock() cherrypy.lib.sessions.expire() diff --git a/src/kimchi/root.py b/src/kimchi/root.py index 37d59e2..4a1a9cd 100644 --- a/src/kimchi/root.py +++ b/src/kimchi/root.py @@ -110,11 +110,11 @@ class KimchiRoot(Root): raise cherrypy.HTTPError(400, e.message) try: - auth.login(userid, password) + user_info = auth.login(userid, password) except OperationFailed: raise cherrypy.HTTPError(401) - return '{}' + return json.dumps(user_info) @cherrypy.expose def logout(self): -- 1.8.5.3

From: Leonardo Garcia <lagarcia@br.ibm.com> kimchiauth tool used to only check if the user was authenticated or not. Now it also checks whether the REST API being accessed is only allowed to users with sudo rights. The necessity to have sudo rights to access a REST API can be easily configured through the UrlSubNode decorator. Similar to the support previously implemented for user authentication in UrlSubNode, an additional boolean parameter was added to UrlSubNode to indicate whether the user needs sudo rights in order to access the corresponding REST API. Signed-off-by: Leonardo Garcia <lagarcia@br.ibm.com> --- src/kimchi/auth.py | 13 ++++++++++--- src/kimchi/control/utils.py | 9 ++++++++- src/kimchi/server.py | 7 ++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/kimchi/auth.py b/src/kimchi/auth.py index e8deb40..ee572cb 100644 --- a/src/kimchi/auth.py +++ b/src/kimchi/auth.py @@ -188,12 +188,19 @@ def logout(): cherrypy.lib.sessions.expire() -def kimchiauth(*args, **kwargs): +def has_permission(admin_methods): + return not admin_methods or \ + cherrypy.request.method not in admin_methods or \ + (cherrypy.request.method in admin_methods and + cherrypy.session[USER_SUDO]) + + +def kimchiauth(admin_methods=None): debug("Entering kimchiauth...") - if check_auth_session(): + if check_auth_session() and has_permission(admin_methods): return - if check_auth_httpba(): + if check_auth_httpba() and has_permission(admin_methods): return if not from_browser(): diff --git a/src/kimchi/control/utils.py b/src/kimchi/control/utils.py index ebfdda7..0d7e84a 100644 --- a/src/kimchi/control/utils.py +++ b/src/kimchi/control/utils.py @@ -107,13 +107,20 @@ def validate_params(params, instance, action): class UrlSubNode(object): - def __init__(self, name, auth=False): + + def __init__(self, name, auth=False, admin_methods=None): + """ + admin_methods must be None, or a list containing zero or more of the + string values ['GET', 'POST', 'PUT', 'DELETE'] + """ self.name = name self.auth = auth + self.admin_methods = admin_methods def __call__(self, fun): fun._url_sub_node_name = {"name": self.name} fun.url_auth = self.auth + fun.admin_methods = self.admin_methods return fun diff --git a/src/kimchi/server.py b/src/kimchi/server.py index 1e131b4..6ac2f64 100644 --- a/src/kimchi/server.py +++ b/src/kimchi/server.py @@ -190,7 +190,12 @@ class Server(object): for ident, node in sub_nodes.items(): if node.url_auth: - self.configObj["/%s" % ident] = {'tools.kimchiauth.on': True} + cfg = self.configObj + ident = "/%s" % ident + cfg[ident] = {'tools.kimchiauth.on': True} + if node.admin_methods: + cfg[ident][ + 'tools.kimchiauth.admin_methods'] = node.admin_methods self.app = cherrypy.tree.mount(KimchiRoot(model_instance, dev_env), config=self.configObj) -- 1.8.5.3

From: Leonardo Garcia <lagarcia@br.ibm.com> There is a need to restrict access to the REST API /host on POST, PUT, and DELETE HTTP methods to user with admin rights. In the context of the authorization feature developed, that means the user needs to have sudo rights to run any commands on the system in order to be able to access the /host REST API using POST, PUT, and DELETE HTTP methods. Signed-off-by: Leonardo Garcia <lagarcia@br.ibm.com> --- src/kimchi/control/host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 053c822..624e0d2 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -27,7 +27,7 @@ from kimchi.control.base import Collection, Resource from kimchi.control.utils import UrlSubNode -@UrlSubNode("host", True) +@UrlSubNode("host", True, ['POST', 'PUT', 'DELETE']) class Host(Resource): def __init__(self, model, id=None): super(Host, self).__init__(model, id) -- 1.8.5.3

I didn't apply this patch because the tests need to be updated to run accordingly to this modification I will update the tests in a new patch set and then apply it and all changes to block Kimchi URIs to non-root users. On 02/13/2014 12:28 AM, Leonardo Garcia wrote:
From: Leonardo Garcia <lagarcia@br.ibm.com>
There is a need to restrict access to the REST API /host on POST, PUT, and DELETE HTTP methods to user with admin rights. In the context of the authorization feature developed, that means the user needs to have sudo rights to run any commands on the system in order to be able to access the /host REST API using POST, PUT, and DELETE HTTP methods.
Signed-off-by: Leonardo Garcia <lagarcia@br.ibm.com> --- src/kimchi/control/host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 053c822..624e0d2 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -27,7 +27,7 @@ from kimchi.control.base import Collection, Resource from kimchi.control.utils import UrlSubNode
-@UrlSubNode("host", True) +@UrlSubNode("host", True, ['POST', 'PUT', 'DELETE']) class Host(Resource): def __init__(self, model, id=None): super(Host, self).__init__(model, id)
participants (2)
-
Aline Manera
-
Leonardo Garcia