Esto es algo que se necesita hacer muy frecuentemente en casi cualquier portal y usualmente no es tan sencillo como parece.
Yo generalmente utilizo permisos por grupo/objeto.
Cada usuario pertenece a un grupo y cada grupo tiene acceso a ciertos objetos. Al final intento crear una clase que verifique que el grupo tenga acceso al objeto especificado y en base a eso se permite el acceso.
Por ejemplo:
Tabla usuarios:
IdUsuario (PK)
Tabla grupos:
IdGrupo (PK)
Tabla usuarios_grupo
IdGrupo (PK)(FK)
IdUsuario (PK)(FK)
Tabla Objetos:
IdObjeto (PK)
NameObjeto (UNIQUE idx)
Tabla Acceso_Objetos
IdGrupo (PK)(FK)
IdObjeto (PK)(FK)
Tendrías que crear una clase que tome el usuario y ejecute un código como este:
SELECT
COUNT(*) AS ACCESO_OBJETOS
FROM acceso_objetos
INNER JOIN
usuarios_grupo ON
usuarios_grupo.IdGrupo=acceso_objetos.IdGrupo
INNER JOIN
usuarios ON
usuarios.IdUsuario=usuarios_grupo.IdUsuario
WHERE
IdUsuario=@IdUsuario AND
NameObjeto=@NameObjeto
Ese select devuelve >0 si existe acceso al objeto o 0 si no.
Podes tener una clase con una funcion algo así:
bool CheckAccess(int IdUsuario,string NameObjeto) {
/*** TODO: PREPARAR CONSULTA ***/
If(sql.ExecuteScalar()> 0) {
return true;
} else {
return false;
}
}
Y en tu código de cada página podes tener algo como:
void OnPageLoad(...)
If(Acceso.CheckAccess(Session['IdUsuario'],"menu_principal")) {
// ejecutar codigo de inicializacion
} else {
// redireccionar a pagina de error o login
}
}
La ventaja con este método es que podes luego definir los permisos como vos querras, es muy granular y fácil de implementar y lo podes "pegar" en los OnPageLoad que tengás.
Ahora... podes ir un poco más lejos y asociar los permisos a cada nombre de clase y cargar las clases correspondientes a la página en tu lista de objetos. Nada te limita a usarlo como querras incluso podes mostrar/ocultar opciones de menu dependiendo del grupo al que pertenescan solo colocando los "If" correspondientes. por ejemplo:
Grupo "Admin"
Objetos:
-->menu
-->menuadmin
-->agregar_usuarios
-->eliminar_usuarios
-->ver_usuarios
Grupo "Usuarios"
Objetos:
-->menu
-->ver_usuarios
Ejemplo:
<% If(Acceso.CheckAccess(Session['IdUsuario'],"menu")) { %>
<ul>
<% If(Acceso.CheckAccess(Session['IdUsuario'],"menuadmin")) { %>
<li>Agregar usuarios</li>
<li>Borrar usuarios</li>
<% } %>
<li>ver Usuarios</li>
</ul>
<% } %>
Ojo que este método es plano, también pueden hacer un "arbol de permisos" que vaya heredando el acceso a objetos, pero para fines prácticos este método de acceso por grupo a nivel de objeto pienso que cubre la mayoría de necesidades de permisos.
Nota: Se que en .Net se pueden usar los RoleProvider, pero esto no es difícil de implementar se puede aplicar a muchos lenguajes y lo siento en lo personal más simple.
P.D.: Algunas recomendaciones extra:
1-La clase que verifica el acceso tiene que tener acceso a una conexión, en mi experiencia he encontrado que es mejor usar la misma conexión que está utilizando la página. Esto es para evitar que esté generando varias conexiones diferentes solo para chequear permisos.
2-Si no quieren estar haciendo un SQL para cada checkAccess, como en el caso que quieran habilitar/deshabilitar funciones en la página que se envía al cliente, pueden en la inicialización de la clase bajar todos los objetos a los que tiene permiso elusuario y guardarlo en memoria con un select como este:
SELECT DISTINCT
NameObjeto
FROM acceso_objetos
INNER JOIN
usuarios_grupo ON
usuarios_grupo.IdGrupo=acceso_objetos.IdGrupo
INNER JOIN
usuarios ON
usuarios.IdUsuario=usuarios_grupo.IdUsuario
WHERE
IdUsuario=@IdUsuario
Nota: Se usa DISTINCT para que elimine nombres de objetos duplicados en caso que el usuario perteneciera a distintos grupos con acceso al mismo objeto. El resultado de esta consulta se puede guardar en un vector de strings.
Luego la función CheckAccess solo necesita el nombre del objeto:
bool CheckAccess(string NameObjeto) {
foreach(string obj in userObjects) {
if(obj.Equals(NameObjeto)) {
return true;
}
}
return false;
}
3-De nuevo repito, yo utilizo mucho este "sistema" de permisos porque es fácil de implementar y no requiere tocar archivos de configuración. Si hay algo que DETESTO con mi corazón es que para montar un sistema de autenticación y permisos sencillo se tenga que estar tocando 20 archivos diferentes o heredar 20 clases diferentes.
4-
MUY, PERO MUY IMPORTANTE: Yo se que esto es básico, pero lo he visto tantas veces que vale la pena mencionarlo: El IdUsuario de sesión NUNCA PERO NUNCA tiene que ser establecido desde variables que vengan del cliente, SIEMPRE tiene que establecerse como variable de sesión en el lado del servidor luego de haber verificado las credenciales del usuario. Si no lo hacen de esta manera corren el riesgo de que su aplicación sea vulnerable a ataques de escalado de privilegios.
5-Por qué me gusta esto más que las ACL. Simple, esto es una ACL pero sin complicaciones, la tabla de acceso_grupos es tu ACL pero es como la "lista de invitados" no está en la lista no entra. Además que una clase armada de esta manera te permite verificar el acceso ADENTRO DE la página. Hay algo que nunca me gustó de CakePHP por ejemplo es que sus ACL te obligan a que toda la vista esté asociada a una ACL. ¿Pero que tal si yo quiero que partes de la página se muestren a los admin y otras a los usuarios? Yo no voy a hacer una vista para un admin y una vista para un usuario normal cuando lo único que cambia es el botón. Si bien hay páginas en que queres tener un sistema como las ACL de CakePHP con permisos heredados y acceso por clases, acciones muchas veces el mismo diseño que tiene te restringe y sale incluso más flexible usando un sistema como este.
6-No todo es color de rosa
si no tienen cuidado pueden tener 40,000 objetos que no saben nisiquiera que hacen. Una buena forma de verificar esto es que la aplicación "registre los objetos" es decir que si se hace un "CheckAccces" para un objeto que no existe este objeto se cree en la BD con la ref al codigo y luego se puede hacer un "cleanup" para ver si realmente se están utilizando todos los objetos definidos en la base.