Formale Code Qualität

Auf einem der letzten “SDX Office Day” (eine monatliche, interne Veranstaltung der SDX mit technischen Vorträgen, Workshops und Diskussionsrunden) am 9.5. habe ich eine Einführung in formaler Code-Qualität geben können.

Über die Qualität von Quellcode (und im Speziellen die Messung derselben  gibt es wahrscheinlich mindestens so viele Meinungen wie Entwickler – es gibt aber durchaus recht objektive Mindestanforderungen, die erfahrungsgemäß “aus Zeitgründen” häufig nicht berücksichtigt werden, dann aber in der Folge zu schwer wartbarem Code und zu Sicherheitsproblemen führen.

Die Themen des Vortrags und die Diskussion erstrecken sich von Standards (Coding-Standards, aber auch offene Standards für die Vokabular innerhalb der IT oder der Fach-Themen) und Analyse-Tools (statische und dynamische Code-Analyse) bis hin zu klaren Regeln für konkrete Code-Konstrukte (z.B. die explizite Angabe der Kultur beim Parsing von Zahlen) und Sicherheitsprinzipien.

Nicht nur in Software-Projekten, auch bei der Präsentation von Vorträgen gibt es das Problem der begrenzten Zeit – als letzter Vortrag des Tages hatte ich aber das Glück, den Raum so lange nutzen zu können, wie Zuhörer und Diskussionspartner bereit waren, sich dem Thema zu widmen – nach ca. 3h haben wir dann das Wochenende eingeleitet.

Vermeidung der OWASP Top 10 für eXperts

Auf einem der letzten “SDX Office Day” (eine monatliche, interne Veranstaltung der SDX mit technischen Vorträgen, Workshops und Diskussionsrunden) konnte ich die OWASP Top 10 (2013) wieder einem breiteren Entwickler-Publikum vorstellen.

Immer noch lag “Injection” im Jahr 2013 auf Platz 1. XSS hat mit “Broken Authentication and Session Management” (jetzt Platz 2) den Platz getauscht und lag 2013 auf Platz 3.

Zu jedem Punkt der Liste gab es Beispiele und Tipps, wie Probleme dieser Art im eigenen Projekt zu vermeiden sind. Zusätzlich konnten wir noch weitere Themen wie unsicheres Passwort-Hashing des ASP.NET Membership-Providers bei Einsatz des Visual Studio 2010 oder problematisches Scaffolding bei Generierung der ASP.NET MVC Controller abhandeln. Zum Abschluss gab es noch eine Übersicht nützlicher Tools für den Selbstversuch (z.B. Havij als eines meiner Lieblings-Tools für den Test auf SQL-Injection).

Unit-Tests für ASP.NET MVC ActionFilter

Unit-Tests sind wichtig – das sollte mittlerweile bei den meisten Entwicklern angekommen sein. Sie sind aber auch manchmal schwierig zu schreiben – zumal dann, wenn der eigene Code sich in Frameworks integriert oder sie erweitert. Häufig werden an dieser Stelle dann Objekte an den eigenen Code übergeben, die bereits einen recht komplexen Aufbau besitzen und deren Funktionalität auch nur mit viel Aufwand nachzubilden ist.

Gerade im Bereich des ASP.Net MVC gibt es einige Fälle, in denen das Testen der Funktionalität schwierig wird. Einen Einstieg in die Problematik bietet folgender Artikel: Building Testable ASP.NET MVC Applications.

Einen Fall, der mir aufgrund einiger Besonderheiten der beteiligten Framework-Klassen besondere “Herausforderungen” beschert hat, möchte ich im Folgenden schildern.

In einem aktuellen Hobby-Projekt habe ich ein ASP.Net-MVC-Controller-Attribut (leitet also von ActionFilterAttribute ab) geschrieben, welches mir beim Aufruf einer Action prüfen soll, ob eine zusätzliche Angabe mit geschickt wurde. In diesem Fall geht es um einen Authentifizierungsfaktor – das ist aber für die Unit-Test-Problematik nebensächlich.

Der Code der Klasse Arbeitet mit dem ActionExecutingContext, welcher an die virtuelle Methode OnActionExecuting übergeben wird, die von meiner Klasse überschrieben wird und in der über den UrlHelper eine neue Instanz eines RedirectResult zurück geliefert wird.

Wenn es nun ans Unit-Testing geht (ich verwende meist moq, welches auch als NuGet-Package verfügbar ist), wird schnell klar, dass die Abhängigkeiten, die man sich mit dem ActionExecutingContext einfängt gar nicht so einfach zu handhaben sind. So brauche ich zur Abfrage der Session-ID schon einen RequestContext mit HttpContext, der eine Session zur Verfügung stellt. Diese sind zwar genau für den Zweck des Unit-Testing von entsprechenden Basisklassen abgeleitet, besitzen aber voneinander abhängige Properties, für deren korrekte Befüllung man sich mit diesen auch erst mal genauer auseinander setzen muss, als dies für die Entwicklung der zu testenden Methode eigentlich notwendig wäre.

Der Aufbau eines ActionResult erfordert dann schon zusätzlich einen UrlHelper, welcher mit einem RequestContext und einer RouteCollection initialisiert wurde – diese RouteCollection muss wiederum eine Route enthalten, für welche die Properties Defaults, Constraints und DataToken entsprechend korrekt initialisierte Instanzen von RouteValueDictionary besitzen. Um also eine einzige Zeile selbst implementierten Codes zu testen, müssen ggf. mehr als 80 Zeilen Initialisierungsarbeit geleistet werden.

Unglücklicher Weise sagen die Exceptions bei falscher oder unvollständiger Initialisierung der Objekte allerdings nicht immer aus, was nicht korrekt vorbesetzt war, sondern quittieren den Aufruf mit einer NullReferenceException (die in sauber programmiertem Code übrigens NIE auftauchen sollte) irgendwo in den Tiefen des MVC-Frameworks.

Die erste Lösung des Problems lieferten diverse Beiträge auf stackoverflow (z.B.: Unit testing an ActionFilter – correctly setting up the ActionExecutingContext ). Der Einsatz von ReSharper zum Dekompilieren des Codes (der Sourcecode ist übrigens auf CodePlex unter https://aspnetwebstack.codeplex.com/ vollständig verfügbar – da ich aber im Zug an meinen privaten Projekten arbeite und dort häufig keine Internet-Verbindung vorhanden ist, ist ein Dekompilieren mit ReSharper häufig die einfachere Alternative) lieferte dann weitere Hinweise, was wie zu initialisieren sei.

Herausgekommen ist eine Methode, die mir einen kompletten ActionExecutingContext zurück liefert, so dass ich mein ActionFilterAttribute problemlos testen kann. Unit-Tests können somit den Kontext innerhalb einer Zeile Code erzeugen (inkl. Übergabe der URL, die simuliert werden soll):

   1: [TestMethod]

   2: public void DeliversImageForCorrectRequest()

   3: {

   4:     var uriString = "http://localhost/test/?42F...D00";

   5:     var uri = new Uri(uriString, UriKind.Absolute);

   6:     var context = MvcTestBase.CreateRequestContext(uri);

   7:     var target = new YubikeyCheckAttribute(

   8:         new YubikeyConfiguration(),

   9:         new YubicoClientAdapter())

  10:         {

  11:             ImageOnly = true

  12:         }; 

  13:     target.OnActionExecuting(context); var fileResult = context.Result as FileResult;

  14:     Assert.IsNotNull(fileResult); Assert.AreEqual("image/png", fileResult.ContentType);

  15: }

 
 

Der Stand der Methode, mit dem ein erstes Testen eines ActionFilterAttribute möglich ist, ist folgender (zum einfachen Kopieren und Wiederverwenden):

   1: /// <summary>

   2: /// The test base class for MVC tests. This class does provide some basic helpers for

   3: /// testing MVC controllers / filters etc.

   4: /// </summary>

   5: public class MvcTestBase

   6: {

   7:     /// <summary>

   8:     /// Creates a new request context that returns a distinct, 

   9:     /// but always the same, session id.

  10:     /// </summary>

  11:     /// <returns> The <see cref="Mock"/> object for the 

  12:     /// request context. </returns>

  13:     public static ActionExecutingContext CreateRequestContext()

  14:     {

  15:         var uri = new Uri("http://localhost/test", UriKind.Absolute);

  16:         var sessionId = Guid.NewGuid().ToString("N");

  17:         var clientIP = Guid.NewGuid().ToString("N");

  18:         return CreateRequestContext(uri, sessionId, clientIP);

  19:     }

  20:  

  21:     /// <summary>

  22:     /// Creates a new request context that returns a distinct, but 

  23:     /// always the same, session id.

  24:     /// </summary>

  25:     /// <param name="requestUrl">The URL that should be simulated 

  26:     /// to be requested.</param>

  27:     /// <returns> The <see cref="Mock"/> object for the 

  28:     /// request context.  </returns>

  29:     public static ActionExecutingContext CreateRequestContext(Uri requestUrl)

  30:     {

  31:         var sessionId = Guid.NewGuid().ToString("N");

  32:         var clientIP = Guid.NewGuid().ToString("N");

  33:         return CreateRequestContext(requestUrl, sessionId, clientIP);

  34:     }

  35:  

  36:     /// <summary>

  37:     /// Creates a new request context that returns a distinct, 

  38:     /// but always the same, session id.

  39:     /// </summary>

  40:     /// <param name="requestUrl">The URL the request should fake.</param>

  41:     /// <param name="sessionId"> The session Id for the request. </param>

  42:     /// <param name="clientIP"> The client IP for the request. </param>

  43:     /// <returns> The <see cref="Mock"/> object for the request context.  </returns>

  44:     public static ActionExecutingContext CreateRequestContext(Uri requestUrl, string sessionId, string clientIP)

  45:     {

  46:         var form = new NameValueCollection();

  47:         return CreateRequestContext(requestUrl, sessionId, clientIP, form);

  48:     }

  49:  

  50:     /// <summary>

  51:     /// Creates a new request context that returns a distinct, 

  52:     /// but always the same, session id.

  53:     /// </summary>

  54:     /// <param name="requestUrl">The URL the request should fake.</param>

  55:     /// <param name="sessionId"> The session Id for the request. </param>

  56:     /// <param name="clientIP"> The client IP for the request. </param>

  57:     /// <param name="formCollection">The collection of form data items.</param>

  58:     /// <returns> The <see cref="Mock"/> object for the request context.  </returns>

  59:     public static ActionExecutingContext CreateRequestContext(Uri requestUrl, string sessionId, string clientIP, NameValueCollection formCollection)

  60:     {

  61:         return CreateRequestContext(requestUrl, sessionId, clientIP, formCollection, "UserName");

  62:     }

  63:  

  64:     /// <summary>

  65:     /// Creates a new request context that returns a distinct, 

  66:     /// but always the same, session id.

  67:     /// </summary>

  68:     /// <param name="requestUrl">The URL the request should fake.</param>

  69:     /// <param name="sessionId"> The session Id for the request. </param>

  70:     /// <param name="clientIP"> The client IP for the request. </param>

  71:     /// <param name="formCollection">The collection of form data items.</param>

  72:     /// <param name="userName">The name of the user the identity should contain.</param>

  73:     /// <returns> The <see cref="Mock"/> object for the request context.  </returns>

  74:     public static ActionExecutingContext CreateRequestContext(Uri requestUrl, string sessionId, string clientIP, NameValueCollection formCollection, string userName)

  75:     {

  76:         object values = new

  77:         {

  78:             controller = "Home",

  79:             action = "Index",

  80:             id = UrlParameter.Optional

  81:         };

  82:         var requestContext = RequestContext(requestUrl, sessionId, clientIP, formCollection, values, userName);

  83:         var routes = RouteCollection(values);

  84:         var controller = Controller(requestContext, routes);

  85:         return new ActionExecutingContext { RequestContext = requestContext, Controller = controller, };

  86:     }

  87:  

  88:     /// <summary>

  89:     /// Creates an initialized <see cref="HtmlHelper"/> object.

  90:     /// </summary>

  91:     /// <returns>

  92:     /// The <see cref="HtmlHelper"/>.

  93:     /// </returns>

  94:     public static HtmlHelper CreateHtmlHelper()

  95:     {

  96:         var container = new Mock<IViewDataContainer>();

  97:         var formCollection = new NameValueCollection();

  98:         var sessionId = Guid.NewGuid().ToString("N");

  99:         var clientIP = Guid.NewGuid().ToString("N");

 100:         var uri = new Uri("http://test/");

 101:         var viewContext = ViewContext(uri, sessionId, clientIP, formCollection);

 102:         return new HtmlHelper(viewContext, container.Object);

 103:     }

 104:  

 105:     /// <summary>

 106:     /// Generates a request context.

 107:     /// </summary>

 108:     /// <param name="requestUrl"> The request url. </param>

 109:     /// <param name="sessionId"> The session id. </param>

 110:     /// <param name="clientIP"> The client IP. </param>

 111:     /// <param name="formCollection"> The http FORM collection. </param>

 112:     /// <param name="values"> The values for the route data. </param>

 113:     /// <param name="username"> The username. </param>

 114:     /// <returns> The request context"/>. </returns>

 115:     private static RequestContext RequestContext(Uri requestUrl, string sessionId, string clientIP, NameValueCollection formCollection, object values, string username)

 116:     {

 117:         var requestBase = RequestBase(requestUrl, clientIP, formCollection);

 118:         var httpContext = HttpContext(sessionId, requestBase, username);

 119:         return RequestContext(httpContext, values);

 120:     }

 121:  

 122:     /// <summary>

 123:     /// Creates an initialized controller.

 124:     /// </summary>

 125:     /// <param name="requestContext"> The request context for the controller. </param>

 126:     /// <param name="routes"> The routes for the <see cref="UrlHelper"/>. </param>

 127:     /// <returns> The <see cref="Mock"/>. </returns>

 128:     private static Controller Controller(RequestContext requestContext, RouteCollection routes)

 129:     {

 130:         var urlHelper = new UrlHelper(requestContext, routes);

 131:         var controller = new Mock<Controller>();

 132:         controller.Object.Url = urlHelper;

 133:         RouteTable.Routes.Clear();

 134:         foreach (var route in routes)

 135:         { RouteTable.Routes.Add(route); }

 136:         return controller.Object;

 137:     }

 138:  

 139:     /// <summary>

 140:     /// Generates a default route collection.

 141:     /// </summary>

 142:     /// <param name="values"> The default values. </param>

 143:     /// <returns> The <see cref="RouteCollection"/>. </returns>

 144:     private static RouteCollection RouteCollection(object values)

 145:     {

 146:         return new RouteCollection

 147:             {

 148:                 new Route("{controller}/{action}/{id}", new MvcRouteHandler())

 149:                 {      

 150:                     Defaults = new RouteValueDictionary(values),

 151:                     Constraints = new RouteValueDictionary((object)null),

 152:                     DataTokens = new RouteValueDictionary(),

 153:                 }      

 154:             };

 155:     }

 156:  

 157:     /// <summary>

 158:     /// Generates an initialized <see cref="HttpContextBase"/>.

 159:     /// </summary>

 160:     /// <param name="sessionId"> The session id. </param>

 161:     /// <param name="requestBase"> The request base. </param>

 162:     /// <param name="username"> The username for the identity of the current user. </param>

 163:     /// <returns> The <see cref="HttpContextBase"/>. </returns>

 164:     private static HttpContextBase HttpContext(string sessionId, HttpRequestBase requestBase, string username)

 165:     {

 166:         var httpContext = new Moq.Mock<HttpContextBase>();

 167:         httpContext.Setup(x => x.Session).Returns(SessionState(sessionId));

 168:         httpContext.Setup(x => x.Request).Returns(requestBase);

 169:         httpContext.Setup(x => x.Response).Returns(Response());

 170:         httpContext.Setup(x => x.User).Returns(User(username));

 171:         return httpContext.Object;

 172:     }

 173:  

 174:     /// <summary>

 175:     /// Creates an initialized <see cref="IPrincipal"/> for a given user name.

 176:     /// </summary>

 177:     /// <param name="username"> The username. </param>

 178:     /// <returns> The <see cref="IPrincipal"/>. </returns>

 179:     private static IPrincipal User(string username)

 180:     {

 181:         var user = new Mock<IPrincipal>();

 182:         var identity = new Mock<IIdentity>();

 183:         identity.Setup(x => x.Name).Returns(username);

 184:         user.Setup(x => x.Identity).Returns(identity.Object);

 185:         return user.Object;

 186:     }

 187:  

 188:     /// <summary>

 189:     /// Generates a ready to use <see cref="HttpResponseBase"/> object.

 190:     /// </summary>

 191:     /// <returns> The <see cref="Mock"/>. </returns>

 192:     private static HttpResponseBase Response()

 193:     {

 194:         var response = new Mock<HttpResponseBase>();

 195:         response.Setup(r => r.ApplyAppPathModifier(It.IsAny<string>()))

 196:         .Returns((string s) => s);

 197:         return response.Object;

 198:     }

 199:  

 200:     /// <summary>

 201:     /// Generates an initialized <see cref="HttpSessionStateBase"/>.

 202:     /// </summary>

 203:     /// <param name="sessionId"> The session id. </param>

 204:     /// <returns> The <see cref="HttpSessionStateBase"/>. </returns>

 205:     private static HttpSessionStateBase SessionState(string sessionId)

 206:     {

 207:         var sessionState = new Mock<HttpSessionStateBase>();

 208:         sessionState.Setup(x => x.SessionID).Returns(sessionId);

 209:         return sessionState.Object;

 210:     }

 211:  

 212:     /// <summary>

 213:     /// Generates an initialized <see cref="HttpRequestBase"/>.

 214:     /// </summary>

 215:     /// <param name="requestUrl"> The request url. </param>

 216:     /// <param name="clientIP"> The client IP this request should simulate. </param>

 217:     /// <param name="formCollection"> The form value collection. </param>

 218:     /// <returns> The <see cref="HttpRequestBase"/>. </returns>

 219:     private static HttpRequestBase RequestBase(Uri requestUrl, string clientIP, NameValueCollection formCollection)

 220:     {

 221:         var path = requestUrl == null ? string.Empty : requestUrl.AbsolutePath;

 222:         var requestBase = new Mock<HttpRequestBase>();

 223:         requestBase.Setup(x => x.ApplicationPath).Returns(path);

 224:         requestBase.Setup(x => x.AppRelativeCurrentExecutionFilePath).Returns("~" + path);

 225:         requestBase.Setup(x => x.CurrentExecutionFilePath).Returns("/");

 226:         requestBase.Setup(x => x.CurrentExecutionFilePathExtension).Returns(string.Empty);

 227:         requestBase.Setup(x => x.Form).Returns(formCollection);

 228:         requestBase.Setup(x => x.HttpMethod).Returns("GET");

 229:         requestBase.Setup(x => x.Path).Returns("/");

 230:         requestBase.Setup(x => x.RawUrl).Returns(path);

 231:         requestBase.Setup(x => x.RequestType).Returns("GET");

 232:         requestBase.Setup(x => x.Url).Returns(requestUrl);

 233:         requestBase.Setup(x => x.UserHostAddress).Returns(clientIP);

 234:         requestBase.Setup(x => x.UserHostName).Returns(clientIP);

 235:         return requestBase.Object;

 236:     }

 237:  

 238:     /// <summary>

 239:     /// Generates an initialized Request Context.

 240:     /// </summary>

 241:     /// <param name="httpContext"> The http context. </param>

 242:     /// <param name="values"> The route values. </param>

 243:     /// <returns> The Request Context. </returns>

 244:     private static RequestContext RequestContext(HttpContextBase httpContext, object values)

 245:     {

 246:         var requestContext = new Moq.Mock<RequestContext>();

 247:         requestContext.Setup(x => x.HttpContext).Returns(httpContext);

 248:         requestContext.Setup(x => x.RouteData).Returns(RouteData(values));

 249:         return requestContext.Object;

 250:     }

 251:  

 252:     /// <summary>

 253:     /// Generates an initialized <see cref="RouteData"/>.

 254:     /// </summary>

 255:     /// <param name="values"> The values. </param>

 256:     /// <returns> The <see cref="RouteData"/>. </returns>

 257:     private static RouteData RouteData(object values)

 258:     {

 259:         var routeData = new RouteData

 260:         {

 261:             Route = new Route("{controller}/{action}/{id}", new MvcRouteHandler())

 262:             {

 263:                 Defaults = new RouteValueDictionary(values),

 264:                 Constraints = new RouteValueDictionary((object)null),

 265:                 DataTokens = new RouteValueDictionary()

 266:             },

 267:         };

 268:         routeData.Values.Add("controller", "Home");

 269:         routeData.Values.Add("action", "Index");

 270:         return routeData;

 271:     }

 272:  

 273:     /// <summary>

 274:     /// Generates an initialized <see cref="ViewContext"/>.

 275:     /// </summary>

 276:     /// <param name="requestUrl"> The request URL. </param>

 277:     /// <param name="sessionId"> The session id. </param>

 278:     /// <param name="clientIP"> The client IP. </param>

 279:     /// <param name="formCollection"> The form value collection. </param>

 280:     /// <returns> The <see cref="ViewContext"/>. </returns>

 281:     private static ViewContext ViewContext(Uri requestUrl, string sessionId, string clientIP, NameValueCollection formCollection)

 282:     {

 283:         object values = new

 284:         {

 285:             controller = "Home",

 286:             action = "Index",

 287:             id = UrlParameter.Optional

 288:         };

 289:         var viewContext = new Mock<ViewContext>();

 290:         var requestContext = RequestContext(requestUrl, sessionId, clientIP, formCollection, values, "userName");

 291:         var routes = RouteCollection(values);

 292:         var controller = Controller(requestContext, routes);

 293:         viewContext.Setup(x => x.Controller).Returns(controller);

 294:         return viewContext.Object;

 295:     }

 296: }

 

Der aktuelle Stand (der auch die Verwendung des UrlHelper im Test-Objekt ermöglicht) kann jeder Zeit auch meinem CodePlex-Projekt entnommen werden: semauthentication.codeplex.com

Entwickler zurück auf die Schulbank – Code-Security

Das vergangene Jahr hat mal wieder durch zahlreiche Daten-Lecks gezeigt, dass angreifbare Software für Unternehmen (und deren Kunden) sehr problematisch ist. Als eines der aktuellen großen “Opfer” der Fehleinschätzung eines “potentiellen Problems” lässt sich Snapchat anführen. Dort wurde die Warnung vor einer Ausspähung von Account-Namen und Telefonnummern der Teilnehmer als “Theoretically” abgetan – mittlerweile steht die Datenbank im Netz zum Download bereit.

Solche vermeidbaren Datenpannen müssen nicht sein, wenn die Entwickler entsprechend geschult sind und die Applikationsverantwortlichen auf entsprechende Warnhinweise mit der gebotenen Sensibilität reagieren. Dass dies kein Einzelfall ist, zeigt eine aktuelle Analyse von IOActive Research, in der allein 40% der getesteten Mobile Banking Apps die Gültigkeit von SSL-Zertifikaten gar nicht erst prüfen – ein klares Zeichen dafür, dass selbst Grundlagen von Applikationssicherheit bei der Entwicklung der Applikationen keine Beachtung gefunden haben und die Applikationen keinen Sicherheits-Review-Prozess durchlaufen haben (zumindest keinen, der auch tatsächlich relevante Dinge prüft).

Ein guter Startpunkt für den Knowhow-Aufbau ist die Webseite der Organisation “OWASP”. Hier gibt es nicht nur die TOP 10 der Webentwicklungssünden (OWASP TOP 10 – die übrigens auch im Jahr 2013 immer noch von “Injection flaws” angeführt wird), sondern auch viele Informationen darüber, wie man als Entwickler eben diesen Problemen vorbeugen kann. Spezifische Informationen zu einzelnen Technologien gibt es auch auf verschiedenen Blogs (exemplarisch für den .Net-Bereich sei hier der Blog von Troy Hunt zu nennen).

Aber allein die Fakten zu kennen reicht in diesem Bereich bei weitem nicht. Die Absicherung von Web-Applikationen gegen Missbrauch gleicht einem ungleichen Wettkampf, bei dem der Verteidiger sich an Regeln zu halten hat und jeden Aspekt der Angriffsfläche über den kompletten Lebenszyklus der Anwendung verteidigen muss, während der Angreifer zu einem beliebigen Zeitpunkt nur eine einzelne Lücke zu erkennen braucht. So sollte bereits bei der Planung der Software analysiert werden, welche Daten welchen Schutzbedarf haben, welche Systeme wie gefährdet werden können und welche Maßnahmen diesen Gefahren entgegen wirken können.

Die beste Planung der Sicherheit ist aber nutzlos, wenn die Implementierung der beschlossenen Maßnahmen nicht erfolgt. Code Reviews durch einen (unvoreingenommenen) Spezialisten, Training der Entwickler und Test der Applikation auf Verwundbarkeiten über Penetrations-Tests stellen sicher, dass es nicht nur bei einer guten Planung bleibt, sondern dass die saubere Umsetzung der Maßnahmen erfolgt.

Maximal 16 Zeichen Sicherheit bei Windows Live-Account-Passwörtern

Die SDX AG verwendet für technische Passwörter (also solche, die nur extrem selten von Menschen verwendet werden) meist eine Kette zufälliger Zeichen, die länger als 20 Zeichen ist, damit erzwungen wird, dass keine LAN Manager Hashes erstellt werden (diese werden per Default nicht mehr erstellt, wenn das Betriebssystem Vista oder höher bzw. Server 2008 oder höher ist[1] – die Regel mehr als 14 Zeichen zu verwenden sollte aus Sicherheitsgründen aber trotzdem eingehalten werden).

Bei der heutigen Anmeldung an Windows Azure bekam ein Kollege nun folgende Meldung, die ihn etwas verwunderte:

LogOnScreen

Wir dürfen also als Passwort nur 16 Zeichen eingeben – und wenn unser ursprünglich gewähltes Passwort länger ist, sollen wir einfach die ersten 16 Zeichen eingeben … das passt dann schon.

Das wirft erst mal die Frage auf, wie mit den ersten 16 Zeichen eine Passwort-Validierung durchgeführt werden kann, wenn das Passwort als “Salted Hash” in der Authentisierungsdatenbank liegt? Wir haben ja gelernt: man speichert keine Passwörter ab, sondern nur einen Hash-Wert, nachdem das Passwort mit einem anderen bekannten Text verknüpft wurde (eben dem “Salt”, welches für jeden Wert wieder anders sein sollte) … wir wollen ja nicht, dass aus der Passwort-Datenbank der Datenbank-Administrator sämtliche Passwörter rekonstruieren kann.

Die Antwort liefert folgender Eintrag in der Support-Datenbank: “Getting error Microsoft account passwords can contain up to 16 characters?”:

Your password has not been shortened.  Windows Live ID passwords have always been limited to 16 characters.  What has changed is the login page now gives you immediate feedback to ensure you understand your password is not more than 16 characters.

Was bedeutet das nun für die Sicherheit von Windows-Live-IDs?

  1. Die Sicherheit von Passwörtern für Windows-Live-IDs ist auf 16 Zeichen beschränkt.
  2. Die Passwörter “können” als “Salted Hash” gespeichert werden und trotzdem reichen die ersten 16 Zeichen aus, um das Passwort zu prüfen – das Kürzen auf 16 Zeichen wurde ja vor der ursprünglichen Hash-Berechnung durchgeführt.
  3. Die Menge an effektiv genutzten Hash-Ergebnissen bezogen auf die Menge der potentiellen Passwörter ist extrem reduziert. Hash-Algorithmen sind keine Transformationen, die eine 1:1-Beziehung zwischen Eingabe und Ausgabe herstellen, sondern eine unendlich große Menge an Eingabe-Werten wird auf eine endliche Menge an Ergebniswerten “projiziert”.
  4. Wenn man bedenkt, dass eine der Strategien zur Bildung “unknackbarer” Passwörter die Verwendung von Passphrasen ist (z.B. enthält “Das Passwort ist nicht knackbar, selbst wenn Du dich noch so sehr anstrengst!” bereits 77 Zeichen mit Sonderzeichen, kleinen und großen Buchstaben und ist trotzdem leicht zu merken – einer Brute-Force-Attack hält dieses Passwort aber sehr lange stand), werden von diesen Passphrasen ja auch immer nur die ersten 16 Zeichen verwendet (in diesem Fall also “Das Passwort ist”), was dazu führt, dass sehr viele Passphrasen im Endeffekt das selbe gekürzte Passwort verwenden.

In einer extrem einfachen “Hash”-Funktion (Addition aller Dezimal-Stellen Modulo 100) könnten solch eine Kürzung die Passwörter “1234” und “4321” den selben “Hash”-Wert haben (in diesem Beispiel 10). Wenn ich jetzt das Passwort “12345” ausprobiere, komme ich auf einen anderen Wert (15). Schneide ich aber nach 4 Zeichen das Passwort ab, hat auch “12345” den selben “Hash”-Wert – nämlich 10. Zudem ist die Menge der erzeugbaren “Hash”-Werte eingeschränkt, da der größte effektive Eingangs-Wert 9999 ist – mit dem “Hash”-Wert 36. Einen Hash-Wert 50 kann ich gar nicht mehr erzeugen, da ich mit der Summe von 4 Zeichen diesen Zahlenwert gar nicht mehr erreichen kann. Kürze ich also das Passwort, dann fallen auch eine ganze Menge an möglichen Hash-Werten weg – die Wahrscheinlichkeit steigt also, dass ich den Hash-Wert für ein unbekanntes Passwort erraten kann. In wie weit dieser letzte Punkt für Live-ID relevant ist, hängt von dem verwendeten Algorithmus ab.

Was bedeutet das für mich? Ganz ehrlich: Unverständnis.

Die in dem Forums-Eintrag zur Live-ID gegebene Erklärung ist:

We are working on increasing the maximum password length. Unfortunately, for historical reasons, the password validation logic is decentralized …

Das “increasing the maximum password length” macht mich skeptisch, ob das Ergebnis dieser Bemühungen für mich akzeptabel sein wird. Aus meiner Sicht darf es gar keine solche Begrenzung geben: Das Passwort sollte als Hash abgelegt werden und dabei ist die Länge des verwendeten Passworts irrelevant: Wenn wir z.B. SHA-256 als eine einigermaßen sichere Hash-Algorithmus-Familie annehmen, kommen wir konstante 256 Bit für die zu speichernde Struktur (+ Salt). Selbst wenn ich den kompletten Text-Inhalt von Goethes Faust als Passwort verwenden würde – die Berechnung des Hash würde eine Millisekunde länger dauern, das Ergebnis wären aber immer noch 256 Bit.

Immerhin werden die Nutzer von LiveID jetzt ordnungsgemäß auf die stark eingeschränkte Passwort-Sicherheit hingewiesen.

Code-Signing-Zertifikate für Sideloading von Windows 8 Store Anwendungen

Zur Erstellung von “Windows Store”-Anwendungen muss die erstellte Anwendung signiert werden. Dafür wird ein Code-Signing-Zertifikat benötigt, welches im Falle des Sideloadings von Applikationen (Bereitstellung von eigenen Applikationen innerhalb eines Unternehmens, ohne den Windows 8 App-Store zu benutzen) auch ein eigenes Zertifikat sein kann. Dieses kann durch die Zertifikatsautorität des Unternehmens ausgestellt werden. Die Vorgehensweise wird z.B. hier (von Microsoft) ausführlich beschrieben.

Was Microsoft dort aber verschweigt ist, dass das zu verwendende Zertifikat auch eine Mindestschlüssellänge aufweisen muss – wir verwenden aktuell 4096 Bit. Wie kann ich das aber bewirken, wenn diese Option im Dialog zum Anfordern des Zertifikats ausgegraut ist und aufgrund des Alters des Zertifikat-Templates dieses noch mit 1024 Bit ausgestellt wird?

ReuquestProperties

Die Schlüssellänge wird in diesem Fall durch Template das für die Zertifikats-Anforderung verwendet wird bestimmt – und das liegt auf der Zertifikats-Autorität. In der Server-Verwaltung der Zertifikats-Autorität kann man die Templates verwalten:

CertAuthority01

Und in dieser Verwaltung ein neues Template durch Duplizieren des ursprünglichen “Code Signing”-Templates ein neues Template anlegen:

CertAuthority02

Das 2008’ter Format bietet mehr Möglichkeiten.

CertAuthority03

Hier lässt sich die Schlüssellänge (und auch der Hash-Algorithmus) anpassen:

CertAuthority04

Und auch die “Basic Constraints” per Default einschalten (das müsste sonst im Dialog der Zertifikats-Anforderung geschehen).

CertAuthority05

Nach der Bestätigung mit der Constraints und Bestätigung des Duplizierungs-Dialogs mit OK muss dieses neue Template allerdings auch noch “aktiviert” (bereitgestellt) werden. Dazu verwendet man wieder die Server-Verwaltung und kann über das Kontext-Menü den entsprechenden Dialog aufrufen:

CertAuthority06

CertAuthority07

Nach dem OK-Klick steht das neue Template bereit und kann für die Generierung von Code-Signing-Zertifikaten verwendet werden.

Diese Code-Signing-Zertifikate sind dann natürlich nicht durch eine öffentliche CA bestätigt, sondern nur innerhalb des Active-Directory gültig – aber gerade für die Bereitstellung von Metro-Apps (im Visual Studio heißen sie “Windows Store App”) innerhalb von Unternehmen sind diese Zertifikate optimal. Für Geräte, die nicht in das AD integriert sind, kann auch das Root-Zertifikat der CA in den Zertifikats-Store dieser Geräte importiert werden – dann wird auch diesen Code-Signing-Zertifikaten vertraut.

JavaScript und “Guter Stil”?

StyleCop entwickelt sich (glücklicherweise) immer mehr zum Standard-Tool, wenn es um Coding-Style in C# geht. Doch heutige Applikationen bestehen nicht mehr nur aus einer Sprache: im Web-Bereich gab es immer schon eine Teilung in JavaScript, CSS und HTML als "Quell-Code" einer Anwendung; auf dem Desktop wird mit Windows 8 ebenfalls JavaScript eine Rolle spielen. Wie geht man nun mit den immer größer werdenden Code-Bereichen in JavaScript um?

JavaScript ist Applikations-Quellcode und hat damit auch die gleichen Aufgaben und "Probleme" wie C#-Quellcode. An JavaScript niedrigere Qualitätsanforderungen zu stellen, als an C#-Quellcode lässt sich unter dieser Annahme schlecht rechtfertigen. Leider gibt es zur Gestaltung des Quellcodes in JavaScript mindestens genau so viele Meinungen, wie es Menschen gibt, die JavaScript-Quellcode schreiben.

Ein recht interessanter Artikel dazu findet sich unter http://addyosmani.com/blog/javascript-style-guides-and-beautifiers/, wobei ich nicht nur die genannten Style-Guides, sondern vor allem auch die Kommentare interessant finde – Coding-Style kann sehr emotional diskutiert werden.

Eine Auswahl an Style-Guides:

Welcher Style-Guide ist jetzt der richtige? Das lässt sich aktuell noch nicht sagen. Ich mag "Crock’s Code Conventions For JavaScript", da er in einigen Bereichen den Vorschlägen von StyleCop (und damit einem von mir bereits eingesetztem Coding-Style) entspricht. Ein klarer Vorteil für einen der Guides wäre eine Tool-Unterstützung für Visual Studio – die habe ich bisher noch für keinen der Styles gefunden. ReSharper bietet die Möglichkeit, auch für JavaScript Guidelines zu definieren, ist aber kostenpflichtig und kommt wieder mit einem eigenen Set an Konventionen. Es bleibt also die Hoffnung, dass sich ein konsistenter Style durchsetzt und entsprechende Tools diesen in Visual Studio auch erzwingen können. Welchen Coding-Style verwenden Sie?

Deklarativer Ansatz mit Code-Attributen

Die Verwendung von Custom-Code-Attributen kann die Übersichtlichkeit von Quellcode durch kompaktere Schreibweise deutlich erhöhen und unübersichtliche Switch-Statements und die Verteilung von zusammen gehörigen Informationen über mehrere Dateien hinweg verhindern.

Der folgende Programm-Code zeigt exemplarisch, wie Informationen über Objekte häufig verarbeitet werden: das Property „Class“ der Instanz von „Device“ wird in Switch-Statements ausgewertet, um Meta-Informationen (Name, Beschreibung) des verwendeten Wertes auszugeben.

namespace SwitchSample
{
    using System;


    class Program
    {
        static void Main()
        {
            var device = new Device { Class = SwitchApproach.DeviceClass.WebServer };
            SwitchApproach.PrintDetails(device);
            Console.ReadLine();
        }
    }

    public class Device
    {
        public SwitchApproach.DeviceClass Class { get; set; }
    }

    public static class SwitchApproach
    {
        public static void PrintDetails(Device device)
        {
            Console.WriteLine(GetUserFriendlyName(device.Class));
            Console.WriteLine(GetDescription(device.Class));
        }

        public enum DeviceClass
        {
            WebServer,
            Switch,
            Router,
            AppServer,
            Client,
        }

        private static string GetDescription(DeviceClass device)
        {
            switch (device)
            {
                case DeviceClass.AppServer:
                    return "- server that is accessible from another server";
                case DeviceClass.Client:
                    return "- machine that might access other machines, 
                              but does not host any service";
                case DeviceClass.WebServer:
                    return "- server that is accessible via http";
                case DeviceClass.Router:
                    return "- ip-based packet routing device";
                default:
                    return "Some computing device";
            }
        }

        private static string GetUserFriendlyName(DeviceClass device)
        {
            switch (device)
            {
                case DeviceClass.WebServer:
                    return "Web-Server";
                case DeviceClass.Switch:
                    return "Network Switch";
                case DeviceClass.AppServer:
                    return "Application Server";
                case DeviceClass.Client:
                    return "End User Client";
                default:
                    return "Some computing device";
            }
        }
    }
}

Daran ist zunächst mal nichts falsches – wenn man von dem Problem absieht, dass für den „Switch“ kein Name und für den „Router“ keine Beschreibung vorhanden ist (bemerkt?). Solche Probleme sind schwierig zu erkennen, weil zusammengehörige Informationen (Name, Beschreibung, Definition der Enum) in verschiedene Programm-Abschnitte verteilt wurden – in Real-World-Szenarien sind das meinst auch noch verschiedene Klassen/Assemblies.

Eine Lösung des Problems wäre die Implementierung einer Basis-Klasse oder besser eines Interfaces (die Basisklasse hat den Nachteil, dass in C# nur von einer Klasse geerbt werden kann), welches die Informationen zur Verfügung stellt. Nachteile dieser Lösung sind, dass

  1. dieses Interface statische Inhalte zurück liefert und in der Implementierung der Klasse viel Platz verbraucht (und somit die Lesbarkeit sinkt)
  2. die Enum in eine Anzahl von Klassen zerfällt. Im Beispiel sind das 5 – in „Real World“-Szenarien dürfte die Anzahl aber stark steigen.

Der folgende Ansatz verwendet hingegen selbst definierte Code-Attribute, um die Informationen Name und Beschreibung direkt an die Enum-Mitglieder zu knüpfen:

namespace AttributeSample
{
    using System;
    using System.Linq;

    class Program
    {
        static void Main()
        {
            var device = new Device { Class = AttributeApproach.DeviceClass.WebServer };
            AttributeApproach.PrintDetails(device);
            Console.ReadLine();
        }
    }

    public class Device
    {
        public AttributeApproach.DeviceClass Class { get; set; }
    }

    public static class AttributeApproach
    {
        public static void PrintDetails(Device device)
        {
            Console.WriteLine(device.Class.GetAttrib<FriendlyNameAttribute>().Name);
            Console.WriteLine(device.Class.GetAttrib<DescriptionAttribute>().Description);
        }

        public enum DeviceClass
        {
            [FriendlyName(Name = "Web-Server")]
            [Description(Description = "- server that is accessible via http")]
            WebServer,

            [Description(Description = "- networking device for distributing 
                                          ethernet packages")]
            Switch,
            
            [FriendlyName(Name = "Nerwork Router")]
            Router,
            
            [FriendlyName(Name = "Application Server")]
            [Description(Description = "- server that is accessible from another server")]
            AppServer,
            
            [FriendlyName(Name = "EndUser Client")]
            [Description(Description = "- machine that might access other machines,
                                          but does not host any service")]
            Client,
        }
    }

    public static class Extension
    {
        public static TAttribute GetAttrib<TAttribute>(this Enum value)
            where TAttribute : Attribute, new()
        {
            var type = value.GetType();
            var fieldInfo = type.GetField(value.ToString());
            var attrib = fieldInfo.GetCustomAttributes(true)
                                  .OfType<TAttribute>().FirstOrDefault();
            return attrib ?? new TAttribute();
        }
    }

    public class FriendlyNameAttribute : Attribute
    {
        public string Name { get; set; }
    }

    public class DescriptionAttribute : Attribute
    {
        public string Description { get; set; }
    }
}

Die Zusatz-Informationen „Name“ und „Description“ werden hier durch zwei „Custom Attributes“ abgebildet, welche die Mitglieder der Enum DeviceClass deklarativ mit diesen Informationen erweitern. Das Suffix „Attribute“ der beiden Klassen entspricht der .Net-Konvention für Code-Attribute und muss bei der Verwendung der Attribute nicht mit angegeben werden.

Auf diese Art lässt sich das Fehlen von Informationen für einzelne Enum-Mitglieder deutlich einfacher erkennen, da selbst bei 10 Enum-Mitgliedern inkl. Attributen alle auf eine Bildschirmseite passen (bei 1080 Pixel vertikaler Auflösung und Standard-Schrift-Einstellungen). Durch den Einsatz einer Extension-Method ist auch das Abfragen des Attributs extrem einfach und intuitiv möglich.

Zusätzlich sinkt die zyklomatische Komplexität, so dass weniger Test-Fälle ausreichen, um den Code vollständig zu testen – das Vorhandensein eines entsprechenden Attributs an jedem Enum-Mitglied lässt sich in einer Test-Methode problemlos so implementieren, dass zusätzliche Mitglieder ohne Attribut einen Fehler auslösen:

[TestMethod]
public void EnsureAllMembersHaveFriendlyNameAttribute()
{
    var invalid =
        typeof(AttributeApproach.DeviceClass)
            .GetEnumValues()
            .Cast<Enum>()
            .Where(x => string.IsNullOrEmpty(x.GetAttrib<FriendlyNameAttribute>().Name));

    Assert.AreEqual(0, invalid.Count());
}

Der Code der Extension-Methode muss natürlich noch um NULL-Prüfung und ggf. Logging (falls das Attribut erwartet aber nicht gefunden wurde) erweitert werden – die Übersichtlichkeit des Codes nimmt aber trotzdem deutlich zu. Die Attribute sollten noch als „sealed“ gekennzeichnet und mit einem AttributeUsage-Attribut ausgestattet werden. Der Übersicht halber wurde in diesem Beispiel vollständig auf Kommentare verzichtet.

Auch diese Technik hat ihre Grenzen und speziellen Anwendungsfälle und der Einsatz von Attributen statt Interfaces muss sorgfältig abgewägt werden (Performance-Implikationen, KowHow im Projekt, was soll tatsächlich ausgedrückt werden), so dass der Gebrauch von Custom Attributes sicher nicht in jedem Fall angeraten ist. Eine Überlegung ist dieser Ansatz meiner Meinung nach aber auf jeden Fall wert. Er kann (richtig angewendet) die Testbarkeit und Eleganz des Codes, sowie die Produktivität stark erhöhen.

Performancefalle Linq

LINQ macht viele Dinge im C#-Code einfacher, ausdrucksvoller und eleganter. Aber manchmal bietet es doch Stolpersteine, die eine Fehlersuche oder das Suchen von schlecht performanten Code stark verkomplizieren.

Häufig sind die Gründe dafür zwar generell bekannt, werden aber nicht ausreichend bei der Arbeit mit LINQ beachtet.

Nehmen wir den folgenden Code:

// let's generate some items in a simple list 

var myEnum = new List<string> { "a", "b", "c", "d", "e" };

// now we order the list 

var myOrderedList = from x in myEnum orderby x.CustomOrderBy() select x;

Wir legen also eine Liste an und fragen alle Elemente in einer bestimmten Reihenfolge ab. Relativ simples Code-Konstrukt. Um nun mit den Daten etwas zu machen und über das Hello-World-Szenario hinaus zu gehen, fragen wir unsere sortierte Liste mit einem where-Statement ab und lassen uns das erste Element geben – das Ganze bringen wir noch in einem Loop unter:

// lets enumerate the list items 

foreach (var listItem in myEnum)   

{

  // for each of the elements filter the list for all items 

  // "less" than the current item 

  var myItem = (from x in myOrderedList

                where x.CustomWhere(listItem)

                select x).FirstOrDefault();   

  Console.WriteLine(myItem);

}

Die Methoden CustomWhere und CustomOrderBy sind trivial in Form von Extension Methods implementiert und nur für das Zählen der Aufrufe gedacht:

public static class Extensions

{

    private static int WhereCounter;

    private static int OrderByCounter;

 

    public static string CustomOrderBy(this string value)

    {

        OrderByCounter++;

        Console.WriteLine("CustomOrderBy: {0} Aufrufe", OrderByCounter);

        return value;

    }

 

    public static bool CustomWhere(this string value, string compareto)

    {

        WhereCounter++;

        Console.WriteLine("CustomWhere:   {0} Aufrufe", WhereCounter);

        return value == compareto;

    }

}

Wie im Code zu sehen, habe ich zwei Methoden in der Sortierung und in der Filterung eingesetzt – diese zählen einfach die Aufrufe, so dass wir überprüfen können, wie die Linq-Statements ausgeführt werden:

CustomOrderBy: 25 Aufrufe
CustomWhere:   15 Aufrufe

Whow! 25 Aufrufe für CustomOrderBy – würde Linq mit einem simplen Sortierungs-Algorithmus die das orderby-Statement ausführen, wäre 5 zu erwarten … es gibt ja nur eine Stelle, an der einmalig die Sortierung durchgeführt wird.

Aber: das LINQ-Statement iteriert und sortiert eben NICHT die Liste; es generiert eine Klasse, welche eine sortierte IEnumerable<string> zurück liefert. Also wird die LINQ-Abfrage jedes Mal durchgeführt, wenn sie angesprochen wird – in unserem Fall fünf Mal innerhalb des ForEach-Statements = 25 Ausführungen des darin enthaltenen where-Statements.

Eine simple Lösung des Problems könnte folgendermaßen aussehen:

var myOrderedList = (from x in myEnum orderby x.CustomOrderBy("d") select x).ToArray();

Der Aufruf von .ToArray() wandelt das IEnumerable<string> einmalig (außerhalb des ForEach) in ein echtes Array mit den sortierten Entitäten. Damit muss nicht mehr bei jedem Zugriff die Liste erneut sortiert werden.

Über diese „Feinheiten“ von LINQ sollte man sich im Klaren sein, wenn man LINQ verwendet. Beide Verhaltensweisen (IEnumerable und List) haben Vor- aber auch Nachteile.

Azure-RoleEntryPoint in eigenes Assembly auslagern

Azure-Projekte beziehen sich auf eine oder mehrere Roles, wobei eine Role ein Template für die VM-Instanzen im Microsoft-Rechenzentrum ist. Jede Role braucht nun eine Klasse, welche von RoleEntryPoint ableitet (für WebRoles optional – siehe RoleEntryPoint Class) und in der Azure-Role-spezifische Events behandelt werden. Standardmäßig wird diese Klasse bei einer WebRole im primären Web-Projekt angelegt, so dass dieses Projekt mit Referenzen auf die Assemblies „Microsoft.WindowsAzure.Diagnostics“ und „Microsoft.WindowsAzure.ServiceRuntime“ versehen werden muss – etwas, was man gerne vermeiden möchte, wenn das Web-Projekt sowohl in Azure als auch On-Premise gehostet werden soll.

Das Auslagern dieser Klasse in ein gesondertes Assembly scheitert daran, dass Azure in der Text-Datei „__entrypoint.txt“ die Angabe des Assemblies erwartet, welches den Role-Entry-Point enthält (siehe auch “Cloudy in Seattle” Post vom Feb 2010) … diese Datei wird aber hart auf das primäre Output-Assembly gesetzt, welches im Falle einer Web-Role eben das Web-Projekt ist – sehr unschön und ggf. ein Punkt der in einem Service-Pack beseitigt werden sollte.

Ein einfacher Weg, das trotzdem hinzubekommen ist, die Datei nach dem Build-Vorgang (im Post-Build-Event des Azure-Projekts) zu verändern bzw. zu überschreiben. Ich nutze in einem aktuellen Projekt dazu eine Batch-Datei mit folgendem Eintrag als „Post-build event command line“:

1: $(ProjectDir)..AzureCopyRoleEntryPointHint.bat $(ProjectDir)

Der Inhalt der Batch-Datei lautet hierbei:

1: copy %1..Azure __entrypoint.txt MeinAzureProjekt.csx
olesMeinWebProjekt\__entrypoint.txt

2: copy %1..MeinAzureProjekt.RoleAddOninDebugMeinAzureProjekt.RoleAddOn.dll            MeinAzureProjekt.csx
olesMeinWebProjektapprootin
3: copy %1..MeinAzureProjekt.RoleAddOninDebugMicrosoft.WindowsAzure.Diagnostics.dll    MeinAzureProjekt.csx
olesMeinWebProjektapprootin
4: copy %1..MeinAzureProjekt.RoleAddOninDebugMicrosoft.WindowsAzure.ServiceRuntime.dll MeinAzureProjekt.csx
olesMeinWebProjektapprootin
5: copy %1..MeinAzureProjekt.RoleAddOninDebugMicrosoft.WindowsAzure.StorageClient.dll  MeinAzureProjekt.csx
olesMeinWebProjektapprootin

Wichtig hierbei:

  • Da das Azure-Projekt die WebSite referenziert und keine zusätzlichen Dateien aufnehmen kann, müssen die zusätzlichen Dateien (vorbereitete „__entrypoint.txt“ und die Batch-Datei) in ein gesondertes Verzeichnis aufgenommen werden (hier das „Azure“-Verzeichnis im Solution-Verzeichnis).
  • In den Eigenschaften der Solution sollte die Abhängigkeit des Azure-Projekts zum RoleEntryPoint-Assembly eingetragen werden, damit das Assembly auf jeden Fall vorher gebuildet wird.
  • Das Ausführungs-Verzeichnis für den Post-Build-Step ist das Target-Verzeichnis des Builds – das kann gerade bei Einsatz eines TFS-Build bei der Nutzung von relativen Pfad-Angaben zu Problemen führen (daher die Übergabe des „$(ProjectDir)“ als Parameter für die Batch-Datei). Je nach Build-System müssen hier noch Anpassungen vorgenommen werden.
  • Der Inhalt der „__entrypoint.txt“ muss in diesem Falle „binMeinRoleAddOn.dll“ (also der relative Pfad zum Assembly der RoleEntryPoint-Klasse ohne „Release“/“Debug“) lauten. Die Angabe der Build-Konfiguration darf nicht enthalten sein, da diese Datei in der Azure-VM Verwendung findet – und da gibt es nur das bin-Verzeichnis.
  • Das einfache Kopieren von ein paar Dateien macht leider noch keine vollständige Azure-Applikation aus. Zusätzlich müssen auch die Konfigurationen in der Web.Config korrekt sein und die Azure-Infrastruktur konfigurieren (z.B. der DiagnosticsConnectionString). Hier kann eine “Config Transform” helfen.
  • Da keine Projekt-Referenz auf das RoleEntryPoint-Projekt besteht, muss die Kopier-Batch-Datei ggf. noch erweitert werden – Abhängigkeiten des RoleEntryPoint-Projekt müssen ebenfalls in diese Datei aufgenommen werden.

Generell ist es eine gute Idee, Abhängigkeiten zu speziellen Umgebungen (Datenbanken, Services, Azure) aus den allgemeineren Projekten heraus zu halten (separation of concerns) und diese Abhängigkeiten zu abstrahieren. Um dies auch in Bezug auf Azure zu schaffen, bietet die hier beschriebene Vorgehensweise eine (relativ) einfache Möglichkeit. So wird es möglich, in Azure den Role-Entry-Point zu nutzen und gleichzeitig die Kompatibilität zum In-House-Hosting der Anwendung sicher zu stellen.