|
1 | | -using System; |
2 | | -using System.Diagnostics; |
3 | | -using System.Diagnostics.CodeAnalysis; |
4 | | -using System.Linq; |
5 | | -using System.Web; |
6 | | - |
7 | | -namespace RutaHttpModule |
| 1 | +namespace RutaHttpModule |
8 | 2 | { |
| 3 | + using System; |
| 4 | + using System.Diagnostics; |
| 5 | + using System.Diagnostics.CodeAnalysis; |
| 6 | + using System.Linq; |
| 7 | + using System.Web; |
| 8 | + |
| 9 | + /// <summary> |
| 10 | + /// This module changes adds all header information needed for interaction with SonarQube and |
| 11 | + /// provide a SSO experience. An existing Authorization header will be removed. |
| 12 | + /// </summary> |
| 13 | + /// <remarks> |
| 14 | + /// See also https://www.iis.net/learn/develop/runtime-extensibility/how-to-add-tracing-to-iis-managed-modules |
| 15 | + /// </remarks> |
9 | 16 | public sealed class RutaModule : IHttpModule |
10 | 17 | { |
| 18 | + /// <summary> |
| 19 | + /// Reference to the implementation needed to query the AD. |
| 20 | + /// </summary> |
11 | 21 | private readonly IAdInteraction adInteraction; |
| 22 | + |
| 23 | + /// <summary> |
| 24 | + /// Reference to the settings needed for constructing the right header. |
| 25 | + /// </summary> |
12 | 26 | private readonly ISettings settings; |
13 | | - private TraceSource traceSource; |
14 | 27 |
|
| 28 | + /// <summary> |
| 29 | + /// Reference to the tracing object. |
| 30 | + /// </summary> |
| 31 | + private ITraceSource traceSource; |
| 32 | + |
| 33 | + /// <summary> |
| 34 | + /// Initializes a new instance of the a <see cref="RutaModule"/> object |
| 35 | + /// </summary> |
15 | 36 | [ExcludeFromCodeCoverage] |
16 | 37 | public RutaModule() |
17 | | - : this(new AdInteraction(), new SettingsWrapper()) |
18 | | - { |
| 38 | + : this(new AdInteraction(), new SettingsWrapper(), new RutaTraceSource(nameof(RutaHttpModule))) |
| 39 | + { |
19 | 40 | } |
20 | 41 |
|
21 | | - internal RutaModule(IAdInteraction adInteraction, ISettings settings) |
| 42 | + /// <summary> |
| 43 | + /// Initializes a new instance of the a <see cref="RutaModule"/> object |
| 44 | + /// </summary> |
| 45 | + /// <param name="adInteraction">Reference to the implementation needed to query the AD.</param> |
| 46 | + /// <param name="settings">Reference to the settings needed for constructing the right header.</param> |
| 47 | + /// <param name="traceSource">Reference to the tracing object.</param> |
| 48 | + internal RutaModule(IAdInteraction adInteraction, ISettings settings, ITraceSource traceSource) |
22 | 49 | { |
23 | 50 | this.adInteraction = adInteraction ?? throw new ArgumentNullException(nameof(adInteraction)); |
24 | 51 | this.settings = settings ?? throw new ArgumentNullException(nameof(settings)); |
| 52 | + this.traceSource = traceSource ?? throw new ArgumentException(nameof(traceSource)); |
25 | 53 | } |
26 | 54 |
|
27 | | - [ExcludeFromCodeCoverage] |
| 55 | + /// <summary> |
| 56 | + /// The name of the module |
| 57 | + /// </summary> |
28 | 58 | public string ModuleName => "RutaModule"; |
29 | 59 |
|
30 | | - // In the Init function, register for HttpApplication |
31 | | - // events by adding your handlers. |
32 | | - [ExcludeFromCodeCoverage] |
33 | | - public void Init(HttpApplication application) |
34 | | - { |
35 | | - application.AuthorizeRequest += AuthorizeRequest; |
36 | | - this.traceSource = new TraceSource(nameof(RutaHttpModule)); // Do I need to do this here? https://www.iis.net/learn/develop/runtime-extensibility/how-to-add-tracing-to-iis-managed-modules |
37 | | - } |
| 60 | + /// <summary> |
| 61 | + /// Initializes a module and prepares it to handle requests (part of the <see cref="IHttpHandler"/> interface). |
| 62 | + /// </summary> |
| 63 | + /// <param name="application">An <see cref="HttpApplication"/> that provides access to the methods, properties, |
| 64 | + /// and events common to all application objects within an ASP.NET application</param> |
| 65 | + public void Init(HttpApplication application) => application.AuthorizeRequest += AuthorizeRequest; |
38 | 66 |
|
39 | | - [ExcludeFromCodeCoverage] |
| 67 | + /// <summary> |
| 68 | + /// Disposes of the resources (other than memory) used this module (part of the <see cref="IHttpHandler"/> interface). |
| 69 | + /// </summary> |
40 | 70 | public void Dispose() { } |
41 | 71 |
|
| 72 | + /// <summary> |
| 73 | + /// Handler executed when a security module has verified user authorization. During execution of the handler the |
| 74 | + /// httpContext will be changed. |
| 75 | + /// </summary> |
| 76 | + /// <param name="source">The source of the event.</param> |
| 77 | + /// <param name="e">An object that contains no event data.</param> |
42 | 78 | [ExcludeFromCodeCoverage] |
43 | 79 | private void AuthorizeRequest(object source, EventArgs e) |
| 80 | + { |
| 81 | + var application = source as HttpApplication; |
| 82 | + HandleAuthorizeRequest(new RutaHttpContext(application.Context)); |
| 83 | + } |
| 84 | + |
| 85 | + /// <summary> |
| 86 | + /// Handle an authorizeRequest using the <paramref name="context"/>. |
| 87 | + /// </summary> |
| 88 | + /// <param name="context">The context to be used when handling an AuthorizeRequest</param> |
| 89 | + internal void HandleAuthorizeRequest(IRutaHttpContext context) |
44 | 90 | { |
45 | 91 | try |
46 | 92 | { |
47 | | - traceSource.TraceEvent(TraceEventType.Start, 0, $"[{nameof(RutaModule)} MODULE] START AuthorizeRequest"); |
48 | | - |
49 | | - HttpApplication application = (HttpApplication)source; |
50 | | - AuthorizeRequest(new RutaHttpContext(application.Context)); |
| 93 | + traceSource.TraceEvent(TraceEventType.Start, 0, "START AuthorizeRequest"); |
| 94 | + HandleAuthorizeRequestInternal(context); |
51 | 95 | } |
52 | | - catch(Exception ex) |
| 96 | + catch (Exception ex) |
53 | 97 | { |
54 | | - traceSource.TraceEvent(TraceEventType.Error, 0, $"[{nameof(RutaModule)} ERROR AuthorizeRequest: ExceptionData: '{ex}' "); |
| 98 | + traceSource.TraceEvent(TraceEventType.Error, 0, $"ERROR AuthorizeRequest: ExceptionData: '{ex}' "); |
55 | 99 | throw; |
56 | 100 | } |
57 | 101 | finally |
58 | 102 | { |
59 | | - traceSource.TraceEvent(TraceEventType.Stop, 0, $"[{nameof(RutaModule)} END AuthorizeRequest"); |
| 103 | + traceSource.TraceEvent(TraceEventType.Stop, 0, "END AuthorizeRequest"); |
60 | 104 | } |
61 | 105 | } |
62 | 106 |
|
63 | | - // Internal for testing purposes |
64 | | - internal void AuthorizeRequest(IRutaHttpContext context) |
| 107 | + /// <summary> |
| 108 | + /// Replace the current context with a version that can be processed by SonarQube. That is done only |
| 109 | + /// if:<br/> |
| 110 | + /// <ul> |
| 111 | + /// <li>The user is authenticated.</li> |
| 112 | + /// <li>The context contains a DomainUserName.</li> |
| 113 | + /// <li>The Ldap information could be extracted.</li> |
| 114 | + /// <li>The user information can be retrieved from AD.</li> |
| 115 | + /// </ul> |
| 116 | + /// </summary> |
| 117 | + /// <param name="context">The context on which the action should be performed.</param> |
| 118 | + private void HandleAuthorizeRequestInternal(IRutaHttpContext context) |
65 | 119 | { |
66 | 120 | if (!context.IsAuthenticated) |
67 | 121 | { |
| 122 | + traceSource.TraceEvent(TraceEventType.Information, 0, "Not authenticated"); |
68 | 123 | return; |
69 | 124 | } |
70 | 125 |
|
71 | 126 | string userName = context.DomainUserName; |
72 | | - |
73 | 127 | if (string.IsNullOrWhiteSpace(userName)) |
74 | 128 | { |
| 129 | + traceSource.TraceEvent(TraceEventType.Information, 0, "No username in context"); |
75 | 130 | return; |
76 | 131 | } |
77 | | - |
78 | | - context.RemoveRequestHeader("Authorization"); // Remove the authorzation header since we are in charge of authentication |
| 132 | + |
79 | 133 | var userInformation = this.adInteraction.GetUserInformation(userName); |
80 | | - |
81 | 134 | if (string.IsNullOrWhiteSpace(userInformation.login)) |
82 | 135 | { |
| 136 | + traceSource.TraceEvent(TraceEventType.Information, 0, "No user information in context"); |
83 | 137 | return; |
84 | 138 | } |
85 | 139 |
|
86 | | - string loginToSend = AppendIfNedded(DowncaseUserIfNeeded(userInformation.login)); |
87 | | - string[] groupsToSend = userInformation.groups.Where(x => x != null).Select(DowncaseGroupIfNeeded).Select(AppendIfNedded).ToArray(); |
| 140 | + traceSource.TraceEvent(TraceEventType.Information, 0, "Set headers"); |
| 141 | + |
| 142 | + string loginToSend = ApplyUserSettings(userInformation.login); |
| 143 | + string[] groupsToSend = userInformation.groups.Where(group => !string.IsNullOrWhiteSpace(group)) |
| 144 | + .Select(ApplyGroupSettings) |
| 145 | + .ToArray(); |
88 | 146 |
|
| 147 | + context.RemoveRequestHeader("Authorization"); // Remove the authorzation header since we are in charge of authentication |
89 | 148 | context.AddRequestHeader(this.settings.LoginHeader, loginToSend); |
90 | 149 | context.AddRequestHeader(this.settings.NameHeader, userInformation.name); |
91 | 150 | context.AddRequestHeader(this.settings.EmailHeader, userInformation.email); |
92 | 151 | context.AddRequestHeader(this.settings.GroupsHeader, string.Join(",", groupsToSend)); |
93 | 152 | } |
94 | 153 |
|
95 | | - private string DowncaseGroupIfNeeded(string s) |
96 | | - { |
97 | | - if (!this.settings.DowncaseGroups) |
98 | | - { |
99 | | - return s; |
100 | | - } |
101 | | - |
102 | | - return s.ToLower(); |
103 | | - } |
104 | | - |
105 | | - private string DowncaseUserIfNeeded(string s) |
| 154 | + /// <summary> |
| 155 | + /// Returns a copy of the <paramref name="group"/> object adjusted as necessary based on |
| 156 | + /// the settings (DowncaseGroups and AppendString). |
| 157 | + /// </summary> |
| 158 | + /// <param name="group">A <see cref="String"/> on which the action should be performed.</param> |
| 159 | + /// <returns>The modified version of <paramref name="group"/>.</returns> |
| 160 | + private string ApplyGroupSettings(string group) => AppendIfNeeded(LowercaseIfNeeded(group, this.settings.DowncaseGroups)); |
| 161 | + |
| 162 | + /// <summary> |
| 163 | + /// Returns a copy of the <paramref name="user"/> object adjusted as necessary based on |
| 164 | + /// the settings (DowncaseUsers and AppendString). |
| 165 | + /// </summary> |
| 166 | + /// <param name="user">A <see cref="String"/> on which the action should be performed.</param> |
| 167 | + /// <returns>The modified version of <paramref name="user"/>.</returns> |
| 168 | + private string ApplyUserSettings(string user) => AppendIfNeeded(LowercaseIfNeeded(user, this.settings.DowncaseUsers)); |
| 169 | + |
| 170 | + /// <summary> |
| 171 | + /// Returns a copy of the <paramref name="source"/> object appended with the contents of the |
| 172 | + /// AppendString settings. The action will not be performed if <paramref name="source"/> or |
| 173 | + /// AppendString are null or containing only whitespace. In all other cases <paramref name="source"/> |
| 174 | + /// will be returned. |
| 175 | + /// </summary> |
| 176 | + /// <param name="source">A <see cref="String"/> on which the action should be performed.</param> |
| 177 | + /// <returns>The modified version of <paramref name="source"/>.</returns> |
| 178 | + private string AppendIfNeeded(string source) |
106 | 179 | { |
107 | | - if (!this.settings.DowncaseUsers) |
| 180 | + if (string.IsNullOrWhiteSpace(this.settings.AppendString)) |
108 | 181 | { |
109 | | - return s; |
| 182 | + return source; |
110 | 183 | } |
111 | 184 |
|
112 | | - return s.ToLower(); |
| 185 | + return $"{source}{this.settings.AppendString}"; |
113 | 186 | } |
114 | 187 |
|
115 | | - private string AppendIfNedded(string s) |
116 | | - { |
117 | | - if (string.IsNullOrWhiteSpace(this.settings.AppendString) || string.IsNullOrWhiteSpace(s)) |
118 | | - { |
119 | | - return s; |
120 | | - } |
121 | | - |
122 | | - return $"{s}{this.settings.AppendString}"; |
123 | | - } |
| 188 | + /// <summary> |
| 189 | + /// Returns a copy of the <paramref name="source"/> object converted to lowercase |
| 190 | + /// using the casing rules of the invariant culture if indicated by <paramref name="applyDowncase"/>. |
| 191 | + /// In all other cases <paramref name="source"/> will be returned. |
| 192 | + /// </summary> |
| 193 | + /// <param name="source">A <see cref="String"/> on which the action should be performed.</param> |
| 194 | + /// <param name="applyLowercase">Should the method return a lowercase version of <paramref name="source"/>?</param> |
| 195 | + /// <returns>The modified version of <paramref name="source"/>.</returns> |
| 196 | + private static string LowercaseIfNeeded(string source, bool applyLowercase) => applyLowercase ? source.ToLowerInvariant() : source; |
124 | 197 | } |
125 | 198 | } |
0 commit comments