diff options
author | Arthur de Jong <arthur@arthurdejong.org> | 2013-03-30 23:10:34 +0100 |
---|---|---|
committer | Arthur de Jong <arthur@arthurdejong.org> | 2013-03-30 23:10:34 +0100 |
commit | 62a409cb43b441c32692f414a1867176d37034ac (patch) | |
tree | 50c70342106c2674d61b5559f7dfa89dc1f506bc | |
parent | aae36cfcfb6ec00776f6da1e0d1fd5f90a72f2dd (diff) | |
parent | 012b18554e5e6a408a11a7157a30c5d068f2d3d1 (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-- | .gitignore | 2 | ||||
-rw-r--r-- | man/Makefile.am | 2 | ||||
-rw-r--r-- | man/chsh.ldap.1.xml | 148 | ||||
-rw-r--r-- | nslcd.h | 30 | ||||
-rw-r--r-- | nslcd/Makefile.am | 4 | ||||
-rw-r--r-- | nslcd/common.h | 1 | ||||
-rw-r--r-- | nslcd/nslcd.c | 1 | ||||
-rw-r--r-- | nslcd/usermod.c | 297 | ||||
-rwxr-xr-x | pynslcd/pynslcd.py | 1 | ||||
-rw-r--r-- | pynslcd/usermod.py | 131 | ||||
-rw-r--r-- | utils/Makefile.am | 4 | ||||
-rwxr-xr-x | utils/chsh.py | 70 | ||||
-rw-r--r-- | utils/cmdline.py | 18 | ||||
-rw-r--r-- | utils/nslcd.py | 23 | ||||
-rw-r--r-- | utils/shells.py | 64 | ||||
-rw-r--r-- | utils/users.py | 60 |
16 files changed, 851 insertions, 5 deletions
@@ -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 <arthur@arthurdejong.org>.</para> + </refsect1> + +</refentry> @@ -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 |