Add support for adding a utmp entry when allocating a new pty.
Requires the BSD login(3) or SYSV/POSIX getutent()/getutxent(). Currently only creates a new entry if the existing tty has a utmp entry.
This commit is contained in:
@@ -313,6 +313,9 @@
|
|||||||
/* Define to 1 if you have the `lockf' function. */
|
/* Define to 1 if you have the `lockf' function. */
|
||||||
#undef HAVE_LOCKF
|
#undef HAVE_LOCKF
|
||||||
|
|
||||||
|
/* Define to 1 if you have the `login' function. */
|
||||||
|
#undef HAVE_LOGIN
|
||||||
|
|
||||||
/* Define to 1 if you have the <login_cap.h> header file. */
|
/* Define to 1 if you have the <login_cap.h> header file. */
|
||||||
#undef HAVE_LOGIN_CAP_H
|
#undef HAVE_LOGIN_CAP_H
|
||||||
|
|
||||||
@@ -552,6 +555,9 @@
|
|||||||
/* Define to 1 if you have the <utime.h> header file. */
|
/* Define to 1 if you have the <utime.h> header file. */
|
||||||
#undef HAVE_UTIME_H
|
#undef HAVE_UTIME_H
|
||||||
|
|
||||||
|
/* Define to 1 if you have the <utmp.h> header file. */
|
||||||
|
#undef HAVE_UTMP_H
|
||||||
|
|
||||||
/* Define to 1 if you have the `vasprintf' function. */
|
/* Define to 1 if you have the `vasprintf' function. */
|
||||||
#undef HAVE_VASPRINTF
|
#undef HAVE_VASPRINTF
|
||||||
|
|
||||||
|
100
configure
vendored
100
configure
vendored
@@ -15039,7 +15039,7 @@ LIBS=$ac_save_LIBS
|
|||||||
|
|
||||||
for ac_func in strrchr sysconf tzset strftime initgroups getgroups fstat \
|
for ac_func in strrchr sysconf tzset strftime initgroups getgroups fstat \
|
||||||
regcomp setlocale nl_langinfo getaddrinfo mbr_check_membership \
|
regcomp setlocale nl_langinfo getaddrinfo mbr_check_membership \
|
||||||
setrlimit64
|
setrlimit64 sysctl
|
||||||
do :
|
do :
|
||||||
as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
|
as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
|
||||||
ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
|
ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
|
||||||
@@ -15083,7 +15083,7 @@ done
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
for ac_func in sysctl getutxid getutid
|
for ac_func in getutxid getutid
|
||||||
do :
|
do :
|
||||||
as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
|
as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
|
||||||
ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
|
ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
|
||||||
@@ -15092,10 +15092,104 @@ eval as_val=\$$as_ac_var
|
|||||||
cat >>confdefs.h <<_ACEOF
|
cat >>confdefs.h <<_ACEOF
|
||||||
#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
|
#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
|
||||||
_ACEOF
|
_ACEOF
|
||||||
break
|
utmp=POSIX; break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if test "${utmp-NONE}" = "NONE"; then
|
||||||
|
for ac_func in login
|
||||||
|
do :
|
||||||
|
ac_fn_c_check_func "$LINENO" "login" "ac_cv_func_login"
|
||||||
|
if test "x$ac_cv_func_login" = x""yes; then :
|
||||||
|
cat >>confdefs.h <<_ACEOF
|
||||||
|
#define HAVE_LOGIN 1
|
||||||
|
_ACEOF
|
||||||
|
|
||||||
|
UTMP=BSD
|
||||||
|
for ac_header in util.h utmp.h
|
||||||
|
do :
|
||||||
|
as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh`
|
||||||
|
ac_fn_c_check_header_mongrel "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default"
|
||||||
|
eval as_val=\$$as_ac_Header
|
||||||
|
if test "x$as_val" = x""yes; then :
|
||||||
|
cat >>confdefs.h <<_ACEOF
|
||||||
|
#define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1
|
||||||
|
_ACEOF
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for login in -lutil" >&5
|
||||||
|
$as_echo_n "checking for login in -lutil... " >&6; }
|
||||||
|
if test "${ac_cv_lib_util_login+set}" = set; then :
|
||||||
|
$as_echo_n "(cached) " >&6
|
||||||
|
else
|
||||||
|
ac_check_lib_save_LIBS=$LIBS
|
||||||
|
LIBS="-lutil $LIBS"
|
||||||
|
cat confdefs.h - <<_ACEOF >conftest.$ac_ext
|
||||||
|
/* end confdefs.h. */
|
||||||
|
|
||||||
|
/* Override any GCC internal prototype to avoid an error.
|
||||||
|
Use char because int might match the return type of a GCC
|
||||||
|
builtin and then its argument prototype would still apply. */
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
#endif
|
||||||
|
char login ();
|
||||||
|
int
|
||||||
|
main ()
|
||||||
|
{
|
||||||
|
return login ();
|
||||||
|
;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
_ACEOF
|
||||||
|
if ac_fn_c_try_link "$LINENO"; then :
|
||||||
|
ac_cv_lib_util_login=yes
|
||||||
|
else
|
||||||
|
ac_cv_lib_util_login=no
|
||||||
|
fi
|
||||||
|
rm -f core conftest.err conftest.$ac_objext \
|
||||||
|
conftest$ac_exeext conftest.$ac_ext
|
||||||
|
LIBS=$ac_check_lib_save_LIBS
|
||||||
|
fi
|
||||||
|
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_util_login" >&5
|
||||||
|
$as_echo "$ac_cv_lib_util_login" >&6; }
|
||||||
|
if test "x$ac_cv_lib_util_login" = x""yes; then :
|
||||||
|
|
||||||
|
UTMP=BSD
|
||||||
|
for ac_header in util.h utmp.h
|
||||||
|
do :
|
||||||
|
as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh`
|
||||||
|
ac_fn_c_check_header_mongrel "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default"
|
||||||
|
eval as_val=\$$as_ac_Header
|
||||||
|
if test "x$as_val" = x""yes; then :
|
||||||
|
cat >>confdefs.h <<_ACEOF
|
||||||
|
#define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1
|
||||||
|
_ACEOF
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$SUDO_LIBS" in
|
||||||
|
*-lutil*) ;;
|
||||||
|
*) SUDO_LIBS="${SUDO_LIBS} -lutil";;
|
||||||
|
esac
|
||||||
|
$as_echo "#define HAVE_LOGIN 1" >>confdefs.h
|
||||||
|
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
for ac_func in openpty
|
for ac_func in openpty
|
||||||
do :
|
do :
|
||||||
|
20
configure.in
20
configure.in
@@ -1968,12 +1968,28 @@ dnl
|
|||||||
AC_FUNC_GETGROUPS
|
AC_FUNC_GETGROUPS
|
||||||
AC_CHECK_FUNCS(strrchr sysconf tzset strftime initgroups getgroups fstat \
|
AC_CHECK_FUNCS(strrchr sysconf tzset strftime initgroups getgroups fstat \
|
||||||
regcomp setlocale nl_langinfo getaddrinfo mbr_check_membership \
|
regcomp setlocale nl_langinfo getaddrinfo mbr_check_membership \
|
||||||
setrlimit64)
|
setrlimit64 sysctl)
|
||||||
AC_CHECK_FUNCS(getline, [], [
|
AC_CHECK_FUNCS(getline, [], [
|
||||||
AC_LIBOBJ(getline)
|
AC_LIBOBJ(getline)
|
||||||
AC_CHECK_FUNCS(fgetln)
|
AC_CHECK_FUNCS(fgetln)
|
||||||
])
|
])
|
||||||
AC_CHECK_FUNCS(sysctl getutxid getutid, [break])
|
AC_CHECK_FUNCS(getutxid getutid, [utmp=POSIX; break])
|
||||||
|
if test "${utmp-NONE}" = "NONE"; then
|
||||||
|
AC_CHECK_FUNCS(login, [
|
||||||
|
UTMP=BSD
|
||||||
|
AC_CHECK_HEADERS(util.h utmp.h, [break])
|
||||||
|
], [
|
||||||
|
AC_CHECK_LIB(util, login, [
|
||||||
|
UTMP=BSD
|
||||||
|
AC_CHECK_HEADERS(util.h utmp.h, [break])
|
||||||
|
case "$SUDO_LIBS" in
|
||||||
|
*-lutil*) ;;
|
||||||
|
*) SUDO_LIBS="${SUDO_LIBS} -lutil";;
|
||||||
|
esac
|
||||||
|
AC_DEFINE(HAVE_LOGIN)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
fi
|
||||||
|
|
||||||
AC_CHECK_FUNCS(openpty, [AC_CHECK_HEADERS(util.h pty.h, [break])], [
|
AC_CHECK_FUNCS(openpty, [AC_CHECK_HEADERS(util.h pty.h, [break])], [
|
||||||
AC_CHECK_LIB(util, openpty, [
|
AC_CHECK_LIB(util, openpty, [
|
||||||
|
@@ -234,7 +234,7 @@ sudo_execve(struct command_details *details, char *argv[], char *envp[],
|
|||||||
log_io = TRUE;
|
log_io = TRUE;
|
||||||
if (!ISSET(details->flags, CD_BACKGROUND)) {
|
if (!ISSET(details->flags, CD_BACKGROUND)) {
|
||||||
sudo_debug(8, "allocate pty for I/O logging");
|
sudo_debug(8, "allocate pty for I/O logging");
|
||||||
pty_setup(details->euid);
|
pty_setup(details->euid, user_details.tty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
180
src/exec_pty.c
180
src/exec_pty.c
@@ -52,6 +52,14 @@
|
|||||||
#if TIME_WITH_SYS_TIME
|
#if TIME_WITH_SYS_TIME
|
||||||
# include <time.h>
|
# include <time.h>
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(HAVE_GETUTXID)
|
||||||
|
# include <utmpx.h>
|
||||||
|
#elif defined(HAVE_GETUTID)
|
||||||
|
# include <utmp.h>
|
||||||
|
#elif defined(HAVE_UTIL_H)
|
||||||
|
# include <util.h>
|
||||||
|
# include <utmp.h>
|
||||||
|
#endif
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
@@ -109,6 +117,159 @@ static void sync_ttysize(int src, int dst);
|
|||||||
static void deliver_signal(pid_t pid, int signo);
|
static void deliver_signal(pid_t pid, int signo);
|
||||||
static int safe_close(int fd);
|
static int safe_close(int fd);
|
||||||
|
|
||||||
|
#if defined(HAVE_GETUTXID) || defined(HAVE_GETUTID)
|
||||||
|
/*
|
||||||
|
* Create ut_id from tty line and the id from the entry we are cloning.
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
utmp_setid(const char *line, const char *old_id, char *new_id, size_t idsize)
|
||||||
|
{
|
||||||
|
size_t idlen;
|
||||||
|
|
||||||
|
/* Skip over "tty" in the id if old entry did too. */
|
||||||
|
if (strncmp(line, "tty", 3) == 0 &&
|
||||||
|
strncmp(old_id, "tty", idsize < 3 ? idsize : 3) != 0)
|
||||||
|
line += 3;
|
||||||
|
|
||||||
|
/* Store as much as will fit, skipping parts of the beginning as needed. */
|
||||||
|
idlen = strlen(line);
|
||||||
|
if (idlen > idsize) {
|
||||||
|
line += (idlen - idsize);
|
||||||
|
idlen = idsize;
|
||||||
|
}
|
||||||
|
strncpy(new_id, line, idlen);
|
||||||
|
}
|
||||||
|
#endif /* HAVE_GETUTXID || HAVE_GETUTID */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Clone a utmp entry, updating the line, id, pid and time.
|
||||||
|
* XXX - if no existing entry, make a new one
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
utmp_doclone(const char *from_line, const char *to_line)
|
||||||
|
{
|
||||||
|
int rval = FALSE;
|
||||||
|
#ifdef HAVE_GETUTXID
|
||||||
|
struct utmpx *ut_old, ut_new;
|
||||||
|
|
||||||
|
memset(&ut_new, 0, sizeof(ut_new));
|
||||||
|
strncpy(ut_new.ut_line, from_line, sizeof(ut_new.ut_line));
|
||||||
|
setutxent();
|
||||||
|
if ((ut_old = getutxid(&ut_new)) != NULL) {
|
||||||
|
if (ut_old != &ut_new)
|
||||||
|
memcpy(&ut_new, ut_old, sizeof(ut_new));
|
||||||
|
strncpy(ut_new.ut_line, to_line, sizeof(ut_new.ut_line));
|
||||||
|
utmp_setid(to_line, ut_old->ut_id, ut_new.ut_id, sizeof(ut_new.ut_id));
|
||||||
|
ut_new.ut_pid = getpid();
|
||||||
|
gettimeofday(&ut_new.ut_tv, NULL);
|
||||||
|
ut_new.ut_type = USER_PROCESS;
|
||||||
|
|
||||||
|
if (pututxline(&ut_new) != NULL)
|
||||||
|
rval = TRUE;
|
||||||
|
}
|
||||||
|
endutxent();
|
||||||
|
#elif HAVE_GETUTID
|
||||||
|
struct utmp *ut_old, ut_new;
|
||||||
|
|
||||||
|
memset(&ut_new, 0, sizeof(ut_new));
|
||||||
|
strncpy(ut_new.ut_line, from_line, sizeof(ut_new.ut_line));
|
||||||
|
setutent();
|
||||||
|
if ((ut_old = getutid(&ut_new)) != NULL) {
|
||||||
|
if (ut_old != &ut_new)
|
||||||
|
memcpy(&ut_new, ut_old, sizeof(ut_new));
|
||||||
|
strncpy(ut_new.ut_line, to_line, sizeof(ut_new.ut_line));
|
||||||
|
utmp_setid(to_line, ut_old->ut_id, ut_new.ut_id, sizeof(ut_new.ut_id));
|
||||||
|
ut_new.ut_pid = getpid();
|
||||||
|
ut_new.ut_time = time(NULL);
|
||||||
|
ut_new.ut_type = USER_PROCESS;
|
||||||
|
|
||||||
|
if (pututline(&ut_new) != NULL)
|
||||||
|
rval = TRUE;
|
||||||
|
}
|
||||||
|
endutent();
|
||||||
|
#elif HAVE_LOGIN
|
||||||
|
FILE *fp;
|
||||||
|
struct utmp ut;
|
||||||
|
|
||||||
|
/* Find existing entry, update line and add as new. */
|
||||||
|
if ((fp = fopen(_PATH_UTMP, "r")) != NULL) {
|
||||||
|
while (fread(&ut, sizeof(ut), 1, fp) == 1) {
|
||||||
|
if (ut.ut_name[0] &&
|
||||||
|
strncmp(ut.ut_line, from_line, sizeof(ut.ut_line)) == 0) {
|
||||||
|
strncpy(ut.ut_line, to_line, sizeof(ut.ut_line));
|
||||||
|
login(&ut);
|
||||||
|
rval = TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return rval;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
utmp_clone(const char *from_line, const char *to_line)
|
||||||
|
{
|
||||||
|
/* Strip off /dev/ prefix from to/from line as needed. */
|
||||||
|
if (strncmp(from_line, _PATH_DEV, sizeof(_PATH_DEV) - 1) == 0)
|
||||||
|
from_line += sizeof(_PATH_DEV) - 1;
|
||||||
|
if (strncmp(to_line, _PATH_DEV, sizeof(_PATH_DEV) - 1) == 0)
|
||||||
|
to_line += sizeof(_PATH_DEV) - 1;
|
||||||
|
|
||||||
|
return utmp_doclone(from_line, to_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remove (zero out) the utmp entry for a line.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
utmp_doremove(const char *line)
|
||||||
|
{
|
||||||
|
int rval = FALSE;
|
||||||
|
#ifdef HAVE_GETUTXID
|
||||||
|
struct utmpx *ut, key;
|
||||||
|
|
||||||
|
memset(&key, 0, sizeof(key));
|
||||||
|
strncpy(key.ut_line, line, sizeof(key.ut_line));
|
||||||
|
setutxent();
|
||||||
|
if ((ut = getutxid(&key)) != NULL) {
|
||||||
|
ut->ut_type = DEAD_PROCESS;
|
||||||
|
(void)gettimeofday(&ut->ut_tv, NULL);
|
||||||
|
if (pututxline(ut) != NULL)
|
||||||
|
rval = TRUE;
|
||||||
|
}
|
||||||
|
endutxent();
|
||||||
|
#elif HAVE_GETUTID
|
||||||
|
struct utmp *ut, key;
|
||||||
|
|
||||||
|
memset(&key, 0, sizeof(key));
|
||||||
|
strncpy(key.ut_line, line, sizeof(key.ut_line));
|
||||||
|
setutent();
|
||||||
|
if ((ut = getutid(&key)) != NULL) {
|
||||||
|
ut->ut_type = DEAD_PROCESS;
|
||||||
|
ut->ut_time = time(NULL);
|
||||||
|
if (pututline(ut) != NULL)
|
||||||
|
rval = TRUE;
|
||||||
|
}
|
||||||
|
endutent();
|
||||||
|
#elif HAVE_LOGIN
|
||||||
|
if (logout(line) != 0)
|
||||||
|
rval = TRUE;
|
||||||
|
#endif /* HAVE_GETUTXID */
|
||||||
|
return rval;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
utmp_remove(const char *line)
|
||||||
|
{
|
||||||
|
/* Strip off /dev/ prefix from to/from line as needed. */
|
||||||
|
if (strncmp(line, _PATH_DEV, sizeof(_PATH_DEV) - 1) == 0)
|
||||||
|
line += sizeof(_PATH_DEV) - 1;
|
||||||
|
|
||||||
|
return utmp_doremove(line);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Cleanup hook for error()/errorx()
|
* Cleanup hook for error()/errorx()
|
||||||
*/
|
*/
|
||||||
@@ -120,6 +281,7 @@ cleanup(int gotsignal)
|
|||||||
#ifdef HAVE_SELINUX
|
#ifdef HAVE_SELINUX
|
||||||
selinux_restore_tty();
|
selinux_restore_tty();
|
||||||
#endif
|
#endif
|
||||||
|
utmp_remove(slavename);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -128,13 +290,28 @@ cleanup(int gotsignal)
|
|||||||
* and slavename globals.
|
* and slavename globals.
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
pty_setup(uid_t uid)
|
pty_setup(uid_t uid, const char *tty)
|
||||||
{
|
{
|
||||||
io_fds[SFD_USERTTY] = open(_PATH_TTY, O_RDWR|O_NOCTTY, 0);
|
io_fds[SFD_USERTTY] = open(_PATH_TTY, O_RDWR|O_NOCTTY, 0);
|
||||||
if (io_fds[SFD_USERTTY] != -1) {
|
if (io_fds[SFD_USERTTY] != -1) {
|
||||||
|
int sfd;
|
||||||
|
|
||||||
if (!get_pty(&io_fds[SFD_MASTER], &io_fds[SFD_SLAVE],
|
if (!get_pty(&io_fds[SFD_MASTER], &io_fds[SFD_SLAVE],
|
||||||
slavename, sizeof(slavename), uid))
|
slavename, sizeof(slavename), uid))
|
||||||
error(1, "Can't get pty");
|
error(1, "Can't get pty");
|
||||||
|
/*
|
||||||
|
* Add entry to utmp/utmpx.
|
||||||
|
* Temporarily point stdin to the pty slave for the benefit of
|
||||||
|
* legacy utmp handling that uses ttyslot().
|
||||||
|
*/
|
||||||
|
if ((sfd = dup(STDIN_FILENO)) == -1)
|
||||||
|
error(1, "Can't save stdin");
|
||||||
|
if (dup2(io_fds[SFD_SLAVE], STDIN_FILENO) == -1)
|
||||||
|
error(1, "Can't dup2 stdin");
|
||||||
|
utmp_clone(tty, slavename);
|
||||||
|
if (dup2(sfd, STDIN_FILENO) == -1)
|
||||||
|
error(1, "Can't restore stdin");
|
||||||
|
close(sfd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,6 +831,7 @@ pty_close(struct command_status *cstat)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
utmp_remove(slavename);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@@ -39,7 +39,7 @@ int suspend_parent(int signo);
|
|||||||
void fd_set_iobs(fd_set *fdsr, fd_set *fdsw);
|
void fd_set_iobs(fd_set *fdsr, fd_set *fdsw);
|
||||||
void handler(int s);
|
void handler(int s);
|
||||||
void pty_close(struct command_status *cstat);
|
void pty_close(struct command_status *cstat);
|
||||||
void pty_setup(uid_t uid);
|
void pty_setup(uid_t uid, const char *tty);
|
||||||
void terminate_child(pid_t pid, int use_pgrp);
|
void terminate_child(pid_t pid, int use_pgrp);
|
||||||
extern int signal_pipe[2];
|
extern int signal_pipe[2];
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user