| | | 1 | | |
| | | 2 | | |
| | | 3 | | |
| | | 4 | | using System.Collections; |
| | | 5 | | using System.Management.Automation; |
| | | 6 | | using System.Security.Claims; |
| | | 7 | | using Kestrun.Hosting; |
| | | 8 | | using Kestrun.Languages; |
| | | 9 | | using Kestrun.Logging; |
| | | 10 | | using Kestrun.Models; |
| | | 11 | | using Kestrun.Utilities; |
| | | 12 | | using Microsoft.AspNetCore.Authentication; |
| | | 13 | | |
| | | 14 | | namespace Kestrun.Authentication; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Defines common options for authentication, including code validation, claim issuance, and claim policy configuration |
| | | 18 | | /// </summary> |
| | | 19 | | public interface IAuthHandler |
| | | 20 | | { |
| | | 21 | | /// <summary> |
| | | 22 | | /// Generates an <see cref="AuthenticationTicket"/> for the specified user and authentication scheme, issuing additi |
| | | 23 | | /// </summary> |
| | | 24 | | /// <param name="Context">The current HTTP context.</param> |
| | | 25 | | /// <param name="user">The user name for whom the ticket is being generated.</param> |
| | | 26 | | /// <param name="Options">Authentication options including claim issuance delegates.</param> |
| | | 27 | | /// <param name="Scheme">The authentication scheme to use.</param> |
| | | 28 | | /// <param name="alias">An optional alias for the user.</param> |
| | | 29 | | /// <returns>An <see cref="AuthenticationTicket"/> representing the authenticated user.</returns> |
| | | 30 | | static async Task<AuthenticationTicket> GetAuthenticationTicketAsync( |
| | | 31 | | HttpContext Context, string user, |
| | | 32 | | IAuthenticationCommonOptions Options, AuthenticationScheme Scheme, string? alias = null) |
| | | 33 | | { |
| | 8 | 34 | | var claims = new List<Claim>(); |
| | | 35 | | |
| | | 36 | | // 1) Issue extra claims if configured |
| | 8 | 37 | | claims.AddRange(await GetIssuedClaimsAsync(Context, user, Options).ConfigureAwait(false)); |
| | | 38 | | |
| | | 39 | | // 2) Ensure a Name claim exists |
| | 8 | 40 | | EnsureNameClaim(claims, user, alias, Options.Logger); |
| | | 41 | | |
| | | 42 | | // 3) Create and return the ticket |
| | 8 | 43 | | return CreateAuthenticationTicket(claims, Scheme); |
| | 8 | 44 | | } |
| | | 45 | | |
| | | 46 | | /// <summary> |
| | | 47 | | /// Issues claims for the specified user based on the provided context and options. |
| | | 48 | | /// </summary> |
| | | 49 | | /// <param name="context">The HTTP context.</param> |
| | | 50 | | /// <param name="user">The user name for whom claims are being issued.</param> |
| | | 51 | | /// <param name="options">Authentication options including claim issuance delegates.</param> |
| | | 52 | | /// <returns>A collection of issued claims.</returns> |
| | | 53 | | private static async Task<IEnumerable<Claim>> GetIssuedClaimsAsync(HttpContext context, string user, IAuthentication |
| | | 54 | | { |
| | 8 | 55 | | if (options.IssueClaims is null) |
| | | 56 | | { |
| | 8 | 57 | | return []; |
| | | 58 | | } |
| | | 59 | | |
| | 0 | 60 | | var extra = await options.IssueClaims(context, user).ConfigureAwait(false); |
| | 0 | 61 | | if (extra is null) |
| | | 62 | | { |
| | 0 | 63 | | return []; |
| | | 64 | | } |
| | | 65 | | |
| | | 66 | | // Filter out nulls and empty values |
| | 0 | 67 | | return [.. extra |
| | 0 | 68 | | .Where(c => c is not null) |
| | 0 | 69 | | .OfType<Claim>() |
| | 0 | 70 | | .Where(c => !string.IsNullOrEmpty(c.Value))]; |
| | 8 | 71 | | } |
| | | 72 | | |
| | | 73 | | /// <summary> |
| | | 74 | | /// Ensures that a Name claim is present in the list of claims. |
| | | 75 | | /// </summary> |
| | | 76 | | /// <param name="claims">The list of claims to check.</param> |
| | | 77 | | /// <param name="user">The user name to use if a Name claim is added.</param> |
| | | 78 | | /// <param name="alias">An optional alias for the user.</param> |
| | | 79 | | /// <param name="logger">The logger instance.</param> |
| | | 80 | | private static void EnsureNameClaim(List<Claim> claims, string user, string? alias, Serilog.ILogger logger) |
| | | 81 | | { |
| | 8 | 82 | | if (claims.Any(c => c.Type == ClaimTypes.Name)) |
| | | 83 | | { |
| | 0 | 84 | | return; |
| | | 85 | | } |
| | | 86 | | |
| | 8 | 87 | | if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug)) |
| | | 88 | | { |
| | 2 | 89 | | logger.Debug("No Name claim found, adding default Name claim"); |
| | | 90 | | } |
| | | 91 | | |
| | 8 | 92 | | var name = string.IsNullOrEmpty(alias) ? user : alias; |
| | 8 | 93 | | claims.Add(new Claim(ClaimTypes.Name, name)); |
| | 8 | 94 | | } |
| | | 95 | | |
| | | 96 | | /// <summary> |
| | | 97 | | /// Creates an authentication ticket from the specified claims and authentication scheme. |
| | | 98 | | /// </summary> |
| | | 99 | | /// <param name="claims">The claims to include in the ticket.</param> |
| | | 100 | | /// <param name="scheme">The authentication scheme to use.</param> |
| | | 101 | | /// <returns>An authentication ticket containing the specified claims.</returns> |
| | | 102 | | private static AuthenticationTicket CreateAuthenticationTicket(IEnumerable<Claim> claims, AuthenticationScheme schem |
| | | 103 | | { |
| | 8 | 104 | | var claimsIdentity = new ClaimsIdentity(claims, scheme.Name); |
| | 8 | 105 | | var principal = new ClaimsPrincipal(claimsIdentity); |
| | 8 | 106 | | return new AuthenticationTicket(principal, scheme.Name); |
| | | 107 | | } |
| | | 108 | | |
| | | 109 | | /// <summary> |
| | | 110 | | /// Authenticates the user using PowerShell script. |
| | | 111 | | /// This method is used to validate the username and password against a PowerShell script. |
| | | 112 | | /// </summary> |
| | | 113 | | /// <param name="code">The PowerShell script code used for authentication.</param> |
| | | 114 | | /// <param name="context">The HTTP context.</param> |
| | | 115 | | /// <param name="credentials">A dictionary containing the credentials to validate (e.g., username and password).</pa |
| | | 116 | | /// <param name="logger">The logger instance.</param> |
| | | 117 | | /// <returns>A task representing the asynchronous operation.</returns> |
| | | 118 | | /// <exception cref="InvalidOperationException"></exception> |
| | | 119 | | static async ValueTask<bool> ValidatePowerShellAsync(string? code, HttpContext context, Dictionary<string, string> c |
| | | 120 | | { |
| | | 121 | | try |
| | | 122 | | { |
| | 9 | 123 | | ValidatePowerShellInputs(code, context, credentials, logger); |
| | 6 | 124 | | var ps = GetPowerShellForValidation(context); |
| | | 125 | | |
| | 6 | 126 | | _ = ps.AddScript(code, useLocalScope: true); |
| | 36 | 127 | | foreach (var kvp in credentials) |
| | | 128 | | { |
| | 12 | 129 | | _ = ps.AddParameter(kvp.Key, kvp.Value); |
| | | 130 | | } |
| | | 131 | | |
| | 6 | 132 | | var psResults = await ExecutePowerShellScriptAsync(ps, context, logger).ConfigureAwait(false); |
| | | 133 | | |
| | 6 | 134 | | return ValidatePowerShellResult(psResults, logger); |
| | | 135 | | } |
| | 3 | 136 | | catch (Exception ex) |
| | | 137 | | { |
| | 3 | 138 | | logger.Error(ex, "Error during validating PowerShell authentication."); |
| | 3 | 139 | | return false; |
| | | 140 | | } |
| | 9 | 141 | | } |
| | | 142 | | |
| | | 143 | | /// <summary> |
| | | 144 | | /// Validates that all required inputs for PowerShell authentication are present and valid. |
| | | 145 | | /// </summary> |
| | | 146 | | /// <param name="code">The PowerShell script code to validate.</param> |
| | | 147 | | /// <param name="context">The HTTP context containing the PowerShell runspace.</param> |
| | | 148 | | /// <param name="credentials">The credentials dictionary to validate.</param> |
| | | 149 | | /// <param name="logger">The logger instance for logging warnings.</param> |
| | | 150 | | /// <exception cref="InvalidOperationException">Thrown when required inputs are missing or invalid.</exception> |
| | | 151 | | private static void ValidatePowerShellInputs(string? code, HttpContext context, Dictionary<string, string>? credenti |
| | | 152 | | { |
| | 9 | 153 | | if (!context.Items.ContainsKey("PS_INSTANCE")) |
| | | 154 | | { |
| | 1 | 155 | | throw new InvalidOperationException("PowerShell runspace not found in context items. Ensure PowerShellRunspa |
| | | 156 | | } |
| | | 157 | | |
| | 8 | 158 | | if (credentials == null || credentials.Count == 0) |
| | | 159 | | { |
| | 1 | 160 | | logger.Warning("Credentials are null or empty."); |
| | 1 | 161 | | throw new InvalidOperationException("Credentials are null or empty."); |
| | | 162 | | } |
| | | 163 | | |
| | 7 | 164 | | if (string.IsNullOrEmpty(code)) |
| | | 165 | | { |
| | 1 | 166 | | throw new InvalidOperationException("PowerShell authentication code is null or empty."); |
| | | 167 | | } |
| | 6 | 168 | | } |
| | | 169 | | |
| | | 170 | | /// <summary> |
| | | 171 | | /// Retrieves and validates the PowerShell instance from the HTTP context. |
| | | 172 | | /// </summary> |
| | | 173 | | /// <param name="context">The HTTP context containing the PowerShell instance.</param> |
| | | 174 | | /// <returns>The validated PowerShell instance.</returns> |
| | | 175 | | /// <exception cref="InvalidOperationException">Thrown when the PowerShell instance or runspace is not found or inva |
| | | 176 | | private static PowerShell GetPowerShellForValidation(HttpContext context) |
| | | 177 | | { |
| | 6 | 178 | | var ps = context.Items["PS_INSTANCE"] as PowerShell |
| | 6 | 179 | | ?? throw new InvalidOperationException("PowerShell instance not found in context items."); |
| | | 180 | | |
| | 6 | 181 | | return ps.Runspace == null |
| | 6 | 182 | | ? throw new InvalidOperationException("PowerShell runspace is not set. Ensure PowerShellRunspaceMiddleware i |
| | 6 | 183 | | : ps; |
| | | 184 | | } |
| | | 185 | | |
| | | 186 | | /// <summary> |
| | | 187 | | /// Executes the PowerShell script with support for request cancellation. |
| | | 188 | | /// </summary> |
| | | 189 | | /// <param name="ps">The PowerShell instance to execute.</param> |
| | | 190 | | /// <param name="context">The HTTP context containing cancellation token and request information.</param> |
| | | 191 | | /// <param name="logger">The logger instance for logging debug information.</param> |
| | | 192 | | /// <returns>A collection of PowerShell objects returned from script execution.</returns> |
| | | 193 | | /// <exception cref="OperationCanceledException">Thrown when the request is cancelled.</exception> |
| | | 194 | | private static async ValueTask<PSDataCollection<PSObject>> ExecutePowerShellScriptAsync(PowerShell ps, HttpContext c |
| | | 195 | | { |
| | | 196 | | try |
| | | 197 | | { |
| | 6 | 198 | | return await ps.InvokeWithRequestAbortAsync( |
| | 6 | 199 | | context.RequestAborted, |
| | 0 | 200 | | onAbortLog: () => logger.DebugSanitized("Request aborted; stopping PowerShell pipeline for {Path}", cont |
| | 6 | 201 | | ).ConfigureAwait(false); |
| | | 202 | | } |
| | 0 | 203 | | catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) |
| | | 204 | | { |
| | 0 | 205 | | if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug)) |
| | | 206 | | { |
| | 0 | 207 | | logger.DebugSanitized("PowerShell pipeline cancelled due to request abortion for {Path}", context.Reques |
| | | 208 | | } |
| | 0 | 209 | | throw; |
| | | 210 | | } |
| | 6 | 211 | | } |
| | | 212 | | |
| | | 213 | | /// <summary> |
| | | 214 | | /// Validates and extracts the boolean result from PowerShell script execution. |
| | | 215 | | /// </summary> |
| | | 216 | | /// <param name="psResults">The collection of PowerShell objects returned from script execution.</param> |
| | | 217 | | /// <param name="logger">The logger instance for logging errors.</param> |
| | | 218 | | /// <returns>The boolean result from the PowerShell script, or false if the result is invalid.</returns> |
| | | 219 | | private static bool ValidatePowerShellResult(PSDataCollection<PSObject>? psResults, Serilog.ILogger logger) |
| | | 220 | | { |
| | 6 | 221 | | if (psResults == null || psResults.Count == 0) |
| | | 222 | | { |
| | 1 | 223 | | logger.Error("PowerShell script did not return a valid boolean result."); |
| | 1 | 224 | | return false; |
| | | 225 | | } |
| | | 226 | | |
| | 5 | 227 | | if (psResults[0]?.BaseObject is not bool isValid) |
| | | 228 | | { |
| | 1 | 229 | | logger.Error("PowerShell script did not return a valid boolean result."); |
| | 1 | 230 | | return false; |
| | | 231 | | } |
| | | 232 | | |
| | 4 | 233 | | return isValid; |
| | | 234 | | } |
| | | 235 | | |
| | | 236 | | /// <summary> |
| | | 237 | | /// Builds a C# validator function for the specified authentication settings. |
| | | 238 | | /// </summary> |
| | | 239 | | /// <param name="host">The Kestrun host instance.</param> |
| | | 240 | | /// <param name="settings">The authentication code settings.</param> |
| | | 241 | | /// <param name="globals">Global variables to include in the validation context.</param> |
| | | 242 | | /// <returns>A function that validates the authentication context.</returns> |
| | | 243 | | internal static Func<HttpContext, IDictionary<string, object?>, Task<bool>> BuildCsValidator( |
| | | 244 | | KestrunHost host, |
| | | 245 | | AuthenticationCodeSettings settings, |
| | | 246 | | params (string Name, object? Prototype)[] globals) |
| | | 247 | | { |
| | 2 | 248 | | var log = host.Logger; |
| | 2 | 249 | | if (log.IsEnabled(Serilog.Events.LogEventLevel.Debug)) |
| | | 250 | | { |
| | 2 | 251 | | log.Debug("Building C# authentication script with globals: {Globals}", globals); |
| | | 252 | | } |
| | | 253 | | |
| | | 254 | | // Place-holders so Roslyn knows the globals that will exist |
| | 10 | 255 | | var stencil = globals.ToDictionary(n => n.Name, n => n.Prototype, |
| | 2 | 256 | | StringComparer.OrdinalIgnoreCase); |
| | 2 | 257 | | if (log.IsEnabled(Serilog.Events.LogEventLevel.Debug)) |
| | | 258 | | { |
| | 2 | 259 | | log.Debug("Compiling C# authentication script with variables: {Variables}", stencil); |
| | | 260 | | } |
| | | 261 | | |
| | 2 | 262 | | var script = CSharpDelegateBuilder.Compile( |
| | 2 | 263 | | host: host, |
| | 2 | 264 | | code: settings.Code, // already scoped by caller |
| | 2 | 265 | | extraImports: settings.ExtraImports, |
| | 2 | 266 | | extraRefs: settings.ExtraRefs, |
| | 2 | 267 | | locals: stencil, |
| | 2 | 268 | | languageVersion: settings.CSharpVersion); |
| | | 269 | | |
| | | 270 | | // Return the runtime delegate |
| | 2 | 271 | | return async (ctx, vars) => |
| | 2 | 272 | | { |
| | 4 | 273 | | if (log.IsEnabled(Serilog.Events.LogEventLevel.Debug)) |
| | 2 | 274 | | { |
| | 4 | 275 | | log.Debug("Running C# authentication script with variables: {Variables}", vars); |
| | 2 | 276 | | } |
| | 2 | 277 | | // --- Kestrun plumbing ------------------------------------------------- |
| | 4 | 278 | | var krReq = await KestrunRequest.NewRequest(ctx); |
| | 4 | 279 | | var krRes = new KestrunResponse(krReq); |
| | 4 | 280 | | var kCtx = new KestrunContext(host, krReq, krRes, ctx); |
| | 2 | 281 | | // --------------------------------------------------------------------- |
| | 4 | 282 | | var globalsDict = new Dictionary<string, object?>( |
| | 4 | 283 | | vars, StringComparer.OrdinalIgnoreCase); |
| | 2 | 284 | | // Merge shared state + user variables |
| | 4 | 285 | | var globals = new CsGlobals( |
| | 4 | 286 | | host.SharedState.Snapshot(), |
| | 4 | 287 | | kCtx, |
| | 4 | 288 | | globalsDict); |
| | 2 | 289 | | |
| | 4 | 290 | | var result = await script.RunAsync(globals).ConfigureAwait(false); |
| | 4 | 291 | | return result.ReturnValue is true; |
| | 6 | 292 | | }; |
| | | 293 | | } |
| | | 294 | | |
| | | 295 | | internal static Func<HttpContext, IDictionary<string, object?>, Task<bool>> BuildVBNetValidator( |
| | | 296 | | KestrunHost host, |
| | | 297 | | AuthenticationCodeSettings settings, |
| | | 298 | | params (string Name, object? Prototype)[] globals) |
| | | 299 | | { |
| | 3 | 300 | | if (host is null) |
| | | 301 | | { |
| | 0 | 302 | | throw new ArgumentNullException(nameof(host), "KestrunHost cannot be null"); |
| | | 303 | | } |
| | 3 | 304 | | if (settings is null) |
| | | 305 | | { |
| | 0 | 306 | | throw new ArgumentNullException(nameof(settings), "AuthenticationCodeSettings cannot be null"); |
| | | 307 | | } |
| | 3 | 308 | | var log = host.Logger; |
| | | 309 | | // Place-holders so Roslyn knows the globals that will exist |
| | 15 | 310 | | var stencil = globals.ToDictionary(n => n.Name, n => n.Prototype, |
| | 3 | 311 | | StringComparer.OrdinalIgnoreCase); |
| | | 312 | | |
| | 3 | 313 | | if (log.IsEnabled(Serilog.Events.LogEventLevel.Debug)) |
| | | 314 | | { |
| | 2 | 315 | | log.Debug("Compiling VB.NET authentication script with variables: {Variables}", stencil); |
| | | 316 | | } |
| | | 317 | | |
| | | 318 | | // Compile the VB.NET script with the provided settings |
| | 3 | 319 | | var script = VBNetDelegateBuilder.Compile<bool>( |
| | 3 | 320 | | host: host, |
| | 3 | 321 | | code: settings.Code, |
| | 3 | 322 | | extraImports: settings.ExtraImports, |
| | 3 | 323 | | extraRefs: settings.ExtraRefs, |
| | 3 | 324 | | locals: stencil, |
| | 3 | 325 | | languageVersion: settings.VisualBasicVersion); |
| | | 326 | | |
| | | 327 | | // Return the runtime delegate |
| | 3 | 328 | | return async (ctx, vars) => |
| | 3 | 329 | | { |
| | 4 | 330 | | if (log.IsEnabled(Serilog.Events.LogEventLevel.Debug)) |
| | 3 | 331 | | { |
| | 4 | 332 | | log.Debug("Running VB.NET authentication script with variables: {Variables}", vars); |
| | 3 | 333 | | } |
| | 3 | 334 | | |
| | 3 | 335 | | // --- Kestrun plumbing ------------------------------------------------- |
| | 4 | 336 | | var krReq = await KestrunRequest.NewRequest(ctx); |
| | 4 | 337 | | var krRes = new KestrunResponse(krReq); |
| | 4 | 338 | | var kCtx = new KestrunContext(host, krReq, krRes, ctx); |
| | 3 | 339 | | // --------------------------------------------------------------------- |
| | 3 | 340 | | |
| | 3 | 341 | | // Merge shared state + user variables |
| | 4 | 342 | | var globals = new CsGlobals( |
| | 4 | 343 | | host.SharedState.Snapshot(), |
| | 4 | 344 | | kCtx, |
| | 4 | 345 | | new Dictionary<string, object?>(vars, StringComparer.OrdinalIgnoreCase)); |
| | 3 | 346 | | |
| | 4 | 347 | | var result = await script(globals).ConfigureAwait(false); |
| | 3 | 348 | | |
| | 4 | 349 | | return result is bool isValid && isValid; |
| | 7 | 350 | | }; |
| | | 351 | | } |
| | | 352 | | |
| | | 353 | | /// <summary> |
| | | 354 | | /// Builds a PowerShell-based function for issuing claims for a user. |
| | | 355 | | /// </summary> |
| | | 356 | | /// <param name="host">The Kestrun host instance.</param> |
| | | 357 | | /// <param name="settings">The authentication code settings containing the PowerShell script.</param> |
| | | 358 | | /// <returns>A function that issues claims using the provided PowerShell script.</returns> |
| | | 359 | | static Func<HttpContext, string, Task<IEnumerable<Claim>>> BuildPsIssueClaims( |
| | | 360 | | KestrunHost host, |
| | | 361 | | AuthenticationCodeSettings settings) => |
| | 2 | 362 | | async (ctx, identity) => |
| | 2 | 363 | | { |
| | 2 | 364 | | return await IssueClaimsPowerShellAsync(settings.Code, ctx, identity, host.Logger); |
| | 4 | 365 | | }; |
| | | 366 | | |
| | | 367 | | /// <summary> |
| | | 368 | | /// Issues claims for a user by executing a PowerShell script. |
| | | 369 | | /// </summary> |
| | | 370 | | /// <param name="code">The PowerShell script code used to issue claims.</param> |
| | | 371 | | /// <param name="context">The HTTP context containing the PowerShell runspace.</param> |
| | | 372 | | /// <param name="identity">The username for which to issue claims.</param> |
| | | 373 | | /// <param name="logger">The logger instance for logging.</param> |
| | | 374 | | /// <returns>A task representing the asynchronous operation, with a collection of issued claims.</returns> |
| | | 375 | | static async Task<IEnumerable<Claim>> IssueClaimsPowerShellAsync(string? code, HttpContext context, string identity, |
| | | 376 | | { |
| | 3 | 377 | | if (string.IsNullOrWhiteSpace(identity)) |
| | | 378 | | { |
| | 1 | 379 | | logger.Warning("Identity is null or empty."); |
| | 1 | 380 | | return []; |
| | | 381 | | } |
| | 2 | 382 | | if (string.IsNullOrEmpty(code)) |
| | | 383 | | { |
| | 0 | 384 | | throw new InvalidOperationException("PowerShell authentication code is null or empty."); |
| | | 385 | | } |
| | | 386 | | |
| | | 387 | | try |
| | | 388 | | { |
| | 2 | 389 | | var ps = GetPowerShell(context); |
| | 2 | 390 | | _ = ps.AddScript(code, useLocalScope: true).AddParameter("identity", identity); |
| | | 391 | | |
| | | 392 | | // var psResults = await ps.InvokeAsync().ConfigureAwait(false); |
| | | 393 | | PSDataCollection<PSObject> psResults; |
| | | 394 | | try |
| | | 395 | | { |
| | 2 | 396 | | psResults = await ps.InvokeWithRequestAbortAsync( |
| | 2 | 397 | | context.RequestAborted, |
| | 0 | 398 | | onAbortLog: () => logger.DebugSanitized("Request aborted; stopping PowerShell pipeline for {Path}", |
| | 2 | 399 | | ).ConfigureAwait(false); |
| | 2 | 400 | | } |
| | 0 | 401 | | catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) |
| | | 402 | | { |
| | 0 | 403 | | if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug)) |
| | | 404 | | { |
| | 0 | 405 | | logger.DebugSanitized("PowerShell pipeline cancelled due to request abortion for {Path}", context.Re |
| | | 406 | | } |
| | | 407 | | // Treat as cancellation, not an error. |
| | 0 | 408 | | return []; |
| | | 409 | | } |
| | 2 | 410 | | if (psResults is null || psResults.Count == 0) |
| | | 411 | | { |
| | 0 | 412 | | return []; |
| | | 413 | | } |
| | | 414 | | |
| | 2 | 415 | | var claims = new List<Claim>(psResults.Count); |
| | 8 | 416 | | foreach (var r in psResults) |
| | | 417 | | { |
| | 2 | 418 | | if (TryToClaim(r?.BaseObject, out var claim)) |
| | | 419 | | { |
| | 2 | 420 | | claims.Add(claim); |
| | | 421 | | } |
| | | 422 | | else |
| | | 423 | | { |
| | 0 | 424 | | logger.Warning("PowerShell script returned an unsupported type: {Type}", r?.BaseObject?.GetType()); |
| | 0 | 425 | | throw new InvalidOperationException("PowerShell script returned an unsupported type."); |
| | | 426 | | } |
| | | 427 | | } |
| | | 428 | | |
| | 2 | 429 | | return claims; |
| | | 430 | | } |
| | 0 | 431 | | catch (Exception ex) |
| | | 432 | | { |
| | 0 | 433 | | logger.Error(ex, "Error during Issue Claims for {Identity}", identity); |
| | 0 | 434 | | return []; |
| | | 435 | | } |
| | 3 | 436 | | } |
| | | 437 | | |
| | | 438 | | /// <summary> |
| | | 439 | | /// Retrieves the PowerShell instance from the HTTP context. |
| | | 440 | | /// </summary> |
| | | 441 | | /// <param name="ctx">The HTTP context containing the PowerShell runspace.</param> |
| | | 442 | | /// <returns>The PowerShell instance associated with the context.</returns> |
| | | 443 | | /// <exception cref="InvalidOperationException">Thrown when the PowerShell runspace is not found.</exception> |
| | | 444 | | private static PowerShell GetPowerShell(HttpContext ctx) |
| | | 445 | | { |
| | 4 | 446 | | return !ctx.Items.TryGetValue("PS_INSTANCE", out var psObj) || psObj is not PowerShell ps || ps.Runspace == null |
| | 4 | 447 | | ? throw new InvalidOperationException("PowerShell runspace not found or not set in context items. Ensure Pow |
| | 4 | 448 | | : ps; |
| | | 449 | | } |
| | | 450 | | |
| | | 451 | | /// <summary> |
| | | 452 | | /// Tries to create a Claim from the provided object. |
| | | 453 | | /// </summary> |
| | | 454 | | /// <param name="obj">The object to create a Claim from.</param> |
| | | 455 | | /// <param name="claim">The created Claim, if successful.</param> |
| | | 456 | | /// <returns>True if the Claim was created successfully; otherwise, false.</returns> |
| | | 457 | | private static bool TryToClaim(object? obj, out Claim claim) |
| | | 458 | | { |
| | 6 | 459 | | switch (obj) |
| | | 460 | | { |
| | | 461 | | case Claim c: |
| | 1 | 462 | | claim = c; |
| | 1 | 463 | | return true; |
| | | 464 | | |
| | 3 | 465 | | case IDictionary dict when dict.Contains("Type") && dict.Contains("Value"): |
| | 3 | 466 | | var typeStr = dict["Type"]?.ToString(); |
| | 3 | 467 | | var valueStr = dict["Value"]?.ToString(); |
| | 3 | 468 | | if (!string.IsNullOrEmpty(typeStr) && !string.IsNullOrEmpty(valueStr)) |
| | | 469 | | { |
| | 3 | 470 | | claim = new Claim(typeStr, valueStr); |
| | 3 | 471 | | return true; |
| | | 472 | | } |
| | | 473 | | break; |
| | | 474 | | |
| | 1 | 475 | | case string s when s.Contains(':'): |
| | 1 | 476 | | var idx = s.IndexOf(':'); |
| | 1 | 477 | | if (idx >= 0 && idx < s.Length - 1) |
| | | 478 | | { |
| | 1 | 479 | | claim = new Claim(s[..idx], s[(idx + 1)..]); |
| | 1 | 480 | | return true; |
| | | 481 | | } |
| | | 482 | | break; |
| | | 483 | | default: |
| | | 484 | | // Unsupported type |
| | | 485 | | break; |
| | | 486 | | } |
| | | 487 | | |
| | 1 | 488 | | claim = default!; |
| | 1 | 489 | | return false; |
| | | 490 | | } |
| | | 491 | | |
| | | 492 | | /// <summary> |
| | | 493 | | /// Builds a C#-based function for issuing claims for a user. |
| | | 494 | | /// </summary> |
| | | 495 | | /// <param name="host">The Kestrun host instance.</param> |
| | | 496 | | /// <param name="settings">The authentication code settings containing the C# script.</param> |
| | | 497 | | /// <returns>A function that issues claims using the provided C# script.</returns> |
| | | 498 | | static Func<HttpContext, string, Task<IEnumerable<Claim>>> BuildCsIssueClaims( |
| | | 499 | | KestrunHost host, |
| | | 500 | | AuthenticationCodeSettings settings) |
| | | 501 | | { |
| | 3 | 502 | | if (host is null) |
| | | 503 | | { |
| | 0 | 504 | | throw new ArgumentNullException(nameof(host), "KestrunHost cannot be null"); |
| | | 505 | | } |
| | 3 | 506 | | var logger = host.Logger; |
| | 3 | 507 | | if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug)) |
| | | 508 | | { |
| | 3 | 509 | | logger.Debug("Compiling C# script for issuing claims."); |
| | | 510 | | } |
| | | 511 | | |
| | | 512 | | // Compile the C# script with the provided settings |
| | 3 | 513 | | var script = CSharpDelegateBuilder.Compile(host: host, |
| | 3 | 514 | | code: settings.Code, |
| | 3 | 515 | | extraImports: settings.ExtraImports, |
| | 3 | 516 | | extraRefs: settings.ExtraRefs, |
| | 3 | 517 | | locals: new Dictionary<string, object?> |
| | 3 | 518 | | { |
| | 3 | 519 | | { "identity", "" } |
| | 3 | 520 | | }, languageVersion: settings.CSharpVersion); |
| | | 521 | | |
| | 3 | 522 | | return async (ctx, identity) => |
| | 3 | 523 | | { |
| | 2 | 524 | | var krRequest = await KestrunRequest.NewRequest(ctx); |
| | 2 | 525 | | var krResponse = new KestrunResponse(krRequest); |
| | 2 | 526 | | var context = new KestrunContext(host, krRequest, krResponse, ctx); |
| | 2 | 527 | | var globals = new CsGlobals(host.SharedState.Snapshot(), context, new Dictionary<string, object?> |
| | 2 | 528 | | { |
| | 2 | 529 | | { "identity", identity } |
| | 2 | 530 | | }); |
| | 2 | 531 | | var result = await script.RunAsync(globals).ConfigureAwait(false); |
| | 2 | 532 | | return result.ReturnValue is IEnumerable<Claim> claims |
| | 2 | 533 | | ? claims |
| | 2 | 534 | | : []; |
| | 5 | 535 | | }; |
| | | 536 | | } |
| | | 537 | | |
| | | 538 | | /// <summary> |
| | | 539 | | /// Builds a VB.NET-based function for issuing claims for a user. |
| | | 540 | | /// </summary> |
| | | 541 | | /// <param name="host">The Kestrun host instance.</param> |
| | | 542 | | /// <param name="settings">The authentication code settings containing the VB.NET script.</param> |
| | | 543 | | /// <returns>A function that issues claims using the provided VB.NET script.</returns> |
| | | 544 | | static Func<HttpContext, string, Task<IEnumerable<Claim>>> BuildVBNetIssueClaims( |
| | | 545 | | KestrunHost host, |
| | | 546 | | AuthenticationCodeSettings settings) |
| | | 547 | | { |
| | 3 | 548 | | if (host is null) |
| | | 549 | | { |
| | 0 | 550 | | throw new ArgumentNullException(nameof(host), "KestrunHost cannot be null"); |
| | | 551 | | } |
| | 3 | 552 | | var logger = host.Logger; |
| | 3 | 553 | | if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug)) |
| | | 554 | | { |
| | 2 | 555 | | logger.Debug("Compiling VB.NET script for issuing claims."); |
| | | 556 | | } |
| | | 557 | | |
| | | 558 | | // Compile the VB.NET script with the provided settings |
| | 3 | 559 | | var script = VBNetDelegateBuilder.Compile<IEnumerable<Claim>>( |
| | 3 | 560 | | host: host, |
| | 3 | 561 | | code: settings.Code, |
| | 3 | 562 | | extraImports: settings.ExtraImports, |
| | 3 | 563 | | extraRefs: settings.ExtraRefs, |
| | 3 | 564 | | locals: new Dictionary<string, object?> |
| | 3 | 565 | | { |
| | 3 | 566 | | { "identity", "" } |
| | 3 | 567 | | }, languageVersion: settings.VisualBasicVersion); |
| | | 568 | | |
| | 3 | 569 | | return async (ctx, identity) => |
| | 3 | 570 | | { |
| | 2 | 571 | | var krRequest = await KestrunRequest.NewRequest(ctx); |
| | 2 | 572 | | var krResponse = new KestrunResponse(krRequest); |
| | 2 | 573 | | var context = new KestrunContext(host, krRequest, krResponse, ctx); |
| | 2 | 574 | | var glob = new CsGlobals(host.SharedState.Snapshot(), context, new Dictionary<string, object?> |
| | 2 | 575 | | { |
| | 2 | 576 | | { "identity", identity } |
| | 2 | 577 | | }); |
| | 3 | 578 | | // Run the VB.NET script and get the result |
| | 3 | 579 | | // Note: The script should return a boolean indicating success or failure |
| | 2 | 580 | | var result = await script(glob).ConfigureAwait(false); |
| | 2 | 581 | | return result is IEnumerable<Claim> claims |
| | 2 | 582 | | ? claims |
| | 2 | 583 | | : []; |
| | 5 | 584 | | }; |
| | | 585 | | } |
| | | 586 | | } |