#include #include #include #include #include #include #include #include #include #include #include "internal.h" #include "userconf.h" #include "../paths.h" #include "userconf.m" #include #include "usercomng.h" #include #include static USERCONF_HELP_FILE help_user ("user"); USERCONF_HELP_FILE help_password ("password"); static USERCONF_HELP_FILE help_delacc ("delaccount"); /* Return true is the status of the user have changed during the edit active will contains the status of the user (enabled or not). */ PUBLIC bool USER::statuschanged (bool &active) const { bool ret = false; active = enabled; if (enabled != was_enabled){ ret = true; } return ret; } static bool user_shells_isok(const char *s) { bool ret = false; if (strcmp(s,"/bin/false")==0){ ret = true; }else{ ret = shells_isok(s); } return ret; } PRIVATE void USER::init( const char *_name, const char *_passwd, int _uid, int _gid, const char *_gecos, const char *_dir, const char *_shell) { priv = NULL; enabled = 1; was_enabled = 1; name.setfrom (_name); passwd.setfrom (_passwd); uid = _uid; gid = _gid; comment.setfrom (_gecos); wrkdir.setfrom (_dir); shell.setfrom (_shell); special = !user_shells_isok(_shell); oldname.setfrom (name); } PUBLIC USER::USER( const char *_name, const char *_passwd, int _uid, int _gid, const char *_gecos, const char *_dir, const char *_shell) { init (_name,_passwd,_uid,_gid,_gecos,_dir,_shell); } PUBLIC USER::USER() { init ("","",-1,-1,"","",""); } PUBLIC USER::USER(const USER *usr) { init (usr->getname(),usr->getpwd(),usr->uid,usr->gid ,usr->getgecos(),usr->gethome(),usr->getshell()); } /* Decompose a /etc/passwd style line into words */ int user_splitline (const char *line, char words[9][100]) { return str_splitline (line,':',words,9); } /* Taken from /etc/passwd directly */ PUBLIC USER::USER(const char *line) { enabled = 1; was_enabled = 1; char words[9][100]; user_splitline (line,words); name.setfrom (words[0]); passwd.setfrom (words[1]); uid = atoi(words[2]); gid = atoi(words[3]); comment.setfrom (words[4]); wrkdir.setfrom (words[5]); shell.setfrom (words[6]); special = !user_shells_isok(words[6]); oldname.setfrom (name); } PUBLIC USER::USER(struct passwd *p) { init (p->pw_name,p->pw_passwd,p->pw_uid,p->pw_gid,p->pw_gecos,p->pw_dir ,p->pw_shell); } PUBLIC USER::~USER() { } /* Return the crypt password field */ PUBLIC const char *USER::getpwd() const { return passwd.get(); } /* Return the home directory field */ PUBLIC const char *USER::gethome() const { return wrkdir.get(); } /* Write one record of /etc/passwd */ PUBLIC void USER::write(FILE_CFG *fout) { fprintf (fout,"%s:%s:%d:%d:%s:%s:%s\n" ,name.get(),passwd.get(),uid,gid,comment.get() ,wrkdir.get(),shell.get()); } /* Return the login name of the user. */ PUBLIC const char *USER::getname() const { return name.get(); } /* Return the original login name of the user. */ PUBLIC const char *USER::getoldname() const { return oldname.get(); } /* Return the long informative name of the user. */ PUBLIC const char *USER::getgecos() const { return comment.get(); } /* Put in buf a fully escaped gecos field usable as an argument to a shell script */ PUBLIC void USER::getgecos_esc (char *buf, int size) const { // The GECOS field is user changeable. The user is allowed // to put whatever he wants there, including special shell characters // so we have to carefully quote the whole thing. It can also // contain quotes, so we have to escape those too. But a single // quote can't be escaped when part of of a single quote sequence // so we have to end the single quote, put the escaped quote and // open up again the single quote // hel'lo -> 'hel'\''lo' const char *gecos = getgecos(); char *pt = buf; *pt++ = '\''; size -= 6; // Some space to add an escape sequence while (*gecos != '\0' && (pt-buf)getname()); /* #Specification: userconf / user / home directory userconf do not allow a user to have an empty directory nor a directory which point to something else than a directory. */ if (code_dir != 0 && (full || code_dir != ENOENT)){ status.append (status_dir.get()); } #endif int ret = 0; if (!status.is_empty()){ xconf_error ("%s",status.get()); ret = -1; } return ret; } /* Check if the home of the user do exist Return -1 : No home directory specified ENOENT : path does not exist. ENOTDIR : path do exist, but is not a directory. EPERM : path do exist but permissions are wrong 0 : all is ok. */ PUBLIC int USER::checkhome ( SSTRING *status,// Will contain an explanation of the problem // if any (may be NULL) const char *group) // Main group of the user { int ret = -1; if (!context_isroot() || ! policies_createhome()) return 0; const char *msg = MSG_U(E_NOHOME,"No home directory specified"); if (!wrkdir.is_empty()){ struct stat st; if (stat(wrkdir.get(),&st)!=-1){ if (S_ISDIR (st.st_mode)){ /* #Specification: user / home directory / owner userconf check that the home directory of a user is own (gid and uid) by him except for special account ( administrative, PPP, uucp, etc...) where we generally allocate the same directory to a bunch of users. */ if (special || !group_homeneeded(group) || (st.st_uid == (uid_t)uid && st.st_gid == (gid_t)gid)){ ret = 0; msg = MSG_U(N_IS_OK,"is ok"); }else{ ret = EPERM; if (st.st_uid == (uid_t)uid){ msg = MSG_U(E_IVLDGRP,"have invalid group"); }else if (st.st_gid == (gid_t)gid){ msg = MSG_U(E_IVLDOWN,"have invalid owner"); }else{ msg = MSG_U(E_IVLDG_O,"have invalid owner and group"); } } }else{ ret = ENOTDIR; msg = MSG_U(E_EXISTDIR,"do exist, but is not a directory"); } }else{ msg = MSG_U(E_DONOTEXIST,"does not exist"); ret = ENOENT; } } if (status != NULL){ status->setfromf ( MSG_U(E_HOMEUSER,"Home directory of user %s: %s\n %s\n") ,name.get(),wrkdir.get(),msg); } return ret; } /* Set the name of the new user */ PUBLIC void USER::setname(const char *_name) { name.setfrom (_name); } PUBLIC void USER::setuid(int _uid) { uid = _uid; } PUBLIC void USER::setgid(int _gid) { gid = _gid; } /* Set the gecos field of the new user */ PUBLIC void USER::setgecos(const char *_gecos) { comment.setfrom (_gecos); } /* Set the shell field of the new user */ PUBLIC void USER::setshell(const char *_shell) { shell.setfrom (_shell); } /* Set the home field of the new user (no check) */ PUBLIC void USER::sethome(const char *_home) { wrkdir.setfrom (_home); } /* Create/update the user home directory. Return -1 if any error. */ PUBLIC int USER::sethome ( PRIVILEGE *priv, const char *group, bool do_recur, USERS &users) { SSTRING status; int ret = checkhome(&status,group); if (ret != 0){ if (ret == -1 || ret == ENOTDIR){ xconf_error ("%s",status.get()); }else{ PRIVILEGE *old = perm_getdefprivi (); if (ret == EPERM){ /* #Specification: user accounts / privileges / chown home We drop privilege since only root is allowed to change ownership of a home directory USERS::editone() had set a default privilege to support co-managers. */ perm_setdefprivi(NULL); priv = NULL; } if (perm_access(priv ,MSG_U(P_SETUSERDIR,"set user %s directory\n") ,name.get())){ const char *dir = wrkdir.get(); if (ret == ENOENT){ /* #Specification: user creation / home directory The HOME directory of a new user is created with permission defined in the policies (default 0700) or defined for the group. The content of the directory /etc/skel is copied in the directory, respecting the permission setting of /etc/skel. All file copied will belong to the new user. */ int perm = group_getcreateperm(group); ret = file_mkdir (dir,uid,gid,perm,NULL); if (ret == 0){ const char *skelpath = ETC_SKEL; char tmp[PATH_MAX]; const char *root = users.getroot(); if (strcmp(root,"/")!=0){ snprintf (tmp,sizeof(tmp)-1,"%s%s",root ,ETC_SKEL); skelpath = tmp; } if (file_exist(skelpath)){ ret = file_copytree (skelpath,dir,uid,gid,-1); } chmod (dir,perm); } } if (ret == EPERM){ if (do_recur){ SSTRING tmp; tmp.setfromf ("-R %d.%d %s",uid,gid,dir); ret = netconf_system_if ("chown",tmp.get()); }else{ if (chown (dir,uid,gid) != -1){ ret = 0; } } } if (ret != 0){ xconf_error ( MSG_U(E_SETUPDIR ,"Can't setup user %s's home directory %s\n" "reason: %s\n") ,name.get(),wrkdir.get() ,strerror (errno)); } } perm_setdefprivi(old); } } return ret; } static int user_str2gid( GROUPS &groups, SSTRING &group) { /* #Specification: user record / gid / format GID may be entered either as a string (a group name) or as a number. */ const char *str = group.get(); int gid = groups.getgid(str); if (gid == -1 && isdigit(str[0])) gid = atoi(str); return gid; } static int check_datedays(const char *str, int &day) { int ret = 0; day = -1; if (str[0] != '\0'){ if (strlen (str)!=10){ ret = -1; }else{ int nbdig = 0; for (int i=0; i<10; i++){ if (isdigit(str[i])) nbdig++; } if (nbdig != 8 && str[4] != '/' && str[7] != '/'){ ret = -1; }else{ struct tm t; t.tm_year = atoi(str)-1900; t.tm_mon = atoi(str+5)-1; t.tm_mday = atoi(str+8); t.tm_hour = 1; t.tm_min = 0; t.tm_sec = 0; t.tm_isdst = -1; time_t numtim = mktime(&t); extern long int timezone; numtim -= timezone; day = numtim / (24*60*60); } } } return ret; } /* Set the home of the user to the default */ PUBLIC void USER::setdefhome( USERS &users, const char *group) { const char *basehome = users.getstdhome(group); bool createhome = group_homeneeded(group); if (createhome){ char buf[PATH_MAX]; sprintf (buf,"%s/%s",basehome,name.get()); wrkdir.setfrom (buf); }else{ wrkdir.setfrom (basehome); } } /* Get confirmation about the delete operation */ PRIVATE bool USER::deldialog(USER_DELOPER &deloper) { /* #Specification: user accounts / delete / removing home When deleting a user accounts, the admin is allowed to archive, delete or keep the user home directory. The scripts used to delete or archive the home directory will only perform there task if the user do own the directory. So if a bunch of users are mapped to a common home (pop users generally), then the directory won't be deleted. */ DIALOG dia; dia.settype (DIATYPE_POPUP); char sel = 0; dia.newf_radio ("",sel,0,MSG_U(I_ARCHACCTDATA ,"Archive the account's data")); dia.newf_radio ("",sel,1,MSG_U(I_DELACCTDATA ,"Delete the account's data")); dia.newf_radio ("",sel,2,MSG_U(I_LEAVEDATA,"Leave the account's data in place")); char title[100]; snprintf (title,sizeof(title)-1 ,MSG_U(T_DELACCT,"Deleting account %s"),getname()); int nof = 0; bool ret = false; if (dia.edit (title ,MSG_U(I_DELACCT,"You are deleting an account.\n" "The home directory and the mail inbox folder\n" "may be archived, deleted or left in place") ,help_delacc ,nof)==MENU_ACCEPT){ static USER_DELOPER tboper[]={ DELOPER_ARCHIVE,DELOPER_DELETE,DELOPER_KEEP }; deloper = tboper[sel]; ret = true; } return ret; } static int user_validate ( USERACCT_COMNGS &comngs, DIALOG &dia, int &field, const char *name, const char *group) { int ret = 0; /* #Specification: user account / login id / lexical validation The following character are invalid in a login id # space, : ( ) [ ] ' " | & ; ` * # and any ASCII character below 32 (space) */ const char *pt = name; while (*pt != '\0'){ if (*pt < ' ') ret = -1; pt++; } const char *ivld = " :()[]\"'|&;,`*@"; while (*ivld != '\0'){ if (strchr(name,*ivld)!=NULL){ ret = -1; } ivld++; } if (ret == -1){ xconf_error (MSG_U(E_IVLDLOGINCHAR, "Invalid character in login name.\n" "The following a illegal:\n" " space , : ( ) [ ] ' \" | & ; @")); field = 2; }else{ comngs.set_str ("name",name); comngs.set_str ("group",group); ret = comngs.validate(dia,field); } return ret; } static void user_fixhomedia ( SSTRING &status, bool &do_sethome, bool &do_nothing, bool &chown_recur) { DIALOG dia; dia.settype (DIATYPE_POPUP); char sel = 1; dia.newf_radio ("",sel,0,MSG_U(F_DONOTHING,"Do nothing")); dia.newf_radio ("",sel,1,MSG_U(F_CHOWNDIR,"Change ownership of the directory")); dia.newf_radio ("",sel,2,MSG_U(F_CHOWNRECUR,"Change ownership recursively")); do_sethome = false; chown_recur = false; do_nothing = false; int nof = 0; if (dia.edit (MSG_U(T_FIXHOME,"Fixing home directory") ,status.get(),help_nil,nof)==MENU_ACCEPT){ do_sethome = true; if (sel == 0){ do_nothing = true; }else if (sel == 2){ chown_recur = true; } } } /* Merge all the line to input the supplemental groups into a single string */ static void user_setaltgr ( SSTRINGS &tb, SSTRING &altgr) { altgr.setempty (); for (int i=0; iget()); } } /* Edit the specification of a user. Return -1 if the user escape without accepting the changes. Return 0 if the user accepted the change Return 1 if the user wish to delete this record. */ PUBLIC int USER::edit( USERS &users, GROUPS &groups, bool is_new, PRIVILEGE *priv, // Privilege required to manage those accounts // or NULL if only root can do this unsigned may_edit, // Control which field may be edited (USRACCT_EDIT____ // it is a bitmap USERACCT_COMNGS &comngs, USER_DELOPER &deloper, // Operation to do on delete const char *domain) { bool is_root = perm_getuid()==0; bool editprivi = (may_edit & USRACCT_EDITPRIV) != 0; bool editgroup = (may_edit & USRACCT_EDITGROUP) != 0; bool editshell = (may_edit & USRACCT_EDITSHELL) != 0; bool is_main_domain = strcmp(domain,"/")==0; int categ = getcateg(); bool editsupgrp = (may_edit & USRACCT_EDITSUPGRP) != 0 && categ == TUSER_STD; deloper = DELOPER_NONE; bool privgroup = policies_privgroup(); DIALOG dia; dia.newf_title (MSG_U(T_BASE,"Base info"),1,"",MSG_R(T_BASE)); SHADOW *shadow = NULL; bool add_shadow = false; if (users.has_shadow()){ shadow = users.getshadow(this); if (shadow == NULL){ shadow = new SHADOW; shadow->passwd.setfrom (passwd); passwd.setfrom ("x"); add_shadow = true; } } enabled = !is_locked(shadow); was_enabled = enabled; dia.newf_chk ("",enabled,MSG_U(I_ENABLED,"The account is enabled")); dia.newf_str (MSG_U(F_LOGIN,"Login name"),name); dia.newf_str (MSG_U(F_FULLNAME,"Full name"),comment); SSTRING group; //group.setfrom (gid); if (gid == -1){ if (!privgroup) group.setfrom (groups.getdefault()); shell.setfrom (shells_getdefault()); }else{ GROUP *grp = groups.getfromgid(gid); if (grp != NULL){ group.setfrom (grp->getname()); } } FIELD_COMBO *grpl = dia.newf_combo ( group.is_empty() ? MSG_U(F_GROUPOPT,"group (opt)") : MSG_U(F_GROUP,"group") ,group); groups.setcombo(grpl); int supgroup_field = dia.getnb(); if (!is_new) groups.getalt (name.get(),altgr); // Supplemental groups may be spread in several lines SSTRINGS tbaltgr; if (editsupgrp){ SSTRING *line = new SSTRING; tbaltgr.add (line); SSTRINGS words; str_splitline (altgr.get(),' ',words); words.sort(); for (int i=0; iget(); if (line->getlen()> 40){ line = new SSTRING; tbaltgr.add (line); } line->appendf ("%s ",w); } for (int i=0; istrip_end(); dia.newf_str (i==0 ? MSG_U(F_SUPGROUP ,"Supplementary groups") : "" ,*pt); } } if (is_root || policies_mayedithome()){ dia.newf_str (MSG_U(F_HOME,"Home directory(opt)"),wrkdir); } if (categ != TUSER_POP && editshell && (is_root || policies_mayeditshell())){ /* #Specification: user account / shell selection / list For different reasons, the list of shell available when managing an account is limited. The admin has to choose one from the list. This list is managed by linuxconf anyway. Here are those reasons -Selecting a shell for normal user which is not defined in /etc/shells will cause different problem later, such as prohibiting ftp access for that user. -Special accounts like PPP and POP often have a co-administrator. This one must be limited somewhat as he would be able to select a special program when creating a POP acount and this account would easily grant him root access. So the story is "Specify with linuxconf which shells are available for PPP, SLIP and normal USER and by happy later". */ /* #Specification: user account / shell selection / POP account POP account have /bin/false as a shell and it is not configurable. For this reason, the field "command interpreter" is not even shown for those type of users. */ const SSTRINGS *tbshells=shells_getuserlist(); { if (categ == TUSER_PPP){ tbshells = shells_getppplist(); }else if (categ == TUSER_SLIP){ tbshells = shells_getsliplist(); }else if (categ == TUSER_UUCP){ tbshells = shells_getuucplist(); } } FIELD_COMBO *shell_l; if (categ == TUSER_STD){ shell_l = dia.newf_combo (MSG_U(F_SHELL ,"Command interpreter(opt)"),shell); }else{ shell_l = dia.newf_list (MSG_R(F_SHELL),shell); } for (int s=0; sgetnb(); s++){ shell_l->addopt (tbshells->getitem(s)->get()); } } SSTRING struid; bool override = users.has_add_override(); if (is_new && !override) uid = users.getnewuid(); if (uid != -1) struid.setfrom (uid); int field_uid = dia.getnb(); if (is_root || policies_mayedithome()){ dia.newf_str (MSG_U(F_UID,"User ID(opt)"),struid); } if (categ==TUSER_ADMIN || categ == TUSER_POP || categ == TUSER_UUCP || categ == TUSER_PPP || !editgroup){ //fhome->set_readonly(); grpl->set_readonly(); //fuid->set_readonly(); } SSTRING disable_str; int disable_field = 0; if (users.has_shadow() && (is_root || policies_mayshowshadow())){ dia.newf_title (MSG_U(T_PARAMS,"Params"),1,"" ,MSG_U(T_PASSMNG,"Password management")); if (shadow->last > 0){ time_t tim = shadow->last*24*60*60+1; char buf[100]; strftime (buf,sizeof(buf)-1,"%Y/%m/%d",gmtime(&tim)); dia.newf_info (MSG_U(F_WASCHG,"Last password change"),buf); } static const char *tb[]={MSG_U(I_IGNORED,"Ignored"),NULL}; static const int tbv[]={-1,0}; dia.newf_chkm_num (MSG_U(F_PASSMAY,"Must keep # days"),shadow->may ,tbv,tb); dia.newf_num (MSG_U(F_PASSMUST,"Must change after # days"),shadow->must); dia.newf_chkm_num (MSG_U(F_PASSWARN,"Warn # days before expiration") ,shadow->warn,tbv,tb); dia.newf_chkm_num (MSG_U(F_PASSEXPIRE,"Account expire after # days") ,shadow->expire,tbv,tb); if (shadow->disable > 0){ time_t tim = shadow->disable*24*60*60+1; char buf[100]; strftime (buf,sizeof(buf)-1,"%Y/%m/%d",gmtime(&tim)); disable_str.setfrom (buf); } disable_field = dia.getnb(); } if (users.has_shadow() && policies_mayshowexpire()){ dia.newf_str (MSG_U(F_WASISDIS,"Expiration date (yyyy/mm/dd)"),disable_str); } comngs.setupdia (dia); PRIVILEGE_DATAS privs; if (editprivi){ dia.newf_title (MSG_U(T_PRIVILEGES,"Privileges"),1 ,"",MSG_R(T_PRIVILEGES)); privilege_setdialog (dia,name.get(),privs); } int field = 0; int ret = -1; int butopt = MENUBUT_ACCEPT|MENUBUT_CANCEL|MENUBUT_DEL; if (!is_new){ dia.setbutinfo (MENU_USR1,MSG_U(B_PASSWD,"Passwd") ,MSG_U(X_PASSWD,"Passwd")); butopt |= MENUBUT_USR1; } if ((is_root || policies_mayedittasks()) && is_main_domain){ dia.setbutinfo (MENU_USR2,MSG_U(B_TASKS,"Tasks") ,MSG_U(X_TASKS,"Tasks")); butopt |= MENUBUT_USR2; } while (1){ MENU_STATUS code = dia.edit ( is_new ? MSG_U(T_NEWUSER,"User account creation") : MSG_U(T_USERINFO,"User information") ,MSG_U(I_USERINTRO ,"You must specify at least the login name\n" "and the full name") ,help_user ,field ,butopt); if (code == MENU_CANCEL || code == MENU_ESCAPE){ break; }else if (!perm_access(priv ,MSG_U(P_USERDATA ,"to maintain the user database"))){ dia.restore(); if (editsupgrp){ user_setaltgr (tbaltgr,altgr); } }else if (code == MENU_DEL){ if (is_main_domain && is_logged()){ xconf_error (MSG_U(E_ISLOGGED ,"User is currently logged.\n" "The account can't be deleted.")); }else if (deldialog(deloper)){ ret = 1; break; } }else if (code == MENU_MESSAGE){ comngs.message(dia); }else if (code == MENU_USR1){ if (editpass(shadow,true,users.may_override(),domain) != -1){ ret = 0; break; } }else if (code == MENU_USR2){ cron_edit(name.get()); }else if (user_validate (comngs,dia,field,name.get(),group.get())!=-1){ /* #Specification: userconf / user account / semicolon A check is made to ensure that the user has not entered a : in any field during edition. */ if (editsupgrp){ user_setaltgr (tbaltgr,altgr); SSTRINGS words; str_splitline (altgr.get(),' ',words); if (words.getnb() > 32){ xconf_notice (MSG_U(N_MAXALTGR ,"You have entered %d supplemental groups\n" "The Linux kernel only support 32 by default") ,words.getnb()); } } if (comment.strchr(':') != NULL || group.strchr(':') != NULL || struid.strchr(':') != NULL || shell.strchr (':') != NULL || wrkdir.strchr (':') != NULL){ xconf_error (MSG_U(E_NO2PT ,"No : in any field allowed")); }else if (shadow != NULL && check_datedays(disable_str.get(),shadow->disable)==-1){ xconf_error (MSG_U(E_IVLDDATE,"Invalid date")); field = disable_field; }else if (group.is_empty() && !privgroup){ xconf_error (MSG_U(E_NOGROUP,"You must specify a group")); }else if (groups.setalt(name.get(),altgr.get(),' ',true)==-1){ xconf_error (MSG_U(E_IVLDSUPGROUP,"Invalid supplementary group list")); field = supgroup_field; }else if (!struid.is_empty() && (struid.getval()<0 || struid.getval()>65535)){ xconf_error (MSG_U(E_UIDRANGE ,"User ID must be between 0 and 65535")); field = field_uid; }else{ if (group.is_empty() && !override){ group.setfrom (name); } if (!group.is_empty()){ gid = user_str2gid(groups,group); if (gid == -1){ char buf[1000]; snprintf (buf,sizeof(buf),MSG_U(I_CREATEGROUP ,"Group %s does not exist\n" "Do you want to create it?") ,group.get()); if (name.cmp(group)==0 && privgroup){ //create_group = true; }else if (dialog_yesno (MSG_U(T_CREATGROUP ,"Create group") ,buf,help_nil)!= MENU_YES){ continue; } } } if (!struid.is_empty()){ uid = struid.getval(); }else if (override){ uid = -1; }else{ uid = users.getnewuid(); } if (wrkdir.is_empty()) setdefhome(users,group.get()); if (editprivi && privilege_validate(privs,field)==-1){ }else if (check(users,groups)==0){ /* #Specification: user edit / bad html dialog This is a good exemple of a bad dialog (or at least complex dialog) for html mode All side effect of the dialog must be done at the exit. The do_sethome kludge is there just for that. */ bool do_sethome = false; bool chown_recur = false; bool do_nothing = false; SSTRING status; int code = checkhome (&status,group.get()); if (code == ENOTDIR){ xconf_error ("%s",status.get()); }else if (code != 0){ if (code == ENOENT){ status.append(MSG_U(Q_CREATE ,"\nDo you want to create it ?")); if (is_new || dialog_yesno( MSG_U(Q_USERHOME,"User home directory") ,status.get(),help_nil)==MENU_YES){ do_sethome = true; } }else if (code == EPERM){ priv = NULL; // Only root can do that status.append( MSG_U(Q_FIXIT,"\nDo you want to fix it ?")); user_fixhomedia (status,do_sethome,do_nothing,chown_recur); } } if (checkhome(NULL,group.get())==0 || do_sethome){ ret = 0; if (ret == 0){ /* #Specification: user creation / group creation When creating a new group on the fly for a user account, we try to allocate a GID equal to the UID. If the GID is already used, the first free one is used. */ if (categ == TUSER_STD){ groups.setalt(name.get(),altgr.get(),' ',false); } if (gid == -1 && !group.is_empty()){ // && !override){ gid = groups.create (group.get(),uid); } if ((!override || !is_new) && !do_nothing && do_sethome){ sethome(priv,group.get(),chown_recur,users); } setmodified(); if (shadow != NULL){ shadow->name.setfrom (name); if (add_shadow) users.addshadow (shadow); } if (enabled != was_enabled){ update_passwd (NULL,shadow,!enabled,domain); } } break; } } } } } if (ret != 0){ dia.restore(); gid = user_str2gid(groups,group); uid = struid.getval(); }else{ if (editprivi) privilege_save (name.get(),privs,priv); } return ret; } /* Check if a password is weak */ int pass_isweak(const char *pass) { /* #Specification: userconf / net password / rejected New password are validated with some rules to ensure they are difficult enough. Here are the rules. # -6 chars minimum -Must have at least one non-letter character # */ int ret = 1; if (pass[0] == '\0' || pass[0] == '*'){ /* #Specification: userconf / user password / empty and * Only root is allowed to set an empty password or one with only * in it. It will be refused (error message) for all other users. */ if (getuid()==0){ ret = 0; }else{ xconf_error ( MSG_U(E_NULLPASS,"Empty password not allowed.\n" "Only root is allowed to do so.\n" "This is not a good idea though!")); } }else{ PASSWD_VALID vl; /* #Specification: userconf / password / checking userconf check the minimum length and the amount of non alpha character in a new password. If the new password does not fullfill the local policies, it is rejected. */ if ((int)strlen(pass)>=vl.minlen){ int nbalpha = 0; while (*pass != '\0'){ if (!isalpha(*pass)) nbalpha++; pass++; } if (nbalpha >= vl.minnonalpha) ret = 0; } if (ret){ xconf_error (MSG_U(E_WEAKPASS ,"Password not accepted\n" "Select a more complicated one\n" "The local policies are\n" "\n" "Minimum length : %d\n" "Minimum number of non-alpha character : %d\n") ,vl.minlen,vl.minnonalpha); } } return ret; } #if 0 static char num_2_64(int num) { if (num < 26){ return (char)(num + 'a'); }else if (num < 52){ return (char)(num - 26 + 'A'); }else if (num < 62){ return (char)(num - 52 + '0'); }else if (num == 62){ return '.'; } return '/'; } #endif /* * i64c - convert an integer to a radix 64 character */ static int i64c(int i) { if (i < 0) return ('.'); else if (i > 63) return ('z'); if (i == 0) return ('.'); if (i == 1) return ('/'); if (i >= 2 && i <= 11) return ('0' - 2 + i); if (i >= 12 && i <= 37) return ('A' - 12 + i); if (i >= 38 && i <= 63) return ('a' - 38 + i); return ('\0'); } static const char *tbargs[]={"user","newpassword","islocked","domain",NULL}; static MESSAGE_DEF chgpasswd ("chgpasswd",tbargs); /* Hash a password using MD5. Return the hashed password. The application must take a copy */ void user_crypt (const char *password, SSTRING &hash) { FILE *fin = fopen ("/dev/urandom","r"); char salt[3+8+1]; strcpy (salt,"$1$"); // Prefix for MD5 password if (fin == NULL){ // Odd, use time as the salt snprintf (salt+3,8,"%08ld",time(NULL) % 100000000); salt[11] = '\0'; }else{ unsigned char tmp[8]; fread(tmp,1,sizeof(tmp),fin); char *cp = salt+3; for (int i=0; i<8; i++) *cp++ = i64c(tmp[i] & 077); *cp = '\0'; } hash.setfrom (crypt(password,salt)); } /* Update the passwd field with a new password and manage SHADOW */ PUBLIC void USER::update_passwd ( const char *newp, // Maybe NULL if we only want to lock the account SHADOW *shadow, bool is_lock, const char *domain) // In which domain are we working { SSTRING *pwd = &passwd; time_t tim = time(NULL); if (shadow != NULL){ int days = tim/(24*60*60); if (is_lock){ int yesterday = days - 1; if (shadow->disable <= 0 || shadow->disable > yesterday){ shadow->disable = yesterday; } }else{ pwd = &shadow->passwd; shadow->last = days; if (shadow->disable > 0 && shadow->disable < days){ shadow->disable = -1; } passwd.setfrom ("x"); } }else if (is_lock){ /* #Specification: user account / passwd / non shadow locking When locking a non-shadow user account, linuxconf insert a '*' in front of the password, making it useless. When the account is unlock, linuxconf remove the '*' and the old password is effective again. This trickery may not work for all module (SMB password may be lost). */ const char *curpass = passwd.get(); if (curpass[0] != '*'){ char tmppass[100]; sprintf (tmppass,"*%s",curpass); passwd.setfrom (tmppass); } newp = NULL; } if (newp != NULL){ if (newp[0] != '\0' && strcmp(newp,"*")!=0){ user_crypt (newp,*pwd); }else{ pwd->setfrom (newp); } }else if (!is_lock && shadow == NULL){ const char *curpass = passwd.get(); if (curpass[0] == '*'){ if (curpass[1] != '\0'){ char tmppass[100]; strcpy (tmppass,curpass+1); passwd.setfrom (tmppass); }else{ xconf_error (MSG_U(E_UNLOCKPASS ,"Unlock not done. This would yield an account\n" "without password")); } } } { /* #Specification: updating password / module messages The module message API is used when a user password is changed. The message "chgpasswd" is sent with the following arguments: # user id new password (clear text) locked account (1 for lock, 0 for unlock) domain (/ for the main, a domain name for virtual email domain) # */ const char *tb[]={ getname(),newp,(is_lock ? "1" : "0"),domain }; module_sendmessage (chgpasswd,4,tb); } } /* Return true if the user account is locked (or has expired) */ PUBLIC bool USER::is_locked(SHADOW *shadow) { bool is_lock = passwd.get()[0] == '*'; if (shadow){ int today = time(NULL)/(24*60*60); is_lock = (shadow->disable > 0 && shadow->disable < today) || (shadow->expire > 0 && shadow->expire + shadow->last >= today); } return is_lock; } /* Return 0 if the user is allowed to change his password. Return -1 if the password is locked (only the admin is allowed to change it). Return the number of days before the user will be allowed to change his password. */ PUBLIC int USER::passwd_locked(SHADOW *shadow) { int ret = 0; if (shadow && shadow->may > 0){ /* #Specification: password policies / user may change A user may change his password if -The minimum time since the last change has elapsed. -The minimum time is lower than the maximum password duration */ int today = time(NULL)/(24*60*60); if (shadow->must != -1 && shadow->may > shadow->must){ ret = -1; }else{ ret = (shadow->last + shadow->may) - today; if (ret < 0) ret = 0; } } return ret; } /* Present a list of generated password to the user. Return -1 if the user cancel this dialog or some error happened. Return 0 if the user selected one generated password (suggested is filled) Return 1 if the user wish to enter a password manually. */ int user_genselpassword ( const char *command, bool force, // The user must select a generated password. // He is not allowed to enter one manually // (No manual button in fact) SSTRING &suggested) // Will contain the suggested password // if the user has selected one { int ret = 1; if (command != NULL && dialog_mode != DIALOG_HTML){ // This dialog does not work in HTML mode because new // password are generated each time we enter the dialog. // We need a way to preserve session information. while (1){ SSTRINGS tbsug,lines; POPEN pop (command,10); if(pop.isok()){ SSTRING cut ("genpass"); while (pop.wait(10)>0){ char line[100]; while (pop.readout(line,sizeof(line)-1)!=-1){ SSTRING word; word.copyword(line); strip_end(line); lines.add (new SSTRING(line)); tbsug.add (new SSTRING(word)); } } } int nbsug = tbsug.getnb(); if (nbsug == 0){ break; }else{ DIALOG dia; dia.settype (DIATYPE_POPUP); char sel = 0; if (nbsug == 0){ dia.newf_info (MSG_U(F_GENPASS,"Proposed password"),lines.getitem(0)->get()); }else{ for (int i=0; iget()); } } int butmask = MENUBUT_CANCEL|MENUBUT_ACCEPT|MENUBUT_USR2; if (!force){ dia.setbutinfo (MENU_USR1,MSG_U(B_MANUAL,"Manual") ,MSG_R(B_MANUAL)); butmask |= MENUBUT_USR1; } dia.setbutinfo (MENU_USR2,MSG_U(B_RETRY,"Retry") ,MSG_R(B_RETRY)); int nof = 0; MENU_STATUS code = dia.edit (MSG_R(T_AUTOPASS) ,MSG_U(I_AUTOPASS ,"One or more passwords were auto-generated\n" "Pick one, or hit the manual button to enter another one.\n" "Hit the retry to generate new ones.") ,help_nil,nof ,butmask); if (code == MENU_CANCEL || code == MENU_ESCAPE){ ret = -1; break; }else if (code == MENU_ACCEPT){ suggested.setfrom (tbsug.getitem(sel)->get()); ret = 0; break; }else if (code == MENU_USR1){ ret = 1; break; }else if (code == MENU_USR2){ // Do nothing, loop } } } } return ret; } /* Edit(set) the password of a user. Return -1 if the user escape without accepting the changes. If the user enter the password correctly and accept it, the object USER is updated with the crypted version. */ PUBLIC int USER::editpass( SHADOW *shadow, bool confirm, bool may_override, // May use the override function for the // password handling (for main domain generally) const char *domain) { SSTRING suggested; HELP_CONTEXT hp (MSG_U(T_AUTOPASS ,"Automatically Generated Password(s)"),"userconf","autopass"); int ret = user_genselpassword(policies_getautopasscmd(),false,suggested); if (ret != -1){ if (may_override && perm_fct_change != NULL){ ret = (*perm_fct_change)(getname(),true,suggested.get(),NULL); if (ret == -1){ xconf_error (MSG_U(E_PASSCHG,"Password was not changed")); } }else if(suggested.is_filled()){ update_passwd (suggested.get(),shadow,false,domain); setmodified(); ret = 0; }else{ int nof = 0; while (1){ DIALOG dia; dia.settype (DIATYPE_POPUP); SSTRING buf1; dia.newf_pass (MSG_U(F_PASSWORD,"Password"),buf1); SSTRING buf2; dia.newf_pass (MSG_U(F_CONFIRM,"Confirmation"),buf2); char title[80]; sprintf (title,MSG_U(T_PASSWORD,"%s's password"),name.get()); if (dia.edit (title ,MSG_U(I_PASSINTRO ,"You must enter the new password twice\n" "To make sure you have enter it\n" "correctly.\n") ,help_password,nof) != MENU_ACCEPT){ break; }else if (buf1.cmp(buf2)!=0){ xconf_error (MSG_U(E_MISMATCH ,"The two new passwords differ\n" "Please try again\n")); nof = 1; }else if (!pass_isweak(buf1.get())){ if (confirm){ xconf_notice (MSG_U(N_ACCEPT,"New password for user %s accepted") ,name.get()); } update_passwd (buf1.get(),shadow,false,domain); setmodified(); ret = 0; break; }else{ nof = 1; } } } } return ret; } /* Return != 0 if the password may be changed by the user. Produce several error message if it can't. */ PUBLIC int USER::pass_maychange(SHADOW *shadow) { int ret = 0; if (is_locked(shadow)){ xconf_error (MSG_U(E_ACCTLOCKED ,"This account is locked\n" "you are not allowed to change the password")); }else{ int before = passwd_locked(shadow); if (before == -1){ xconf_error (MSG_U(E_PASSLOCKED ,"You are not allowed to change your password\n" "Only the administrator is allowed to do it.")); }else if (before > 0){ xconf_error (MSG_U(E_PASSWAIT ,"You must wait %d day(s) before changing your password again.")); }else{ ret = 1; } } return ret; } /* Edit the password of the current user. Ask for his current password to allow him to continue. */ PUBLIC int USER::edithispass( SHADOW *shadow, bool may_override, const char *domain) { int ret = -1; if (may_override && perm_fct_change != NULL){ ret = (*perm_fct_change)(getname(),false,"",NULL); }else{ /* #Specification: userconf / passwd clone userconf can be a clone of the /bin/passwd program, If the proper symlink is done. When a user attempt to change his own password, the program prompt for the current one. */ if (pass_maychange(shadow)){ char buf1[MAX_LEN+1]; buf1[0] = '\0'; bool is_root = name.cmp("root")==0; const char *title = is_root ? MSG_U(T_CHGROOTPASS,"Changing super user password") : MSG_U(T_CHGYOURPASS,"Changing your password"); const char *intro = is_root ? MSG_U(I_ENTERROOTPASS,"Please enter root's password") : MSG_U(I_ENTERYOURPASS,"Please enter your current password"); if (dialog_inputpass (title,intro ,help_password ,buf1)==MENU_ACCEPT){ const char *pw = passwd.get(); if (shadow != NULL) pw = shadow->passwd.get(); if (strcmp(crypt(buf1,pw),pw)==0){ ret = editpass(shadow,true,may_override,domain); }else{ xconf_error (MSG_R(E_IVLDPASS)); } } } } return ret; } /* non tty oriented passwd changing facility copy the functionnality of the old passwd program. */ PUBLIC int USER::edithispass_notty( SHADOW *shadow, const char *domain) { int ret = -1; if (pass_maychange(shadow)){ printf (MSG_U(W_CHGPASS,"Changing password for %s\n"),getname()); printf (MSG_U(Q_ENTEROLDPASS,"Enter old password:")); fflush (stdout); char old[100]; if (fgets (old,sizeof(old)-1,stdin) != NULL){ printf (MSG_U(Q_ENTERNEWPASS,"Enter new password:")); fflush (stdout); char newp[100]; if (fgets (newp,sizeof(newp)-1,stdin) != NULL){ printf (MSG_U(Q_RETYPE,"Re-type new password:")); fflush (stdout); char newp2[100]; if (fgets (newp2,sizeof(newp2)-1,stdin) != NULL){ const char *pw = passwd.get(); if (shadow != NULL) pw = shadow->passwd.get(); if (strcmp(crypt(old,pw),pw)!=0){ }else if (strcmp(newp,newp2)!=0){ }else if (pass_isweak(newp)){ }else{ update_passwd (newp,shadow,false,domain); ret = 0; } } } } } return ret; } #ifdef TEST int main (int argc, char *argv[]) { dialog_clear(); USERS users; GROUPS groups; if (argc == 1){ USER *user = new USER; if (user->edit(users,groups,1)==0){ users.add (user); users.write(); }else{ delete user; } }else{ USER *user = users.getitem(argv[1]); if (user != NULL && user.edit(users,groups,0)==0){ users.write(); } } return 0; } #endif