Skip to content

Commit

Permalink
feat: #331 implement custom property restrictions
Browse files Browse the repository at this point in the history
giattributes update

test

gitattributes

doc fixes
  • Loading branch information
ascott18 committed Nov 8, 2023
1 parent 55bb555 commit d9ede6b
Show file tree
Hide file tree
Showing 62 changed files with 830 additions and 264 deletions.
6 changes: 4 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
*.g.cs linguist-generated=true
*.g.ts linguist-generated=true
*.generated.d.ts linguist-generated=true
src/Coalesce.Web/Scripts/Coalesce/* linguist-generated=true
src/Coalesce.Web/Views/Generated/* linguist-generated=true
playground/**/Scripts/Coalesce/* linguist-generated=true
playground/**/Scripts/Coalesce/**/* linguist-generated=true
playground/**/Views/Generated/* linguist-generated=true
playground/**/Views/Generated/**/* linguist-generated=true

* text eol=lf
39 changes: 39 additions & 0 deletions docs/modeling/model-components/attributes/restrict.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# [Restrict]

In addition to [role-based](/modeling/model-components/attributes/security-attribute.md) property restrictions, you can also define property restrictions that can execute custom code for each model instance if your logic require more nuanced decisions than can be made with roles.

``` c#:no-line-numbers
using IntelliTect.Coalesce.DataAnnotations;
public class Employee
{
public int Id { get; set; }
[Read]
public string UserId { get; set; }
[Restrict<SalaryRestriction>]
public decimal Salary { get; set; }
}
public class SalaryRestriction(MyUserService userService) : IPropertyRestriction<Employee>
{
public bool UserCanRead(IMappingContext context, string propertyName, Employee model)
=> context.User.GetUserId() == model.UserId || userService.IsPayroll(context.User);
public bool UserCanWrite(IMappingContext context, string propertyName, Employee model, object incomingValue)
=> userService.IsPayroll(context.User);
public bool UserCanFilter(IMappingContext context, string propertyName)
=> userService.IsPayroll(context.User);
}
```

Restriction classes support dependency injection, so you can inject any supplemental services needed to make a determination.

The `UserCanRead` method controls whether values of the restricted property will be mapped from model instances to the generated DTO. Similarly, `UserCanWrite` controls whether the property can be mapped back to the model instance from the generated DTO.

The `UserCanFilter` method has a default implementation that returns `false`, but can be implemented if there is an appropriate, instance-agnostic way to determine if a user can sort, search, or filter values of that property.

Multiple different restrictions can be placed on a single property; all of them must succeed for the operation to be permitted. Restrictions also stack on top of role attribute restrictions (`[Read]` and `[Edit]`).

A non-generic variant of `IPropertyRestriction` also exists for restrictions that might be reused across multiple model types.
2 changes: 1 addition & 1 deletion docs/modeling/model-components/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ Properties will be ignored if received by the client if authorization checks aga
The [Default Loading Behavior](/modeling/model-components/data-sources.md#default-loading-behavior), any custom functionality defined in [Data Sources](/modeling/model-components/data-sources.md), and [[DtoIncludes] & [DtoExcludes]](/modeling/model-components/attributes/dto-includes-excludes.md) may also restrict which properties are sent to the client when requested.

### NotMapped
While Coalesce does not do anything special for the `[NotMapped]` attribute, it is still and important attribute to keep in mind while building your model, as it prevents EF Core from doing anything with the property.
While Coalesce does not do anything special for the `[NotMapped]` attribute, it is still an important attribute to keep in mind while building your model, as it prevents EF Core from doing anything with the property.
2 changes: 1 addition & 1 deletion docs/stacks/agnostic/dtos.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ Every class that is exposed through Coalesce's generated API will have a corresp

The [[Read] and [Edit] attributes](/modeling/model-components/attributes/security-attribute.md) can be used to apply property-level security, which manifests as conditional logic in the mapping methods on the generated DTOs.

See the [Security](/topics/security.md#attributes) page to read more about property-level security, as well as all other security mechanisms in Coalesce.
See the [Security](/topics/security.md#property-column-security) page to read more about property-level security, as well as all other security mechanisms in Coalesce.

2 changes: 1 addition & 1 deletion docs/topics/audit-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class AuditLog : DefaultAuditLog
}
```

This entity only needs to implement `IAuditLog`, but a default implementation of this interface `DefaultAuditLog` is provided for your convenience. `DefaultAuditLog` contains additional properties `ClientIp`, `Referrer`, and `Endpoint` for recording information about the HTTP request (if available), and also has attributes to disable Create, Edit, and Delete APIs.
This entity only needs to implement `IAuditLog`, but a default implementation of this interface `DefaultAuditLog` is provided for your convenience. `DefaultAuditLog` contains additional properties `ClientIp`, `Referrer`, and `Endpoint` for recording information about the HTTP request (if available), and also comes pre-configured for security with Create, Edit, and Delete APIs disabled.

You should further augment this type with any additional properties that you would like to track on each change record. A property to track the user who performed the change should be added, since it is not provided by the default implementation so that you can declare it yourself with the correct type for the foreign key and navigation property.

Expand Down
80 changes: 65 additions & 15 deletions docs/topics/security.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@

# Security

This page is a comprehensive overview of all the techniques that can be used in a Coalesce application to restrict the usage of API endpoints that Coalesce generates.
This page is a comprehensive overview of all the techniques that can be used in a Coalesce application to restrict the capabilities of API endpoints that Coalesce generates.

The following table is a quick reference of scenarios you might encounter and how you might handle them. If you're unfamiliar with these techniques, though, then you are encouraged to read through this page to get a deeper understanding of what's available before selecting a solution.

| <div style="width:290px">When I want to...</div> | ... I should use ... |
| - | - |
| Remove an entity CRUD operation | [Class Security Attributes](#class-security-attributes) with `DenyAll` |
| Restrict an entity CRUD operation with roles | [Class Security Attributes](#class-security-attributes) |
| Restrict a method or service with roles | [Method Security Attributes](#method-security-attributes) |
| Remove a property from Coalesce | • annotate with [[InternalUse]](#internal-properties) <br> • `internal` access modifier |
| Restrict a property by roles | [Property Security Attributes](#role-restrictions) |
| Restrict a property with custom logic | • save operations: [custom Behaviors](#behaviors) <br> • nav prop loading: [custom Default Data Source](#data-sources) <br> • any property: [custom Property Restrictions](#custom-restrictions) |
| Make a property read-only | • make the setter `internal` <br> • add a `[Read]` attribute without `[Edit]` <br> • [other techniques](#read-only-properties) |
| Make a property write-once (init-only) | use an [`init`-only setter](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/init) |
| Prevent [auto-include](/modeling/model-components/data-sources.md#default-loading-behavior) | annotate the navigation property or the included type's class with `[Read(NoAutoInclude = true)]` |
| Restrict results of `/get`, `/list` | [custom Default Data Source](#data-sources)
| Restrict `/save`, `/bulkSave`, `/delete` | • any custom logic: [custom Behaviors](#behaviors) <br> • restrict targets: [custom Default Data Source](#data-sources) <br> • static role restrictions: [Class Security Attributes](#class-security-attributes) |
| Restrict targets of instance methods |[custom Default Data Source](#data-sources) <br> • specify data source: [LoadFromDataSource](/modeling/model-components/methods.md#loadfromdatasource-type-datasourcetype) <br> • [custom logic](#custom-methods-and-services) in the method |
| Apply server-side data validation | • implement [validation attributes](#attribute-validation) <br> • [custom Behaviors](#saves-and-deletes) (for entity CRUD) <br> • [custom logic](#custom-methods-and-services) (for methods/services) |

[[toc]]

## Endpoint Security

Expand All @@ -13,7 +30,7 @@ Classes can be hidden from Coalesce entirely by annotating them with `[InternalU

`DbSet<>` properties on your `DbContext` class can also be annotated with `[InternalUse]`, causing that type to be treated by Coalesce like an [External Type](/modeling/model-types/external-types.md) rather than an [Entity](/modeling/model-types/entities.md), once again preventing generation of API endpoints but _without_ preventing properties of that type from being exposed.

### Standard CRUD Endpoints
### Class Security Attributes
For each of your [Entities](/modeling/model-types/entities.md) and [Custom DTOs](/modeling/model-types/dtos.md), Coalesce generates a set of CRUD API endpoints (`/get`, `/list`, `/count`, `/save`, `/bulkSave`, and `/delete`).

The default behavior is that all endpoints require an authenticated user (anonymous users are rejected).
Expand All @@ -33,15 +50,13 @@ This security is applied to the generated [controllers](https://learn.microsoft.
<tr>
<td>

`/get`, `/list`, `/count`, `/bulkSave`
`/get`, `/list`, `/count`
</td>
<td>

``` c#:no-line-numbers
[ReadAttribute]
```

Note: the root model for a bulk save operation requires read permission. All other entities affected by the bulk save operation require their respective attribute for Create/Edit/Delete.
</td>
</tr>
<tr>
Expand Down Expand Up @@ -69,6 +84,24 @@ Note: the root model for a bulk save operation requires read permission. All oth
```
</td>
</tr>
<tr>
<td>

`/bulkSave`
</td>
<td>

``` c#:no-line-numbers
// Read permission required for the root entity:
[ReadAttribute]
// Control of each entity affected by the bulk save:
[CreateAttribute]
[EditAttribute]
[DeleteAttribute]
```
</td>
</tr>
</table>

Here are some examples of applying security attributes to an entity class. If a particular action doesn't need to be restricted, you can omit that attribute, but this example shows usages of all four:
Expand All @@ -88,7 +121,7 @@ public class Employee
}
```

### Custom Methods and Services
### Method Security Attributes

To secure the endpoints generated for your [Custom Methods](/modeling/model-components/methods.md) and [Services](/modeling/model-types/services.md), the [[Execute] attribute](/modeling/model-components/attributes/execute.md) can be used to specify a set of required roles for that endpoint, or to open that endpoint to anonymous users.

Expand All @@ -115,6 +148,13 @@ public class Employee

## Property/Column Security

Security applied via attributes to properties in Coalesce affects all usages of that property across all Coalesce-generated APIs. This includes usages of that property on types that occur as children of other types, which is a spot where class-level or endpoint-level security generally does not apply. [These attributes](/modeling/model-components/attributes/security-attribute.md) can be placed on the properties on your [Entities](/modeling/model-types/entities.md) and [External Types](/modeling/model-types/external-types.md) to apply role-based restrictions to that property.

* `ReadAttribute` limits the roles that can read values from that property in responses from the server.
* `EditAttribute` limits the roles that can write values to that property in requests made to the server.
* `RestrictAttribute` registers an implementation of [IPropertyRestriction](#custom-restrictions) that allows for writing custom code to implement these restrictions.

This security is executed and enforced by the mapping that occurs in the [generated DTOs](/stacks/agnostic/dtos.md), meaning it affects both entity CRUD APIs as well as [Custom Methods](/modeling/model-components/methods.md). It is also checked by the [Standard Data Source](/modeling/model-components/data-sources.md#standard-data-source) to prevent sorting, searching, and filtering by properties that a user is not permitted to read.

### Internal Properties

Expand Down Expand Up @@ -146,12 +186,6 @@ public class Department
}
```

### Attributes
The [[Read] and [Edit] attributes](/modeling/model-components/attributes/security-attribute.md) can be placed on the properties on your [Entities](/modeling/model-types/entities.md) and [External Types](/modeling/model-types/external-types.md) to apply role-based restrictions to the usage of that property.

This security is primarily executed and enforced by the mapping that occurs in the [generated DTOs](/stacks/agnostic/dtos.md). It is also checked by the [Standard Data Source](/modeling/model-components/data-sources.md#standard-data-source) to prevent sorting, searching, and filtering by properties that a user is not permitted to read.


### Read-Only Properties

A property in Coalesce can be made read-only in any of the following ways:
Expand All @@ -176,13 +210,16 @@ public class Employee
// Non-public setter:
public DateTime StartDate { get; internal set; }
// No setter:
public string EmploymentDuration => (DateTime.Now - StartDate).ToString();
// Edits denied:
[Edit(SecurityPermissionLevels.DenyAll)]
public string EmployeeNumber { get; set; }
}
```

### Read/Write Properties
### Role Restrictions

Reading and writing a property in Coalesce can be restricted by roles:

Expand Down Expand Up @@ -220,6 +257,11 @@ If you have a situation where a property should be editable without knowing the
use a custom method on the model to accept and set the new value.


### Custom Restrictions

@[import-md "after":"# [Restrict]"](../modeling/model-components/attributes/restrict.md)


## Row-level Security

### Data Sources
Expand Down Expand Up @@ -390,8 +432,16 @@ public override async Task TransformResultsAsync(
}
```

### Behaviors

In Coalesce, [Behaviors](/modeling/model-components/behaviors.md) are the extension point to implement row-level security or other customizations of create/edit/delete operations on your [Entities](/modeling/model-types/entities.md) and [Custom DTOs](/modeling/model-types/dtos.md). Behaviors are implemented on top of data sources, meaning the client request will be rejected if the requested entity for modification cannot be loaded from the entity's default data source.

By default, each entity will use the [Standard Behaviors](/modeling/model-components/behaviors.md#behaviors), but you can declare a [custom behaviors class](/modeling/model-components/behaviors.md#defining-behaviors) for each of your entities to override this default functionality.

For most use cases, all your security rules will be implemented in the [BeforeSave/BeforeSaveAsync](/modeling/model-components/behaviors.md#member-beforesaveasync) and [BeforeDelete/BeforeDeleteAsync](/modeling/model-components/behaviors.md#member-beforedeleteasync) methods.

For a more complete explanation of everything you can do with behaviors, see the full [Behaviors](/modeling/model-components/behaviors.md) documentation page.

For a more complete explanation of everything you can do with data sources, see the full [Data Sources](/modeling/model-components/data-sources.md) documentation page.

### EF Global Query Filters

Expand Down
18 changes: 17 additions & 1 deletion playground/Coalesce.Domain/Case.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,15 @@ public enum Statuses

[Read]
public long AttachmentSize { get; set; }
[Read]
[Read, Restrict<TestRestriction>]
public string AttachmentName { get; set; }
[Restrict<TestRestriction>]
public string AttachmentType { get; set; }
[Read, MaxLength(32)]
public byte[] AttachmentHash { get; set; }
[InternalUse]
public CaseAttachmentContent AttachmentContent { get; set; }

public class CaseAttachmentContent
{
public int CaseKey { get; set; }
Expand Down Expand Up @@ -232,5 +234,19 @@ public static CaseSummary GetCaseSummary(AppDbContext db)

return CaseSummary.GetCaseSummary(db);
}

public class TestRestriction(AppDbContext db) : IPropertyRestriction<Case>
{
public bool UserCanRead(IMappingContext context, string propertyName, Case model)
{
// Nonsense arbitrary logic
return db.Cases.Any() && propertyName != null;
}

public bool UserCanWrite(IMappingContext context, string propertyName, Case? model, object? incomingValue)
{
return false;
}
}
}
}
4 changes: 2 additions & 2 deletions playground/Coalesce.Web.Ko/Api/Generated/CaseController.g.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d9ede6b

Please sign in to comment.