Álvaro González Sotillo

Servidor linux para exámenes

El problema

A la hora de realizar un trabajo o examen práctico sobre un ordenador, los profesores tenemos varias alternativas:

  1. Los alumnos realizan el examen sobre su propio ordenador o máquina virtual. Después, cada alumno muestra el resultado al profesor. Esta solución se vuelve difícil si hay muchos alumnos, porque deben ir esperando su turno para presentar el resultado al profesor.
  2. Como la opción 1, pero el profesor recoge las máquinas virtuales y las estudia posteriormente. En este caso, el problema es la copia de las máquinas virtuales, cada una con varios gigabytes, en un disco duro que hay que ir acoplando a cada ordenador de los alumnos.
  3. Abandonar la idea de práctico, y realizar el examen sobre papel 😱

Sin embargo, hay exámenes en los que el alumno no necesita ser administrador de la máquina. En estos casos, el profesor puede montar un hosting casero y dejar que los alumnos realicen el examen en su propio ordenador o máquina virtual.

A continuación, describiré qué pasos sigo para crear un entorno Linux para mis alumnos. Este entorno lo he adaptado para exámenes de bases de datos (Oracle y MySQL), y exámenes LAMP.

Entorno Linux

Cada alumno tendrá su propia contraseña. Generar las contraseñas aleatoriamente no es una buena idea, puesto que dificulta su recuperación en caso de pérdida. Es mejor que la contraseña dependa del nombre del usuario, por ejemplo los primeros caracteres del hash del usuario.

Mi solución es utilizar el código ASCII de varios caracteres del nombre del usuario: para los alumnos es inicialmente algo imposible de descifrar, y cuando preguntan puedo contar cosas como el sistema de numeración binaria, código ASCII, cadenas de caracteres, criptoanálisis...

password_para_alumno(){
    local USER=$1
    local uno=${USER:0:1}
    local dos=${USER:1:1}
    local tres=${USER:2:1}

    LC_CTYPE=C printf '%d' "'$uno'" "'$dos'" "'$tres'"
}

echo La contraseña para MARIA sería $(password_para_alumno MARIA)

Los usuarios se crean la contraseña especificada por la función anterior. El directorio del alumno no es accesible por otros alumnos, y el alumno no puede cambiar los permisos de su home para dejarse copiar.

crear_usuario_linux()
{
    local USER=$1
    local PASS=$(password_para_alumno $USER)

    
    sudo useradd $USER -m -s /bin/bash
    echo "$USER:$PASS" | sudo chpasswd

    sudo chown root:$USER /home/$USER
    sudo chmod 770 /home/$USER
}

También elimino los canales de comunicación entre alumnos más comunes: mensajería, conexiones TCP... Hay demasiadas formas como para deshabilitarlas todas. De las que no se eliminan, la más evidente son los ficheros temporales en /tmp.

deshabilita_comunicacion(){
    sudo "chmod 500 $(which wall) $(which mail) $(which write) $(which nc)"
}

En el momento del examen, es necesario que los alumnos conozcan su contraseña de una forma más o menos confidencial. Para eso utilizo una tabla de org-mode, que imprimo y recorto cada fila, para repartir a cada alumno.

lista_usuarios(){
    rm usuarios_linux.org
    local USUARIOS="$*"
    
    for USER in $USUARIOS
    do
        local PASS=$(password_para_alumno $USER)
        printf '| Usuario | %s | Password | %s | \n' $USER $PASS >> usuarios_linux.org
    done
}

Con todas estas funciones, ya solo queda definir la lista de alumnos e invocarlas por cada alumno.

# LISTA DE USUARIOS: GENERALMENTE, USO LOS APELLIDOS DE LOS ALUMNOS
usuarios="alumno1 alumno2 alumno3"

deshabilita_comunicacion

for usuario in $usuarios
do
    crear_usuario_linux $usuario
done

lista_usuarios $usuarios

Oracle

Utilizo la versión Oracle XE, para evitar problemas de licencia, y porque es bastante simple de instalar a partir de paquetes RPM en un Centos/Fedora.

En los exámenes basados en Oracle es necesario crear un usuario para cada alumno. Por simplicidad, el mismo usuario y contraseña de Linux se reutilizan para la base de datos.

Al comienzo defino las variables ORACLE_HOME y ORACLE_SID, que son necesarias para que funcione correctamente el cliente de Oracle sqlplus. Para no dejar la contraseña del administrador escrita en el script, utilizo la variable de entorno SYSPASS.

export ORACLE_HOME=/u01/app/oracle/product/11.2.0/xe
export ORACLE_SID=XE
export NLS_LANG=`$ORACLE_HOME/bin/nls_lang.sh`
export PATH=$ORACLE_HOME/bin:$PATH


if [ -z "$SYSPASS" ]
then
    echo La variable SYSPASS debe tener la contraseña SYS de la base de datos
    exit
fi

Para cada alumno se crea un usuario con permisos básicos (crear tablas, índices, vistas...). En algunos exámenes también necesito que los alumnos creen directory y utilicen algunas vistas dinámicas de sesiones, usuarios y procesos.

Dentro del heredoc necesito que se utilizar las variables $user y $pass, así que debo permitir su expansión. Pero algunas vistas contienen el caracter $, que se intentaría expandir. Para evitar problemas, uso una variable con el valor $=, que defino usando comilla simple para que evitar su expansión.

Después se carga un script de SQL inicial para la creación de tablas, que depende de cada examen.

crear_usuario_oracle(){
    #https://oraclespin.com/2008/12/18/how-to-grant-access-to-vsession-to-other-users/
    local user=$1
    local pass=$2
    local S='$'

    sqlplus sys/$SYSPASS as sysdba <<EOF
    drop user $user cascade;
    create user $user identified by $pass;
    alter user $user identified by $pass;
    alter user $user default tablespace USERS;
    alter user $user quota 100M on USERS;

    grant resource,connect to $user;
    grant create view to $user;
    grant create any directory to $user;
    

    GRANT SELECT ON sys.v_${S}session TO $user;
    GRANT SELECT ON sys.v_${S}gsession TO $user;
    GRANT SELECT ON sys.v_${S}sqltext TO $user;
    GRANT SELECT ON sys.v_${S}lock TO $user;
    GRANT SELECT ON sys.v_${S}locked_object TO $user;
    GRANT SELECT ON sys.v_${S}process TO $user;
    GRANT SELECT ON sys.gv_${S}process TO $user;
    GRANT SELECT ON sys.v_${S}sess_io TO $user;
    GRANT SELECT ON sys.ALL_OBJECTS to $user;
    GRANT SELECT ON sys.DBA_WAITERS to $user;

    alter system set sessions=300 scope=spfile;
    alter system set processes=300 scope=spfile;

    commit;
EOF

    sqlplus $user/$pass <<EOF
    @tablas-iniciales.sql
EOF
}

LAMP

Para los exámenes LAMP se necesita una base de datos y un sitio web por cada alumno.

crea_base_de_datos()
{
  local USER=$1
  local PASS=$(password_para_alumno $USER)

  mysql --user=root --password=$SYSPASS <<EOF
    DROP DATABASE $USER;
    CREATE DATABASE IF NOT EXISTS $USER;
    GRANT ALL ON $USER.* TO '$USER' IDENTIFIED BY '$PASS';
    FLUSH PRIVILEGES;
EOF
}

Apache2 dispone de la directiva UserDir para crear un sitio web para cada usuario. De todas formas, para tener un control más fino sobre cada opción y directorio de alumno, he decidido crear un site por alumno.

La siguiente función crea un site para un alumno en entorno Debian/Apache2.

crea_sitio_web()
{
  local USER=$1

  if [ ! -z "$USER" ]
    then

    local APACHE=www-data
    local DOCUMENTROOT=/home/$USER/public_html
    local SITE=/etc/apache2/sites-available/alumno_$USER

    mkdir -p /home/$USER
    chown -R $USER:$USER /home/$USER

    mkdir -p $DOCUMENTROOT
    echo "Sitio de $USER, en el directorio $DOCUMENTROOT, con AllowOverride All" >  $DOCUMENTROOT/index.html

    # AJUSTE DE PERMISOS: $HOME sigue siendo privado para otros alumnos, pero 
    # $APACHE puede accceder a $DOCUMENTROOT 
    setfacl -R -m u:$APACHE:rxw /home/$USER
    chown -R $USER:$APACHE $DOCUMENTROOT
    chmod -R 770 $DOCUMENTROOT
    chmod +s $DOCUMENTROOT

    cat <<EOF > $SITE
    <Directory "$DOCUMENTROOT"> 
      AllowOverride All 
    </Directory>
    alias /$USER $DOCUMENTROOT 
EOF
  fi
}

Para evitar cientos de preguntas al inicio del examen dejo una página inicial de Apache donde explico:

  • Que pueden conectarse mediante ssh y sftp
  • Que tienen disponible phpMyAdmin
  • Que su usuario y contraseña es la misma en todos los casos
  crea_pagina_inicio()
  {
      local USERS="$1"
      local IPADDRESS=$(hostname -I)
      local IPADDRESS="${IPADDRESS#"${IPADDRESS%%[![:space:]]*}"}"
      local IPADDRESS="${IPADDRESS%"${IPADDRESS##*[![:space:]]}"}"
    
      local HOSTNAME=$(hostname).local
      #local HOSTNAME=$IPADDRESS

      local INDEXHTML=/var/www/html/index.html
      cat <<EOF > $INDEXHTML 
      <h1>Aplicaciones Web. Examen 1 evaluacion 3</h1>
      <p>Conexion con ssh a la IP:<b>$IPADDRESS</b>  ($HOSTNAME)</p>
      <table border=1>
  EOF
    
      for i in $USERS
      do
          cat <<EOF >> $INDEXHTML
            <tr>
              <td>
                <a href=$i/phpBB3>$i</a>
              </td>
              <td>Misma contrase&ntilde;a inicial</td>
              <td>
                <a href=sftp://$i@$HOSTNAME/home/$i>SFTP</a>
              </td>
              <td>
                <a href=phpMyAdmin>phpMyAdmin</a>
              </td>
            </tr>
  EOF
      done

      echo "</table>" >> $INDEXHTML
  }

En algunos exámenes, los alumnos no empiezan con un LAMP vacío, sino que instalo previamente un Joomla o Wordpress que tienen que modificar. Para ello, creo un usurio plantilla donde instalo lo necesario, y después copio la base de datos y los ficheros a cada alumno

copia_base_de_datos()
{
  local DBEXISTENTE=$1
  local DBACREAR=$2
  yes | mysqladmin --user=root --password=$SYSPASS drop $DBACREAR
  mysqladmin --user=root --password=$SYSPASS create $DBACREAR
  mysqldump --user=root --password=$ROOTPASS $DBEXISTENTE | mysql --user=root --password=$ROOTPASS $DBACREAR 
}

copia_ficheros_sitio_web()
{
  local PLANTILLA=$1
  local USER=$2
  local APACHE=www-data
  local PLANTILLADOCUMENTROOT=/home/$PLANTILLA/public_html
  local DOCUMENTROOT=/home/$USER/public_html
  sudo cp -R $PLANTILLADOCUMENTROOT /home/$USER/
  sudo chown -R $USER:$APACHE $DOCUMENTROOT
  sudo chmod -R 770 $DOCUMENTROOT
  sudo chmod +s $DOCUMENTROOT
}

Dependiendo de la aplicación web copiada, puede ser necesario realizar más ajustes. Por ejemplo, si se clona un Joomla, en su fichero de configuración hay que cambiar las apariciones de plantilla por el nombre del usuario del alumno. Además, es necesario cambiar el usuario administrador de Joomla en la base de datos:

copia_joomla()
{
  local PLANTILLA=$1
  local USER=$2
  local DOCUMENTROOT=/home/$USER/public_html

  copia_base_de_datos $PLANTILLA $USER
  copia_ficheros_sitio_web $PLANTILLA $USER
  sed -i -- "s/$PLANTILLA/$USER/g" $DOCUMENTROOT/configuration.php

  rm $DOCUMENTROOT/index.html

  mysql --user=$USER --password=$USER -e "use $USER; update isvfo_users set username='$USER' where username='plantilla';"
}

Una vez se tienen todas estas funciones, basta con iterar sobre los alumnos, y acabar habilitando todos los sitios web nuevos (uso a2ensite de la distribución de Debian)

for user in $USERS
do
  echo ________________________________________ NOMBRE DE USUARIO: $user
  crea_usuario $user
  crea_sitio_web $user
  crea_base_de_datos $user
  copia_joomla plantilla $user
done

sudo a2ensite 'alumno*'
sudo service apache2 restart