diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5694956..06ea13f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -12,10 +12,11 @@ The documentation uses [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). This documentation is both provided on readthedocs and built in to the application itself. -The application reads the configuration on start up and then populates an in-memory database with users and OpenID -connect clients. Additionally, users and clients can be created at runtime through the web interface at `/users` and -`/clients` respectively. Note that there is no persistence between application restarts, as the in-memory database is -wiped and a new one is used - any users or clients created at runtime will be lost upon restart. +The application reads the configuration on start up and then populates the database with users and OpenID connect +clients. By default, an in-memory database is used and all data is lost on restart — any users or clients created at +runtime through the web interface will not survive a restart. Optionally, a SQLite database file can be configured to +persist data between restarts; see the [Database configuration](DevOidcToolkit.Documentation/docs/configuration.md) +for details. The frontend is styled using basic styling, using the [Sakura CSS library](https://github.com/oxalorg/sakura). diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aa0dd0..931928c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Add optional SQLite persistence via `Database.SqliteFile` config option; defaults to in-memory when not set (see [#20](https://github.com/BusinessSimulations/dev-oidc-toolkit/issues/20)) ## [0.5.0] - Add configurable user roles through `DevOidcToolkit__Users__INDEX__Roles__INDEX` (see [#17](https://github.com/BusinessSimulations/dev-oidc-toolkit/pull/17)) - Add runtime user registration at `/users` page (see [#15](https://github.com/BusinessSimulations/dev-oidc-toolkit/issues/15)) diff --git a/DevOidcToolkit.Documentation/docs/configuration.md b/DevOidcToolkit.Documentation/docs/configuration.md index d996995..cb06473 100644 --- a/DevOidcToolkit.Documentation/docs/configuration.md +++ b/DevOidcToolkit.Documentation/docs/configuration.md @@ -59,6 +59,12 @@ This is a list of all of the environment variables that can be used to configure https://fake-issuer.example.com None (derived from request URL) + + DevOidcToolkit__Database__SqliteFile + The path to the SQLite database file. When set, data is persisted to this file and survives restarts. When not set, an in-memory database is used and all data is lost on restart. + /data/dev-oidc-toolkit.db + None (in-memory) + DevOidcToolkit__Logging__MinimumLevel The minimum log level, possible values are Trace, Debug, Information, Warning, Error, Critical. @@ -204,6 +210,13 @@ details](#example-json-configuration)). https://fake-issuer.example.com None + + Database + object + The database configuration, see Database for more information. + See Database for more information. + None (in-memory) + Https object @@ -352,6 +365,38 @@ details](#example-json-configuration)). +#### Database + +The database configuration controls how data is stored. By default, an in-memory database is used and all data +(including users and clients created at runtime) is lost when the application stops. Set `SqliteFile` to a file path to +use a SQLite database instead, which persists data between restarts. + +!!! note "Limitations" + The SQLite database schema is created automatically on first run using `EnsureCreated`. There are no migrations + supported — if the schema changes in a future version of dev-oidc-toolkit you may need to delete and recreate the + database file. + + + + + + + + + + + + + + + + + + + + +
PropertyTypeDescriptionExampleDefault Value
SqliteFilestringPath to the SQLite database file. When set, all data is persisted to this file. When omitted, an in-memory database is used and data is lost on restart./data/dev-oidc-toolkit.dbNone (in-memory)
+ #### Users @@ -435,6 +480,8 @@ details](#example-json-configuration)). ### Example JSON configuration +In-memory database (default, no persistence): + ```json { "DevOidcToolkit": { @@ -459,3 +506,32 @@ details](#example-json-configuration)). } } ``` + +SQLite database (data persists across restarts): + +```json +{ + "DevOidcToolkit": { + "Port": 8080, + "Database": { + "SqliteFile": "/data/dev-oidc-toolkit.db" + }, + "Users": [ + { + "Email": "sudo@localhost", + "FirstName": "Test", + "LastName": "User" + } + ], + "Clients": [ + { + "Id": "test", + "Secret": "ThisIsNotARealSecret", + "RedirectUris": [ + "http://localhost:3000/callback" + ] + } + ] + } +} +``` diff --git a/DevOidcToolkit.Documentation/docs/runtime-management.md b/DevOidcToolkit.Documentation/docs/runtime-management.md index 0a297f7..d08ed2a 100644 --- a/DevOidcToolkit.Documentation/docs/runtime-management.md +++ b/DevOidcToolkit.Documentation/docs/runtime-management.md @@ -56,6 +56,6 @@ URIs are validated on submission to ensure they are properly formatted. ## Important Notes -- **No Persistence**: Users and clients created at runtime exist only in the in-memory database. They will be lost when the application restarts. +- **Persistence**: By default, users and clients created at runtime exist only in the in-memory database and will be lost when the application restarts. To persist runtime changes, configure a [SQLite database](configuration.md#database). - **Configuration + Runtime**: You can use both configuration-based users/clients and runtime-created ones simultaneously. - **Role Management**: Roles created at runtime persist for the lifetime of the application and can be assigned to multiple users. diff --git a/DevOidcToolkit/DevOidcToolkit.csproj b/DevOidcToolkit/DevOidcToolkit.csproj index 8fe40f1..64d5b58 100644 --- a/DevOidcToolkit/DevOidcToolkit.csproj +++ b/DevOidcToolkit/DevOidcToolkit.csproj @@ -19,6 +19,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/DevOidcToolkit/Infrastructure/Configuration/Configuration.cs b/DevOidcToolkit/Infrastructure/Configuration/Configuration.cs index 3198f64..83a58e9 100644 --- a/DevOidcToolkit/Infrastructure/Configuration/Configuration.cs +++ b/DevOidcToolkit/Infrastructure/Configuration/Configuration.cs @@ -18,6 +18,7 @@ public class DevOidcToolkitConfiguration [ValidateObjectMembers] public HttpsConfiguration? Https { get; set; } [ValidateObjectMembers] public LoggingConfiguration Logging { get; set; } = new LoggingConfiguration(); + [ValidateObjectMembers] public DatabaseConfiguration Database { get; set; } = new DatabaseConfiguration(); } public class UserConfiguration @@ -89,4 +90,9 @@ public class LoggingConfiguration { public LogEventLevel MinimumLevel { get; set; } = LogEventLevel.Information; public bool UseXForwardedForHeader { get; set; } = false; +} + +public class DatabaseConfiguration +{ + public string? SqliteFile { get; set; } } \ No newline at end of file diff --git a/DevOidcToolkit/Program.cs b/DevOidcToolkit/Program.cs index 196528a..bfd46b9 100644 --- a/DevOidcToolkit/Program.cs +++ b/DevOidcToolkit/Program.cs @@ -5,6 +5,7 @@ using DevOidcToolkit.Infrastructure.Database; using Microsoft.AspNetCore.Identity; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.FileProviders; @@ -30,7 +31,18 @@ builder.Services.AddDbContext(options => { - options.UseInMemoryDatabase("dev-auth"); + if (config.Database.SqliteFile is not null) + { + var connectionString = new SqliteConnectionStringBuilder + { + DataSource = config.Database.SqliteFile + }.ToString(); + options.UseSqlite(connectionString); + } + else + { + options.UseInMemoryDatabase("dev-auth"); + } options.UseOpenIddict(); }); @@ -179,12 +191,23 @@ var services = scope.ServiceProvider; var db = services.GetRequiredService(); + if (config.Database.SqliteFile is not null) + { + db.Database.EnsureCreated(); + } + // Set up users and clients in the DB var userManager = services.GetRequiredService>(); var roleManager = services.GetRequiredService>(); for (var i = 0; i < config.Users.Count; i++) { var user = config.Users[i]; + + if (await userManager.FindByEmailAsync(user.Email) is not null) + { + continue; + } + var userEntity = new DevOidcToolkitUser() { Id = i.ToString(), @@ -215,6 +238,11 @@ var openIddictManager = scope.ServiceProvider.GetRequiredService(); foreach (var client in config.Clients) { + if (await openIddictManager.FindByClientIdAsync(client.Id) is not null) + { + continue; + } + var clientApp = new OpenIddictApplicationDescriptor() { ClientId = client.Id,