diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55e26b7..630d960 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,21 +8,31 @@ on: jobs: verify_tag: - name: Verify tag 'release-*' on the head + name: Verify tag 'release-*' exists on the current commit runs-on: ubuntu-latest permissions: contents: read - + steps: - uses: actions/checkout@v6 - - name: Abort - if: github.ref != 'refs/heads/main' || !startsWith(github.ref, 'refs/tags/release-') - run: exit 1 - - - name: Success - if: github.ref == 'refs/heads/main' && startsWith(github.ref, 'refs/tags/release-') - run: exit 0 + - name: Check for release tag on current commit + run: | + # Get current commit hash + CURRENT_COMMIT=$(git rev-parse HEAD) + + # Check if any release-* tag points to current commit + RELEASE_TAG=$(git tag --points-at $CURRENT_COMMIT | grep '^release-' | head -1) + + if [ -z "$RELEASE_TAG" ]; then + echo "❌ No release-* tag found on current commit $CURRENT_COMMIT" + echo "Available tags on this commit:" + git tag --points-at $CURRENT_COMMIT || echo "No tags found" + exit 1 + else + echo "✅ Found release tag: $RELEASE_TAG on commit $CURRENT_COMMIT" + echo "RELEASE_TAG=$RELEASE_TAG" >> $GITHUB_ENV + fi verify_release_notes: name: Verify RELEASE_NOTES.md has been changed before Release diff --git a/.gitignore b/.gitignore index bf3675e..bd559ef 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ bld/ [Bb]in/ [Oo]bj/ -AutoBackend.sln.DotSettings.user \ No newline at end of file +AutoBackend.sln.DotSettings.user +.idea \ No newline at end of file diff --git a/.idea/.idea.AutoBackend/.idea/.gitignore b/.idea/.idea.AutoBackend/.idea/.gitignore deleted file mode 100644 index 8ce2ff0..0000000 --- a/.idea/.idea.AutoBackend/.idea/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/projectSettingsUpdater.xml -/modules.xml -/contentModel.xml -/.idea.AutoBackend.iml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.idea.AutoBackend/.idea/.name b/.idea/.idea.AutoBackend/.idea/.name deleted file mode 100644 index d2c5a76..0000000 --- a/.idea/.idea.AutoBackend/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -AutoBackend \ No newline at end of file diff --git a/.idea/.idea.AutoBackend/.idea/encodings.xml b/.idea/.idea.AutoBackend/.idea/encodings.xml deleted file mode 100644 index df87cf9..0000000 --- a/.idea/.idea.AutoBackend/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/.idea.AutoBackend/.idea/indexLayout.xml b/.idea/.idea.AutoBackend/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/.idea/.idea.AutoBackend/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/.idea.AutoBackend/.idea/jsonSchemas.xml b/.idea/.idea.AutoBackend/.idea/jsonSchemas.xml deleted file mode 100644 index 13038b0..0000000 --- a/.idea/.idea.AutoBackend/.idea/jsonSchemas.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/.idea.AutoBackend/.idea/markdown.xml b/.idea/.idea.AutoBackend/.idea/markdown.xml deleted file mode 100644 index f6d2542..0000000 --- a/.idea/.idea.AutoBackend/.idea/markdown.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.AutoBackend/.idea/vcs.xml b/.idea/.idea.AutoBackend/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/.idea.AutoBackend/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index b5510ab..0f1b2b6 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [![MyGet](https://img.shields.io/myget/autobackend-dev/v/AutoBackend.SDK?label=myget&style=for-the-badge)](https://www.myget.org/feed/autobackend-dev/package/nuget/AutoBackend.SDK) | [![MyGet](https://img.shields.io/myget/autobackend/v/AutoBackend.SDK?label=myget&style=for-the-badge)](https://www.myget.org/feed/autobackend/package/nuget/AutoBackend.SDK) [![NuGet](https://img.shields.io/nuget/v/AutoBackend.SDK?label=nuget&style=for-the-badge)](https://www.nuget.org/packages/AutoBackend.SDK) | -This package provides infrastructure templates for facilitating the creation of backend services. -It is a personal project without a commercial foundation and offers no guarantees regarding future development. +This package provides infrastructure templates to facilitate the creation of backend services. +This is a personal project with no commercial backing and offers no guarantees regarding future development. # Features @@ -24,12 +24,11 @@ It is a personal project without a commercial foundation and offers no guarantee - [Mutations](#mutations) - [Modeling](#modeling) - [Filtering](#filtering) - - _Authorization: currently unsupported (may be introduced in future releases)_ + - [Authorization](#authorization) # Initialization A sample usage scenario is available [here](src/samples/Sample). -Copies of those samples are also provided. ## Initialize AutoBackend from your Program.cs file @@ -80,13 +79,15 @@ public class Participating } ``` -## Configuration of all features is detailed below +## Feature Configuration + +All features are detailed in the sections below. --- # Database -Currently supported relational databases (including in-memory) are: +The following relational databases are currently supported (including in-memory): - SqlServer - Postgres @@ -101,8 +102,7 @@ There are typically three types of database tables: - Single-column primary key, - Multi-column primary key. -Depending on the number of columns in your entity, follow the instructions below to enable AutoBackend.SDK to track -changes. +Depending on the number of primary key columns in your entity, follow the instructions below to enable AutoBackend.SDK to track changes. ### Keyless Entities @@ -118,8 +118,7 @@ public class TransactionVersion ### Single-PK Entities -Use the `[GenericEntity()]` attribute to designate a model as an entity with a single -property primary key. +Use the `[GenericEntity()]` attribute to designate a model as an entity with a single-column primary key. ```csharp [GenericEntity( @@ -140,7 +139,7 @@ primary key. > Currently, the maximum number of properties in a composite key is **8**. > -> It is anticipated that this limit will suffice for most use cases. +> This limit should be sufficient for most use cases. ```csharp [GenericEntity( @@ -159,7 +158,7 @@ public class Participating ## Providers -For database connection management, configure the "Database" section. +To configure database connection management, set up the "Database" section in your configuration. ```json "Database": { @@ -178,29 +177,23 @@ The `PrimaryProvider` property accepts one of the following string values: - `SqlServer`, - `Postgres`. -The `Providers` property must contain an object with optional properties: `InMemory`, `SqlServer`, or `Postgres`. The -property corresponding to the chosen `PrimaryProvider` value is mandatory. +The `Providers` property must contain an object with optional properties: `InMemory`, `SqlServer`, or `Postgres`. The property corresponding to the selected `PrimaryProvider` value is required. ## Migrations First, [install Entity Framework Core Tools](https://learn.microsoft.com/en-us/ef/core/cli/dotnet). -You can create a new migration with one of the following commands executed from the project root directory. - -`dotnet ef migrations add "" -o Migrations/SqlServer -c SqlServerGenericDbContext` for SqlServer. +You can create a new migration by executing one of the following commands from the project root directory: -`dotnet ef migrations add "" -o Migrations/Postgres -c PostgresGenericDbContext` for Postgres. +- For SqlServer: `dotnet ef migrations add "" -o Migrations/SqlServer -c SqlServerGenericDbContext` +- For Postgres: `dotnet ef migrations add "" -o Migrations/Postgres -c PostgresGenericDbContext` -Alternatively, use scripts [to add a new migration](scripts/add_migration.sh) -or [to remove the last migration](scripts/remove_migration.sh) for both database providers. +Alternatively, you can use the provided scripts [to add a new migration](scripts/add_migration.sh) or [to remove the last migration](scripts/remove_migration.sh) for both database providers. -If you opt not to have AutoBackend manage database migrations (see above), you can perform migrations manually by -executing `dotnet ef database update` from the project root directory. +If you choose not to use AutoBackend's migration management (see above), you can perform migrations manually by executing `dotnet ef database update` from the project root directory. ### Migrate on Startup -If using a relational database (e.g., SqlServer or Postgres), you can configure AutoBackend to either automatically -migrate the database at application startup or not, by passing the `migrateRelationalOnStartup` parameter to -the `RunAsync` method where AutoBackend is initialized. For example: +When using a relational database (e.g., SqlServer or Postgres), you can configure AutoBackend to automatically migrate the database at application startup by passing the `migrateRelationalOnStartup` parameter to the `RunAsync` method during AutoBackend initialization. For example: ```csharp await new AutoBackend.Sdk.AutoBackendHost().RunAsync(args, migrateRelationalOnStartup: true); @@ -208,14 +201,14 @@ await new AutoBackend.Sdk.AutoBackendHost().RunAsync(args, migrateRelat # API -AutoBackend.SDK currently supports two types of application interfaces: +AutoBackend.SDK currently supports two types of API interfaces: -- Traditional HTTP API -- Basic GraphQL +- HTTP REST API +- GraphQL ## HTTP API -Use the `[GenericController]` attribute on models to generate HTTP API endpoints by AutoBackend. +Apply the `[GenericController]` attribute to your models to have AutoBackend generate HTTP API endpoints. > ❗**Disclaimer** > @@ -223,7 +216,7 @@ Use the `[GenericController]` attribute on models to generate HTTP API endpoints ### Response Container and Endpoints -The current and only API version is v1. API v1 supports JSON and uses the following response container format: +The current API version is v1. API v1 uses JSON and follows the following response container format: ``` { @@ -235,7 +228,7 @@ The current and only API version is v1. API v1 supports JSON and uses the follow } ``` -For detailed information on generated endpoints, access `/swagger`. +For detailed information on generated endpoints, visit `/swagger`. ### Endpoints @@ -276,15 +269,14 @@ public class User ## GraphQL -Use the `[GenericGqlQuery]` and `[GenericGqlMutation]` attributes on models to generate GraphQL **queries** and * -*mutations**, respectively. +Apply the `[GenericGqlQuery]` and `[GenericGqlMutation]` attributes to your models to generate GraphQL **queries** and **mutations**, respectively. > ❗**Disclaimer** > > The `[GenericGqlQuery]` and `[GenericGqlMutation]` attributes are compatible only with models also marked > with `[GenericEntity]`. -For detailed information on generated queries and mutations, access `/graphql`. +For detailed information on generated queries and mutations, visit `/graphql`. ### Queries @@ -347,14 +339,12 @@ public class User ## Modeling -AutoBackend.SDK generates request and response models for any entity with configured HTTP API or GraphQL generation. -These models include all original entity properties, unless specific properties are explicitly included in request or -response models, in which case non-specified properties will be omitted. +AutoBackend.SDK automatically generates request and response models for any entity with HTTP API or GraphQL generation configured. +By default, these models include all original entity properties. However, if you explicitly specify properties using the `[GenericRequest]` or `[GenericResponse]` attributes, only those specified properties will be included, and all others will be omitted. ### Request Models -The `[GenericRequest]` attribute defines which properties are permitted for reflection from the request model to the -entity. +The `[GenericRequest]` attribute specifies which properties can be mapped from the request model to the entity. #### Code Samples @@ -382,8 +372,7 @@ public class Transaction ### Response Models -The `[GenericResponse]` attribute defines which properties are permitted for reflection from the response model to the -entity. +The `[GenericResponse]` attribute specifies which properties can be mapped from the entity to the response model. #### Code Samples @@ -408,6 +397,119 @@ public class Transaction } ``` +## Authorization + +AutoBackend.SDK supports permission-based authorization using JWT tokens. You can control access to CRUD operations at both entity and property levels. + +### Configuration + +Configure JWT settings in your `appsettings.json`: + +```json +"Jwt": { + "PublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----", + "ValidIssuer": "AutoBackendServer", + "ValidAudience": "AutoBackendUser" +} +``` + +- `PublicKey`: RSA public key in PEM format (required) - used to verify JWT token signatures +- `ValidIssuer`: Optional issuer validation +- `ValidAudience`: Optional audience validation + +### Permission Attributes + +Use permission attributes on entity classes to require authorization for specific operations: + +- `[GenericCreatePermission]` - Requires permission to create entities +- `[GenericReadPermission]` - Requires permission to read entities +- `[GenericUpdatePermission]` - Requires permission to update entities +- `[GenericDeletePermission]` - Requires permission to delete entities + +#### Code Samples + +```csharp +[GenericEntity( + nameof(Id) +)] +[GenericController] +[GenericGqlQuery] +[GenericGqlMutation] +[GenericCreatePermission] +[GenericReadPermission] +[GenericUpdatePermission] +[GenericDeletePermission] +public class Budget +{ + public Guid Id { get; set; } + + // ... +} +``` + +### Property-Level Permissions + +> ⚠️ **Note**: Property-level permissions are currently only processed for **Update** operations. + +You can apply permission attributes to individual properties for fine-grained access control during update operations: + +```csharp +[GenericEntity( + nameof(Id) +)] +[GenericController] +[GenericGqlMutation] +[GenericUpdatePermission] +public class Transaction +{ + public Guid Id { get; set; } + + [GenericUpdatePermission] + public string? SecretKey { get; set; } + + // ... +} +``` + +When updating an entity, AutoBackend will check property-level permissions only for properties that are actually being modified. This allows you to restrict access to sensitive fields while allowing updates to other properties. + +### JWT Token Format + +JWT tokens must be sent in the `Authorization` header with the `Bearer` prefix: + +``` +Authorization: Bearer +``` + +The JWT token must contain claims with: +- **Type**: `permissions` +- **Value**: Permission strings in the format `{EntityName}{PermissionType}` or `{EntityName}{PropertyName}{PermissionType}` + +#### Permission Name Format + +- **Entity permissions**: `{EntityName}{PermissionType}` + - Examples: `BudgetCreate`, `BudgetRead`, `BudgetUpdate`, `BudgetDelete` +- **Property permissions**: `{EntityName}{PropertyName}{PermissionType}` (only for Update operations) + - Examples: `TransactionSecretKeyUpdate` + +#### Example JWT Claims + +```json +{ + "permissions": [ + "BudgetCreate", + "BudgetRead", + "BudgetUpdate", + "BudgetDelete", + "TransactionSecretKeyUpdate" + ] +} +``` + +### Error Handling + +When a request lacks required permissions, AutoBackend returns an `Unauthorized` error with details about missing permissions. + ## Filtering AutoBackend.SDK generates filter models for any entity with configured HTTP API or GraphQL generation. By default, these @@ -416,15 +518,14 @@ the `[GenericFilter]` attribute. ### Defaults -By default, two filter parameters are always available for any GET request (returning a list of entities) or GraphQL -queries, to manage pagination: +By default, two filter parameters are always available for any GET request (returning a list of entities) or GraphQL queries to manage pagination: - `skipCount`: number - `takeCount`: number -### Generic +### Custom Filtering -The `[GenericFilter]` attribute designates a model property as filterable in the generated entity. +The `[GenericFilter]` attribute marks a model property as filterable in the generated filter model. ```csharp [GenericEntity( @@ -448,11 +549,17 @@ public class Budget } ``` -Consequently, AutoBackend will construct a filter model with parameters that can be utilized in API endpoints -like `/api/v1/` or `/api/v1//count`, and in GraphQL queries. +AutoBackend will construct a filter model with parameters that can be used in API endpoints like `/api/v1/` or `/api/v1//count`, as well as in GraphQL queries. -- API filter parameter names are generated following the pattern: `.`. -- GraphQL queries will have filtering models with condition properties generated. +- API filter parameter names follow the pattern: `.` +- GraphQL queries will have filtering models with condition properties generated -The following conditions are supported: -`equal`, `notEqual`, `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`, `in`, `isNull`, `equal`. \ No newline at end of file +The following filter conditions are supported: +- `equal` +- `notEqual` +- `greaterThan` +- `greaterThanOrEqual` +- `lessThan` +- `lessThanOrEqual` +- `in` +- `isNull` \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8ac041e..0c65667 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,31 +2,39 @@ #### Enhancements: -- Released version v0.1.9 with a comprehensive range of updates. -- Implemented full filter support for GraphQL queries and mutations. -- Introduced generic request and response models with support for primitive mapping. -- Conducted refactoring of directories, mappers, and overall codebase for optimization purposes. -- Updated issue templates, code of conduct, contribution, and security guidelines. -- Added a new CODE_OF_CONDUCT.md document. -- Enhanced the example project and detailed documentation within README.md. -- Refined GitHub Actions and workflow processes. +- Released version v0.1.10 with major infrastructure updates. +- Added JWT authorization support with configurable public key validation. +- Refactored AutoBackendHost configuration by consolidating StartupBoilerplate logic inline. +- Enhanced sample project with PostgreSQL as primary database provider. +- Updated README.md with comprehensive authorization documentation. #### Package Updates: -- Upgraded Microsoft.EntityFrameworkCore packages from 7.0.3 to 8.0.4 incrementally. -- Updated Npgsql.EntityFrameworkCore.PostgreSQL from version 7.0.3 to 8.0.2. -- Incremented NSwag.AspNetCore from 13.18.2 to 14.0.7. -- Elevated coverlet.collector version from 3.2.0 to 6.0.2. -- Updated tj-actions/changed-files from version 35 to 41. -- Target framework advanced to .NET 8.0, with associated package versions updated accordingly. - -#### Bug Fixes: - -- Addressed and corrected mapper errors and instituted mapper caching enhancements. -- Resolved minor refactoring issues and remedied warning notifications. +- Target framework advanced to .NET 10.0 with SDK version 10.0.0. +- Upgraded Microsoft.EntityFrameworkCore packages from 8.0.4 to 10.0.1. +- Updated Npgsql.EntityFrameworkCore.PostgreSQL from version 8.0.2 to 10.0.0. +- Upgraded HotChocolate.AspNetCore from 13.9.0 to 15.1.11. +- Updated HotChocolate.Data.EntityFramework from 13.9.0 to 15.1.11. +- Incremented NSwag.AspNetCore from 14.0.7 to 14.6.3. +- Upgraded Microsoft.NET.Test.Sdk from 17.9.0 to 18.0.1. +- Updated MSTest.TestAdapter from 3.3.1 to 4.0.2. +- Updated MSTest.TestFramework from 3.3.1 to 4.0.2. +- Elevated coverlet.collector version from 6.0.2 to 6.0.4. +- Removed Flurl.Http dependency. + +#### Infrastructure Updates: + +- Updated Dockerfile base images to .NET 10.0. +- Enhanced GitHub Actions workflows with latest versions. +- Upgraded actions/checkout from v4 to v6. +- Updated actions/setup-dotnet from v4 to v5. +- Incremented tj-actions/changed-files from v41 to v47. +- Upgraded actions/github-script from v7 to v8. +- Enhanced Dependabot configuration with GitHub Actions support. +- Added parallel test execution configuration. #### Documentation and Organization: -- Finalized and refined README.md documentation. -- Systematized solution documents and rectified the location of the Contributor Covenant Code of Conduct. -- Edited [sample project](src/samples/Sample) \ No newline at end of file +- Expanded README.md with authorization section and improved descriptions. +- Updated sample project configuration with JWT settings. +- Enhanced PostgreSQL connection string with error detail inclusion. \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Attributes/GenericCreatePermissionAttribute.cs b/src/lib/AutoBackend.Sdk/Attributes/GenericCreatePermissionAttribute.cs new file mode 100644 index 0000000..6da10e5 --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Attributes/GenericCreatePermissionAttribute.cs @@ -0,0 +1,4 @@ +namespace AutoBackend.Sdk.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class GenericCreatePermissionAttribute : Attribute; \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Attributes/GenericDeletePermissionAttribute.cs b/src/lib/AutoBackend.Sdk/Attributes/GenericDeletePermissionAttribute.cs new file mode 100644 index 0000000..c9256c5 --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Attributes/GenericDeletePermissionAttribute.cs @@ -0,0 +1,4 @@ +namespace AutoBackend.Sdk.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class GenericDeletePermissionAttribute : Attribute; \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Attributes/GenericReadPermissionAttribute.cs b/src/lib/AutoBackend.Sdk/Attributes/GenericReadPermissionAttribute.cs new file mode 100644 index 0000000..cf61e5e --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Attributes/GenericReadPermissionAttribute.cs @@ -0,0 +1,4 @@ +namespace AutoBackend.Sdk.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class GenericReadPermissionAttribute : Attribute; \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Attributes/GenericUpdatePermissionAttribute.cs b/src/lib/AutoBackend.Sdk/Attributes/GenericUpdatePermissionAttribute.cs new file mode 100644 index 0000000..44a981b --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Attributes/GenericUpdatePermissionAttribute.cs @@ -0,0 +1,4 @@ +namespace AutoBackend.Sdk.Attributes; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] +public sealed class GenericUpdatePermissionAttribute : Attribute; \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Configuration/JwtConfiguration.cs b/src/lib/AutoBackend.Sdk/Configuration/JwtConfiguration.cs new file mode 100644 index 0000000..a92cac3 --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Configuration/JwtConfiguration.cs @@ -0,0 +1,11 @@ +namespace AutoBackend.Sdk.Configuration; + +// ReSharper disable once ClassNeverInstantiated.Global +internal sealed record JwtConfiguration +{ + public string PublicKey { get; init; } = null!; + + public string? ValidIssuer { get; init; } + + public string? ValidAudience { get; init; } +} \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Constants.cs b/src/lib/AutoBackend.Sdk/Constants.cs index 88d325b..e4addc2 100644 --- a/src/lib/AutoBackend.Sdk/Constants.cs +++ b/src/lib/AutoBackend.Sdk/Constants.cs @@ -2,15 +2,10 @@ namespace AutoBackend.Sdk; internal static class Constants { - #region Services - - internal const string ClusterDiscoveryServiceUrl = "/__discovery"; - - #endregion - #region Headers - internal const string XForwardedForHeaderName = "X-Forwarded-For"; + internal const string AuthorizationHeaderName = "Authorization"; + internal const string AuthorizationBearerPrefix = "Bearer "; #endregion @@ -59,6 +54,11 @@ internal static class Constants internal const string PropertySetterName = "set_{0}"; internal const string GenericTypeBeautifulName = "{0}<{1}>"; + + internal const string JwtConfigurationSectionName = "Jwt"; + internal const string GenericEntityPermissionName = "{0}{1}"; + internal const string GenericPropertyPermissionName = "{0}{1}{2}"; + public const string GenericPermissionsClaimType = "permissions"; #endregion @@ -92,9 +92,6 @@ internal static class Constants internal const string UnableToFindAMethodWithParametersForTheType = "Unable to find a method {0} with {1} parameters for the type {2}."; - internal const string UnableToFindAMethodWithParametersAndGenericArgumentsForTheType = - "Unable to find a method {0} with {1} parameters and {2} generic arguments for the type {3}."; - internal const string TheEntityTypeKeySetDoesNotMatchTheGivenKeys = "The entity type key set doesn't match the given keys."; @@ -124,6 +121,9 @@ internal static class Constants internal const string UnableToBuildAGenericGraphQlMutationForTypeThePropertyHasNotBeenFound = "Unable to build a generic GraphQL mutation for type {0}. The property {1} has not been found."; + + internal const string Unauthorized = "Unauthorized"; + internal const string UnauthorizedMissingPermissions = "Unauthorized. Missing permissions: [{0}]"; #endregion @@ -137,17 +137,7 @@ internal static class Constants #region Environment - internal const string HostEnvironmentVariable = "HOST"; internal const string PortEnvironmentVariable = "PORT"; #endregion - - #region Logs - - internal const string ClusterDiscoveryRequestLogName = "discovery request"; - - internal const string ClusterDiscoveryRequestLogMessage = - "Discover at (node: ({NodeId}, {NodeCreatedUtc}, {NodeLastRequestToUtc}, {NodeLastRequestFromUtc}, {NodeRequestTimeMs})) with params (node: ({RemoteNodeId}, {RemoteNodeCreated}, {RemoteNodeLastRequestTo}, {RemoteNodeLastRequestFrom}, {RemoteNodeRequestTimeMs}))"; - - #endregion } \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Controllers/GenericController.cs b/src/lib/AutoBackend.Sdk/Controllers/GenericController.cs index 4781d3a..d4eee0f 100644 --- a/src/lib/AutoBackend.Sdk/Controllers/GenericController.cs +++ b/src/lib/AutoBackend.Sdk/Controllers/GenericController.cs @@ -35,28 +35,27 @@ private void SetupExceptionHandler() { HttpContext .RequestServices - .GetRequiredService().SetCurrentHandler( + .GetRequiredService().SetCurrentApiHandler( HttpContext, HandleExceptionStatusCodeAsync, HandleExceptionResponseAsync); } - private Task HandleExceptionStatusCodeAsync(Exception exception) + private static int HandleExceptionStatusCodeAsync(Exception exception) { - return Task.FromResult(exception.ToApiException().StatusCode); + return exception.ToApiException().StatusCode; } - private Task HandleExceptionResponseAsync(Exception exception) + private GenericControllerResponse HandleExceptionResponseAsync(Exception exception) { var statusCode = exception.ToApiException().StatusCode; - return Task.FromResult( - new GenericControllerResponse( - false, - statusCode, - statusCode == StatusCodes.Status500InternalServerError - ? Constants.AnUnexpectedInternalServerErrorHasHappened - : exception.Message) - .WithRequestTime(HttpContext)); + return new GenericControllerResponse( + false, + statusCode, + statusCode == StatusCodes.Status500InternalServerError + ? Constants.AnUnexpectedInternalServerErrorHasHappened + : exception.Message) + .WithRequestTime(HttpContext); } } @@ -68,7 +67,7 @@ internal abstract class GenericController< IGenericResponseMapper genericResponseMapper, IGenericRepository genericRepository, ICancellationTokenProvider cancellationTokenProvider) : GenericController - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter { diff --git a/src/lib/AutoBackend.Sdk/Data/Mappers/GenericResponseMapper.cs b/src/lib/AutoBackend.Sdk/Data/Mappers/GenericResponseMapper.cs index f0cc1ee..a68542d 100644 --- a/src/lib/AutoBackend.Sdk/Data/Mappers/GenericResponseMapper.cs +++ b/src/lib/AutoBackend.Sdk/Data/Mappers/GenericResponseMapper.cs @@ -9,7 +9,7 @@ namespace AutoBackend.Sdk.Data.Mappers; internal class GenericResponseMapper(IMapperExpressionsCache cache) : IGenericResponseMapper { public TResponse ToModel(TEntity entity) - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() { var func = cache.GetOrAddAndCompile(MapExpr()); @@ -22,14 +22,14 @@ IEnumerable IGenericResponseMapper.ToModel(IEnume } public TResponse? ToModelNullable(TEntity? entity) - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() { return entity is null ? null : ToModel(entity); } public ICollection ToModelCollectionNullable(ICollection? entities) - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() { return entities?.Select(entity => @@ -37,12 +37,11 @@ public ICollection ToModelCollectionNullable(ICol ?? throw new InconsistentDataException()) .ToArray() ?? - [ - ]; + []; } private Expression> MapExpr() - where TEntity : class + where TEntity : class, new() where TModel : class, IGenericResponse, new() { var parameter = Expression.Parameter(typeof(TEntity)); diff --git a/src/lib/AutoBackend.Sdk/Data/Mappers/IGenericResponseMapper.cs b/src/lib/AutoBackend.Sdk/Data/Mappers/IGenericResponseMapper.cs index b72c1ad..9fbafbd 100644 --- a/src/lib/AutoBackend.Sdk/Data/Mappers/IGenericResponseMapper.cs +++ b/src/lib/AutoBackend.Sdk/Data/Mappers/IGenericResponseMapper.cs @@ -5,10 +5,10 @@ namespace AutoBackend.Sdk.Data.Mappers; internal interface IGenericResponseMapper { TResponse ToModel(TEntity entity) - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new(); IEnumerable ToModel(IEnumerable entities) - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new(); } \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepository.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepository.cs index 54e11ec..dbc2f80 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepository.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepository.cs @@ -1,18 +1,23 @@ using AutoBackend.Sdk.Data.Storage; +using AutoBackend.Sdk.Enums; using AutoBackend.Sdk.Exceptions.Data; using AutoBackend.Sdk.Exceptions.Reflection; using AutoBackend.Sdk.Filters; +using AutoBackend.Sdk.Services.PermissionValidator; using Microsoft.EntityFrameworkCore; namespace AutoBackend.Sdk.Data.Repositories; -internal class GenericRepository(IGenericStorage genericStorage) +internal class GenericRepository( + IPermissionValidator permissionValidator, + IGenericStorage genericStorage) : IGenericRepository - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter { public Task GetAllAsync(TFilter? filter, CancellationToken cancellationToken) { + permissionValidator.Validate(PermissionType.Read); var query = genericStorage.GetQuery(filter); if (filter?.SkipCount is { } skipCountValue) query = query.Skip(skipCountValue); if (filter?.TakeCount is { } takeCountValue) query = query.Take(takeCountValue); @@ -21,6 +26,7 @@ public Task GetAllAsync(TFilter? filter, CancellationToken cancellati public Task GetCountAsync(TFilter? filter, CancellationToken cancellationToken) { + permissionValidator.Validate(PermissionType.Read); return genericStorage.GetQuery(filter).LongCountAsync(cancellationToken); } @@ -28,6 +34,7 @@ public Task GetCountAsync(TFilter? filter, CancellationToken cancellationT CancellationToken cancellationToken, params object[] keys) { + permissionValidator.Validate(PermissionType.Read); return await genericStorage.FindAsync(keys, cancellationToken); } @@ -36,6 +43,7 @@ protected async Task CreateInternalAsync( CancellationToken cancellationToken, params object[]? keys) { + permissionValidator.Validate(PermissionType.Create); if (keys?.Any() ?? false) { ValidateEntityKeys(entity, keys); @@ -57,6 +65,7 @@ protected async Task UpdateInternalAsync( CancellationToken cancellationToken, params object[] keys) { + permissionValidator.Validate(PermissionType.Update); ValidateEntityKeys(entity, keys); var current = await genericStorage.FindAsync(keys, cancellationToken); if (current is null) @@ -65,6 +74,7 @@ protected async Task UpdateInternalAsync( Constants.AnEntityWithTheGivenKeyDoesNotExist, string.Join(", ", keys.Select(key => key.ToString())))); + var affectedProperties = new HashSet(); foreach (var entryProperty in genericStorage.Entry(current).Properties) { var entityProperty = typeof(TEntity).GetProperty(entryProperty.Metadata.Name); @@ -76,8 +86,10 @@ protected async Task UpdateInternalAsync( typeof(TEntity).Name)); var newValue = entityProperty.GetValue(entity); entityProperty.SetValue(current, newValue); + if (!Equals(entryProperty.OriginalValue, entryProperty.CurrentValue)) affectedProperties.Add(entryProperty.Metadata.Name); } + permissionValidator.Validate(PermissionType.Update, affectedProperties); genericStorage.Update(current); await genericStorage.SaveChangesAsync(cancellationToken); return entity; @@ -87,6 +99,7 @@ protected async Task DeleteInternalAsync( CancellationToken cancellationToken, params object[] keys) { + permissionValidator.Validate(PermissionType.Delete); var current = await genericStorage.FindAsync(keys, cancellationToken); if (current is null) throw new NotFoundDataException( diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.3.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.3.cs index 3217d17..2aac387 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.3.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.3.cs @@ -1,5 +1,6 @@ using AutoBackend.Sdk.Data.Storage; using AutoBackend.Sdk.Filters; +using AutoBackend.Sdk.Services.PermissionValidator; namespace AutoBackend.Sdk.Data.Repositories; @@ -9,17 +10,17 @@ internal sealed class GenericRepositoryWithComplexKey< TKey1, TKey2, TKey3 ->(IGenericStorage genericStorage) : GenericRepository< +>(IPermissionValidator permissionValidator, IGenericStorage genericStorage) : GenericRepository< TEntity, TFilter ->(genericStorage), IGenericRepositoryWithComplexKey< +>(permissionValidator, genericStorage), IGenericRepositoryWithComplexKey< TEntity, TFilter, TKey1, TKey2, TKey3 > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.4.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.4.cs index 8384159..9e1e240 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.4.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.4.cs @@ -1,5 +1,6 @@ using AutoBackend.Sdk.Data.Storage; using AutoBackend.Sdk.Filters; +using AutoBackend.Sdk.Services.PermissionValidator; namespace AutoBackend.Sdk.Data.Repositories; @@ -10,10 +11,10 @@ internal sealed class GenericRepositoryWithComplexKey< TKey2, TKey3, TKey4 ->(IGenericStorage genericStorage) : GenericRepository< +>(IPermissionValidator permissionValidator, IGenericStorage genericStorage) : GenericRepository< TEntity, TFilter ->(genericStorage), IGenericRepositoryWithComplexKey< +>(permissionValidator, genericStorage), IGenericRepositoryWithComplexKey< TEntity, TFilter, TKey1, @@ -21,7 +22,7 @@ internal sealed class GenericRepositoryWithComplexKey< TKey3, TKey4 > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.5.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.5.cs index aabffbf..c2348fd 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.5.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.5.cs @@ -1,5 +1,6 @@ using AutoBackend.Sdk.Data.Storage; using AutoBackend.Sdk.Filters; +using AutoBackend.Sdk.Services.PermissionValidator; namespace AutoBackend.Sdk.Data.Repositories; @@ -11,10 +12,10 @@ internal sealed class GenericRepositoryWithComplexKey< TKey3, TKey4, TKey5 ->(IGenericStorage genericStorage) : GenericRepository< +>(IPermissionValidator permissionValidator, IGenericStorage genericStorage) : GenericRepository< TEntity, TFilter ->(genericStorage), IGenericRepositoryWithComplexKey< +>(permissionValidator, genericStorage), IGenericRepositoryWithComplexKey< TEntity, TFilter, TKey1, @@ -23,7 +24,7 @@ internal sealed class GenericRepositoryWithComplexKey< TKey4, TKey5 > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.6.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.6.cs index 564934c..e26954c 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.6.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.6.cs @@ -1,5 +1,6 @@ using AutoBackend.Sdk.Data.Storage; using AutoBackend.Sdk.Filters; +using AutoBackend.Sdk.Services.PermissionValidator; namespace AutoBackend.Sdk.Data.Repositories; @@ -12,10 +13,10 @@ internal sealed class GenericRepositoryWithComplexKey< TKey4, TKey5, TKey6 ->(IGenericStorage genericStorage) : GenericRepository< +>(IPermissionValidator permissionValidator, IGenericStorage genericStorage) : GenericRepository< TEntity, TFilter ->(genericStorage), IGenericRepositoryWithComplexKey< +>(permissionValidator, genericStorage), IGenericRepositoryWithComplexKey< TEntity, TFilter, TKey1, @@ -25,7 +26,7 @@ internal sealed class GenericRepositoryWithComplexKey< TKey5, TKey6 > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.7.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.7.cs index cabf8ba..4edeeee 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.7.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.7.cs @@ -1,5 +1,6 @@ using AutoBackend.Sdk.Data.Storage; using AutoBackend.Sdk.Filters; +using AutoBackend.Sdk.Services.PermissionValidator; namespace AutoBackend.Sdk.Data.Repositories; @@ -13,10 +14,10 @@ internal sealed class GenericRepositoryWithComplexKey< TKey5, TKey6, TKey7 ->(IGenericStorage genericStorage) : GenericRepository< +>(IPermissionValidator permissionValidator, IGenericStorage genericStorage) : GenericRepository< TEntity, TFilter ->(genericStorage), IGenericRepositoryWithComplexKey< +>(permissionValidator, genericStorage), IGenericRepositoryWithComplexKey< TEntity, TFilter, TKey1, @@ -27,7 +28,7 @@ internal sealed class GenericRepositoryWithComplexKey< TKey6, TKey7 > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.8.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.8.cs index 1fadc96..3c8d97b 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.8.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.8.cs @@ -1,5 +1,6 @@ using AutoBackend.Sdk.Data.Storage; using AutoBackend.Sdk.Filters; +using AutoBackend.Sdk.Services.PermissionValidator; namespace AutoBackend.Sdk.Data.Repositories; @@ -14,10 +15,10 @@ internal sealed class GenericRepositoryWithComplexKey< TKey6, TKey7, TKey8 ->(IGenericStorage genericStorage) : GenericRepository< +>(IPermissionValidator permissionValidator, IGenericStorage genericStorage) : GenericRepository< TEntity, TFilter ->(genericStorage), IGenericRepositoryWithComplexKey< +>(permissionValidator, genericStorage), IGenericRepositoryWithComplexKey< TEntity, TFilter, TKey1, @@ -29,7 +30,7 @@ internal sealed class GenericRepositoryWithComplexKey< TKey7, TKey8 > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.cs index ae7d3ec..0f58f93 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithComplexKey.cs @@ -1,5 +1,6 @@ using AutoBackend.Sdk.Data.Storage; using AutoBackend.Sdk.Filters; +using AutoBackend.Sdk.Services.PermissionValidator; namespace AutoBackend.Sdk.Data.Repositories; @@ -8,16 +9,16 @@ internal sealed class GenericRepositoryWithComplexKey< TFilter, TKey1, TKey2 ->(IGenericStorage genericStorage) : GenericRepository< +>(IPermissionValidator permissionValidator, IGenericStorage genericStorage) : GenericRepository< TEntity, TFilter ->(genericStorage), IGenericRepositoryWithComplexKey< +>(permissionValidator, genericStorage), IGenericRepositoryWithComplexKey< TEntity, TFilter, TKey1, TKey2 > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithNoKey.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithNoKey.cs index 5ad9b45..5436c7f 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithNoKey.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithNoKey.cs @@ -1,19 +1,20 @@ using AutoBackend.Sdk.Data.Storage; using AutoBackend.Sdk.Filters; +using AutoBackend.Sdk.Services.PermissionValidator; namespace AutoBackend.Sdk.Data.Repositories; internal sealed class GenericRepositoryWithNoKey< TEntity, TFilter ->(IGenericStorage genericStorage) : GenericRepository< +>(IPermissionValidator permissionValidator, IGenericStorage genericStorage) : GenericRepository< TEntity, TFilter ->(genericStorage), IGenericRepositoryWithNoKey< +>(permissionValidator, genericStorage), IGenericRepositoryWithNoKey< TEntity, TFilter > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter { public Task CreateAsync(TEntity entity, CancellationToken cancellationToken) diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithPrimaryKey.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithPrimaryKey.cs index 28aa878..175dbc0 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithPrimaryKey.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/GenericRepositoryWithPrimaryKey.cs @@ -1,5 +1,6 @@ using AutoBackend.Sdk.Data.Storage; using AutoBackend.Sdk.Filters; +using AutoBackend.Sdk.Services.PermissionValidator; namespace AutoBackend.Sdk.Data.Repositories; @@ -7,15 +8,15 @@ internal sealed class GenericRepositoryWithPrimaryKey< TEntity, TFilter, TKey ->(IGenericStorage genericStorage) : GenericRepository< +>(IPermissionValidator permissionValidator, IGenericStorage genericStorage) : GenericRepository< TEntity, TFilter ->(genericStorage), IGenericRepositoryWithPrimaryKey< +>(permissionValidator, genericStorage), IGenericRepositoryWithPrimaryKey< TEntity, TFilter, TKey > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey : notnull { diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepository.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepository.cs index 9ad0996..fd7ce1a 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepository.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepository.cs @@ -3,7 +3,7 @@ namespace AutoBackend.Sdk.Data.Repositories; internal interface IGenericRepository - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter { Task GetAllAsync(TFilter? filter, CancellationToken cancellationToken); diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.3.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.3.cs index 31d8e79..4dcb633 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.3.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.3.cs @@ -9,7 +9,7 @@ internal interface IGenericRepositoryWithComplexKey< in TKey2, in TKey3 > : IGenericRepository - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.4.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.4.cs index 4846fa0..c914f2d 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.4.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.4.cs @@ -10,7 +10,7 @@ internal interface IGenericRepositoryWithComplexKey< in TKey3, in TKey4 > : IGenericRepository - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.5.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.5.cs index 4020a85..2e450fc 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.5.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.5.cs @@ -11,7 +11,7 @@ internal interface IGenericRepositoryWithComplexKey< in TKey4, in TKey5 > : IGenericRepository - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.6.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.6.cs index 479b832..484d717 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.6.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.6.cs @@ -12,7 +12,7 @@ internal interface IGenericRepositoryWithComplexKey< in TKey5, in TKey6 > : IGenericRepository - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.7.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.7.cs index 64ba195..58bcb90 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.7.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.7.cs @@ -13,7 +13,7 @@ internal interface IGenericRepositoryWithComplexKey< in TKey6, in TKey7 > : IGenericRepository - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.8.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.8.cs index bff21d9..5db7280 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.8.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.8.cs @@ -14,7 +14,7 @@ internal interface IGenericRepositoryWithComplexKey< in TKey7, in TKey8 > : IGenericRepository - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.cs index d0a6920..8bffe08 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithComplexKey.cs @@ -8,7 +8,7 @@ internal interface IGenericRepositoryWithComplexKey< in TKey1, in TKey2 > : IGenericRepository - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey1 : notnull where TKey2 : notnull diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithNoKey.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithNoKey.cs index 79aca20..43cc168 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithNoKey.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithNoKey.cs @@ -9,7 +9,7 @@ in TFilter TEntity, TFilter > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter { Task CreateAsync(TEntity entity, CancellationToken cancellationToken); diff --git a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithPrimaryKey.cs b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithPrimaryKey.cs index 1b38181..9b20058 100644 --- a/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithPrimaryKey.cs +++ b/src/lib/AutoBackend.Sdk/Data/Repositories/IGenericRepositoryWithPrimaryKey.cs @@ -10,7 +10,7 @@ in TKey TEntity, TFilter > - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter where TKey : notnull { diff --git a/src/lib/AutoBackend.Sdk/Data/Storage/GenericStorage.cs b/src/lib/AutoBackend.Sdk/Data/Storage/GenericStorage.cs index 55eba1d..2c08022 100644 --- a/src/lib/AutoBackend.Sdk/Data/Storage/GenericStorage.cs +++ b/src/lib/AutoBackend.Sdk/Data/Storage/GenericStorage.cs @@ -8,7 +8,7 @@ namespace AutoBackend.Sdk.Data.Storage; internal sealed class GenericStorage(GenericDbContext db) : IGenericStorage - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter { private DbSet Set => db.Set(); diff --git a/src/lib/AutoBackend.Sdk/Data/Storage/IGenericStorage.cs b/src/lib/AutoBackend.Sdk/Data/Storage/IGenericStorage.cs index 9e067bf..df69538 100644 --- a/src/lib/AutoBackend.Sdk/Data/Storage/IGenericStorage.cs +++ b/src/lib/AutoBackend.Sdk/Data/Storage/IGenericStorage.cs @@ -4,7 +4,7 @@ namespace AutoBackend.Sdk.Data.Storage; internal interface IGenericStorage - where TEntity : class + where TEntity : class, new() where TFilter : class, IGenericFilter { ValueTask FindAsync(object[] keyValues, CancellationToken cancellationToken); diff --git a/src/lib/AutoBackend.Sdk/Enums/PermissionType.cs b/src/lib/AutoBackend.Sdk/Enums/PermissionType.cs new file mode 100644 index 0000000..feaa806 --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Enums/PermissionType.cs @@ -0,0 +1,10 @@ +namespace AutoBackend.Sdk.Enums; + +[Flags] +internal enum PermissionType +{ + Create = 1, + Read = 1 << 1, + Update = 1 << 2, + Delete = 1 << 3 +} \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Exceptions/Api/UnauthorizedApiException.cs b/src/lib/AutoBackend.Sdk/Exceptions/Api/UnauthorizedApiException.cs new file mode 100644 index 0000000..a2142aa --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Exceptions/Api/UnauthorizedApiException.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; + +namespace AutoBackend.Sdk.Exceptions.Api; + +internal sealed class UnauthorizedApiException : ApiException +{ + internal UnauthorizedApiException(string message, Exception innerException) : base(message, innerException) + { + } + + internal override int StatusCode => StatusCodes.Status401Unauthorized; +} \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Exceptions/Data/UnauthorizedAccessDataException.cs b/src/lib/AutoBackend.Sdk/Exceptions/Data/UnauthorizedAccessDataException.cs new file mode 100644 index 0000000..bab9c21 --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Exceptions/Data/UnauthorizedAccessDataException.cs @@ -0,0 +1,11 @@ +namespace AutoBackend.Sdk.Exceptions.Data; + +internal class UnauthorizedAccessDataException : DataException +{ + internal UnauthorizedAccessDataException(string message) : base(message) + { + } + internal UnauthorizedAccessDataException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Extensions/ExceptionExtensions.cs b/src/lib/AutoBackend.Sdk/Extensions/ExceptionExtensions.cs index be5872d..28bb153 100644 --- a/src/lib/AutoBackend.Sdk/Extensions/ExceptionExtensions.cs +++ b/src/lib/AutoBackend.Sdk/Extensions/ExceptionExtensions.cs @@ -26,6 +26,9 @@ internal ApiException ToApiException() NotFoundDataException => new NotFoundApiException( Constants.NoDataHasBeenFound, exception), + UnauthorizedAccessDataException => new UnauthorizedApiException( + exception.Message, + exception), _ => fallbackValue }, _ => fallbackValue diff --git a/src/lib/AutoBackend.Sdk/Extensions/ServiceCollectionExtensions.cs b/src/lib/AutoBackend.Sdk/Extensions/ServiceCollectionExtensions.cs index 00c7094..609384e 100644 --- a/src/lib/AutoBackend.Sdk/Extensions/ServiceCollectionExtensions.cs +++ b/src/lib/AutoBackend.Sdk/Extensions/ServiceCollectionExtensions.cs @@ -10,7 +10,9 @@ using AutoBackend.Sdk.NSwag; using AutoBackend.Sdk.Services.CancellationTokenProvider; using AutoBackend.Sdk.Services.DateTimeProvider; +using AutoBackend.Sdk.Services.EntityMetadataProvider; using AutoBackend.Sdk.Services.ExceptionHandler; +using AutoBackend.Sdk.Services.PermissionValidator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -31,6 +33,7 @@ internal IServiceCollection AddAutoBackend(IConfiguration configuratio .AddGenericSwagger(typeof(TProgram).Assembly.FullName!) .AddGenericDbContext(configuration) .AddGenericStorage() + .AddGenericPermissions(configuration) .AddInternalServices(); } @@ -206,12 +209,26 @@ private IServiceCollection AddGenericStorage() return services; } + private IServiceCollection AddGenericPermissions(IConfiguration configuration) + { + services.Configure( + configuration + .GetSection(Constants.JwtConfigurationSectionName)); + + services + .AddScoped(); + + return services; + } + private IServiceCollection AddInternalServices() { return services .AddSingleton() .AddSingleton() - .AddScoped(); + .AddSingleton() + .AddScoped() + .AddHttpContextAccessor(); } } } \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQuery.cs b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQuery.cs index 411d0f8..b18ec08 100644 --- a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQuery.cs +++ b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQuery.cs @@ -11,7 +11,7 @@ internal abstract class GenericGqlQuery< TResponse, TFilter > - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter { diff --git a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.3.cs b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.3.cs index ac088c1..3a3d83e 100644 --- a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.3.cs +++ b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.3.cs @@ -17,7 +17,7 @@ internal abstract class GenericGqlQueryWithComplexKey< TResponse, TFilter > - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter where TKey1 : notnull diff --git a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.4.cs b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.4.cs index 1ea8508..758f2d2 100644 --- a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.4.cs +++ b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.4.cs @@ -18,7 +18,7 @@ internal abstract class GenericGqlQueryWithComplexKey< TResponse, TFilter > - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter where TKey1 : notnull diff --git a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.5.cs b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.5.cs index 5fdfea6..4b84640 100644 --- a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.5.cs +++ b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.5.cs @@ -19,7 +19,7 @@ internal abstract class GenericGqlQueryWithComplexKey< TResponse, TFilter > - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter where TKey1 : notnull diff --git a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.6.cs b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.6.cs index 5ff861c..26edd6c 100644 --- a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.6.cs +++ b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.6.cs @@ -20,7 +20,7 @@ internal abstract class GenericGqlQueryWithComplexKey< TResponse, TFilter > - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter where TKey1 : notnull diff --git a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.7.cs b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.7.cs index 460e311..a73cb48 100644 --- a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.7.cs +++ b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.7.cs @@ -21,7 +21,7 @@ internal abstract class GenericGqlQueryWithComplexKey< TResponse, TFilter > - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter where TKey1 : notnull diff --git a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.8.cs b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.8.cs index 0d0f04f..752c1f8 100644 --- a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.8.cs +++ b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.8.cs @@ -22,7 +22,7 @@ internal abstract class GenericGqlQueryWithComplexKey< TResponse, TFilter > - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter where TKey1 : notnull diff --git a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.cs b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.cs index a2e3ae4..2b12d24 100644 --- a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.cs +++ b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithComplexKey.cs @@ -16,7 +16,7 @@ internal abstract class GenericGqlQueryWithComplexKey< TResponse, TFilter > - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter where TKey1 : notnull diff --git a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithNoKey.cs b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithNoKey.cs index e4f3741..4034684 100644 --- a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithNoKey.cs +++ b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithNoKey.cs @@ -12,6 +12,6 @@ internal abstract class GenericGqlQueryWithNoKey< TResponse, TFilter > - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter; \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithPrimaryKey.cs b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithPrimaryKey.cs index afe1675..1212a38 100644 --- a/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithPrimaryKey.cs +++ b/src/lib/AutoBackend.Sdk/GraphQL/Queries/GenericGqlQueryWithPrimaryKey.cs @@ -16,7 +16,7 @@ internal abstract class GenericGqlQueryWithPrimaryKey< TResponse, TFilter > - where TEntity : class + where TEntity : class, new() where TResponse : class, IGenericResponse, new() where TFilter : class, IGenericFilter where TKey : notnull diff --git a/src/lib/AutoBackend.Sdk/Middleware/ApiExceptionHandler/ApiExceptionHandlerMiddleware.cs b/src/lib/AutoBackend.Sdk/Middleware/ApiExceptionHandler/ApiExceptionHandlerMiddleware.cs index 634d714..05790ab 100644 --- a/src/lib/AutoBackend.Sdk/Middleware/ApiExceptionHandler/ApiExceptionHandlerMiddleware.cs +++ b/src/lib/AutoBackend.Sdk/Middleware/ApiExceptionHandler/ApiExceptionHandlerMiddleware.cs @@ -12,9 +12,9 @@ public async Task Invoke( IExceptionHandlerFactory exceptionHandlerFactory, ILogger logger) { - exceptionHandlerFactory.SetCurrentHandler( + exceptionHandlerFactory.SetCurrentApiHandler( httpContext, - HandleExceptionStatusCodeDefaultAsync, + HandleExceptionStatusCodeDefault, HandleExceptionResponseDefaultAsyncProvider(logger)); try { @@ -22,25 +22,25 @@ public async Task Invoke( } catch (Exception exception) { - await exceptionHandlerFactory.CurrentHandler!.HandleAsync(exception, CancellationToken.None); + await exceptionHandlerFactory.CurrentApiExceptionHandler!.HandleAsync(exception, CancellationToken.None); } } - private Task HandleExceptionStatusCodeDefaultAsync(Exception exception) + private static int HandleExceptionStatusCodeDefault(Exception exception) { - return Task.FromResult(exception.ToApiException().StatusCode); + return exception.ToApiException().StatusCode; } - private static Func> HandleExceptionResponseDefaultAsyncProvider(ILogger logger) + private static Func HandleExceptionResponseDefaultAsyncProvider(ILogger logger) { return HandleExceptionResponseDefaultAsync; - Task HandleExceptionResponseDefaultAsync(Exception exception) + string HandleExceptionResponseDefaultAsync(Exception exception) { logger.LogCritical( exception, Constants.AnUnexpectedInternalServerErrorHasHappenedOutOfTheControllerContext); - return Task.FromResult(Constants.AnUnexpectedInternalServerErrorHasHappened); + return Constants.AnUnexpectedInternalServerErrorHasHappened; } } } \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/EntityMetadataProvider.cs b/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/EntityMetadataProvider.cs new file mode 100644 index 0000000..b6156cc --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/EntityMetadataProvider.cs @@ -0,0 +1,82 @@ +using System.Reflection; +using AutoBackend.Sdk.Attributes; +using AutoBackend.Sdk.Enums; + +namespace AutoBackend.Sdk.Services.EntityMetadataProvider; + +internal sealed class EntityMetadataProvider : IEntityMetadataProvider +{ + private readonly Dictionary _cache = new(); + + public GenericEntityMetadata GetMetadata() where TEntity : class, new() + { + var type = typeof(TEntity); + if (_cache.TryGetValue(type, out var metadata)) return metadata; + metadata = CreateMetadata(type); + _cache.Add(type, metadata); + return metadata; + } + + private GenericEntityMetadata CreateMetadata(Type type) + { + var propertyInfos = type.GetProperties(); + var properties = propertyInfos.Select(CreateMetadata).ToList(); + var permissions = CreatePermissionType(type); + return new GenericEntityMetadata( + type.Name, + permissions, + properties); + } + + private GenericPropertyMetadata CreateMetadata(PropertyInfo propertyInfo) + { + var permissions = CreatePermissionType(propertyInfo); + return new GenericPropertyMetadata( + propertyInfo.Name, + permissions); + } + + private static PermissionType CreatePermissionType(Type type) + { + PermissionType permissionType = 0; + if (type.GetCustomAttribute() is not null) + { + permissionType |= PermissionType.Create; + } + if (type.GetCustomAttribute() is not null) + { + permissionType |= PermissionType.Read; + } + if (type.GetCustomAttribute() is not null) + { + permissionType |= PermissionType.Update; + } + if (type.GetCustomAttribute() is not null) + { + permissionType |= PermissionType.Delete; + } + return permissionType; + } + + private static PermissionType CreatePermissionType(PropertyInfo propertyInfo) + { + PermissionType permissionType = 0; + if (propertyInfo.GetCustomAttribute() is not null) + { + permissionType |= PermissionType.Create; + } + if (propertyInfo.GetCustomAttribute() is not null) + { + permissionType |= PermissionType.Read; + } + if (propertyInfo.GetCustomAttribute() is not null) + { + permissionType |= PermissionType.Update; + } + if (propertyInfo.GetCustomAttribute() is not null) + { + permissionType |= PermissionType.Delete; + } + return permissionType; + } +} \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/GenericEntityMetadata.cs b/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/GenericEntityMetadata.cs new file mode 100644 index 0000000..2bccfd2 --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/GenericEntityMetadata.cs @@ -0,0 +1,8 @@ +using AutoBackend.Sdk.Enums; + +namespace AutoBackend.Sdk.Services.EntityMetadataProvider; + +internal record GenericEntityMetadata( + string Name, + PermissionType Permissions, + IEnumerable Properties); \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/GenericPropertyMetadata.cs b/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/GenericPropertyMetadata.cs new file mode 100644 index 0000000..807063a --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/GenericPropertyMetadata.cs @@ -0,0 +1,7 @@ +using AutoBackend.Sdk.Enums; + +namespace AutoBackend.Sdk.Services.EntityMetadataProvider; + +internal record GenericPropertyMetadata( + string Name, + PermissionType Permissions); \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/IEntityMetadataProvider.cs b/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/IEntityMetadataProvider.cs new file mode 100644 index 0000000..0f4aed7 --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Services/EntityMetadataProvider/IEntityMetadataProvider.cs @@ -0,0 +1,6 @@ +namespace AutoBackend.Sdk.Services.EntityMetadataProvider; + +internal interface IEntityMetadataProvider +{ + GenericEntityMetadata GetMetadata() where TEntity : class, new(); +} \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/ExceptionHandlerFactory.cs b/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/ExceptionHandlerFactory.cs index a4ca23f..f53e9a3 100644 --- a/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/ExceptionHandlerFactory.cs +++ b/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/ExceptionHandlerFactory.cs @@ -5,29 +5,29 @@ namespace AutoBackend.Sdk.Services.ExceptionHandler; internal sealed class ExceptionHandlerFactory : IExceptionHandlerFactory { - public IExceptionHandler? CurrentHandler { get; private set; } + public IApiExceptionHandler? CurrentApiExceptionHandler { get; private set; } - public void SetCurrentHandler( + public void SetCurrentApiHandler( HttpContext httpContext, - Func> handleExceptionStatusCodeAsync, - Func> handleExceptionResponseAsync) + Func handleExceptionStatusCode, + Func handleExceptionResponse) { - CurrentHandler = new ExceptionHandler( + CurrentApiExceptionHandler = new ApiExceptionHandler( httpContext, - handleExceptionStatusCodeAsync, - handleExceptionResponseAsync); + handleExceptionStatusCode, + handleExceptionResponse); } - private sealed class ExceptionHandler( + private sealed class ApiExceptionHandler( HttpContext httpContext, - Func> processStatusCode, - Func> processResponse) - : IExceptionHandler + Func processStatusCode, + Func processResponse) + : IApiExceptionHandler { public async Task HandleAsync(Exception exception, CancellationToken cancellationToken) { - var statusCode = await processStatusCode(exception); - var response = await processResponse(exception); + var statusCode = processStatusCode(exception); + var response = processResponse(exception); httpContext.Response.StatusCode = statusCode; httpContext.Response.ContentType = MediaTypeNames.Application.Json; await httpContext.Response.WriteAsJsonAsync(response, cancellationToken); diff --git a/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/IExceptionHandler.cs b/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/IApiExceptionHandler.cs similarity index 77% rename from src/lib/AutoBackend.Sdk/Services/ExceptionHandler/IExceptionHandler.cs rename to src/lib/AutoBackend.Sdk/Services/ExceptionHandler/IApiExceptionHandler.cs index 6f6eb03..6f9641f 100644 --- a/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/IExceptionHandler.cs +++ b/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/IApiExceptionHandler.cs @@ -1,6 +1,6 @@ namespace AutoBackend.Sdk.Services.ExceptionHandler; -internal interface IExceptionHandler +internal interface IApiExceptionHandler { Task HandleAsync(Exception exception, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/IExceptionHandlerFactory.cs b/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/IExceptionHandlerFactory.cs index db1c2ea..40616ef 100644 --- a/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/IExceptionHandlerFactory.cs +++ b/src/lib/AutoBackend.Sdk/Services/ExceptionHandler/IExceptionHandlerFactory.cs @@ -4,10 +4,10 @@ namespace AutoBackend.Sdk.Services.ExceptionHandler; internal interface IExceptionHandlerFactory { - IExceptionHandler? CurrentHandler { get; } - - void SetCurrentHandler( + IApiExceptionHandler? CurrentApiExceptionHandler { get; } + + void SetCurrentApiHandler( HttpContext httpContext, - Func> handleExceptionStatusCodeAsync, - Func> handleExceptionResponseAsync); + Func handleExceptionStatusCode, + Func handleExceptionResponse); } \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Services/PermissionValidator/IPermissionValidator.cs b/src/lib/AutoBackend.Sdk/Services/PermissionValidator/IPermissionValidator.cs new file mode 100644 index 0000000..fe4e086 --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Services/PermissionValidator/IPermissionValidator.cs @@ -0,0 +1,8 @@ +using AutoBackend.Sdk.Enums; + +namespace AutoBackend.Sdk.Services.PermissionValidator; + +internal interface IPermissionValidator +{ + void Validate(PermissionType type, HashSet? affectedProperties = null) where TEntity : class, new(); +} \ No newline at end of file diff --git a/src/lib/AutoBackend.Sdk/Services/PermissionValidator/PermissionValidator.cs b/src/lib/AutoBackend.Sdk/Services/PermissionValidator/PermissionValidator.cs new file mode 100644 index 0000000..b1a537e --- /dev/null +++ b/src/lib/AutoBackend.Sdk/Services/PermissionValidator/PermissionValidator.cs @@ -0,0 +1,106 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Cryptography; +using AutoBackend.Sdk.Configuration; +using AutoBackend.Sdk.Enums; +using AutoBackend.Sdk.Exceptions.Data; +using AutoBackend.Sdk.Services.EntityMetadataProvider; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace AutoBackend.Sdk.Services.PermissionValidator; + +internal sealed class PermissionValidator( + IEntityMetadataProvider entityMetadataProvider, + IHttpContextAccessor httpContextAccessor, + IOptions options): IPermissionValidator +{ + public void Validate(PermissionType type, HashSet? affectedProperties) where TEntity : class, new() + { + var requiredPermissions = GetRequiredPermissions(type, affectedProperties ?? []); + if (requiredPermissions.Length == 0) return; + var givenPermissions = GetValidGivenPermissions(); + CheckPermissions(givenPermissions, requiredPermissions); + } + + private string[] GetRequiredPermissions( + PermissionType permissionType, + HashSet affectedProperties) where TEntity : class, new() + { + var permissions = new List(); + var metadata = entityMetadataProvider.GetMetadata(); + if (metadata.Permissions.HasFlag(permissionType)) + permissions.Add( + string.Format( + Constants.GenericEntityPermissionName, + metadata.Name, + Enum.GetName(permissionType) ?? string.Empty)); + permissions.AddRange( + metadata.Properties + .Where(property => affectedProperties.Contains(property.Name)) + .Where(property => property.Permissions.HasFlag(permissionType)) + .Select(property => string.Format( + Constants.GenericPropertyPermissionName, + metadata.Name, + property.Name, + Enum.GetName(permissionType) ?? string.Empty))); + return permissions.ToArray(); + } + + private string[] GetValidGivenPermissions() + { + var authorization = httpContextAccessor.HttpContext?.Request.Headers[Constants.AuthorizationHeaderName].FirstOrDefault(); + if (authorization is not null && + authorization.StartsWith(Constants.AuthorizationBearerPrefix, StringComparison.OrdinalIgnoreCase)) + { + var token = authorization[Constants.AuthorizationBearerPrefix.Length..].Trim(); + if (!string.IsNullOrWhiteSpace(token)) + { + var rsa = RSA.Create(); + rsa.ImportFromPem(options.Value.PublicKey); + var parameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new RsaSecurityKey(rsa) + }; + if (!string.IsNullOrWhiteSpace(options.Value.ValidIssuer)) + { + parameters.ValidateIssuer = true; + parameters.ValidIssuer = options.Value.ValidIssuer; + } + if (!string.IsNullOrWhiteSpace(options.Value.ValidAudience)) + { + parameters.ValidateAudience = true; + parameters.ValidAudience = options.Value.ValidAudience; + } + + var handler = new JwtSecurityTokenHandler(); + try + { + var claimsPrincipal = handler.ValidateToken(token, parameters, out _); + return claimsPrincipal.Claims + .Where(claim => claim.Type == Constants.GenericPermissionsClaimType) + .Select(claim => claim.Value) + .ToArray(); + } + catch (Exception exception) + { + throw new UnauthorizedAccessDataException( + Constants.Unauthorized, + exception); + } + } + } + return []; + } + + private static void CheckPermissions(string[] givenPermissions, string[] requiredPermissions) + { + var missingPermissions = requiredPermissions.Except(givenPermissions).ToArray(); + if (missingPermissions.Length > 0) + throw new UnauthorizedAccessDataException( + string.Format( + Constants.UnauthorizedMissingPermissions, + string.Join(", ", missingPermissions))); + } +} \ No newline at end of file diff --git a/src/samples/Sample/Data/Budget.cs b/src/samples/Sample/Data/Budget.cs index 5b06a8d..ada8d2d 100644 --- a/src/samples/Sample/Data/Budget.cs +++ b/src/samples/Sample/Data/Budget.cs @@ -25,6 +25,10 @@ namespace Sample.Data; nameof(Name), nameof(OwnerId) )] +[GenericCreatePermission] +[GenericReadPermission] +[GenericUpdatePermission] +[GenericDeletePermission] public class Budget { [GenericFilter] diff --git a/src/samples/Sample/Data/Participating.cs b/src/samples/Sample/Data/Participating.cs index 5ccba26..54af282 100644 --- a/src/samples/Sample/Data/Participating.cs +++ b/src/samples/Sample/Data/Participating.cs @@ -19,6 +19,10 @@ namespace Sample.Data; nameof(UserId), nameof(BudgetId) )] +[GenericCreatePermission] +[GenericReadPermission] +[GenericUpdatePermission] +[GenericDeletePermission] public class Participating { [GenericFilter] diff --git a/src/samples/Sample/Data/Transaction.cs b/src/samples/Sample/Data/Transaction.cs index 3ec6b07..e418714 100644 --- a/src/samples/Sample/Data/Transaction.cs +++ b/src/samples/Sample/Data/Transaction.cs @@ -26,8 +26,13 @@ namespace Sample.Data; nameof(BudgetId), nameof(Amount), nameof(DateTimeUtc), - nameof(Comment) + nameof(Comment), + nameof(SecretKey) )] +[GenericCreatePermission] +[GenericReadPermission] +[GenericUpdatePermission] +[GenericDeletePermission] public class Transaction { [GenericFilter] @@ -51,6 +56,7 @@ public class Transaction public Budget Budget { get; set; } = null!; [GenericFilter] + [GenericUpdatePermission] [Precision(20, 4)] public decimal Amount { get; set; } diff --git a/src/samples/Sample/Data/TransactionVersion.cs b/src/samples/Sample/Data/TransactionVersion.cs index 5f97fcf..48e3899 100644 --- a/src/samples/Sample/Data/TransactionVersion.cs +++ b/src/samples/Sample/Data/TransactionVersion.cs @@ -29,6 +29,10 @@ namespace Sample.Data; nameof(OriginalTransactionId), nameof(VersionDateTimeUtc) )] +[GenericCreatePermission] +[GenericReadPermission] +[GenericUpdatePermission] +[GenericDeletePermission] public class TransactionVersion { [GenericFilter] diff --git a/src/samples/Sample/Data/User.cs b/src/samples/Sample/Data/User.cs index 54f493d..66086f8 100644 --- a/src/samples/Sample/Data/User.cs +++ b/src/samples/Sample/Data/User.cs @@ -24,6 +24,10 @@ namespace Sample.Data; nameof(TimeZone), nameof(ActiveBudgetId) )] +[GenericCreatePermission] +[GenericReadPermission] +[GenericUpdatePermission] +[GenericDeletePermission] public class User { [GenericFilter] diff --git a/src/samples/Sample/appsettings.json b/src/samples/Sample/appsettings.json index 4a62c8a..59a3675 100644 --- a/src/samples/Sample/appsettings.json +++ b/src/samples/Sample/appsettings.json @@ -7,11 +7,16 @@ }, "AllowedHosts": "*", "Database": { - "PrimaryProvider": "Postgres", + "PrimaryProvider": "InMemory", "Providers": { "InMemory": "InMemoryDatabase", "SqlServer": "Server=localhost;Database=autobackend;User Id=sa;Password=sqlserver;TrustServerCertificate=True;", - "Postgres": "Server=localhost;Port=5432;Database=autobackend;User Id=postgres;Password=postgres;" + "Postgres": "Server=localhost;Port=5432;Database=autobackend;User Id=postgres;Password=postgres;Include Error Detail=true;" } + }, + "Jwt": { + "PublicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArxu0Xdvb2EBWW2qm9RVn\nt2x/t0LkoqKOYKfCdHhEfEvqZ4mszyfLRYWFcjju7k3ku4B/xU9lz7t5ZDIkiBiS\nbfav+iuh90lYKLvYW0R9sk5eCllEyJfz1jPiSptgmUzqdnaiUQrY2nQiEIBhR5JT\nfBYu1b7Bj518Mc46t7RWAK7MEOEVw8tET8qCauqEj6HYKub1F+5KUgQL68pIiFYJ\ndQtt1FWyq/WYXXvKHcr4r/tGSV7oNqYyeGT6Aps5IYSPqcW3SnqRMcnd6mOFtyx5\n0Tn/w3rUB/Ie/5N4L/rkl6JY9v7ly2Ygc3tQb69DpoKQRKTwKGBqA+cwME5d0j53\nGQIDAQAB\n-----END PUBLIC KEY-----", + "ValidIssuer": "AutoBackendServer", + "ValidAudience": "AutoBackendUser" } }