Skip to content

Commit

Permalink
[ODS-5298] Improve Error Reporting for Bad Connection String (#453)
Browse files Browse the repository at this point in the history
* Fix admin and security database initializers

* Add Lazy to the security repository

* Fix unit tests

* Update reference to DataAccess admin & security

* Fix Postman tests

* Update reference to DataAccess admin & security
  • Loading branch information
AxelMarquez authored and axelmarquezh committed Apr 16, 2024
1 parent d4b6ebb commit 4cb58ff
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 87 deletions.
3 changes: 2 additions & 1 deletion Application/EdFi.Admin.DataAccess/Contexts/UsersContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public abstract class UsersContext : DbContext, IUsersContext
protected UsersContext(string connectionString)
: base(connectionString)
{
Database.SetInitializer(new ValidateDatabase<UsersContext>());
Database.SetInitializer(new ValidateDatabase<SqlServerUsersContext>());
Database.SetInitializer(new ValidateDatabase<PostgresUsersContext>());
}
public const string UserTableName = "Users";

Expand Down
7 changes: 1 addition & 6 deletions Application/EdFi.Admin.DataAccess/Utils/ValidateDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,7 @@ public void InitializeDatabase(TContext context)
{
if (!context.Database.Exists())
{
throw new ConfigurationErrorsException("Admin database does not exist.");
}

if (!context.Database.CompatibleWithModel(true))
{
throw new InvalidOperationException($"The {context.Database.Connection.Database} database is not compatible with the entity model.");
throw new ConfigurationErrorsException("Unable to open connection to the Admin database.");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// See the LICENSE and NOTICES files in the project root for more information.

using System.Collections.Generic;
using System.Configuration;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
Expand Down Expand Up @@ -59,6 +60,11 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
return authenticationResult.AuthenticateResult;
}
}
catch(ConfigurationException)
{
// The Security repository couldn't open a connection to the Security database
throw;
}
catch
{
return AuthenticateResult.Fail("Invalid Authorization Header");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public abstract class SecurityContext : DbContext, ISecurityContext
protected SecurityContext(string connectionString)
: base(connectionString)
{
Database.SetInitializer(new ValidateDatabase<SecurityContext>());
Database.SetInitializer(new ValidateDatabase<SqlServerSecurityContext>());
Database.SetInitializer(new ValidateDatabase<PostgresSecurityContext>());
}

public DbSet<Application> Applications { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private void RefreshCacheIfNeeded()

try
{
LoadSecurityConfigurationFromDatabase();
Reset();
_lastCacheUpdate = SystemClock.Now();
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
// See the LICENSE and NOTICES files in the project root for more information.

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EdFi.Common;
using EdFi.Security.DataAccess.Contexts;
using EdFi.Security.DataAccess.Models;

namespace EdFi.Security.DataAccess.Repositories
{
Expand All @@ -18,53 +20,81 @@ public class SecurityRepository : SecurityRepositoryBase, ISecurityRepository
public SecurityRepository(ISecurityContextFactory securityContextFactory)
{
_securityContextFactory = Preconditions.ThrowIfNull(securityContextFactory, nameof(securityContextFactory));
LoadSecurityConfigurationFromDatabase();

Initialize(
GetApplication,
GetActions,
GetClaimSets,
GetResourceClaims,
GetAuthorizationStrategies,
GetClaimSetResourceClaims,
GetResourceClaimAuthorizationMetadata);
}

private Application GetApplication()
{
using var context = _securityContextFactory.CreateContext();

return context.Applications.First(
app => app.ApplicationName.Equals("Ed-Fi ODS API", StringComparison.InvariantCultureIgnoreCase));
}

private List<Models.Action> GetActions()
{
using var context = _securityContextFactory.CreateContext();

return context.Actions.ToList();
}

private List<ClaimSet> GetClaimSets()
{
using var context = _securityContextFactory.CreateContext();

return context.ClaimSets.Include(cs => cs.Application).ToList();
}

protected void LoadSecurityConfigurationFromDatabase()
private List<ResourceClaim> GetResourceClaims()
{
using (var context = _securityContextFactory.CreateContext())
{
var application =
context.Applications.First(
app => app.ApplicationName.Equals("Ed-Fi ODS API", StringComparison.InvariantCultureIgnoreCase));

var actions = context.Actions.ToList();

var claimSets = context.ClaimSets.Include(cs => cs.Application)
.ToList();

var resourceClaims = context.ResourceClaims.Include(rc => rc.Application)
.Include(rc => rc.ParentResourceClaim)
.Where(rc => rc.Application.ApplicationId.Equals(application.ApplicationId))
.ToList();

var authorizationStrategies = context.AuthorizationStrategies.Include(auth => auth.Application)
.Where(auth => auth.Application.ApplicationId.Equals(application.ApplicationId))
.ToList();

var claimSetResourceClaims = context.ClaimSetResourceClaims.Include(csrc => csrc.Action)
.Include(csrc => csrc.ClaimSet)
.Include(csrc => csrc.ResourceClaim)
.Where(csrc => csrc.ResourceClaim.Application.ApplicationId.Equals(application.ApplicationId))
.ToList();

var resourceClaimAuthorizationMetadata =
context.ResourceClaimAuthorizationMetadatas.Include(rcas => rcas.Action)
.Include(rcas => rcas.AuthorizationStrategy)
.Include(rcas => rcas.ResourceClaim)
.Where(rcas => rcas.ResourceClaim.Application.ApplicationId.Equals(application.ApplicationId))
.ToList();

Initialize(
application,
actions,
claimSets,
resourceClaims,
authorizationStrategies,
claimSetResourceClaims,
resourceClaimAuthorizationMetadata);
}
using var context = _securityContextFactory.CreateContext();

return context.ResourceClaims
.Include(rc => rc.Application)
.Include(rc => rc.ParentResourceClaim)
.Where(rc => rc.Application.ApplicationId.Equals(Application.Value.ApplicationId))
.ToList();
}

private List<AuthorizationStrategy> GetAuthorizationStrategies()
{
using var context = _securityContextFactory.CreateContext();

return context.AuthorizationStrategies
.Include(auth => auth.Application)
.Where(auth => auth.Application.ApplicationId.Equals(Application.Value.ApplicationId))
.ToList();
}

private List<ClaimSetResourceClaim> GetClaimSetResourceClaims()
{
using var context = _securityContextFactory.CreateContext();

return context.ClaimSetResourceClaims.Include(csrc => csrc.Action)
.Include(csrc => csrc.ClaimSet)
.Include(csrc => csrc.ResourceClaim)
.Where(csrc => csrc.ResourceClaim.Application.ApplicationId.Equals(Application.Value.ApplicationId))
.ToList();
}

private List<ResourceClaimAuthorizationMetadata> GetResourceClaimAuthorizationMetadata()
{
using var context = _securityContextFactory.CreateContext();

return context.ResourceClaimAuthorizationMetadatas
.Include(rcas => rcas.Action)
.Include(rcas => rcas.AuthorizationStrategy)
.Include(rcas => rcas.ResourceClaim)
.Where(rcas => rcas.ResourceClaim.Application.ApplicationId.Equals(Application.Value.ApplicationId))
.ToList();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,57 @@
using System.Collections.Generic;
using System.Linq;
using EdFi.Security.DataAccess.Models;
using EdFi.Security.DataAccess.Utils;
using Action = EdFi.Security.DataAccess.Models.Action;

namespace EdFi.Security.DataAccess.Repositories
{
public abstract class SecurityRepositoryBase
{
protected Application Application { get; private set; }
protected ResettableLazy<Application> Application { get; private set; }

protected List<Action> Actions { get; private set; }
protected ResettableLazy<List<Action>> Actions { get; private set; }

protected List<ClaimSet> ClaimSets { get; private set; }
protected ResettableLazy<List<ClaimSet>> ClaimSets { get; private set; }

protected List<ResourceClaim> ResourceClaims { get; private set; }
protected ResettableLazy<List<ResourceClaim>> ResourceClaims { get; private set; }

protected List<AuthorizationStrategy> AuthorizationStrategies { get; private set; }
protected ResettableLazy<List<AuthorizationStrategy>> AuthorizationStrategies { get; private set; }

protected List<ClaimSetResourceClaim> ClaimSetResourceClaims { get; private set; }
protected ResettableLazy<List<ClaimSetResourceClaim>> ClaimSetResourceClaims { get; private set; }

protected List<ResourceClaimAuthorizationMetadata> ResourceClaimAuthorizationMetadata { get; private set; }
protected ResettableLazy<List<ResourceClaimAuthorizationMetadata>> ResourceClaimAuthorizationMetadata { get; private set; }

protected void Initialize(
Application application,
List<Action> actions,
List<ClaimSet> claimSets,
List<ResourceClaim> resourceClaims,
List<AuthorizationStrategy> authorizationStrategies,
List<ClaimSetResourceClaim> claimSetResourceClaims,
List<ResourceClaimAuthorizationMetadata> resourceClaimAuthorizationMetadata)
Func<Application> application,
Func<List<Action>> actions,
Func<List<ClaimSet>> claimSets,
Func<List<ResourceClaim>> resourceClaims,
Func<List<AuthorizationStrategy>> authorizationStrategies,
Func<List<ClaimSetResourceClaim>> claimSetResourceClaims,
Func<List<ResourceClaimAuthorizationMetadata>> resourceClaimAuthorizationMetadata)
{
Application = application;
Actions = actions;
ClaimSets = claimSets;
ResourceClaims = resourceClaims;
AuthorizationStrategies = authorizationStrategies;
ClaimSetResourceClaims = claimSetResourceClaims;
ResourceClaimAuthorizationMetadata = resourceClaimAuthorizationMetadata;
Application = new ResettableLazy<Application>(application);
Actions = new ResettableLazy<List<Action>>(actions);
ClaimSets = new ResettableLazy<List<ClaimSet>>(claimSets);
ResourceClaims = new ResettableLazy<List<ResourceClaim>>(resourceClaims);
AuthorizationStrategies = new ResettableLazy<List<AuthorizationStrategy>>(authorizationStrategies);
ClaimSetResourceClaims = new ResettableLazy<List<ClaimSetResourceClaim>>(claimSetResourceClaims);
ResourceClaimAuthorizationMetadata = new ResettableLazy<List<ResourceClaimAuthorizationMetadata>>(resourceClaimAuthorizationMetadata);
}

/// <summary>
/// Clears the cache, the database will be hit lazily.
/// </summary>
protected void Reset()
{
Application.Reset();
Actions.Reset();
ClaimSets.Reset();
ResourceClaims.Reset();
AuthorizationStrategies.Reset();
ClaimSetResourceClaims.Reset();
ResourceClaimAuthorizationMetadata.Reset();
}

public virtual Action GetActionByHttpVerb(string httpVerb)
Expand Down Expand Up @@ -70,18 +85,18 @@ public virtual Action GetActionByHttpVerb(string httpVerb)

public virtual Action GetActionByName(string actionName)
{
return Actions.First(a => a.ActionName.Equals(actionName, StringComparison.InvariantCultureIgnoreCase));
return Actions.Value.First(a => a.ActionName.Equals(actionName, StringComparison.InvariantCultureIgnoreCase));
}

public virtual AuthorizationStrategy GetAuthorizationStrategyByName(string authorizationStrategyName)
{
return AuthorizationStrategies.First(
return AuthorizationStrategies.Value.First(
a => a.AuthorizationStrategyName.Equals(authorizationStrategyName, StringComparison.InvariantCultureIgnoreCase));
}

public virtual IEnumerable<ClaimSetResourceClaim> GetClaimsForClaimSet(string claimSetName)
{
return ClaimSetResourceClaims.Where(c => c.ClaimSet.ClaimSetName.Equals(claimSetName, StringComparison.InvariantCultureIgnoreCase));
return ClaimSetResourceClaims.Value.Where(c => c.ClaimSet.ClaimSetName.Equals(claimSetName, StringComparison.InvariantCultureIgnoreCase));
}

/// <summary>
Expand All @@ -104,6 +119,7 @@ private IEnumerable<ResourceClaim> GetResourceClaimLineageForResourceClaim(strin
try
{
resourceClaim = ResourceClaims
.Value
.SingleOrDefault(rc => rc.ClaimName.Equals(resourceClaimUri, StringComparison.InvariantCultureIgnoreCase));
}
catch (InvalidOperationException ex)
Expand Down Expand Up @@ -144,6 +160,7 @@ private void AddStrategiesForResourceClaimLineage(List<ResourceClaimAuthorizatio
{
//check for exact match on resource and action
var claimAndStrategy = ResourceClaimAuthorizationMetadata
.Value
.SingleOrDefault(
rcas =>
rcas.ResourceClaim.ClaimName.Equals(resourceClaimUri, StringComparison.InvariantCultureIgnoreCase)
Expand All @@ -155,8 +172,9 @@ private void AddStrategiesForResourceClaimLineage(List<ResourceClaimAuthorizatio
strategies.Add(claimAndStrategy);
}

var resourceClaim =
ResourceClaims.FirstOrDefault(rc => rc.ClaimName.Equals(resourceClaimUri, StringComparison.InvariantCultureIgnoreCase));
var resourceClaim = ResourceClaims
.Value
.FirstOrDefault(rc => rc.ClaimName.Equals(resourceClaimUri, StringComparison.InvariantCultureIgnoreCase));

// if there's a parent resource, recurse
if (resourceClaim != null && resourceClaim.ParentResourceClaim != null)
Expand All @@ -167,7 +185,9 @@ private void AddStrategiesForResourceClaimLineage(List<ResourceClaimAuthorizatio

public virtual ResourceClaim GetResourceByResourceName(string resourceName)
{
return ResourceClaims.FirstOrDefault(rc => rc.ResourceName.Equals(resourceName, StringComparison.InvariantCultureIgnoreCase));
return ResourceClaims
.Value
.FirstOrDefault(rc => rc.ResourceName.Equals(resourceName, StringComparison.InvariantCultureIgnoreCase));
}
}
}
24 changes: 24 additions & 0 deletions Application/EdFi.Security.DataAccess/Utils/ResettableLazy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;

namespace EdFi.Security.DataAccess.Utils
{
public class ResettableLazy<T>
{
private readonly Func<T> _valueFactory;
private Lazy<T> _lazy;

public bool IsValueCreated => _lazy.IsValueCreated;
public T Value => _lazy.Value;

public ResettableLazy(Func<T> valueFactory)
{
_valueFactory = valueFactory;
_lazy = new Lazy<T>(_valueFactory);
}

public void Reset()
{
_lazy = new Lazy<T>(_valueFactory);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,7 @@ public void InitializeDatabase(TContext context)
{
if (!context.Database.Exists())
{
throw new ConfigurationErrorsException("Security database does not exist.");
}

if (!context.Database.CompatibleWithModel(true))
{
throw new InvalidOperationException($"The {context.Database.Connection.Database} database is not compatible with the entity model.");
throw new ConfigurationErrorsException("Unable to open connection to the Security database.");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ protected void ArrangeBase()
protected void InitializeSystemUnderTest(int cacheTimeoutInMinutes)
{
_systemUnderTest = new CachedSecurityRepository(_securityContextFactory, cacheTimeoutInMinutes);
_systemUnderTest.GetClaimsForClaimSet("ClaimSet");

// Should hit the database on construction
// Should hit the database after calling a getter because it has lazy initialization
A.CallTo(() => _securityContextFactory.CreateContext()).MustHaveHappened(1, Times.Exactly);
Fake.ClearRecordedCalls(_securityContextFactory);
}
Expand Down

0 comments on commit 4cb58ff

Please sign in to comment.