>From 01c7ab48e720f34c2aa891a8fa07812b1c66c316 Mon Sep 17 00:00:00 2001
From: Dan Walsh <dwalsh@redhat.com>
Date: Fri, 28 Jun 2013 13:50:58 -0400
Subject: [PATCH] virt-login-shell joins users into lxc container.

Openshift wants to have their gears stuck into a container when they login
to the system.  virt-login-shell will join a running gear with the username of
the person running it, or attempt to start the container if it is not running.
(Currently containers do not exist if they are not running, so I can not test
this feature. But the code is there).

This tool needs to be setuid since joining a container (nsjoin) requires privs.
The root user is not allowed to execute this command. When this tool is
run by a normal user it will only join the "users" container.

Only users who are listed as valid_users in /etc/libvirt/virt-login-shell.conf
are allowed to join containers using this tool. By default no users are allowed.
---
 .gitignore                  |   1 +
 libvirt.spec.in             |   3 +
 po/POTFILES.in              |   1 +
 tools/Makefile.am           |  30 +++-
 tools/virt-login-shell.c    | 350 ++++++++++++++++++++++++++++++++++++++++++++
 tools/virt-login-shell.conf |  26 ++++
 tools/virt-login-shell.pod  |  62 ++++++++
 7 files changed, 472 insertions(+), 1 deletion(-)
 create mode 100644 tools/virt-login-shell.c
 create mode 100644 tools/virt-login-shell.conf
 create mode 100644 tools/virt-login-shell.pod

diff --git a/.gitignore b/.gitignore
index 738c6ba..d848107 100644
--- a/.gitignore
+++ b/.gitignore
@@ -215,6 +215,7 @@
 /tools/libvirt-guests.init
 /tools/libvirt-guests.service
 /tools/libvirt-guests.sh
+/tools/virt-login-shell
 /tools/virsh
 /tools/virsh-*-edit.c
 /tools/virt-*-validate
diff --git a/libvirt.spec.in b/libvirt.spec.in
index fce7f91..fd8f532 100644
--- a/libvirt.spec.in
+++ b/libvirt.spec.in
@@ -2006,14 +2006,17 @@ fi
 %doc AUTHORS ChangeLog.gz NEWS README COPYING COPYING.LESSER TODO
 
 %config(noreplace) %{_sysconfdir}/libvirt/libvirt.conf
+%config(noreplace) %{_sysconfdir}/libvirt/virt-login-shell.conf
 %{_mandir}/man1/virsh.1*
 %{_mandir}/man1/virt-xml-validate.1*
 %{_mandir}/man1/virt-pki-validate.1*
 %{_mandir}/man1/virt-host-validate.1*
+%{_mandir}/man1/virt-login-shell.1*
 %{_bindir}/virsh
 %{_bindir}/virt-xml-validate
 %{_bindir}/virt-pki-validate
 %{_bindir}/virt-host-validate
+%attr(4755, root, root) %{_bindir}/virt-login-shell
 %{_libdir}/lib*.so.*
 
 %if %{with_dtrace}
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 1fd84af..884b70a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -231,3 +231,4 @@ tools/virt-host-validate-common.c
 tools/virt-host-validate-lxc.c
 tools/virt-host-validate-qemu.c
 tools/virt-host-validate.c
+tools/virt-login-shell.c
diff --git a/tools/Makefile.am b/tools/Makefile.am
index 644a86d..00c582a 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -37,6 +37,7 @@ EXTRA_DIST = \
 	virt-pki-validate.in				\
 	virt-sanlock-cleanup.in				\
 	virt-sanlock-cleanup.8				\
+	virt-login-shell.pod				\
 	virsh.pod					\
 	libvirt-guests.sysconf				\
 	virsh-edit.c					\
@@ -52,8 +53,11 @@ EXTRA_DIST = \
 
 DISTCLEANFILES =
 
+confdir = $(sysconfdir)/libvirt
+conf_DATA = virt-login-shell.conf
+
 bin_SCRIPTS = virt-xml-validate virt-pki-validate
-bin_PROGRAMS = virsh virt-host-validate
+bin_PROGRAMS = virsh virt-host-validate virt-login-shell
 libexec_SCRIPTS = libvirt-guests.sh
 
 if WITH_SANLOCK
@@ -65,6 +69,7 @@ dist_man1_MANS = \
 		virt-host-validate.1 \
 		virt-pki-validate.1 \
 		virt-xml-validate.1 \
+		virt-login-shell.1 \
 		virsh.1
 if WITH_SANLOCK
 dist_man8_MANS = virt-sanlock-cleanup.8
@@ -128,6 +133,24 @@ virt_host_validate_CFLAGS = \
 		$(COVERAGE_CFLAGS)				\
 		$(NULL)
 
+virt_login_shell_SOURCES =					\
+		virt-login-shell.conf				\
+		virt-login-shell.c
+
+virt_login_shell_LDFLAGS = $(COVERAGE_LDFLAGS)
+virt_login_shell_LDADD =					\
+		$(STATIC_BINARIES)				\
+		$(PIE_LDFLAGS)					\
+		$(RELRO_LDFLAGS) \
+		../src/libvirt.la				\
+		../src/libvirt-lxc.la				\
+		../gnulib/lib/libgnu.la
+
+virt_login_shell_CFLAGS =					\
+		$(WARN_CFLAGS)					\
+		$(PIE_CFLAGS)					\
+		$(COVERAGE_CFLAGS)
+
 virsh_SOURCES =							\
 		console.c console.h				\
 		virsh.c virsh.h					\
@@ -189,6 +212,11 @@ virsh_win_icon.$(OBJEXT): virsh_win_icon.rc
 	  --output-format coff --output $@
 endif
 
+virt-login-shell.1: virt-login-shell.pod $(top_srcdir)/configure.ac
+	$(AM_V_GEN)$(POD2MAN) $< $(srcdir)/$@ \
+	    && if grep 'POD ERROR' $(srcdir)/$@ ; then \
+		rm $(srcdir)/$@; exit 1; fi
+
 virsh.1: virsh.pod $(top_srcdir)/configure.ac
 	$(AM_V_GEN)$(POD2MAN) $< $(srcdir)/$@ \
 	    && if grep 'POD ERROR' $(srcdir)/$@ ; then \
diff --git a/tools/virt-login-shell.c b/tools/virt-login-shell.c
new file mode 100644
index 0000000..ffbc713
--- /dev/null
+++ b/tools/virt-login-shell.c
@@ -0,0 +1,350 @@
+/*
+ * virt-login-shell.c: a shell to connect to a container
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * 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, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Daniel Walsh <dwalsh@redhat.com>
+ */
+#include <config.h>
+
+#include <stdarg.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <fnmatch.h>
+
+#include "internal.h"
+#include "virerror.h"
+#include "virconf.h"
+#include "virutil.h"
+#include "virfile.h"
+#include "virprocess.h"
+#include "configmake.h"
+#include "virstring.h"
+#include "viralloc.h"
+#include "vircommand.h"
+#define VIR_FROM_THIS VIR_FROM_NONE
+
+static ssize_t nfdlist = 0;
+static int *fdlist = NULL;
+static const char *conf_file = SYSCONFDIR "/libvirt/virt-login-shell.conf";
+
+static void virLoginShellFini(virConnectPtr conn, virDomainPtr  dom)
+{
+    size_t i;
+
+    for (i = 0; i < nfdlist; i++)
+        VIR_FORCE_CLOSE(fdlist[i]);
+    VIR_FREE(fdlist);
+    nfdlist = 0;
+    if (dom)
+        virDomainFree(dom);
+    if (conn)
+        virConnectClose(conn);
+}
+
+static int virLoginShellAllowedUser(virConfPtr conf,
+                                    const char *name,
+                                    gid_t *groups)
+{
+    virConfValuePtr p;
+    int ret = -1;
+    char *ptr = NULL;
+    size_t i;
+    char *gname = NULL;
+
+    p = virConfGetValue(conf, "allowed_users");
+    if (p && p->type == VIR_CONF_LIST) {
+        virConfValuePtr pp;
+
+        /* Calc length and check items */
+        for (pp = p->list; pp; pp = pp->next) {
+            if (pp->type != VIR_CONF_STRING) {
+                virReportSystemError(EINVAL, "%s", _("shell must be a list of strings"));
+                goto cleanup;
+            } else {
+                /*
+                  If string begins with a % this indicates a linux group.
+                  Check to see if the user is in the Linux Group.
+                */
+                if (pp->str[0] == '%') {
+                    ptr = &pp->str[1];
+                    if (!ptr)
+                        continue;
+                    for (i = 0; groups[i]; i++) {
+                        if (!(gname = virGetGroupName(groups[i])))
+                            continue;
+                        if (fnmatch(ptr, gname, 0) == 0) {
+                            ret = 0;
+                            goto cleanup;
+                        }
+                        VIR_FREE(gname);
+                    }
+                    VIR_FREE(groups);
+                    continue;
+                }
+                if (fnmatch(pp->str, name, 0) == 0) {
+                    ret = 0;
+                    goto cleanup;
+                }
+            }
+        }
+    }
+    virReportSystemError(EPERM, _("%s not listed as an allowed_users in %s"), name, conf_file);
+cleanup:
+    VIR_FREE(gname);
+    VIR_FREE(groups);
+    return ret;
+}
+
+static char **virLoginShellGetShellArgv(virConfPtr conf)
+{
+    size_t i;
+    char **shargv=NULL;
+    virConfValuePtr p;
+
+    p = virConfGetValue(conf, "shell");
+    if (!p)
+        return virStringSplit("/bin/sh -l", " ", 3);
+
+    if (p && p->type == VIR_CONF_LIST) {
+        size_t len;
+        virConfValuePtr pp;
+
+        /* Calc length and check items */
+        for (len = 0, pp = p->list; pp; len++, pp = pp->next) {
+            if (pp->type != VIR_CONF_STRING) {
+                virReportSystemError(EINVAL, "%s", _("shell must be a list of strings"));
+                goto error;
+            }
+        }
+
+        if (VIR_ALLOC_N(shargv, len + 1) < 0)
+            goto error;
+        for (i = 0, pp = p->list; pp; i++, pp = pp->next) {
+            if (VIR_STRDUP(shargv[i], pp->str) < 0)
+                goto error;
+        }
+        shargv[len] = NULL;
+    }
+    return shargv;
+error:
+    virStringFreeList(shargv);
+    return NULL;
+}
+
+static char *progname;
+
+/*
+ * Print usage
+ */
+static void
+usage(void)
+{
+    fprintf(stdout, _("\n"
+                      "%s is a privileged program that allows non root users \n"
+                      "specified in %s to join a Linux container \n"
+                      "with a matching user name and launch a shell. \n"
+                      "\n%s [options]\n\n"
+                      "  options:\n"
+                      "    -h | --help             this help:\n\n"), progname, conf_file, progname);
+    return;
+}
+
+int
+main(int argc, char **argv)
+{
+    virConfPtr conf = NULL;
+    const char *login_shell_path = conf_file;
+    pid_t cpid;
+    int ret = EXIT_FAILURE;
+    int status;
+    int status2;
+    uid_t uid = getuid();
+    gid_t gid = getgid();
+    char *name = NULL;
+    char **shargv = NULL;
+    virSecurityModelPtr secmodel = NULL;
+    virSecurityLabelPtr seclabel = NULL;
+    virDomainPtr dom = NULL;
+    virConnectPtr conn = NULL;
+    char *homedir = NULL;
+    int arg;
+    int longindex = -1;
+    int ngroups;
+    gid_t *groups = NULL;
+
+    struct option opt[] = {
+        {"help", no_argument, NULL, 'h'},
+        {NULL, 0, NULL, 0}
+    };
+    if (virInitialize() < 0) {
+        fprintf(stderr, _("Failed to initialize libvirt Error Handling"));
+        return EXIT_FAILURE;
+    }
+
+    virSetErrorFunc(NULL, NULL);
+    virSetErrorLogPriorityFunc(NULL);
+
+    progname = argv[0];
+    if (!setlocale(LC_ALL, "")) {
+        perror("setlocale");
+        /* failure to setup locale is not fatal */
+    }
+    if (!bindtextdomain(PACKAGE, LOCALEDIR)) {
+        perror("bindtextdomain");
+        return ret;
+    }
+    if (!textdomain(PACKAGE)) {
+        perror("textdomain");
+        return ret;
+    }
+
+    /* The only option we support is help
+     */
+    while ((arg = getopt_long(argc, argv, "h", opt, &longindex)) != -1) {
+        switch (arg) {
+        case 'h':
+            usage();
+            exit(EXIT_SUCCESS);
+            break;
+        }
+    }
+
+    if (argc > optind) {
+        virReportSystemError(EINVAL, _("%s takes no options"), progname);
+        errno = EINVAL;
+        goto cleanup;
+    }
+
+    if (uid == 0) {
+        virReportSystemError(EPERM, _("%s must be run by non root users"), progname);
+        goto cleanup;
+    }
+
+    name = virGetUserName(uid);
+    if (!name)
+        goto cleanup;
+
+    homedir = virGetUserDirectoryByUID(uid);
+    if (!homedir)
+        goto cleanup;
+
+    if (!(conf = virConfReadFile(login_shell_path, 0)))
+        goto cleanup;
+
+    if ((ngroups = virGetGroupList(uid, gid, &groups)) < 0)
+        goto cleanup;
+
+    if (virLoginShellAllowedUser(conf, name, groups) < 0)
+        goto cleanup;
+
+    if (!(shargv = virLoginShellGetShellArgv(conf)))
+        goto cleanup;
+
+    conn = virConnectOpen("lxc:///");
+    if (!conn)
+        goto cleanup;
+
+    dom = virDomainLookupByName(conn, name);
+    if (!dom)
+        goto cleanup;
+
+    if (!virDomainIsActive(dom) && virDomainCreate(dom)) {
+        virErrorPtr last_error;
+        last_error = virGetLastError();
+        if (last_error->code != VIR_ERR_OPERATION_INVALID) {
+            virReportSystemError(last_error->code,_("Can't create %s container: %s"), name, virGetLastErrorMessage());
+            goto cleanup;
+        }
+    }
+
+    if ((nfdlist = virDomainLxcOpenNamespace(dom, &fdlist, 0)) < 0)
+        goto cleanup;
+    if (VIR_ALLOC(secmodel) < 0)
+        goto cleanup;
+    if (VIR_ALLOC(seclabel) < 0)
+        goto cleanup;
+    if (virNodeGetSecurityModel(conn, secmodel) < 0)
+        goto cleanup;
+    if (virDomainGetSecurityLabel(dom, seclabel) < 0)
+        goto cleanup;
+
+    if (virFork(&cpid) < 0)
+        goto cleanup;
+
+    if (cpid == 0) {
+        pid_t ccpid;
+
+        /* Fork once because we don't want to affect
+         * virt-login-shell's namespace itself
+         */
+        if (virSetUIDGID(0, 0, NULL, 0) < 0)
+            return EXIT_FAILURE;
+
+        if (virDomainLxcEnterSecurityLabel(secmodel,
+                                           seclabel,
+                                           NULL,
+                                           0) < 0)
+            return EXIT_FAILURE;
+
+        if (nfdlist > 0) {
+            if (virDomainLxcEnterNamespace(dom,
+                                           nfdlist,
+                                           fdlist,
+                                           NULL,
+                                           NULL,
+                                           0) < 0)
+                return EXIT_FAILURE;
+        }
+
+        ret = virSetUIDGID(uid, gid, groups, ngroups);
+        VIR_FREE(groups);
+        if (ret < 0)
+            return EXIT_FAILURE;
+
+        if (virFork(&ccpid) < 0)
+            return EXIT_FAILURE;
+
+        if (ccpid == 0) {
+            if (chdir(homedir) < 0) {
+                virReportSystemError(errno, _("Unable chdir(%s)"), homedir);
+                return EXIT_FAILURE;
+            }
+            if (execv(shargv[0], (char *const*) shargv) < 0) {
+                virReportSystemError(errno, _("Unable exec shell %s"), shargv[0]);
+                return -errno;
+            }
+        }
+        return virProcessWait(ccpid, &status2);
+    }
+    ret = virProcessWait(cpid, &status);
+
+cleanup:
+    virConfFree(conf);
+    virLoginShellFini(conn, dom);
+    virStringFreeList(shargv);
+    VIR_FREE(name);
+    VIR_FREE(homedir);
+    VIR_FREE(seclabel);
+    VIR_FREE(secmodel);
+    VIR_FREE(groups);
+    if (ret)
+        virDispatchError(NULL);
+    return ret;
+}
diff --git a/tools/virt-login-shell.conf b/tools/virt-login-shell.conf
new file mode 100644
index 0000000..835fd3f
--- /dev/null
+++ b/tools/virt-login-shell.conf
@@ -0,0 +1,26 @@
+# Master configuration file for the virt-login-shell program.
+# All settings described here are optional - if omitted, sensible
+# defaults are used.
+
+# By default, virt-login-shell will connect you to a container running
+# with the /bin/sh program.  Modify the shell variable if you want your
+# users to run a different shell or a setup container when joining a
+# container.  Shell commands must be a list of commands/options separated by
+# comma and delimited by square brackets. Defaults to: /bin/sh -l.
+# Modify and uncomment the following to modify the login shell.
+# shell = [ "/bin/sh",  "-l" ]
+
+# allowed_users specifies the user names of all users that are allowed to
+# execute virt-login-shell.  You can specify the users as a comma
+# separated list of usernames or user groups.
+# The list of names support glob syntax.
+# To disallow all users (default)
+# allowed_users = []
+# If you do not specify any names (default) then no one is allowed
+# to use this executable.
+# To allow fred and joe only
+# allowed_users = ["fred", "joe"]
+# To allow all users within a specific group prefix the group name with %.
+# allowed_users = ["%engineers"]
+# To allow all users specify the following
+# allowed_users = [ "*" ]
diff --git a/tools/virt-login-shell.pod b/tools/virt-login-shell.pod
new file mode 100644
index 0000000..0cd35cf
--- /dev/null
+++ b/tools/virt-login-shell.pod
@@ -0,0 +1,62 @@
+=head1 NAME
+
+virt-login-shell - tool to execute a shell within a container matching the users name
+
+=head1 SYNOPSIS
+
+B<virt-login-shell>
+
+=head1 DESCRIPTION
+
+The B<virt-login-shell> program is setuid shell that is used to join
+an LXC container that matches the users name.  If the container is not
+running virt-login-shell will attempt to start the container.
+virt-sandbox-shell is not allowed to be run by root.  Normal users will get
+added to a container that matches their username, if it exists.  And they are
+configured in /etc/libvirt/virt-login-shell.conf.
+
+The basic structure of most virt-login-shell usage is:
+
+  virt-login-shell
+
+=head1 CONFIG
+
+By default, virt-login-shell will execute the /bin/sh program for the user.
+You can modify this behaviour by defining the shell variable in /etc/libvirt/virt-login-shell.conf.
+
+eg.  shell = [ "/bin/ksh", "--login"]
+
+By default no users are allowed to user virt-login-shell, if you want to allow
+certain users to use virt-login-shell, you need to modify the allowed_users variable in /etc/libvirt/virt-login-shell.conf.
+
+eg. allowed_users = [ "tom", "dick", "harry" ]
+
+=head1 BUGS
+
+Report any bugs discovered to the libvirt community via the mailing
+list C<http://libvirt.org/contact.html> or bug tracker C<http://libvirt.org/bugs.html>.
+Alternatively report bugs to your software distributor / vendor.
+
+=head1 AUTHORS
+
+  Please refer to the AUTHORS file distributed with libvirt.
+
+  Daniel Walsh <dwalsh at redhat dot com>
+
+=head1 COPYRIGHT
+
+Copyright (C) 2013 Red Hat, Inc., and the authors listed in the
+libvirt AUTHORS file.
+
+=head1 LICENSE
+
+virt-login-shell is distributed under the terms of the GNU LGPL v2+.
+This is free software; see the source for copying conditions. There
+is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR
+PURPOSE
+
+=head1 SEE ALSO
+
+L<virsh(1)>, L<http://www.libvirt.org/>
+
+=cut
-- 
1.8.3.1

