Nell'admin rest di ArcGIS Server possiamo vedere la configurazione di default che imposta l'identity store integrato (built-in), nella sezione security -> config -> updateIdentityStore
Ma tra le varie possibilità ArcGIS Server permette anche di crearsi il proprio identity store. Difatti la nostra organizzazione potrebbe già gestire ad esempio un database con i propri utenti ed i ruoli ai quali appartengono gli utenti e quindi se dovessimo affidarci al built-in di ArcGIS Server avremmo una duplicazione di informazioni.
ArcGIS Server fornisce il supporto per i provider ASP.NET Membership e Role.
Per poter implementare i provider personalizzati occorre implementare i seguenti metodi:
Membership provider:
- FindUsersByName
- GetAllUsers
- GetUser
- ValidateUser
- GetAllRoles
- GetRolesForUser
- GetUsersInRole
In questo caso impostiamo anche le azioni di CASCATE sulle integrità referenziali così da eliminare automaticamente le associazioni ruoli-utenti quando si eliminano ruoli o utenti. Anche in questo caso è una scelta progettuale se si opta per forzare la cancellazione.
A questo punto in Visual Studio ci creiamo una libreria utilizzando il framework 3.5.
Ci creamo due classi: una che eredita dal MembershipProvider e che rappresenta il nostro Membership Provider ed un'altra che eredita da RoleProvider e che rappresenta il nostro Role Provider.
Vediamo nel dettaglio il MembershipProvider.
Il primo metodo che andiamo ad implementare è l'Initialize dove vengono passate le proprietà di configurazione del Membership Provider. A titolo esemplificativo impostiamo una proprietà dove indicare la stringa di connessione al database e una proprietà per indicare se desideriamo ricevere eventuali errori sull'event viewer di Windows o direttamente sulla pagina. Le proprietà successivamente verranno impostate in ArcGIS Server quando indicheremo il nostro custom provider.
/// <summary> /// initialize provider /// </summary> /// <param name="name">name provider</param> /// <param name="config">properties in config</param> public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { if (config == null) { throw new ArgumentNullException("config"); } if (string.IsNullOrEmpty(name)) { name = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", this.GetType().Namespace, this.GetType().Name); } base.Initialize(name, config); this.providerName = name; string connectionStringName = config["connectionStringName"]; if (string.IsNullOrEmpty(connectionStringName)) { throw new ArcGisServerCustomProviderException("connectionStringName"); } else { this.connectionString = connectionStringName; } this.WriteExceptionsToEventLog = Convert.ToBoolean(Helper.GetConfigValue(config["writeExceptionsToEventLog"], "false"), CultureInfo.InvariantCulture); // test for connection try { using (SqlConnection connection = new SqlConnection(this.connectionString)) { connection.Open(); } } catch { throw new ArcGisServerCustomProviderException("Check your DB connection!"); } }
Il secondo metodo che andiamo a riscrivere è il CreateUser che è quello che viene richiamato quando in ArcGIS Server Manager aggiungiamo un utente (pulsante New User):
/// <summary> /// create a user /// </summary> /// <param name="username">value of username</param> /// <param name="password">value of password</param> /// <param name="email">value email</param> /// <param name="passwordQuestion">password Question</param> /// <param name="passwordAnswer">password Answer</param> /// <param name="isApproved">is Approved</param> /// <param name="providerUserKey">provider User Key</param> /// <param name="status">value of status</param> /// <returns>object MembershipUser</returns> public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { MembershipUser newUser = null; try { newUser = this.GetUser(username, false); if (newUser == null) { using (SqlConnection connection = new SqlConnection(this.connectionString)) { using (SqlCommand cmd = new SqlCommand("INSERT INTO Users (Username, Password) VALUES (@UserName, @Password)", connection)) { cmd.Parameters.Add("@UserName", SqlDbType.NVarChar).Value = username; cmd.Parameters.Add("@Password", SqlDbType.NVarChar).Value = password; connection.Open(); int recordAdded = cmd.ExecuteNonQuery(); if (recordAdded > 0) { status = MembershipCreateStatus.Success; newUser = this.GetUser(username); } else { status = MembershipCreateStatus.UserRejected; } } } } else { status = MembershipCreateStatus.DuplicateUserName; } } catch (Exception e) { status = MembershipCreateStatus.ProviderError; if (this.WriteExceptionsToEventLog) { ArcGisServerMembershipProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name); } else { throw; } } if (status != MembershipCreateStatus.Success) { throw new ArcGisServerCustomProviderException(ArcGisServerMembershipProvider.GetErrorMessage(status)); } return newUser; }
Il metodo FindUsersByName serve ad ArcGIS Server per filtrare gli utenti con criterio parte-del-campo sul nome utente quindi in questo caso utilizzeremo un LIKE (casella di testo Find User) mentre il pageIndex e il pageSize è per la gestione del paging:
/// <summary> /// Find Users By Name /// </summary> /// <param name="usernameToMatch">username To Match</param> /// <param name="pageIndex">page Index</param> /// <param name="pageSize">page Size</param> /// <param name="totalRecords">total Records</param> /// <returns>object MembershipUserCollection</returns> public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords) { MembershipUserCollection users = new MembershipUserCollection(); try { using (SqlConnection connection = new SqlConnection(this.connectionString)) { using (SqlCommand cmd = new SqlCommand("SELECT Count(*) FROM Users WHERE Username LIKE @Username", connection)) { cmd.Parameters.Add("@Username", SqlDbType.NVarChar).Value = string.Format(CultureInfo.InvariantCulture, "%{0}%", usernameToMatch); connection.Open(); totalRecords = (int)cmd.ExecuteScalar(); if (totalRecords <= 0) { return users; } } using (SqlCommand cmd = new SqlCommand("SELECT Id, Username FROM Users WHERE Username LIKE @Username ORDER BY Username ASC", connection)) { cmd.Parameters.Add("@Username", SqlDbType.NVarChar).Value = string.Format(CultureInfo.InvariantCulture, "%{0}%", usernameToMatch); using (SqlDataReader reader = cmd.ExecuteReader()) { if (reader.HasRows) { int counter = 0; int startIndex = pageSize * pageIndex; int endIndex = startIndex + pageSize - 1; while (reader.Read()) { if (counter >= startIndex) { MembershipUser user = this.GetUserByReader(reader); users.Add(user); } if (counter >= endIndex) { cmd.Cancel(); } counter++; } } } } } } catch (Exception e) { if (this.WriteExceptionsToEventLog) { ArcGisServerMembershipProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name); throw new ProviderException(ExceptionMessage); } else { throw; } } return users; }
Il metodo GetAllUsers è per visualizzare tutti gli utenti presenti. Anche in questo caso abbiamo la gestione del paging:
/// <summary> /// get all users /// </summary> /// <param name="pageIndex">page Index</param> /// <param name="pageSize">page Size</param> /// <param name="totalRecords">total Records</param> /// <returns>objects MembershipUserCollection</returns> public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords) { MembershipUserCollection users = new MembershipUserCollection(); try { using (SqlConnection connection = new SqlConnection(this.connectionString)) { using (SqlCommand cmd = new SqlCommand("SELECT Count(*) FROM Users", connection)) { connection.Open(); totalRecords = (int)cmd.ExecuteScalar(); if (totalRecords <= 0) { return users; } } using (SqlCommand cmd = new SqlCommand("SELECT Id, Username FROM Users ORDER BY Username", connection)) { using (SqlDataReader reader = cmd.ExecuteReader()) { if (reader.HasRows) { int counter = 0; int startIndex = pageSize * pageIndex; int endIndex = startIndex + pageSize - 1; while (reader.Read()) { if (counter >= startIndex) { MembershipUser user = this.GetUserByReader(reader); users.Add(user); } if (counter >= endIndex) { cmd.Cancel(); } counter++; } } } } } } catch (Exception e) { if (this.WriteExceptionsToEventLog) { ArcGisServerMembershipProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name); throw new ProviderException(ExceptionMessage); } else { throw; } } totalRecords = users.Count; return users; }
Il metodo DeleteUser elimina l'utente. In ArcGIS Server Manager viene richiamato quando si clicca sul pulsante di eliminazione dell'utente:
/// <summary> /// delete user /// </summary> /// <param name="username">value of username</param> /// <param name="deleteAllRelatedData">delete All Related Data</param> /// <returns>true is ok</returns> public override bool DeleteUser(string username, bool deleteAllRelatedData) { int rowsAffected = 0; try { using (SqlConnection connection = new SqlConnection(this.connectionString)) { using (SqlCommand cmd = new SqlCommand("DELETE FROM Users WHERE Username = @Username", connection)) { cmd.Parameters.Add("@Username", SqlDbType.NVarChar).Value = username; connection.Open(); rowsAffected = cmd.ExecuteNonQuery(); } } } catch (Exception e) { if (this.WriteExceptionsToEventLog) { ArcGisServerMembershipProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name); throw new ProviderException(ExceptionMessage); } else { throw; } } return rowsAffected > 0; }
Il metodo GetUser restituisce l'utente una volta dato il suo username mentre il ValidateUser verifica se l'utente esiste. Per metodi che restituiscono gli oggetti della classe MembershipUser creeremo un oggetto fornendo al costruttore la username e il nome del provider mentre per gli altri argomenti forniremo dati fittizi o nulli poiché non gestiamo altre informazioni per l'utente (e-mail ecc.).
Per tutti gli altri metodi che non andiamo a riscrivere gettiamo un'eccezione di tipo NotImplementedException:
/// <summary> /// Get Number Of Users Online /// </summary> /// <returns>number of users online</returns> public override int GetNumberOfUsersOnline() { throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "Method not implemented: {0}", MethodBase.GetCurrentMethod().Name)); }
Vediamo ora nel dettaglio il RoleProvider.
Anche qui nell'Initialize come per il Membership Provider vengo passate le proprietà di configurazione che in questo caso d'esempio sono le stesse: stringa di connessione del database e notifica di errori nell'event viewer o direttamente nella pagina dell'ArcGIS Manager. Pertanto il metodo è analogo a quello del Membership Provider.
Il metodo CreateRole crea il ruolo e viene richiamato quando in ArcGIS Server Manager si clicca sul pulsante New Role. Attenzione che il tipo di ruolo - cioè se User, Publisher o Administrator - è un'informazione che memorizza ArcGIS Server autonomamente.
/// <summary> /// create a role /// </summary> /// <param name="roleName">role name</param> public override void CreateRole(string roleName) { try { if (this.RoleExists(roleName)) { throw new ProviderException("Role exists!"); } using (SqlConnection connection = new SqlConnection(this.connectionString)) { using (SqlCommand cmd = new SqlCommand("INSERT INTO Roles (Rolename) VALUES (@Role)", connection)) { cmd.Parameters.Add("@Role", SqlDbType.NVarChar).Value = roleName; connection.Open(); cmd.ExecuteNonQuery(); } } } catch (Exception e) { if (this.WriteExceptionsToEventLog) { ArcGisServerRoleProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name); } else { throw; } } }
I metodi DeleteRole e GetAllRoles sono analoghi a quelli del Membership Provider e servono per eliminare un ruolo e listare tutti i ruoli presenti.
I metodi GetRolesForUser e GetUsersInRole vengono utilizzati da ArcGIS Server Manager per visualizzare l'associazione tra utenti e ruoli dato l'utente o il ruolo.
/// <summary> /// Get of roles for user /// </summary> /// <param name="username">name of user</param> /// <returns>list of roles</returns> public override string[] GetRolesForUser(string username) { List<string> roles = new List<string>(); try { using (SqlConnection connection = new SqlConnection(this.connectionString)) { using (SqlCommand cmd = new SqlCommand("SELECT Roles.Rolename FROM Roles INNER JOIN UsersRoles ON Roles.Id = UsersRoles.IdRolename INNER JOIN Users ON UsersRoles.IdUsername = Users.Id WHERE Users.Username = @UserName ORDER BY Roles.Rolename", connection)) { cmd.Parameters.Add("@UserName", SqlDbType.NVarChar).Value = username; connection.Open(); using (SqlDataReader reader = cmd.ExecuteReader()) { if (reader.HasRows) { while (reader.Read()) { roles.Add(reader.GetString(0)); } } } } } } catch (Exception e) { if (this.WriteExceptionsToEventLog) { ArcGisServerRoleProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name); throw new ProviderException(ExceptionMessage); } else { throw; } } return roles.ToArray(); } /// <summary> /// Get Users In Role /// </summary> /// <param name="roleName">name of role</param> /// <returns>list of users</returns> public override string[] GetUsersInRole(string roleName) { List<string> users = new List<string>(); try { using (SqlConnection connection = new SqlConnection(this.connectionString)) { connection.Open(); using (SqlCommand cmd = new SqlCommand("SELECT Users.Username FROM Roles INNER JOIN UsersRoles ON Roles.Id = UsersRoles.IdRolename INNER JOIN Users ON UsersRoles.IdUsername = Users.Id WHERE (Roles.Rolename = @RoleName) ORDER BY Users.Username", connection)) { cmd.Parameters.Add("@RoleName", SqlDbType.NVarChar).Value = roleName; using (SqlDataReader reader = cmd.ExecuteReader()) { if (reader.HasRows) { while (reader.Read()) { users.Add(reader.GetString(0)); } } } } } } catch (Exception e) { if (this.WriteExceptionsToEventLog) { ArcGisServerRoleProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name); throw new ProviderException(ExceptionMessage); } else { throw; } } return users.ToArray(); }
Infine il metodo RemoveUsersFromRoles rimuove l'associazione ruolo - utente quando c'è una eliminazione di un ruolo o di un utente e pertanto viene chiamata prima del metodo di eliminazione utente o ruolo. Se abbiamo impostato l'azione di CASCATE in eliminazione questo metodo può anche non essere riscritto.
/// <summary> /// Remove Users From Roles /// </summary> /// <param name="usernames">list of users</param> /// <param name="roleNames">list of roles</param> public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames) { SqlTransaction transaction = null; try { int[] idUsers = this.Users(usernames); int[] idRoles = this.Roles(roleNames); using (SqlConnection connection = new SqlConnection(this.connectionString)) { connection.Open(); transaction = connection.BeginTransaction(MethodBase.GetCurrentMethod().Name); SqlCommand cmd = connection.CreateCommand(); cmd.Connection = connection; cmd.Transaction = transaction; foreach (int u in idUsers) { foreach (int r in idRoles) { cmd.CommandText = "DELETE FROM UsersRoles" + " WHERE IdRolename = @IdRole AND IdUsername = @IdUser"; cmd.Parameters.Clear(); cmd.Parameters.Add("@IdRole", SqlDbType.Int).Value = r; cmd.Parameters.Add("@IdUser", SqlDbType.Int).Value = u; cmd.ExecuteNonQuery(); } } transaction.Commit(); } } catch (Exception e) { if (this.WriteExceptionsToEventLog) { ArcGisServerRoleProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name); } else { throw; } if (transaction != null) { try { transaction.Rollback(); } catch (Exception e2) { if (this.WriteExceptionsToEventLog) { ArcGisServerRoleProvider.WriteToEventLog(e2, MethodBase.GetCurrentMethod().Name); } else { throw; } } } } }
Il seguente metodo di supporto scrive le eventuali eccezioni nell'event viewer di Windows:
/// <summary> /// WriteToEventLog /// A helper function that writes exception detail to the event log. Exceptions /// are written to the event log as a security measure to avoid private database /// details from being returned to the browser. If a method does not return a status /// or boolean indicating the action succeeded or failed, a generic exception is also /// thrown by the caller. /// </summary> /// <param name="e">object exception</param> /// <param name="action">value of action</param> private static void WriteToEventLog(Exception e, string action) { try { EventLog log = new EventLog(); log.Source = ArcGisServerMembershipProvider.EventSource; log.Log = ArcGisServerMembershipProvider.EventLog; string message = "An exception occurred communicating with the data source.\n\n"; message += "Action: " + action + "\n\n"; message += "Exception: " + e.ToString(); log.WriteEntry(message); } catch { throw; } }
Se si opta per l'event viewer occorre registrare l'event source. E' possibile anche tramite riga di comando aprendo la console di Powershell e digitando:
New-EventLog -LogName Application -Source AGSMembershipProvider
New-EventLog -LogName Application -Source AGSRoleProvider
mentre per deregistrali:
Remove-EventLog -Source AGSMembershipProvider
Remove-EventLog -Source AGSRoleProvider
In questo esempio abbiamo utilizzato l'event log Application. In questo caso con il filtro sulla source possiamo crearci una Custom View per facilitarci la lettura.
La dll, una volta firmata e compilata, la registriamo nella GAC tramite il comando:
gacutil /i ArcGisServerCustomProvider.dll
o se non abbiamo sul server l'utility gacutil (ad esempio non è installato l'sdk .NET) possiamo tramite drag and drop trascinarla nella cartella di assembly.
Per disinstallarla:
gacutil /u ArcGisServerCustomProvider
oppure dalla cartella di assembly selezionare l'assembly e tramite il menu che compare cliccando il tasto destro del mouse cliccare su disinstalla.
A questo punto registriamo il nostro custom identity store in ArcGIS Server Administrator.
La configurazione dovrà essere in formato json.
Le proprietà sono:
type: indicare 'ASP_NET'
class: indicare i riferimenti dell'assembly e della classe che implementa il Membership e il Role
properties: lista delle proprietà che verranno passate all'initialize
Pertanto per lo user store configuration sarà:
{
"type": "ASP_NET",
"class": "ArcGisServerCustomProvider.ArcGisServerMembershipProvider,ArcGisServerCustomProvider,Version=1.0.0.0,Culture=Neutral,PublicKeyToken=e70ef8c9eb62a069",
"properties": {
"connectionStringName": "Data Source=.\\SQLEXPRESS;Initial Catalog=YourDB;User Id=YourUser;Password=YourPwd;",
"writeExceptionsToEventLog": "true"
}
}
mentre per il role store configuration sarà:
{
"type": "ASP_NET",
"class": "ArcGisServerCustomProvider.ArcGisServerRoleProvider,ArcGisServerCustomProvider,Version=1.0.0.0,Culture=Neutral,PublicKeyToken=e70ef8c9eb62a069",
"properties": {
"connectionStringName": "Data Source=.\\SQLEXPRESS;Initial Catalog=YourDB;User Id=YourUser;Password=YourPwd;",
"writeExceptionsToEventLog": "true"
}
}
Consiglio: prima di aggiornare la configurazione testare il controllo formale dei propri json tramite strumenti tipo jsonlint
Qui è possibile scaricare la soluzione completa.
8 commenti:
Salve,
ho sviluppato anch'io un custom membership provider per utilizzarlo in ArcGis Server, molto simile a quello di questo post, a parte l'accesso al database per cui io utilizzo l'Entity Framework.
Tutto funziona, tranne l'aggiunta o rimozione di utenti ad un ruolo, operazione per cui l'interfaccia del server non dà nessun errore ma non aggiunge o rimuove gli utenti dai ruoli. Ho aggiunto dei messaggi di log e pare che il metodo AddUsersToRoles non venga neppure chiamato e anche controllando l'accesso al db tramite profiler non si muove assolutamente niente... Qualche idea di quale potrebbe essere il motivo? Io non so più dove sbattere la testa... Incollo qui sotto il codice del metodo incriminato:
public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
using (MyEntities _db = new MyEntities(connectionString))
{
foreach (string userName in usernames)
{
var wUser = (from dbUser in _db.Users
where dbUser.LoginName == userName
select dbUser).FirstOrDefault();
if (wUser != null)
{
foreach (string wCategoryName in roleNames)
{
UserCategory wCategory = (from dbCategory in _db.UserCategories
where dbCategory.Name == wCategoryName
select dbCategory).FirstOrDefault();
if (wCategory != null)
{
wCategory.Users.Add(wUser);
}
}
}
}
_db.SaveChanges();
}
}
Hai provato ad utilizzare, per semplice test, la mia soluzione che ho già installato su più macchine e funziona?
Che versione di arcgis server stai utilizzando ?
Sto usando la 10.3.1.
Per il momento non ho ancora avuto modo di provare la tua soluzione perchè altri colleghi hanno bisogno di ArcGis e non posso cambiare l'autenticazione in questo momento, peró nei prossimi giorni spero di trovare un attimo per fare la prova e ti diró.
Ho provato, ma nel momento in cui provo ad aggiornare la membership di ArcGis con la tua mi dá questo errore:
Failed to update the identity store configuration. Could not configure the identity store as one or more of the supplied parameters is incorrect. Verify that you can connect to the identity store outside of ArcGIS Server using the same parameters.
Faró altre prove...
Ok, non avevo configurato bene i permessi del db, effettivamente il tuo provider funziona perfettamente. Ora si tratta di capire cosa c'è di diverso che non fa funzionare il mio...
Ok, ora forse dovrebbe essere più facile capire qual è il problema.
Risolto!!
Incredibilmente, il problema era nel MembershipProvider, non nel RoleProvider.
Prima di aggiornare i ruoli di appartenenza, ArcGis Server apparentemente chiama il metodo GetUser(string username, bool userIsOnline) passando userIsOnline=false.
Io nella mia implementazione di questo metodo ritornavo erroneamente l'utente solo se era online, a prescindere dal valore del parametro userIsOnline. Così ArcGis non ritrovava l'utente e invece che dare errore semplicemente non faceva nulla!
Grazie per aver messo a disposizione il tuo codice, non so quanto tempo in più avrei perso senza avere una implementazione funzionante da confrontare!
L'importante che tu abbia risolto! Mi fa piacere esserti stato d'aiuto 'indirettamente'
Posta un commento