freebsd-skq/usr.sbin/rpc.yppasswdd/yppasswdd_server.c
Bill Paul 8256fad9b7 Import new rpc.yppasswdd. (Note: accompanying changes to passwd(1) and
chpass(1) are on the way too.) This version supports all the features
of the old one and adds several new ones:

- Supports real multi-domain operation (optional, can be turned
  on with a command-line flag). This means you can actually have
  several different domains all served from one NIS server and
  allow users in any of the supported domains to change their passwords.
  The old yppasswdd only allowed changing passwords in the domain
  that was set as the system default domain name on the NIS master
  server. The new one can change passwords in any domain by trying
  to match the user information passed to it against all the passwd
  maps it can find. This is something of a hack, but the yppasswd.x
  protocol definiton does not allow for a domain to be passwd as an
  argument to rpc.yppasswdd, so the server has no choice but to
  grope around for a likely match. Since this method can fail if
  the same user exists in two domains, this feature is off by default.
  If the feature is turned on and the server becomes confused by
  duplicate entries, it will abort the update.

- Does not require NIS client services to be available. NIS servers do
  _NOT_ necessarily have to be configured as NIS clients in order to
  function: the ypserv, ypxfr and yppush programs I've written recently
  will operate fine even if the system domain name isn't set, ypbind isn't
  running and there are no magic '+' entries in any of the /etc files.
  Now rpc.yppasswdd is the same way. The old yppasswdd would not work
  like this because it depended on getpwent(3) and friends to look up
  users: this will obviously only work if the system where yppasswdd is
  running is configured as an NIS client. The new rpc.yppasswdd doesn't
  use getpwent(3) at all: instead it searches through the master.passwd
  map databases directly. This also makes it easier for it to handle
  multiple domains.

- Allows the superuser on the NIS master server to change any user's
  password without requiring password authentication. rpc.yppasswdd
  creates a UNIX domain socket (/var/run/ypsock) which it monitors
  using the same svc_run() loop used to handle incoming RPC requests.
  It also clears all the permission bits for /var/run/ypsock; since
  this socket is owned by root, this prevents anyone except root from
  successfully connect()ing to it. (Using a UNIX domain socket also
  prevents IP spoofing attacks.) By building code into passwd(1) and
  chpass(1) to take advantage of this 'trusted' channel, the superuser
  can use them to send private requests to rpc.yppasswdd.

- Allows the superuser on the NIS master to use chpass(1) to update _all_
  of a user's master.passwd information. The UNIX domain access point
  accepts a full master.passwd style structure (along with a domain
  name and other information), which allows the superuser to update all
  of a user's master.passwd information in the NIS master.passwd maps.
  Normal users on NIS clients are still only allowed to change their full
  name and shell information with chpass.

- Allows the superuser on the NIS master to _add_ records to the NIS
  master.passwd maps using chpass(1). This feature is also switchable
  with a command-line flag and is off by default.
1996-02-12 15:09:01 +00:00

650 lines
16 KiB
C

/*
* Copyright (c) 1995, 1996
* Bill Paul <wpaul@ctr.columbia.edu>. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by Bill Paul.
* 4. Neither the name of the author nor the names of any co-contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY Bill Paul AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL Bill Paul OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* $Id: yppasswdd_server.c,v 1.8 1996/02/09 04:38:19 wpaul Exp $
*/
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <limits.h>
#include <db.h>
#include <pwd.h>
#include <errno.h>
#include <signal.h>
#include <rpc/rpc.h>
#include <rpcsvc/yp.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/param.h>
struct dom_binding {};
#include <rpcsvc/ypclnt.h>
#include "yppasswdd_extern.h"
#include "yppasswd.h"
#include "yppasswd_private.h"
#include "yppasswd_comm.h"
#ifndef lint
static const char rcsid[] = "$Id: yppasswdd_server.c,v 1.8 1996/02/09 04:38:19 wpaul Exp $";
#endif /* not lint */
char *tempname;
void reaper(sig)
int sig;
{
extern pid_t pid;
extern int pstat;
int st;
if (sig > 0) {
if (sig == SIGCHLD)
while(wait3(&st, WNOHANG, NULL) > 0) ;
} else {
pid = waitpid(pid, &pstat, 0);
}
return;
}
void install_reaper(on)
int on;
{
if (on) {
signal(SIGCHLD, reaper);
} else {
signal(SIGCHLD, SIG_DFL);
}
return;
}
static struct passwd yp_password;
static void copy_yp_pass(p, x, m)
char *p;
int x, m;
{
register char *t, *s = p;
static char *buf;
yp_password.pw_fields = 0;
buf = (char *)realloc(buf, m + 10);
bzero(buf, m + 10);
/* Turn all colons into NULLs */
while (strchr(s, ':')) {
s = (strchr(s, ':') + 1);
*(s - 1)= '\0';
}
t = buf;
#define EXPAND(e) e = t; while ((*t++ = *p++));
EXPAND(yp_password.pw_name);
yp_password.pw_fields |= _PWF_NAME;
EXPAND(yp_password.pw_passwd);
yp_password.pw_fields |= _PWF_PASSWD;
yp_password.pw_uid = atoi(p);
p += (strlen(p) + 1);
yp_password.pw_fields |= _PWF_UID;
yp_password.pw_gid = atoi(p);
p += (strlen(p) + 1);
yp_password.pw_fields |= _PWF_GID;
if (x) {
EXPAND(yp_password.pw_class);
yp_password.pw_fields |= _PWF_CLASS;
yp_password.pw_change = atol(p);
p += (strlen(p) + 1);
yp_password.pw_fields |= _PWF_CHANGE;
yp_password.pw_expire = atol(p);
p += (strlen(p) + 1);
yp_password.pw_fields |= _PWF_EXPIRE;
}
EXPAND(yp_password.pw_gecos);
yp_password.pw_fields |= _PWF_GECOS;
EXPAND(yp_password.pw_dir);
yp_password.pw_fields |= _PWF_DIR;
EXPAND(yp_password.pw_shell);
yp_password.pw_fields |= _PWF_SHELL;
return;
}
static int validchars(arg)
char *arg;
{
int i;
for (i = 0; i < strlen(arg); i++) {
if (iscntrl(arg[i])) {
yp_error("string contains a control character");
return(1);
}
if (arg[i] == ':') {
yp_error("string contains a colon");
return(1);
}
/* Be evil: truncate strings with \n in them silently. */
if (arg[i] == '\n') {
arg[i] = '\0';
return(0);
}
}
return(0);
}
static int validate_master(opw, npw)
struct passwd *opw;
struct x_master_passwd *npw;
{
if (npw->pw_name[0] == '+' || npw->pw_name[0] == '-') {
yp_error("client tried to modify an NIS entry");
return(1);
}
if (validchars(npw->pw_shell)) {
yp_error("specified shell contains invalid characters");
return(1);
}
if (validchars(npw->pw_gecos)) {
yp_error("specified gecos field contains invalid characters");
return(1);
}
if (validchars(npw->pw_passwd)) {
yp_error("specified password contains invalid characters");
return(1);
}
return(0);
}
static int validate(opw, npw)
struct passwd *opw;
struct x_passwd *npw;
{
if (npw->pw_name[0] == '+' || npw->pw_name[0] == '-') {
yp_error("client tried to modify an NIS entry");
return(1);
}
if (npw->pw_uid != opw->pw_uid) {
yp_error("UID mismatch: client says user %s has UID %d",
npw->pw_name, npw->pw_uid);
yp_error("database says user %s has UID %d", opw->pw_name,
opw->pw_uid);
return(1);
}
if (npw->pw_gid != opw->pw_gid) {
yp_error("GID mismatch: client says user %s has GID %d",
npw->pw_name, npw->pw_gid);
yp_error("database says user %s has GID %d", opw->pw_name,
opw->pw_gid);
return(1);
}
/*
* Don't allow the user to shoot himself in the foot,
* even on purpose.
*/
if (!ok_shell(npw->pw_shell)) {
yp_error("%s is not a valid shell", npw->pw_shell);
return(1);
}
if (validchars(npw->pw_shell)) {
yp_error("specified shell contains invalid characters");
return(1);
}
if (validchars(npw->pw_gecos)) {
yp_error("specified gecos field contains invalid characters");
return(1);
}
if (validchars(npw->pw_passwd)) {
yp_error("specified password contains invalid characters");
return(1);
}
return(0);
}
/*
* Kludge alert:
* In order to have one rpc.yppasswdd support multiple domains,
* we have to cheat: we search each directory under /var/yp
* and try to match the user in each master.passwd.byname
* map that we find. If the user matches (username, uid and gid
* all agree), then we use that domain. If we match the user in
* more than one database, we must abort.
*/
static char *find_domain(pw)
struct x_passwd *pw;
{
struct stat statbuf;
struct dirent *dirp;
DIR *dird;
char yp_mapdir[MAXPATHLEN + 2];
char *domain = NULL;
char *tmp = NULL;
DBT key, data;
int hit = 0;
yp_error("performing multidomain lookup");
if ((dird = opendir(yp_dir)) == NULL) {
yp_error("opendir(%s) failed: %s", yp_dir, strerror(errno));
return(NULL);
}
while ((dirp = readdir(dird)) != NULL) {
snprintf(yp_mapdir, sizeof(yp_mapdir), "%s/%s",
yp_dir, dirp->d_name);
if (stat(yp_mapdir, &statbuf) < 0) {
yp_error("stat(%s) failed: %s", yp_mapdir,
strerror(errno));
closedir(dird);
return(NULL);
}
if (S_ISDIR(statbuf.st_mode)) {
tmp = (char *)dirp->d_name;
key.data = pw->pw_name;
key.size = strlen(pw->pw_name);
if (yp_get_record(tmp,"master.passwd.byname",
&key, &data, 0) != YP_TRUE) {
continue;
}
*(char *)(data.data + data.size) = '\0';
copy_yp_pass(data.data, 1, data.size);
if (yp_password.pw_uid == pw->pw_uid &&
yp_password.pw_gid == pw->pw_gid) {
hit++;
domain = tmp;
}
}
}
closedir(dird);
if (hit > 1) {
yp_error("found same user in two different domains");
return(NULL);
} else
return(domain);
}
int *
yppasswdproc_update_1_svc(yppasswd *argp, struct svc_req *rqstp)
{
static int result;
struct sockaddr_in *rqhost;
DBT key, data;
int rval = 0;
int pfd, tfd;
int pid;
int passwd_changed = 0;
int shell_changed = 0;
int gecos_changed = 0;
char *oldshell = NULL;
char *oldgecos = NULL;
char *passfile_hold;
char passfile_buf[MAXPATHLEN + 2];
char template[] = "/etc/yppwtmp.XXXXX";
char *domain = yppasswd_domain;
/*
* Normal user updates always use the 'default' master.passwd file.
*/
passfile = passfile_default;
result = 1;
rqhost = svc_getcaller(rqstp->rq_xprt);
/*
* Step one: find the user. (It's kinda pointless to
* proceed if the user doesn't exist.) We look for the
* user in the master.passwd.byname database, _NOT_ by
* using getpwent() and friends! We can't use getpwent()
* since the NIS master server is not guaranteed to be
* configured as an NIS client.
*/
if (multidomain) {
if ((domain = find_domain(&argp->newpw)) == NULL) {
yp_error("multidomain lookup failed - aborting update");
return(&result);
} else
yp_error("updating user %s in domain %s",
argp->newpw.pw_name, domain);
}
key.data = argp->newpw.pw_name;
key.size = strlen(argp->newpw.pw_name);
if ((rval=yp_get_record(domain,"master.passwd.byname",
&key, &data, 0)) != YP_TRUE) {
if (rval == YP_NOKEY) {
yp_error("user %s not found in passwd database",
argp->newpw.pw_name);
} else {
yp_error("database access error: %s",
yperr_string(rval));
}
return(&result);
}
/* Nul terminate, please. */
*(char *)(data.data + data.size) = '\0';
copy_yp_pass(data.data, 1, data.size);
/* Step 2: check that the supplied oldpass is valid. */
if (strcmp(crypt(argp->oldpass, yp_password.pw_passwd),
yp_password.pw_passwd)) {
yp_error("rejected change attempt -- bad password");
yp_error("client address: %s username: %s",
inet_ntoa(rqhost->sin_addr),
argp->newpw.pw_name);
return(&result);
}
/* Step 3: validate the arguments passed to us by the client. */
if (validate(&yp_password, &argp->newpw)) {
yp_error("rejecting change attempt: bad arguments");
yp_error("client address: %s username: %s",
inet_ntoa(rqhost->sin_addr),
argp->newpw.pw_name);
svcerr_decode(rqstp->rq_xprt);
return(&result);
}
/* Step 4: update the user's passwd structure. */
if (!no_chsh && strcmp(argp->newpw.pw_shell, yp_password.pw_shell)) {
oldshell = yp_password.pw_shell;
yp_password.pw_shell = argp->newpw.pw_shell;
shell_changed++;
}
if (!no_chfn && strcmp(argp->newpw.pw_gecos, yp_password.pw_gecos)) {
oldgecos = yp_password.pw_gecos;
yp_password.pw_gecos = argp->newpw.pw_gecos;
gecos_changed++;
}
if (strcmp(argp->newpw.pw_passwd, yp_password.pw_passwd)) {
yp_password.pw_passwd = argp->newpw.pw_passwd;
passwd_changed++;
}
/*
* If the caller specified a domain other than our 'default'
* domain, change the path to master.passwd accordingly.
*/
if (strcmp(domain, yppasswd_domain)) {
snprintf(passfile_buf, sizeof(passfile_buf),
"/var/yp/%s/master.passwd", domain);
passfile = (char *)&passfile_buf;
}
/* Step 5: make a new password file with the updated info. */
if ((pfd = pw_lock()) < 0) {
return (&result);
}
if ((tfd = pw_tmp()) < 0) {
return (&result);
}
if (pw_copy(pfd, tfd, &yp_password)) {
yp_error("failed to created updated password file -- \
cleaning up and bailing out");
unlink(tempname);
return(&result);
}
passfile_hold = mktemp((char *)&template);
rename(passfile, passfile_hold);
if (strcmp(passfile, _PATH_MASTERPASSWD)) {
rename(tempname, passfile);
} else {
if (pw_mkdb() < 0) {
yp_error("pwd_mkdb failed");
return(&result);
}
}
switch((pid = fork())) {
case 0:
/* unlink(passfile_hold); */
execlp(MAP_UPDATE_PATH, MAP_UPDATE, passfile,
yppasswd_domain, NULL);
yp_error("couldn't exec map update process: %s",
strerror(errno));
unlink(passfile);
rename(passfile_hold, passfile);
exit(1);
break;
case -1:
yp_error("fork() failed: %s", strerror(errno));
return(&result);
unlink(passfile);
rename(passfile_hold, passfile);
break;
default:
break;
}
if (verbose) {
yp_error("update completed for user %s (uid %d):",
argp->newpw.pw_name,
argp->newpw.pw_uid);
if (passwd_changed)
yp_error("password changed");
if (gecos_changed)
yp_error("gecos changed ('%s' -> '%s')",
oldgecos, argp->newpw.pw_gecos);
if (shell_changed)
yp_error("shell changed ('%s' -> '%s')",
oldshell, argp->newpw.pw_shell);
}
result = 0;
return (&result);
}
/*
* Note that this function performs a little less sanity checking
* than the last one. Since only the superuser is allowed to use it,
* it is assumed that the caller knows what he's doing.
*/
static int update_master(master_yppasswd *argp)
{
int result;
int pfd, tfd;
int pid;
int rval = 0;
DBT key, data;
char *passfile_hold;
char passfile_buf[MAXPATHLEN + 2];
char template[] = "/etc/yppwtmp.XXXXX";
result = 1;
passfile = passfile_default;
key.data = argp->newpw.pw_name;
key.size = strlen(argp->newpw.pw_name);
/*
* The superuser may add entries to the passwd maps if
* rpc.yppasswdd is started with the -a flag. Paranoia
* prevents me from allowing additions by default.
*/
if ((rval = yp_get_record(argp->domain, "master.passwd.byname",
&key, &data, 0)) != YP_TRUE) {
if (rval == YP_NOKEY) {
yp_error("user %s not found in passwd database",
argp->newpw.pw_name);
if (allow_additions)
yp_error("notice: adding user %s to \
master.passwd database for domain %s", argp->newpw.pw_name, argp->domain);
else
yp_error("restart %s with the -a flag to \
allow additions to be made to the password database", progname);
} else {
yp_error("database access error: %s",
yperr_string(rval));
}
if (!allow_additions)
return(result);
} else {
/* Nul terminate, please. */
*(char *)(data.data + data.size) = '\0';
copy_yp_pass(data.data, 1, data.size);
}
/*
* Perform a small bit of sanity checking.
*/
if (validate_master(rval == YP_TRUE ? &yp_password:NULL,&argp->newpw)){
yp_error("rejecting update attempt for %s: bad arguments",
argp->newpw.pw_name);
return(result);
}
/*
* If the caller specified a domain other than our 'default'
* domain, change the path to master.passwd accordingly.
*/
if (strcmp(argp->domain, yppasswd_domain)) {
snprintf(passfile_buf, sizeof(passfile_buf),
"/var/yp/%s/master.passwd", argp->domain);
passfile = (char *)&passfile_buf;
}
if ((pfd = pw_lock()) < 0) {
return (result);
}
if ((tfd = pw_tmp()) < 0) {
return (result);
}
if (pw_copy(pfd, tfd, (struct passwd *)&argp->newpw)) {
yp_error("failed to created updated password file -- \
cleaning up and bailing out");
unlink(tempname);
return(result);
}
passfile_hold = mktemp((char *)&template);
rename(passfile, passfile_hold);
if (strcmp(passfile, _PATH_MASTERPASSWD)) {
rename(tempname, passfile);
} else {
if (pw_mkdb() < 0) {
yp_error("pwd_mkdb failed");
return(result);
}
}
switch((pid = fork())) {
case 0:
close(yp_sock);
execlp(MAP_UPDATE_PATH, MAP_UPDATE, passfile,
argp->domain, NULL);
yp_error("couldn't exec map update process: %s",
strerror(errno));
unlink(passfile);
rename(passfile_hold, passfile);
exit(1);
break;
case -1:
yp_error("fork() failed: %s", strerror(errno));
unlink(passfile);
rename(passfile_hold, passfile);
return(result);
break;
default:
break;
}
yp_error("performed update of user %s (uid %d) domain %s",
argp->newpw.pw_name,
argp->newpw.pw_uid,
argp->domain);
result = 0;
return(result);
}
/*
* Pseudo-dispatcher for private 'superuser-only' update handler.
*/
void do_master()
{
struct master_yppasswd *pw;
int resp;
if ((pw = getdat(yp_sock)) == NULL) {
return;
}
yp_error("received update request from superuser on localhost");
resp = update_master(pw);
sendresp(resp);
/* Remember to free args. */
xdr_free(xdr_master_yppasswd, (char *)pw);
return;
}