summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArthur de Jong <arthur@arthurdejong.org>2013-03-30 23:10:34 +0100
committerArthur de Jong <arthur@arthurdejong.org>2013-03-30 23:10:34 +0100
commit62a409cb43b441c32692f414a1867176d37034ac (patch)
tree50c70342106c2674d61b5559f7dfa89dc1f506bc
parentaae36cfcfb6ec00776f6da1e0d1fd5f90a72f2dd (diff)
parent012b18554e5e6a408a11a7157a30c5d068f2d3d1 (diff)
Implement used modification functionality
This adds user information modification functionality to nslcd and pynslcd and implements a chsh.ldap utility that can be used to change the login shell of a user (similar to the normal chsh command). The user modification functionality should allow for generic modifications of user information. More utility commands to perform modifications remain to be implemented.
-rw-r--r--.gitignore2
-rw-r--r--man/Makefile.am2
-rw-r--r--man/chsh.ldap.1.xml148
-rw-r--r--nslcd.h30
-rw-r--r--nslcd/Makefile.am4
-rw-r--r--nslcd/common.h1
-rw-r--r--nslcd/nslcd.c1
-rw-r--r--nslcd/usermod.c297
-rwxr-xr-xpynslcd/pynslcd.py1
-rw-r--r--pynslcd/usermod.py131
-rw-r--r--utils/Makefile.am4
-rwxr-xr-xutils/chsh.py70
-rw-r--r--utils/cmdline.py18
-rw-r--r--utils/nslcd.py23
-rw-r--r--utils/shells.py64
-rw-r--r--utils/users.py60
16 files changed, 851 insertions, 5 deletions
diff --git a/.gitignore b/.gitignore
index d079ba9..d3a7ecc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,8 @@ stamp-*
/splint.txt
# /man/
+/man/chsh.ldap.1
+/man/chsh.ldap.1.html
/man/getent.ldap.1
/man/getent.ldap.1.html
/man/nslcd.8
diff --git a/man/Makefile.am b/man/Makefile.am
index 8975eec..5b8d7f4 100644
--- a/man/Makefile.am
+++ b/man/Makefile.am
@@ -18,7 +18,7 @@
# 02110-1301 USA
PAM_MANS = pam_ldap.8
-UTILS_MANS = getent.ldap.1
+UTILS_MANS = getent.ldap.1 chsh.ldap.1
NSLCD_MANS = nslcd.conf.5 nslcd.8
PYNSLCD_MANS = nslcd.conf.5 pynslcd.8
ALL_MANS = $(PAM_MANS) $(UTILS_MANS) $(NSLCD_MANS) $(PYNSLCD_MANS)
diff --git a/man/chsh.ldap.1.xml b/man/chsh.ldap.1.xml
new file mode 100644
index 0000000..c5927d8
--- /dev/null
+++ b/man/chsh.ldap.1.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.1.2//EN"
+ "http://www.oasis-open.org/docbook/xml/4.1.2/docbookx.dtd">
+
+<!--
+ chsh.ldap.1.xml - docbook manual page for chsh.ldap
+
+ Copyright (C) 2013 Arthur de Jong
+
+ 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
+-->
+
+<refentry id="chshldap1">
+
+ <refentryinfo>
+ <author>
+ <firstname>Arthur</firstname>
+ <surname>de Jong</surname>
+ </author>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>chsh.ldap</refentrytitle>
+ <manvolnum>1</manvolnum>
+ <refmiscinfo class="version">Version 0.8.11</refmiscinfo>
+ <refmiscinfo class="manual">User Commands</refmiscinfo>
+ <refmiscinfo class="date">Oct 2012</refmiscinfo>
+ </refmeta>
+
+ <refnamediv id="name">
+ <refname>chsh.ldap</refname>
+ <refpurpose>change login shell in LDAP</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv id="synopsis">
+ <cmdsynopsis>
+ <command>chsh.ldap</command>
+ <arg choice="opt"><replaceable>options</replaceable></arg>
+ <arg choice="opt"><replaceable>LOGIN</replaceable></arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="description">
+ <title>Description</title>
+ <para>
+ The <command>chsh.ldap</command> command can be used to change user's
+ login shell (command interpreter).
+ </para>
+ <para>
+ The actual change in <acronym>LDAP</acronym> is performed by the
+ <command>nslcd</command> daemon and is subject to the access controls
+ configured in the <acronym>LDAP</acronym> server.
+ </para>
+ </refsect1>
+
+ <refsect1 id="options">
+ <title>Options</title>
+ <para>
+ The options that may be specified to the <command>chsh.ldap</command>
+ command are:
+ </para>
+ <variablelist remap="TP">
+
+ <varlistentry id="shell">
+ <term>
+ <option>-s</option>, <option>--shell</option>
+ <replaceable>SHELL</replaceable>
+ </term>
+ <listitem>
+ <para>
+ The name of the user's new login shell.
+ Setting this field to blank causes the system to select the default
+ login shell.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="listshells">
+ <term>
+ <option>-l</option>, <option>--list-shells</option>
+ </term>
+ <listitem>
+ <para>
+ Print the list of shells found in <file>/etc/shells</file> and exit.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="help">
+ <term>
+ <option>-h</option>, <option>--help</option>
+ </term>
+ <listitem>
+ <para>Display short help and exit.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="version">
+ <term>
+ <option>-V, --version</option>
+ </term>
+ <listitem>
+ <para>Output version information and exit.</para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+ <para>
+ If no option is specified <command>chsh.ldap</command> will prompt the
+ user to enter a value for the shell.
+ </para>
+ </refsect1>
+
+ <refsect1 id="files">
+ <title>Files</title>
+ <para>
+ <filename>/etc/shells</filename> - list of valid login shells
+ </para>
+ </refsect1>
+
+ <refsect1 id="see_also">
+ <title>See Also</title>
+ <para>
+ <citerefentry><refentrytitle>chsh</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>shells</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>nslcd</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+ </para>
+ </refsect1>
+
+ <refsect1 id="author">
+ <title>Author</title>
+ <para>This manual was written by Arthur de Jong &lt;arthur@arthurdejong.org&gt;.</para>
+ </refsect1>
+
+</refentry>
diff --git a/nslcd.h b/nslcd.h
index 456e2df..c9857d6 100644
--- a/nslcd.h
+++ b/nslcd.h
@@ -251,6 +251,36 @@
STRING error message */
#define NSLCD_ACTION_PAM_PWMOD 0x000d0005
+/* User information change request. This request allows one to change
+ their full name and other information. The request parameters for this
+ request are:
+ STRING user name
+ INT32 asroot: 0=passwd is user passwd, 1=passwd is root passwd
+ STRING password
+ followed by one or more of the below, terminated by NSLCD_USERMOD_END
+ INT32 NSLCD_USERMOD_*
+ STRING new value
+ the response consists of one or more of the entries below, terminated
+ by NSLCD_USERMOD_END:
+ INT32 NSLCD_USERMOD_*
+ STRING response
+ (if the response is blank, the change went OK, otherwise the string
+ contains an error message)
+ */
+#define NSLCD_ACTION_USERMOD 0x000e0001
+
+/* These are the possible values for the NSLCD_ACTION_USERMOD operation
+ above. */
+#define NSLCD_USERMOD_END 0 /* end of change values */
+#define NSLCD_USERMOD_RESULT 1 /* global result value */
+#define NSLCD_USERMOD_FULLNAME 2 /* full name */
+#define NSLCD_USERMOD_ROOMNUMBER 3 /* room number */
+#define NSLCD_USERMOD_WORKPHONE 4 /* office phone number */
+#define NSLCD_USERMOD_HOMEPHONE 5 /* home phone number */
+#define NSLCD_USERMOD_OTHER 6 /* other info */
+#define NSLCD_USERMOD_HOMEDIR 7 /* home directory */
+#define NSLCD_USERMOD_SHELL 8 /* login shell */
+
/* Request result codes. */
#define NSLCD_RESULT_BEGIN 1
#define NSLCD_RESULT_END 2
diff --git a/nslcd/Makefile.am b/nslcd/Makefile.am
index d901d64..60560f8 100644
--- a/nslcd/Makefile.am
+++ b/nslcd/Makefile.am
@@ -31,8 +31,8 @@ nslcd_SOURCES = nslcd.c ../nslcd.h ../common/nslcd-prot.h \
cfg.c cfg.h \
attmap.c attmap.h \
nsswitch.c nscd.c \
- alias.c config.c ether.c group.c host.c netgroup.c network.c \
- passwd.c protocol.c rpc.c service.c shadow.c pam.c
+ config.c alias.c ether.c group.c host.c netgroup.c network.c \
+ passwd.c protocol.c rpc.c service.c shadow.c pam.c usermod.c
nslcd_LDADD = ../common/libtio.a ../common/libdict.a \
../common/libexpr.a ../compat/libcompat.a \
@nslcd_LIBS@ @PTHREAD_LIBS@
diff --git a/nslcd/common.h b/nslcd/common.h
index 2965de6..a6c2c4d 100644
--- a/nslcd/common.h
+++ b/nslcd/common.h
@@ -240,6 +240,7 @@ int nslcd_pam_authz(TFILE *fp, MYLDAP_SESSION *session);
int nslcd_pam_sess_o(TFILE *fp, MYLDAP_SESSION *session);
int nslcd_pam_sess_c(TFILE *fp, MYLDAP_SESSION *session);
int nslcd_pam_pwmod(TFILE *fp, MYLDAP_SESSION *session, uid_t calleruid);
+int nslcd_usermod(TFILE *fp, MYLDAP_SESSION *session, uid_t calleruid);
/* macros for generating service handling code */
#define NSLCD_HANDLE(db, fn, action, readfn, mkfilter, writefn) \
diff --git a/nslcd/nslcd.c b/nslcd/nslcd.c
index 5f70963..9e22682 100644
--- a/nslcd/nslcd.c
+++ b/nslcd/nslcd.c
@@ -408,6 +408,7 @@ static void handleconnection(int sock, MYLDAP_SESSION *session)
case NSLCD_ACTION_PAM_SESS_O: (void)nslcd_pam_sess_o(fp, session); break;
case NSLCD_ACTION_PAM_SESS_C: (void)nslcd_pam_sess_c(fp, session); break;
case NSLCD_ACTION_PAM_PWMOD: (void)nslcd_pam_pwmod(fp, session, uid); break;
+ case NSLCD_ACTION_USERMOD: (void)nslcd_usermod(fp, session, uid); break;
default:
log_log(LOG_WARNING, "invalid request id: 0x%08x", (unsigned int)action);
break;
diff --git a/nslcd/usermod.c b/nslcd/usermod.c
new file mode 100644
index 0000000..61f6bbe
--- /dev/null
+++ b/nslcd/usermod.c
@@ -0,0 +1,297 @@
+/*
+ usermod.c - routines for changing user information such as full name,
+ login shell, etc
+
+ Copyright (C) 2013 Arthur de Jong
+
+ 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
+*/
+
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifdef HAVE_STDINT_H
+#include <stdint.h>
+#endif /* HAVE_STDINT_H */
+#include <unistd.h>
+#include <sys/stat.h>
+
+#include "common.h"
+#include "log.h"
+#include "myldap.h"
+#include "cfg.h"
+#include "attmap.h"
+
+/* ensure that both userdn and username are filled in from the entry,
+ returns an LDAP result code */
+static MYLDAP_ENTRY *validate_user(MYLDAP_SESSION *session,
+ char *username, int *rcp)
+{
+ int rc;
+ MYLDAP_ENTRY *entry = NULL;
+ /* check username for validity */
+ if (!isvalidname(username))
+ {
+ log_log(LOG_WARNING, "request denied by validnames option");
+ *rcp = LDAP_NO_SUCH_OBJECT;
+ return NULL;
+ }
+ /* get the user entry based on the username */
+ entry = uid2entry(session, username, &rc);
+ if (entry == NULL)
+ {
+ if (rc == LDAP_SUCCESS)
+ rc = LDAP_NO_SUCH_OBJECT;
+ log_log(LOG_DEBUG, "\"%s\": user not found: %s", username, ldap_err2string(rc));
+ *rcp = rc;
+ return NULL;
+ }
+ return entry;
+}
+
+static int is_valid_homedir(const char *homedir)
+{
+ struct stat sb;
+ /* should be absolute path */
+ if (homedir[0] != '/')
+ return 0;
+ /* get directory status */
+ if (stat(homedir, &sb))
+ {
+ log_log(LOG_DEBUG, "cannot stat() %s: %s", homedir, strerror(errno));
+ return 0;
+ }
+ /* check if a directory */
+ if (!S_ISDIR(sb.st_mode))
+ {
+ log_log(LOG_DEBUG, "%s: not a directory", homedir);
+ return 0;
+ }
+ /* FIXME: check ownership */
+ return 1;
+}
+
+static int is_valid_shell(const char *shell)
+{
+ int valid = 0;
+ char *l;
+ setusershell();
+ while ((l = getusershell()) != NULL)
+ {
+ if (strcmp(l, shell) == 0)
+ {
+ valid = 1;
+ break;
+ }
+ }
+ endusershell();
+ return valid;
+}
+
+static MYLDAP_SESSION *get_session(const char *binddn, const char *userdn,
+ const char *password, int *rcp)
+{
+ MYLDAP_SESSION *session;
+ char buffer[256];
+ /* set up a new connection */
+ session = myldap_create_session();
+ if (session == NULL)
+ {
+ *rcp = LDAP_UNAVAILABLE;
+ return NULL;
+ }
+ /* set up credentials for the session */
+ myldap_set_credentials(session, binddn, password);
+ /* perform search for own object (just to do any kind of search to set
+ up the connection with fail-over) */
+ if ((lookup_dn2uid(session, userdn, rcp, buffer, sizeof(buffer)) == NULL) ||
+ (*rcp != LDAP_SUCCESS))
+ {
+ myldap_session_close(session);
+ return NULL;
+ }
+ return session;
+}
+
+#define ADD_MOD(attribute, value) \
+ if ((value != NULL) && (attribute[0] != '"')) \
+ { \
+ strvals[i * 2] = (char *)value; \
+ strvals[i * 2 + 1] = NULL; \
+ mods[i].mod_op = LDAP_MOD_REPLACE; \
+ mods[i].mod_type = (char *)attribute; \
+ mods[i].mod_values = strvals + (i * 2); \
+ modsp[i] = mods + i; \
+ i++; \
+ }
+
+static int change(MYLDAP_SESSION *session, const char *userdn,
+ const char *homedir, const char *shell)
+{
+ #define NUMARGS 2
+ char *strvals[(NUMARGS + 1) * 2];
+ LDAPMod mods[(NUMARGS + 1)], *modsp[(NUMARGS + 1)];
+ int i = 0;
+ /* build the list of modifications */
+ ADD_MOD(attmap_passwd_homeDirectory, homedir);
+ ADD_MOD(attmap_passwd_loginShell, shell);
+ /* terminate the list of modifications */
+ modsp[i] = NULL;
+ /* execute the update */
+ return myldap_modify(session, userdn, modsp);
+}
+
+int nslcd_usermod(TFILE *fp, MYLDAP_SESSION *session, uid_t calleruid)
+{
+ int32_t tmpint32;
+ int rc = LDAP_SUCCESS;
+ char username[256];
+ int asroot, isroot;
+ char password[64];
+ int32_t param;
+ char buffer[4096];
+ size_t buflen = sizeof(buffer);
+ size_t bufptr = 0;
+ const char *value = NULL;
+ const char *fullname = NULL, *roomnumber = NULL, *workphone = NULL;
+ const char *homephone = NULL, *other = NULL, *homedir = NULL;
+ const char *shell = NULL;
+ const char *binddn = NULL; /* the user performing the modification */
+ MYLDAP_ENTRY *entry;
+ MYLDAP_SESSION *newsession;
+ char errmsg[1024];
+ /* read request parameters */
+ READ_STRING(fp, username);
+ READ_INT32(fp, asroot);
+ READ_STRING(fp, password);
+ /* read the usermod parameters */
+ while (1)
+ {
+ READ_INT32(fp, param);
+ if (param == NSLCD_USERMOD_END)
+ break;
+ READ_BUF_STRING(fp, value);
+ switch (param)
+ {
+ case NSLCD_USERMOD_FULLNAME: fullname = value; break;
+ case NSLCD_USERMOD_ROOMNUMBER: roomnumber = value; break;
+ case NSLCD_USERMOD_WORKPHONE: workphone = value; break;
+ case NSLCD_USERMOD_HOMEPHONE: homephone = value; break;
+ case NSLCD_USERMOD_OTHER: other = value; break;
+ case NSLCD_USERMOD_HOMEDIR: homedir = value; break;
+ case NSLCD_USERMOD_SHELL: shell = value; break;
+ default: /* other parameters are silently ignored */ break;
+ }
+ }
+ /* log call */
+ log_setrequest("usermod=\"%s\"", username);
+ log_log(LOG_DEBUG, "nslcd_usermod(\"%s\",%s,\"%s\")",
+ username, asroot ? "asroot" : "asuser", *password ? "***" : "");
+ if (fullname != NULL)
+ log_log(LOG_DEBUG, "nslcd_usermod(fullname=\"%s\")", fullname);
+ if (roomnumber != NULL)
+ log_log(LOG_DEBUG, "nslcd_usermod(roomnumber=\"%s\")", roomnumber);
+ if (workphone != NULL)
+ log_log(LOG_DEBUG, "nslcd_usermod(workphone=\"%s\")", workphone);
+ if (homephone != NULL)
+ log_log(LOG_DEBUG, "nslcd_usermod(homephone=\"%s\")", homephone);
+ if (other != NULL)
+ log_log(LOG_DEBUG, "nslcd_usermod(other=\"%s\")", other);
+ if (homedir != NULL)
+ log_log(LOG_DEBUG, "nslcd_usermod(homedir=\"%s\")", homedir);
+ if (shell != NULL)
+ log_log(LOG_DEBUG, "nslcd_usermod(shell=\"%s\")", shell);
+ /* write the response header */
+ WRITE_INT32(fp, NSLCD_VERSION);
+ WRITE_INT32(fp, NSLCD_ACTION_USERMOD);
+ /* validate request */
+ entry = validate_user(session, username, &rc);
+ if (entry == NULL)
+ {
+ /* for user not found we just say no result, otherwise break the protocol */
+ if (rc == LDAP_NO_SUCH_OBJECT)
+ {
+ WRITE_INT32(fp, NSLCD_RESULT_END);
+ }
+ return -1;
+ }
+ /* check if it is a modification as root */
+ isroot = (calleruid == 0) && asroot;
+ if (asroot)
+ {
+ if (nslcd_cfg->rootpwmoddn == NULL)
+ {
+ log_log(LOG_NOTICE, "rootpwmoddn not configured");
+ /* we break the protocol */
+ return -1;
+ }
+ binddn = nslcd_cfg->rootpwmoddn;
+ /* check if rootpwmodpw should be used */
+ if ((*password == '\0') && isroot && (nslcd_cfg->rootpwmodpw != NULL))
+ {
+ if (strlen(nslcd_cfg->rootpwmodpw) >= sizeof(password))
+ {
+ log_log(LOG_ERR, "nslcd_pam_pwmod(): rootpwmodpw will not fit in password");
+ return -1;
+ }
+ strcpy(password, nslcd_cfg->rootpwmodpw);
+ }
+ }
+ else
+ binddn = myldap_get_dn(entry);
+ WRITE_INT32(fp, NSLCD_RESULT_BEGIN);
+ /* home directory change requires either root or valid directory */
+ if ((homedir != NULL) && (!isroot) && !is_valid_homedir(homedir))
+ {
+ log_log(LOG_NOTICE, "invalid directory: %s", homedir);
+ WRITE_INT32(fp, NSLCD_USERMOD_HOMEDIR);
+ WRITE_STRING(fp, "invalid directory");
+ homedir = NULL;
+ }
+ /* shell change requires either root or a valid shell */
+ if ((shell != NULL) && (!isroot) && !is_valid_shell(shell))
+ {
+ log_log(LOG_NOTICE, "invalid shell: %s", shell);
+ WRITE_INT32(fp, NSLCD_USERMOD_SHELL);
+ WRITE_STRING(fp, "invalid shell");
+ shell = NULL;
+ }
+ /* perform requested changes */
+ newsession = get_session(binddn, myldap_get_dn(entry), password, &rc);
+ if (newsession != NULL)
+ {
+ rc = change(newsession, myldap_get_dn(entry), homedir, shell);
+ myldap_session_close(newsession);
+ }
+ /* return response to caller */
+ if (rc != LDAP_SUCCESS)
+ {
+ log_log(LOG_WARNING, "%s: modification failed: %s",
+ myldap_get_dn(entry), ldap_err2string(rc));
+ mysnprintf(errmsg, sizeof(errmsg) - 1, "change failed: %s", ldap_err2string(rc));
+ WRITE_INT32(fp, NSLCD_USERMOD_RESULT);
+ WRITE_STRING(fp, errmsg);
+ WRITE_INT32(fp, NSLCD_USERMOD_END);
+ WRITE_INT32(fp, NSLCD_RESULT_END);
+ return 0;
+ }
+ log_log(LOG_NOTICE, "changed information for %s", myldap_get_dn(entry));
+ WRITE_INT32(fp, NSLCD_USERMOD_END);
+ WRITE_INT32(fp, NSLCD_RESULT_END);
+ return 0;
+}
diff --git a/pynslcd/pynslcd.py b/pynslcd/pynslcd.py
index eedab78..35ecb08 100755
--- a/pynslcd/pynslcd.py
+++ b/pynslcd/pynslcd.py
@@ -187,6 +187,7 @@ handlers.update(common.get_handlers('protocol'))
handlers.update(common.get_handlers('rpc'))
handlers.update(common.get_handlers('service'))
handlers.update(common.get_handlers('shadow'))
+handlers.update(common.get_handlers('usermod'))
def acceptconnection(session):
diff --git a/pynslcd/usermod.py b/pynslcd/usermod.py
new file mode 100644
index 0000000..c957b97
--- /dev/null
+++ b/pynslcd/usermod.py
@@ -0,0 +1,131 @@
+
+# usermod.py - functions for modifying user information
+#
+# Copyright (C) 2013 Arthur de Jong
+#
+# 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 ctypes
+import ctypes.util
+import logging
+import os
+import os.path
+
+import ldap
+
+import cache
+import cfg
+import common
+import constants
+import pam
+import passwd
+
+
+def list_shells():
+ """List the shells from /etc/shells."""
+ libc = ctypes.CDLL(ctypes.util.find_library("c"))
+ libc.setusershell()
+ while True:
+ shell = ctypes.c_char_p(libc.getusershell()).value
+ if not shell:
+ break
+ yield shell
+ libc.endusershell()
+
+
+class UserModRequest(pam.PAMRequest):
+
+ action = constants.NSLCD_ACTION_USERMOD
+
+ def read_parameters(self, fp):
+ username = fp.read_string()
+ asroot = fp.read_int32()
+ password = fp.read_string()
+ mods = {}
+ while True:
+ key = fp.read_int32()
+ if key == constants.NSLCD_USERMOD_END:
+ break
+ mods[key] = fp.read_string()
+ return dict(username=username,
+ asroot=asroot,
+ password=password,
+ mods=mods)
+
+ def write_result(self, mod, message):
+ self.fp.write_int32(mod)
+ self.fp.write_string(message)
+
+ def handle_request(self, parameters):
+ # fill in any missing userdn, etc.
+ self.validate(parameters)
+ is_root = (self.calleruid == 0) and parameters['asroot']
+ mods = []
+ # check if the the user passed the rootpwmoddn
+ if parameters['asroot']:
+ binddn = cfg.rootpwmoddn
+ # check if rootpwmodpw should be used
+ if not parameters['password'] and is_root and cfg.rootpwmodpw:
+ password = cfg.rootpwmodpw
+ else:
+ password = parameters['password']
+ else:
+ binddn = parameters['userdn']
+ password = parameters['password']
+ # write response header
+ self.fp.write_int32(constants.NSLCD_RESULT_BEGIN)
+ # check home directory modification
+ homedir = parameters['mods'].get(constants.NSLCD_USERMOD_HOMEDIR)
+ if homedir:
+ if is_root:
+ mods.append((ldap.MOD_REPLACE, passwd.attmap['homeDirectory'], [homedir]))
+ elif not os.path.isabs(homedir):
+ self.write_result(constants.NSLCD_USERMOD_HOMEDIR,
+ 'should be an absolute path')
+ elif not os.path.isdir(homedir):
+ self.write_result(constants.NSLCD_USERMOD_HOMEDIR,
+ 'not a directory')
+ else:
+ mods.append((ldap.MOD_REPLACE, passwd.attmap['homeDirectory'], [homedir]))
+ # check login shell modification
+ shell = parameters['mods'].get(constants.NSLCD_USERMOD_SHELL)
+ if shell:
+ if is_root:
+ mods.append((ldap.MOD_REPLACE, passwd.attmap['loginShell'], [shell]))
+ elif shell not in list_shells():
+ self.write_result(constants.NSLCD_USERMOD_SHELL,
+ 'unlisted shell')
+ elif not os.path.isfile(shell) or not os.access(shell, os.X_OK):
+ self.write_result(constants.NSLCD_USERMOD_SHELL,
+ 'not an executable')
+ else:
+ mods.append((ldap.MOD_REPLACE, passwd.attmap['loginShell'], [shell]))
+ # get a connection and perform the modification
+ if mods:
+ try:
+ conn, authz, msg = pam.authenticate(binddn, password)
+ conn.modify_s(parameters['userdn'], mods)
+ logging.info('changed information for %s', parameters['userdn'])
+ except (ldap.INVALID_CREDENTIALS, ldap.INSUFFICIENT_ACCESS), e:
+ try:
+ msg = e[0]['desc']
+ except:
+ msg = str(e)
+ logging.debug('modification failed: %s', msg)
+ self.write_result(constants.NSLCD_USERMOD_RESULT, msg)
+ # write closing statement
+ self.fp.write_int32(constants.NSLCD_USERMOD_END)
+ self.fp.write_int32(constants.NSLCD_RESULT_END)
diff --git a/utils/Makefile.am b/utils/Makefile.am
index e9233d8..a10e61d 100644
--- a/utils/Makefile.am
+++ b/utils/Makefile.am
@@ -19,7 +19,7 @@
utilsdir = $(datadir)/nslcdutils
-utils_PYTHON = cmdline.py getent.py nslcd.py
+utils_PYTHON = cmdline.py nslcd.py getent.py chsh.py shells.py users.py
nodist_utils_PYTHON = constants.py
CLEANFILES = $(nodist_utils_PYTHON)
@@ -36,7 +36,7 @@ constants.py: ../pynslcd/constants.py
# create symbolic links to the commands and fix permissions
install-data-hook:
$(MKDIR_P) $(DESTDIR)$(bindir)
- set -ex; for cmd in getent ; do \
+ set -ex; for cmd in getent chsh ; do \
chmod a+rx $(DESTDIR)$(utilsdir)/$$cmd.py ; \
[ -L $(DESTDIR)$(bindir)/$$cmd.ldap ] || $(LN_S) $(utilsdir)/$$cmd.py $(DESTDIR)$(bindir)/$$cmd.ldap ; \
done
diff --git a/utils/chsh.py b/utils/chsh.py
new file mode 100755
index 0000000..30c5c12
--- /dev/null
+++ b/utils/chsh.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# chsh.py - program for changing the login shell using nslcd
+#
+# Copyright (C) 2013 Arthur de Jong
+#
+# 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 argparse
+
+from cmdline import VersionAction, ListShellsAction
+import constants
+import nslcd
+import shells
+import users
+
+
+# set up command line parser
+parser = argparse.ArgumentParser(
+ description='Change the user login shell in LDAP.',
+ epilog='Report bugs to <%s>.' % constants.PACKAGE_BUGREPORT
+ )
+parser.add_argument('-V', '--version', action=VersionAction)
+parser.add_argument('-s', '--shell', help='login shell for the user account')
+parser.add_argument('-l', '--list-shells', action=ListShellsAction)
+parser.add_argument('username', metavar='USER', nargs='?',
+ help="the user who's shell to change")
+
+
+def ask_shell(oldshell):
+ """Ask the user to provide a shell."""
+ shell = raw_input(' Login Shell [%s]: ' % oldshell)
+ return shell or oldshell
+
+
+# parse arguments
+args = parser.parse_args()
+# check username part
+user = users.User(args.username)
+user.check()
+# check the command line shell if one was provided (to fail early)
+shell = args.shell
+if shell is not None:
+ shells.check(shell, user.asroot)
+# prompt for a password if required
+password = user.get_passwd()
+# prompt for a shell if it was not specified on the command line
+if shell is None:
+ print 'Enter the new value, or press ENTER for the default'
+ shell = ask_shell(user.shell)
+ shells.check(shell, user.asroot)
+# perform the modification
+result = nslcd.usermod(user.username, user.asroot, password, {
+ constants.NSLCD_USERMOD_SHELL: shell,
+ })
+# TODO: print proper response
diff --git a/utils/cmdline.py b/utils/cmdline.py
index eb84fe3..3d7d58f 100644
--- a/utils/cmdline.py
+++ b/utils/cmdline.py
@@ -48,3 +48,21 @@ class VersionAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
print version_string
parser.exit()
+
+
+class ListShellsAction(argparse.Action):
+
+ def __init__(self, option_strings, dest,
+ help='list the shells found in /etc/shells'):
+ super(ListShellsAction, self).__init__(
+ option_strings=option_strings,
+ dest=argparse.SUPPRESS,
+ default=argparse.SUPPRESS,
+ nargs=0,
+ help=help)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ import shells
+ for shell in shells.list_shells():
+ print shell
+ parser.exit()
diff --git a/utils/nslcd.py b/utils/nslcd.py
index 06165cc..65e5822 100644
--- a/utils/nslcd.py
+++ b/utils/nslcd.py
@@ -111,3 +111,26 @@ class NslcdClient(object):
def __del__(self):
self.close()
+
+
+def usermod(username, asroot=False, password=None, args=None):
+ # open a connection to nslcd
+ con = NslcdClient(constants.NSLCD_ACTION_USERMOD)
+ # write the request information
+ con.write_string(username)
+ con.write_int32(1 if asroot else 0)
+ con.write_string(password)
+ for k, v in args.items():
+ con.write_int32(k)
+ con.write_string(v)
+ con.write_int32(constants.NSLCD_USERMOD_END)
+ # read the response
+ assert con.get_response() == constants.NSLCD_RESULT_BEGIN
+ response = {}
+ while True:
+ key = con.read_int32()
+ if key == constants.NSLCD_USERMOD_END:
+ break
+ response[key] = con.read_string()
+ # return the response
+ return response
diff --git a/utils/shells.py b/utils/shells.py
new file mode 100644
index 0000000..cc3fca1
--- /dev/null
+++ b/utils/shells.py
@@ -0,0 +1,64 @@
+# coding: utf-8
+
+# shells.py - functions for validating user shells
+#
+# Copyright (C) 2013 Arthur de Jong
+#
+# 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 ctypes
+import ctypes.util
+import os
+import sys
+
+
+def list_shells():
+ """List the shells from /etc/shells."""
+ libc = ctypes.CDLL(ctypes.util.find_library("c"))
+ libc.setusershell()
+ while True:
+ shell = ctypes.c_char_p(libc.getusershell()).value
+ if not shell:
+ break
+ yield shell
+ libc.endusershell()
+
+
+def shellexists(shell):
+ """Check if the provided shell exists and is executable."""
+ return os.path.isfile(shell) and os.access(shell, os.X_OK)
+
+
+def check(shell, asroot=False):
+ """Check if the specified shell is valid and exit if it isn't."""
+ # if the shell is listed in /etc/shells, everything should be OK
+ if shell in list_shells():
+ return
+ # if we are not root, bail out
+ if not asroot:
+ if not shell:
+ # FIXME: print to stderr
+ print '%s: empty shell not allowed' % sys.argv[0]
+ else:
+ # FIXME: print to stderr
+ print '%s: %s is an invalid shell' % (sys.argv[0], shell)
+ sys.exit(1)
+ # warn if something seems wrong
+ if not shell:
+ # FIXME: print to stderr
+ print '%s: Warning: setting empty shell' % sys.argv[0]
+ elif not shellexists(shell):
+ print '%s: Warning: %s does not exist' % (sys.argv[0], shell)
diff --git a/utils/users.py b/utils/users.py
new file mode 100644
index 0000000..02216d6
--- /dev/null
+++ b/utils/users.py
@@ -0,0 +1,60 @@
+# coding: utf-8
+
+# users.py - functions for validating the user to change information for
+#
+# Copyright (C) 2013 Arthur de Jong
+#
+# 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 getpass
+import os
+import pwd
+import sys
+
+
+class User(object):
+
+ def __init__(self, username):
+ self.myuid = os.getuid()
+ if username:
+ userinfo = pwd.getpwnam(username)
+ else:
+ self.asroot = False
+ userinfo = pwd.getpwuid(self.myuid)
+ (self.username, ignore, self.uid, self.gid, self.gecos, self.homedir,
+ self.shell) = userinfo
+ # if we are trying to modify another user we should be root
+ self.asroot = self.myuid != self.uid
+
+ def check(self):
+ """Check if the user we want to modify is an LDAP user and whether
+ we may modify the user information."""
+ if self.asroot and self.myuid != 0:
+ print "%s: you may not modify user '%s'.\n" % \
+ (sys.argv[0], self.username)
+ sys.exit(1)
+ # FIXME: check if the user is an LDAP user
+
+ def get_passwd(self):
+ """Ask and return a password that is required to change the user."""
+ # FIXME: only ask the password if we require it
+ # (e.g. when root and nslcd has userpwmoddn we don't need to)
+ return getpass.getpass(
+ 'LDAP administrator password: '
+ if self.asroot else
+ 'LDAP password for %s: ' % self.username
+ )
+ # FIXME: check if the provided password is valid