Because the httpd is running as the user nobody
, and because the passwd file doesn't (or shouldn't) belong to that user, it's a bit of a quandary to have an exec
'd CGI program change that file. There are alternatives, however. You can run a secondary server on a different port, under the www
userid, whose only purpose would be to execute the CGI program that changes the user's password, for instance. Another alternative might be to implement something with SafeCGIPerl
, discussed later, and the HTTPD::UserAdmin
module just introduced. Coding it up is left as an exercise for the wary administrator. Be very, very careful.
Monitoring Userids
As a final note in this section, we'll add a bit more code to our ongoing example to keep track of userids in the .htpasswd file for each <Directory>
entry in access.conf.
use HTTPD::Config; require HTTPD::UserAdmin; require "stat.pl"; $conf = `/usr/local/etc/httpd/conf'; @files = qw(httpd.conf srm.conf access.conf); $V= new HTTPD::Config (SERVER => Apache, SERVER_ROOT => $conf, FILES => [@files]); print "Userid: ", $V->user,"\n"; print "Group: ", $V->group,"\n"; print "Administrator is: ", $V->server_admin,"\n"; print "Running at port: ", $V->port,"\n"; print "Access filename: ",$V->access_file_name,"\n"; print "User Directory: ", $V->user_dir,"\n"; print "Global Types:\t", join("\n\t\t",$V->add_type),"\n"; print "\n\n"; foreach $dir (keys %{$V->{`Directory'}}){ print "Options for Directory: $dir\n"; while(($opt,$val) = each %{$V->{`Directory'}{$dir}{`OPTIONS'}}){ print "\t",$opt, " : ", @{$val},"\n"; if($opt eq "AuthUserFile"){ ($path = join(`',@{$val})) =~ s/^(.*)\/.*/$1/; print "\tCurrent users in $opt:\n"; $users = new HTTPD::UserAdmin(DBType => "Text", Path => $path, Locking => 0, Server => "apache"); #@users = $users->list; foreach $user (sort($users->list)){ print "\t\t$user : "; print $users->password($user),"\n"; } } } print "\tLimit: @{$V->{Directory}{$dir}{LIMIT}{METHODS}}\n"; while(($key,$val) = each %{$V->{Directory}{$dir}{LIMIT}{OPTIONS}}) { print "\t\t$key = @{$val}\n"; } print "\n"; } # rudimentary permissions checking $webuser = (getpwnam($V->user))[2]; opendir(ROOT,$V->server_root); @files = grep(!/\.\.?/,readdir(ROOT)); closedir(ROOT); foreach $f (@files){ @s = Stat($V->server_root."/$f"); if($s[$ST_UID] == $webuser){ print "Warning: ",$V->server_root,"/$f is owned by ", $V->user,"\n\n"; } if($f eq "httpd"){ if(($s[$ST_MODE] != 0100700) or ($s[$ST_UID] != 0)){ print "\tWarning: ",$V->server_root,"/httpd may have\n"; print "\tpermission problems. Recommend root ownership\n"; print "\tand readable, writable and executable only by\n"; print "\troot user\n"; } } }
We get a fairly nice report back that looks like this:
Userid: nobody Group: nogroup Administrator is: wmiddlet@adobe.com Running at port: 80 Access filename: .privaccess User Directory: public_html Global Types: text/html .shtml Options for Directory: /usr/local/etc/httpd/cgi-bin Options : None AllowOverride : None Limit: Options for Directory: /usr/local/etc/httpd/htdocs/test1 Options : None AllowOverride : None Limit: GET POST require = valid-user Options for Directory: /usr/local/etc/httpd/htdocs PerlHandler : main::handler AuthUserFile : /usr/local/etc/httpd/conf/.htpasswd Current users in AuthUserFile: bmiddlet : ztTO6y2K.3qLE bozo : 0Euk1WMWyXRgg josie : _LhTDVY/y6tPo AddType : text/html .shtml AuthName : PasswordAdmin Options : Indexes SymLinksIfOwnerMatch IncludesNOEXEC AuthType : Basic AllowOverride : AuthConfig FileInfo Indexes Limit Limit: GET deny = from .bozo.com order = mutual-failure allow = from .metronet.com
Of course, if your user database gets very large, then this script may produce more output than you want to see. The idea from the beginning has been that this script would run automatically via cron, having its output compared to the expected output, possibly from the day before, then sending on only the differences for your inspection.