public static Dictionary <string, RouteInformation> Scan() { var routes = new Dictionary <string, RouteInformation>(StringComparer.OrdinalIgnoreCase); var actions = typeof(RouteScanner).GetTypeInfo().Assembly.GetTypes() .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)) .Where(type => type.IsDefined(typeof(RavenActionAttribute))) .ToList(); foreach (var memberInfo in actions) { foreach (var route in memberInfo.GetCustomAttributes <RavenActionAttribute>()) { RouteInformation routeInfo; var routeKey = route.Method + route.Path; if (routes.TryGetValue(routeKey, out routeInfo) == false) { routes[routeKey] = routeInfo = new RouteInformation(route.Method, route.Path, route.NoAuthorizationRequired); } else { throw new InvalidOperationException($"A duplicate route found: {routeKey} on {memberInfo.DeclaringType}.{memberInfo.Name}"); } routeInfo.Build(memberInfo); } } return(routes); }
private static void ThrowUnknownAuthStatus(RouteInformation route) { throw new ArgumentOutOfRangeException("Unknown route auth status: " + route.AuthorizationStatus); }
private bool TryAuthorize(RouteInformation route, HttpContext context, DocumentDatabase database) { var feature = context.Features.Get <IHttpAuthenticationFeature>() as RavenServer.AuthenticateConnection; if (feature.WrittenToAuditLog == 0) // intentionally racy, we'll check it again later { var auditLog = LoggingSource.AuditLog.IsInfoEnabled ? LoggingSource.AuditLog.GetLogger("RequestRouter", "Audit") : null; if (auditLog != null) { // only one thread will win it, technically, there can't really be threading // here, because there is a single connection, but better to be safe if (Interlocked.CompareExchange(ref feature.WrittenToAuditLog, 1, 0) == 0) { if (feature.WrongProtocolMessage != null) { auditLog.Info($"Connection from {context.Connection.RemoteIpAddress}:{context.Connection.RemotePort} " + $"used the wrong protocol and will be rejected. {feature.WrongProtocolMessage}"); } else { auditLog.Info($"Connection from {context.Connection.RemoteIpAddress}:{context.Connection.RemotePort} " + $"with certificate '{feature.Certificate?.Subject} ({feature.Certificate?.Thumbprint})', status: {feature.StatusForAudit}, " + $"databases: [{string.Join(", ", feature.AuthorizedDatabases.Keys)}]"); var conLifetime = context.Features.Get <IConnectionLifetimeFeature>(); if (conLifetime != null) { var msg = $"Connection {context.Connection.RemoteIpAddress}:{context.Connection.RemotePort} closed. Was used with: " + $"with certificate '{feature.Certificate?.Subject} ({feature.Certificate?.Thumbprint})', status: {feature.StatusForAudit}, " + $"databases: [{string.Join(", ", feature.AuthorizedDatabases.Keys)}]"; conLifetime.ConnectionClosed.Register(() => { auditLog.Info(msg); }); } } } } } var authenticationStatus = feature?.Status; switch (route.AuthorizationStatus) { case AuthorizationStatus.UnauthenticatedClients: var userWantsToAccessStudioMainPage = context.Request.Path == "/studio/index.html"; if (userWantsToAccessStudioMainPage) { switch (authenticationStatus) { case null: case RavenServer.AuthenticationStatus.NoCertificateProvided: case RavenServer.AuthenticationStatus.Expired: case RavenServer.AuthenticationStatus.NotYetValid: case RavenServer.AuthenticationStatus.None: case RavenServer.AuthenticationStatus.UnfamiliarCertificate: UnlikelyFailAuthorization(context, database?.Name, feature, route.AuthorizationStatus); return(false); } } return(true); case AuthorizationStatus.ClusterAdmin: case AuthorizationStatus.Operator: case AuthorizationStatus.ValidUser: case AuthorizationStatus.DatabaseAdmin: switch (authenticationStatus) { case null: case RavenServer.AuthenticationStatus.NoCertificateProvided: case RavenServer.AuthenticationStatus.Expired: case RavenServer.AuthenticationStatus.NotYetValid: case RavenServer.AuthenticationStatus.None: case RavenServer.AuthenticationStatus.UnfamiliarCertificate: UnlikelyFailAuthorization(context, database?.Name, feature, route.AuthorizationStatus); return(false); case RavenServer.AuthenticationStatus.Allowed: if (route.AuthorizationStatus == AuthorizationStatus.Operator || route.AuthorizationStatus == AuthorizationStatus.ClusterAdmin) { goto case RavenServer.AuthenticationStatus.None; } if (database == null) { return(true); } if (feature.CanAccess(database.Name, route.AuthorizationStatus == AuthorizationStatus.DatabaseAdmin)) { return(true); } goto case RavenServer.AuthenticationStatus.None; case RavenServer.AuthenticationStatus.Operator: if (route.AuthorizationStatus == AuthorizationStatus.ClusterAdmin) { goto case RavenServer.AuthenticationStatus.None; } return(true); case RavenServer.AuthenticationStatus.ClusterAdmin: return(true); default: throw new ArgumentOutOfRangeException(); } default: ThrowUnknownAuthStatus(route); return(false); // never hit } }
private bool TryAuthorize(RouteInformation route, HttpContext context, DocumentDatabase database) { var feature = context.Features.Get <IHttpAuthenticationFeature>() as RavenServer.AuthenticateConnection; var authenticationStatus = feature?.Status; switch (route.AuthorizationStatus) { case AuthorizationStatus.UnauthenticatedClients: var userWantsToAccessStudioMainPage = context.Request.Path == "/studio/index.html"; if (userWantsToAccessStudioMainPage) { switch (authenticationStatus) { case null: case RavenServer.AuthenticationStatus.NoCertificateProvided: case RavenServer.AuthenticationStatus.Expired: case RavenServer.AuthenticationStatus.NotYetValid: case RavenServer.AuthenticationStatus.None: case RavenServer.AuthenticationStatus.UnfamiliarCertificate: UnlikelyFailAuthorization(context, database?.Name, feature); return(false); } } return(true); case AuthorizationStatus.ClusterAdmin: case AuthorizationStatus.Operator: case AuthorizationStatus.ValidUser: case AuthorizationStatus.DatabaseAdmin: switch (authenticationStatus) { case null: case RavenServer.AuthenticationStatus.NoCertificateProvided: case RavenServer.AuthenticationStatus.Expired: case RavenServer.AuthenticationStatus.NotYetValid: case RavenServer.AuthenticationStatus.None: case RavenServer.AuthenticationStatus.UnfamiliarCertificate: UnlikelyFailAuthorization(context, database?.Name, feature); return(false); case RavenServer.AuthenticationStatus.Allowed: if (route.AuthorizationStatus == AuthorizationStatus.Operator || route.AuthorizationStatus == AuthorizationStatus.ClusterAdmin) { goto case RavenServer.AuthenticationStatus.None; } if (database == null) { return(true); } if (feature.CanAccess(database.Name, route.AuthorizationStatus == AuthorizationStatus.DatabaseAdmin)) { return(true); } goto case RavenServer.AuthenticationStatus.None; case RavenServer.AuthenticationStatus.Operator: if (route.AuthorizationStatus == AuthorizationStatus.ClusterAdmin) { goto case RavenServer.AuthenticationStatus.None; } return(true); case RavenServer.AuthenticationStatus.ClusterAdmin: return(true); default: throw new ArgumentOutOfRangeException(); } default: ThrowUnknownAuthStatus(route); return(false); // never hit } }
public static Dictionary <string, RouteInformation> Scan(Func <RavenActionAttribute, bool> predicate = null) { var routes = new Dictionary <string, RouteInformation>(StringComparer.OrdinalIgnoreCase); var corsHandler = typeof(CorsPreflightHandler).GetMethod(nameof(CorsPreflightHandler.HandlePreflightRequest)); var actions = typeof(RouteScanner).Assembly.GetTypes() .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)) .Where(type => type.IsDefined(typeof(RavenActionAttribute))) .ToList(); foreach (var memberInfo in actions) { foreach (var route in memberInfo.GetCustomAttributes <RavenActionAttribute>()) { if (predicate != null && predicate(route) == false) { continue; } if (route.CorsMode != CorsMode.None) { // register endpoint for preflight request var optionsRouteKey = "OPTIONS" + route.Path; // we don't check for duplicates here, as single endpoint like: /admin/cluster/node might have 2 verbs (PUT, DELETE), // but we need single OPTIONS handler if (routes.TryGetValue(optionsRouteKey, out RouteInformation optionsRouteInfo) == false) { routes[optionsRouteKey] = optionsRouteInfo = new RouteInformation( "OPTIONS", route.Path, route.RequiredAuthorization, route.EndpointType, route.SkipUsagesCount, route.SkipLastRequestTimeUpdate, route.CorsMode, route.IsDebugInformationEndpoint, route.DisableOnCpuCreditsExhaustion); optionsRouteInfo.Build(corsHandler); } } var routeKey = route.Method + route.Path; if (routes.TryGetValue(routeKey, out RouteInformation routeInfo) == false) { routes[routeKey] = routeInfo = new RouteInformation( route.Method, route.Path, route.RequiredAuthorization, route.EndpointType, route.SkipUsagesCount, route.SkipLastRequestTimeUpdate, route.CorsMode, route.IsDebugInformationEndpoint, route.DisableOnCpuCreditsExhaustion); } else { throw new InvalidOperationException($"A duplicate route found: {routeKey} on {memberInfo.DeclaringType}.{memberInfo.Name}"); } routeInfo.Build(memberInfo); } } return(routes); }
internal bool CanAccessRoute(RouteInformation route, HttpContext context, string databaseName, RavenServer.AuthenticateConnection feature, out RavenServer.AuthenticationStatus authenticationStatus) { authenticationStatus = feature?.Status ?? RavenServer.AuthenticationStatus.None; switch (route.AuthorizationStatus) { case AuthorizationStatus.UnauthenticatedClients: var userWantsToAccessStudioMainPage = context.Request.Path == "/studio/index.html"; if (userWantsToAccessStudioMainPage) { switch (authenticationStatus) { case RavenServer.AuthenticationStatus.NoCertificateProvided: case RavenServer.AuthenticationStatus.Expired: case RavenServer.AuthenticationStatus.NotYetValid: case RavenServer.AuthenticationStatus.None: case RavenServer.AuthenticationStatus.UnfamiliarCertificate: case RavenServer.AuthenticationStatus.UnfamiliarIssuer: return(false); } } return(true); case AuthorizationStatus.ClusterAdmin: case AuthorizationStatus.Operator: case AuthorizationStatus.ValidUser: case AuthorizationStatus.DatabaseAdmin: case AuthorizationStatus.RestrictedAccess: switch (authenticationStatus) { case RavenServer.AuthenticationStatus.NoCertificateProvided: case RavenServer.AuthenticationStatus.Expired: case RavenServer.AuthenticationStatus.NotYetValid: case RavenServer.AuthenticationStatus.None: return(false); case RavenServer.AuthenticationStatus.UnfamiliarCertificate: case RavenServer.AuthenticationStatus.UnfamiliarIssuer: // we allow an access to the restricted endpoints with an unfamiliar certificate, since we will authorize it at the endpoint level if (route.AuthorizationStatus == AuthorizationStatus.RestrictedAccess) { return(true); } ; goto case RavenServer.AuthenticationStatus.None; case RavenServer.AuthenticationStatus.Allowed: if (route.AuthorizationStatus == AuthorizationStatus.Operator || route.AuthorizationStatus == AuthorizationStatus.ClusterAdmin) { goto case RavenServer.AuthenticationStatus.None; } if (databaseName == null) { return(true); } if (feature.CanAccess(databaseName, route.AuthorizationStatus == AuthorizationStatus.DatabaseAdmin, route.EndpointType == EndpointType.Write)) { return(true); } goto case RavenServer.AuthenticationStatus.None; case RavenServer.AuthenticationStatus.Operator: if (route.AuthorizationStatus == AuthorizationStatus.ClusterAdmin) { goto case RavenServer.AuthenticationStatus.None; } return(true); case RavenServer.AuthenticationStatus.ClusterAdmin: return(true); default: throw new ArgumentOutOfRangeException(); } default: ThrowUnknownAuthStatus(route); return(false); // never hit } }
internal async ValueTask <(bool Authorized, RavenServer.AuthenticationStatus Status)> TryAuthorizeAsync(RouteInformation route, HttpContext context, DocumentDatabase database) { var feature = context.Features.Get <IHttpAuthenticationFeature>() as RavenServer.AuthenticateConnection; if (feature.WrittenToAuditLog == 0) // intentionally racy, we'll check it again later { var auditLog = LoggingSource.AuditLog.IsInfoEnabled ? LoggingSource.AuditLog.GetLogger("RequestRouter", "Audit") : null; if (auditLog != null) { // only one thread will win it, technically, there can't really be threading // here, because there is a single connection, but better to be safe if (Interlocked.CompareExchange(ref feature.WrittenToAuditLog, 1, 0) == 0) { if (feature.WrongProtocolMessage != null) { auditLog.Info($"Connection from {context.Connection.RemoteIpAddress}:{context.Connection.RemotePort} " + $"used the wrong protocol and will be rejected. {feature.WrongProtocolMessage}"); } else { auditLog.Info($"Connection from {context.Connection.RemoteIpAddress}:{context.Connection.RemotePort} " + $"with certificate '{feature.Certificate?.Subject} ({feature.Certificate?.Thumbprint})', status: {feature.StatusForAudit}, " + $"databases: [{string.Join(", ", feature.AuthorizedDatabases.Keys)}]"); var conLifetime = context.Features.Get <IConnectionLifetimeFeature>(); if (conLifetime != null) { var msg = $"Connection {context.Connection.RemoteIpAddress}:{context.Connection.RemotePort} closed. Was used with: " + $"with certificate '{feature.Certificate?.Subject} ({feature.Certificate?.Thumbprint})', status: {feature.StatusForAudit}, " + $"databases: [{string.Join(", ", feature.AuthorizedDatabases.Keys)}]"; CancellationTokenRegistration cancellationTokenRegistration = default; cancellationTokenRegistration = conLifetime.ConnectionClosed.Register(() => { auditLog.Info(msg); cancellationTokenRegistration.Dispose(); }); } } } } } if (CanAccessRoute(route, context, database?.Name, feature, out var authenticationStatus) == false) { await UnlikelyFailAuthorizationAsync(context, database?.Name, feature, route.AuthorizationStatus); return(false, authenticationStatus); } return(true, authenticationStatus); }