FAQ About Copertina Emacs

Articoli


Password, gruppi ed altro

Ora che tutti hanno un server Web con cui giocare, magari qualcuno potrà essere tentato di usare il proprio patrimonio di informazioni per metterle a disposizione delle persone più fidate.

È ovvio che, essendo informazioni private, si vuole avere un minimo di controllo su chi vi accederà.
Questo può essere ottenuto mediante un'opportuna protezione tramite password.

In questo momento sto usando il server Web Apache 1.1.1, quindi descriverò il modo in cui questo server gestisce gli accessi. Altri server (ad esempio l'NCSA) agiscono in modo molto simile.

Nel caso di Apache è possibile proteggere una directory mediante password creando all'interno di questa un file di nome .htaccess, che contenga queste istruzioni:

AuthType Basic
AuthName mia
AuthUserFile /etc/apache/passwd
AuthGroupFile /etc/apache/group
<Limit GET POST>
require group mio
</Limit>

La keyword AuthType serve per specificare il tipo di gestione delle password (inutile preoccuparsi dei vari tipi di autenticazione, dato che al momento è implementata solo la Basic :-).
La keyword AuthName specifica il nome dell'autorizzazione. In pratica, quando vi comparirà la dialog di richiesta della password, leggerete qualcosa come:

Enter username for mia at xxx:

dove xxx è il nome della vostra macchina.

Le keywords AuthUserFile e AuthGroupFile servono per definire i nomi dei files degli utenti e dei gruppi.
Esattamente come accade a livello di login al sistema, esiste un file che associa gli utenti alle proprie password. In questo caso però l'appartenenza ai gruppi viene gestita mediante un file in cui si associano gli utenti ai gruppi.
Ho scelto di sistemare il file dei gruppi e quello delle password in /etc/apache, directory in cui la distribuzione che uso, la Debian, inserisce i files di configurazione del server.

Il passo successivo è quello di limitare l'accesso alla directory (e tutte le sue subdirectory) con la keyword Limit, richiedendo l'appartenenza ad uno specifico gruppo con la require group.
Ovviamente mio è il nome del gruppo di cui si deve fare parte per accedere alla lettura delle pagine riservate.

Quando un utente cercherà di accedere ad una pagina protetta, il sistema richiederà l'immissione di uno username e di una password.
Ottenuti questi dati, cercherà nel file dei gruppi, per assicurarsi che l'utente faccia parte del gruppo autorizzato e verificherà nel file delle password che l'utente sia a conoscenza della password giusta.

Questo meccanismo ci permette di poter specificare la nostra password di utente una volta per tutte e di poter girare in tutte le directory in cui è permesso l'accesso ad uno dei gruppi di cui facciamo parte, senza dover ripetere al sistema la nostra password. Normalmente questa sarà ricordata dal nostro browser, che la trasmetterà al server su richiesta.

Fin qui tutto bene.
Creare il file dei gruppi è molto semplice: basta elencare i gruppi e gli utenti che ne fanno parte. Ad esempio:

mio: nando miriam greta
admin: nando

Il file delle password può essere creato con l'utility htpasswd, che, specificato il pathname del file delle password e il nome dell'utente, richiede la sua password, la critta e la salva nel file.

Questa è però un'operazione da eseguire rigorosamente da riga di comando e loggandosi come root (se ci tenete alla sicurezza).
Se amate le interfacce grafiche o se l'amministrazione del vostro sito Web è affidata ad un vostro collega finestrodipendente a cui non dareste mai la password di root, potete però crearvi un vostro tool grafico di amministrazione con cui si possa operare anche senza essere superuser.

Iniziamo creando una form per la modifica delle password:

<html>
<head>
<title>Modifica della password</title>
</head>
<body>
<font size=4>
<center>
Crea un utente o modifica la sua password
</center>
<p>

<!--#include virtual="/cgi-bin/listautenti"-->

<p>
Inserisci il nome dell'utente nel campo <b>Nome utente</b>.
<p>
La sua password dovr&agrave; essere inserita nei campi <b>Password</b>
e <b>Verifica</b>. Il contenuto dei due campi dovr&agrave; essere uguale,
altrimenti l'utente non sar&agrave; creato o la sua password non verr&agrave;
modificata.
<p>
Per attivare la procedura di creazione/modifica dell'utente premere il tasto
<b>Crea/Cambia</b>.

<form name="utente" method=POST action="/cgi-bin/chpasswd">
Nome utente: <input size=20 name=utente>

<!--#include virtual="/cgi-bin/listagruppi"-->

<p>
Password: <input type=password size=8 name=passwd> <br>
Verifica: <input type=password size=8 name=passwd2>
<p>
<input type=submit value="Crea/Cambia">
<input type=reset value=Cancella>
<input type="button" value="Torna al Menu" onClick="location='/admin/'">
<br>
</font>
</body>
</html>

Chiamate questa pagina con un nome del tipo chpasswd.shtml. L'estensione shtml è importante, perché identifica le pagine che devono essere preprocessate dal server.

Il server Apache permette l'uso dei SSI o Server Side Include, che sono delle particolari istruzioni, inserite come commenti HTML all'interno della pagina, che vengono interpretate dal server, che esegue il programma specificato e ne riporta l'output nella pagina che spedisce al browser.
Questa peculiarità permette quindi la costruzione di pagine dinamiche, il cui contenuto può variare di volta in volta.

Per attivare l'uso dei SSI bisogna aggiungere nel file di configurazione di Apache srm.conf la direttiva:

AddType text/x-server-parsed-html .shtml

Nella nostra form abbiamo un SSI:

<!--#include virtual="/cgi-bin/listautenti"-->

che lancia un programma Perl che lista tutti gli utenti e i relativi gruppi presenti in quel momento sul server.
Il programma è molto semplice:

#!/usr/bin/perl

$passwd = "/etc/apache/passwd";
$group  = "/etc/apache/group";

print "Content-Type: text/html\r\n\r\n";

open GROUP, $group or die "Non posso aprire il file dei gruppi: $!";
while(<GROUP>){
	($gr, $ut) = split /:/;
	@utenti = split /\s/, $ut;
	foreach $utente (@utenti){
		next if $utente eq '';
		push @{ $user{$utente} }, $gr;
	}
}
close GROUP;

open PASS, $passwd or die "Non posso aprire il file delle password: $!";
print <<EOF;
Gli utenti attualmente abilitati e i gruppi di cui fanno parte sono:
<p>
<ud>
EOF
while(<PASS>){
	$utente = (split /:/)[0];
	print "<li><font color='#ff4040'>$utente</font>: <font color='#00ee00'>",
		join(" ", @{ $user{$utente} }), "</font>\n";
}
close PASS;
print "</ud>\n";

Le formalità a cui deve adempiere un programma SSI sono all'incirca le stesse a cui devono sottostare i programmi che colloquino con il server http tramite il protocollo CGI. La comunicazione deve iniziare con una stringa di identificazione:

Content-Type: text/html

seguita da due CR/LF.
Da questo punto in poi tutto l'output generato dal nostro programma sarà inserito nel testo della form.

Il loop:

while(<GROUP>){
	($gr, $ut) = split /:/;
	@utenti = split /\s/, $ut;
	foreach $utente (@utenti){
		push @{ $user{$utente} }, $gr;
	}
}

legge una riga dal file dei gruppi di Apache e la divide all'altezza del carattere ':'. La prima parte è il nome del gruppo, la seconda è una stringa che contiene i nomi di tutti gli utenti appartenenti al gruppo, separati da spazi.

A sua volta l'elenco degli utenti viene diviso e per ognuno degli utenti viene creato un array associativo la cui chiave è il nome dell'utente e il suo valore è l'array dei gruppi di cui l'utente fa parte.

Questo genere di struttura dati è un po' complessa, quindi conviene spenderci qualche parola. La base è un array di tipo particolare (array associativo o hash), il cui indice, invece di essere un numero, può essere una qualsiasi stringa, in questo caso il nome dell'utente.
Ognuno degli elementi di questo array è a sua volta un array: la lista dei gruppi a cui l'utente appartiene.
Il nome dell'hash è user, un suo elemento è indicizzato da $user{$utente}, in cui $utente contiene un nome. L'elemento in questione è una lista, a cui appendiamo il nome del gruppo di cui l'utente fa parte, per mezzo dell'operatore push.

L'operatore push accetta due argomenti: l'array in cui inserire l'elemento e lo scalare da inserire.
La forma @{ $user{$utente} } viene utilizzata per indicare che l'elemento dell'hash è un array.

Spero che siate riusciti a seguirmi in questa complessa spiegazione. In caso vi stiate perdendo d'animo devo ammettere che le strutture dati complesse sono una delle cose più difficili non solo del Perl, ma di qualsiasi linguaggio di programmazione.
L'analogo in C avrebbe comportato un'array di struct i cui elementi sarebbero stati un puntatore e un puntatore ad un array... non molto più semplice.

Certo esistono modi più semplici di fare le cose, dal punto di vista dei dati, ma si deve scrivere più codice... e io sono pigro.

Proseguendo con l'analisi del programma, arriviamo alla seconda parte, che legge il file delle password e per ogni utente trovato ne stampa il nome e l'elenco dei gruppi di cui fa parte, incapsulando il tutto con dell'HTML.
L'unica riga criptica di questa parte è (spero :-):

$utente = (split /:/)[0];

La cosa può essere spiegata facilmente: l'operatore split, come la maggior parte degli operatori e funzioni del Perl, può agire su una variabile di default: $_.
In questo caso lo split agisce sulla riga corrente appena letta dal file delle password e la divide all'altezza del carattere ':', generando una lista con il risultato dell'operazione.
A noi interessa solo il primo elemento della lista, il nome dell'utente, e non intendiamo toccare la sua password (per il momento), quindi prendiamo solo l'elemento [0] di questo array.

Ma non abbiamo ancora terminato con i SSI: ce n'è ancora uno, quello che nelle mie intenzioni deve produrre una serie di checkbox, una per ogni gruppo presente, e permettere quindi di assegnare l'utente creato ad uno o più gruppi:

#!/usr/bin/perl

$group  = "/etc/apache/group";

print "Content-Type: text/html\r\n\r\n";

open GROUP, $group or die "Non posso aprire il file dei gruppi: $!";
print "Gruppi: ";
while(<GROUP>){
	$gr = (split /:/)[0];
	print "$gr: <input type='checkbox' name='gruppo' value='$gr'>\n";
}
close GROUP;

Se siete riusciti a seguire il programmino precedente, questo vi risulterà banale.

Finito di imbellettare la nostra form, abbiamo bisogno di un programma che si preoccupi di modificare il file delle password, gestendo i dati immessi dall'utente.

Questo programma dovrà leggere e soprattutto scrivere file di proprietà di root, in una directory che sulla mia macchina ha delle permission di questo tipo:

drwxr-xr-x   2 root     root         1024 Jan 10 19:46 /etc/apache/

Questo rappresenta un problema, se si tiene conto che i programi CGI vengono eseguiti con le permission dell'utente nobody.
Il nostro programma dovrà essere suid root, per poter essere eseguito dall'utente proprietario dei dati da modificare.
Quando si parla di script suid, il discorso si fa spinoso e molti preferiscono dire che "non si deve", ma in casi come questo, a meno di non scriverci un apposito modulo per Apache, l'unica soluzione è quella di rilassare un po' il nostro livello di paranoia.

Ci sono due modi di eseguire uno script con le permission di un altro utente: creare un programma C che sia suid e che si preoccupi di invocare il nostro script una volta entrato in esecuzione con le permission giuste (C wrapper) o usare il suidperl, una variante dell'eseguibile del Perl, che permette l'esecuzione suid degli script ed è molto più paranoico di quello che potrebbe essere l'utente normale che si metta a scrivere un C wrapper.

Di solito preferisco questa ultima soluzione, quindi lo script che presento userà il suidperl, ma potete di cambiare la prima riga per puntare al normale perl e scrivervi un C wrapper.

#!/usr/bin/suidperl

use CGI::Request;
use CGI::Carp;

sub errore
{
	my($motivo) = shift;

	print <<EOF;
	<html>
	<head>
	<title>Modifica non effettuata</title>
	</head>
	<body bgcolor="#cc0000">
	$motivo
	<p>
	<form name="indietro">
	<input type="button" value="Torna indietro" onClick="history.back()">
	<input type="button" value="Torna al Menu" onClick="location='/admin/'">
	</form>
	</body>
	</html>
EOF
}

# Legge gli elementi della form

$req       = GetRequest();
$user      = $req->param('utente');
$passwd    = $req->param('passwd');
$passwd2   = $req->param('passwd2');
@gruppi    = $req->param('gruppo');

# E` stato immesso il nome dell'utente?

if($user eq ''){
	$motivo = <<EOF;
	<em>Non</em> &egrave; possibile creare o modificare l'utente, perch&eacute;
	non &egrave stato specificato alcun nome.
EOF
	errore($motivo);
	return;
}

# E` stato immessa la password?

if($passwd eq ''){
	$motivo = <<EOF;
	La password dell'utente <b>$user</b> <em>non</em> &egrave; stata creata o
	modificata, perch&eacute; non &egrave; stata specificata alcuna password.
EOF
	errore($motivo);
	return;
}

# Test della password immessa

if($passwd ne $passwd2){
	$motivo = <<EOF;
	La password dell'utente <b>$user</b> <em>non</em> &egrave; stata creata o
	modificata, perch&eacute; la password specificata e la sua replica sono
	diverse.
EOF
	errore($motivo);
	return;
}

$newfile = "/etc/apache/passwd";
$oldfile = "${newfile}.old";

# Muove il file delle password

unlink $oldfile or
	die "Non posso cancellare il file: $!"
	if -e $oldfile;
rename $newfile, $oldfile or
	die "Non posso rinominare il file: $!"
	if -e $newfile;

$newstring = crypt($passwd,'aa');

# Modifica o aggiunge l'utente

open PASSOLD, $oldfile  || die "Non posso leggere il file: $!";
open PASSNEW, ">$newfile" || die "Non posso scrivere il file: $!";

$flag = 0;
while (<PASSOLD>) {
	if(s/^${user}:.*/${user}:${newstring}/){
		$flag=1;
	}
	print PASSNEW;
}

print PASSNEW "${user}:$newstring\n" if $flag == 0;

close PASSOLD;
close PASSNEW;

# Modifica i gruppi

$newgroup = "/etc/apache/group";
$oldgroup = "${newgroup}.old";

# Muove il file dei gruppi

unlink $oldgroup or
	die "Non posso cancellare il file: $!"
	if -e $oldgroup;
rename $newgroup, $oldgroup or
	die "Non posso rinominare il file: $!"
	if -e $newgroup;

open GROLD, $oldgroup  || die "Non posso leggere il file: $!";
open GRNEW, ">$newgroup" || die "Non posso scrivere il file: $!";

while (<GROLD>) {
	chomp;
	($gr, $ut) = split /:/;
	@utenti = split /\s/, $ut;

	if(grep /$gr/, @gruppi){			# L'utente deve far parte del gruppo
		unless(grep /$user/, @utenti){	# Se non ne fa parte
			push @utenti, $user;
		}
	}else{								# L'utente non deve far parte del gruppo
		@utenti = grep {!/\b${user}\b/} @utenti;
	}
	
	print GRNEW "$gr:", join(" ", @utenti), "\n";
}

close GROLD;
close GRNEW;

# Feedback

print <<EOF;
<html>
<head>
<title>Modifica effettuata</title>
</head>
<body bgcolor="#669900">
La password dell'utente <b>$user</b> &egrave; stata creata o modificata
se esistente.
<p>
EOF

# Warnings

print <<EOF if $passwd eq $user;
	<p>
	<hr>
	<b>Attenzione</b>: la password &egrave; uguale al logname
	dell'utente e la cosa non &egrave; considerata ottimale
	dal punto di vista della sicurezza.
	<p>
	<hr>
EOF

print <<EOF if length($passwd) < 4;
	<p>
	<hr>
	<b>Attenzione</b>: la password &egrave; composta da meno di quattro
	caratteri e la cosa non &egrave; considerata ottimale
	dal punto di vista della sicurezza.
	<p>
	<hr>
EOF

print <<EOF if !defined @gruppi;
	<p>
	<hr>
	<b>Attenzione</b>: l'utente <b>$user</b> non fa parte di nessun gruppo.
	<p>
	<hr>
EOF

# Fine pagina

print <<EOF;
<form name="indietro">
<input type="button" value="Torna indietro" onClick="history.back()">
<input type="button" value="Torna al Menu" onClick="location='/admin/'">
</form>
</body>
</html>
EOF

In questo programmino uso due moduli esterni che fanno parte di una libreria di moduli per la programmazione con il protocollo CGI: la CGI-modules-2.75.tar.gz. Potete procurarvela su un qualsiasi sito aderente al CPAN (Comprehensive Perl Archive Network), ad esempio questo è il sito italiano del network.

Il primo modulo, CGI::Request, gestisce la lettura dell'input contenuto nella form, il secondo, CGI::Carp, serve per redirigere tutti i nostri messaggi di errore generati con l'operatore die nel file di log degli errori del server Apache.

All'inizio del programmino c'è una subroutine che serve a generare una pagina HTML per mostrare gli eventuali messaggi di errore generati dal programma.
Perdonatemi, ma questa volta non ho resistito e mi sono scappate alcune estensioni di Netscape, non propriamente standard. Del resto sembra che per alcune persone sia un problema dover cliccare il tasto nella barra degli strumenti di Netscape, così ho inserito un tasto Torna indietro.
L'altro tasto porta al menu dell'Amministrazione del Web, di cui questo programmino potrebbe essere solo una delle tante opzioni (ho messo questo menu nella directory /admin).

Dopo aver letto i valori degli elementi della form, comincio ad analizzare alcune possibili condizioni di errore (almeno le più banali).
Se il test risulta positivo, come nel caso:

if($user eq ''){

in cui il problema è quello che l'amministratore ha dimenticato di inserire il nome dell'utente, allora valorizzo una variabile, $motivo, usando la tecnica dell'here document:

	$motivo = <<EOF;
	<em>Non</em> &egrave; possibile creare o modificare l'utente, perch&eacute;
	non &egrave stato specificato alcun nome.
EOF

Il Perl eredita questa sintassi dalle shell Unix, ma la estende, rendendola applicabile a qualsiasi situazione che implichi un "input", quindi anche all'assegnazione di una stringa ad una variabile.
Tutto ciò che compare tra l'assegnazione e la stringa che abbiamo battezzato come fine dell'here document (EOF), viene inserito nella variabile $motivo, andate a capo comprese.

La variabile viene poi passata alla subroutine che la inserisce in una pagina e la mostra all'utente.

A questo punto inizia l'elaborazione vera e propria: elimino un eventuale file /etc/apache/passwd.old e rinomino l'attuale passwd in passwd.old, poi uso la funzione crypt per generare la nuova password dell'utente.

In questo programmino uso sempre lo stesso seme per la funzione crypt, la stringa 'aa'. Questo può non essere ciò che si vuole, ma lascio la generazione di una stringa di due caratteri casuali come esercizio al lettore.

Apro il vecchio file delle password e lo leggo riga per riga. Prima di stampare la riga letta cerco di modificarla:

	if(s/^${user}:.*/${user}:${newstring}/){
		$flag=1;
	}

uso l'operatore di sostituzione s///. La prima parte è la regular expression che rappresenta la stringa da modificare: deve essere il nome dell'utente immesso nella form ($user), seguito da un carattere ':', seguito da altri caratteri (la password). La seconda parte rappresenta ciò che deve essere sostituito: il nome dell'utente, i due punti e la nuova password.

Se l'operazione di sostituzione è andata a buon fine, se quindi l'utente immesso dall'amministratore era già elencato nel file delle password, allora accendo un flag.
Alla fine del ciclo di lettura/scrittura testo il contenuto del flag: se risulta spento, vuol dire che l'utente è nuovo, non era presente nel file, quindi lo devo aggiungere.

Passo al file dei gruppi: elimino un eventuale file /etc/apache/group.old e rinomino l'attuale group in group.old
Apro in lettura il vecchio file dei gruppi e in scrittura il nuovo.
Leggo ogni riga (che contiene un gruppo e l'elenco dei suoi utenti) e se l'utente che l'amministratore sta inserendo dovrà appartenere al gruppo in esame, viene inserito nell'elenco (se non è già presente).
L'elaborazione continua così fino alla fine del file.

Non ci resta che avvisare il nostro pubblico della riuscita dell'operazione, non senza dimostrarci saccenti e critici verso le sue scelte.

Come vedete ci vuole poco per rendere user friendly anche un'attività così bistrattata come l'amministrazione di sistema.

Ho risolto questo problema con un pizzico di pigrizia, come al solito, e invece di scrivermi un programma in C sotto X, ho usato direttamente l'interfaccia utente gentilmente messami a disposizione dal mio browser.

Quale può essere la morale nascosta in tutto ciò?
Forse l'epoca dell'object oriented programming sta per lasciare il passo a quella del document oriented programming?


A rileggerci al prossimo numero del Pluto Journal.

di Nando Santagata

FAQ About Copertina Emacs