Skip to content

Commit 42a4c82

Browse files
author
Developer
committed
Prepare NuGet package for publication
0 parents  commit 42a4c82

38 files changed

Lines changed: 2396 additions & 0 deletions

‎.github/workflows/ci.yml‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- name: Setup .NET
14+
uses: actions/setup-dotnet@v4
15+
with:
16+
dotnet-version: "8.0.x"
17+
18+
- name: Test
19+
run: dotnet test -c Release
20+
21+
- name: Pack
22+
run: dotnet pack -c Release src/OwnershipGuard/OwnershipGuard.csproj --no-build
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: release-nuget
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
tags:
7+
- "v*"
8+
9+
jobs:
10+
publish:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Setup .NET
21+
uses: actions/setup-dotnet@v4
22+
with:
23+
dotnet-version: "8.0.x"
24+
25+
- name: Validate version against tag
26+
if: startsWith(github.ref, 'refs/tags/v')
27+
shell: bash
28+
run: |
29+
TAG_VERSION="${GITHUB_REF_NAME#v}"
30+
PROJECT_VERSION="$(grep -oPm1 '(?<=<Version>)[^<]+' src/OwnershipGuard/OwnershipGuard.csproj)"
31+
if [ "$TAG_VERSION" != "$PROJECT_VERSION" ]; then
32+
echo "Tag version ($TAG_VERSION) does not match csproj version ($PROJECT_VERSION)."
33+
exit 1
34+
fi
35+
36+
- name: Restore
37+
run: dotnet restore OwnershipGuard.sln
38+
39+
- name: Test
40+
run: dotnet test OwnershipGuard.sln -c Release --no-restore
41+
42+
- name: Pack
43+
run: dotnet pack -c Release src/OwnershipGuard/OwnershipGuard.csproj --no-restore -o artifacts
44+
45+
- name: Push package
46+
run: dotnet nuget push "artifacts/*.nupkg" --api-key "${{ secrets.NUGET_API_KEY }}" --source "https://api.nuget.org/v3/index.json" --skip-duplicate
47+
48+
- name: Push symbols
49+
run: dotnet nuget push "artifacts/*.snupkg" --api-key "${{ secrets.NUGET_API_KEY }}" --source "https://api.nuget.org/v3/index.json" --skip-duplicate

‎.gitignore‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Build
2+
bin/
3+
obj/
4+
5+
# IDE
6+
.vs/
7+
*.user
8+
*.suo
9+
*.userosscache
10+
*.sln.docstates
11+
12+
# NuGet
13+
*.nupkg
14+
*.snupkg
15+
project.lock.json
16+
project.fragment.lock.json
17+
packages/
18+
19+
# Test
20+
TestResults/
21+
coverage*.json
22+
coverage*.xml
23+
24+
# Misc
25+
*.log
26+
.DS_Store
27+
.tmp_*/

‎LICENSE‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2026 OwnershipGuard
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

‎NUGET_PUBLISH.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# NuGet Publish Checklist
2+
3+
## Prerequisites
4+
5+
- NuGet API key with push permissions.
6+
- `dotnet` SDK 8.x.
7+
8+
## Manual Publish
9+
10+
1. Update package version in `src/OwnershipGuard/OwnershipGuard.csproj` (`<Version>`).
11+
2. Run validation:
12+
13+
```bash
14+
dotnet restore OwnershipGuard.sln
15+
dotnet test OwnershipGuard.sln -c Release
16+
dotnet pack -c Release src/OwnershipGuard/OwnershipGuard.csproj -o artifacts
17+
```
18+
19+
3. Push package and symbols:
20+
21+
```bash
22+
dotnet nuget push "artifacts/*.nupkg" --api-key "<NUGET_API_KEY>" --source "https://api.nuget.org/v3/index.json" --skip-duplicate
23+
dotnet nuget push "artifacts/*.snupkg" --api-key "<NUGET_API_KEY>" --source "https://api.nuget.org/v3/index.json" --skip-duplicate
24+
```
25+
26+
## GitHub Actions Publish
27+
28+
Workflow: `.github/workflows/release-nuget.yml`
29+
30+
- Trigger on tag `v*` (for example: `v0.1.1`) or manually via `workflow_dispatch`.
31+
- Required secret: `NUGET_API_KEY`.
32+
- Workflow validates that tag version matches `<Version>` in `src/OwnershipGuard/OwnershipGuard.csproj`.

‎OwnershipGuard.sln‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OwnershipGuard", "src\OwnershipGuard\OwnershipGuard.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OwnershipGuard.Tests", "tests\OwnershipGuard.Tests\OwnershipGuard.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
9+
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OwnershipGuard.DemoApi", "samples\OwnershipGuard.DemoApi\OwnershipGuard.DemoApi.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
11+
EndProject
12+
Global
13+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
14+
Debug|Any CPU = Debug|Any CPU
15+
Release|Any CPU = Release|Any CPU
16+
EndGlobalSection
17+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
18+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
20+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
21+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
22+
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23+
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
24+
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
25+
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
30+
EndGlobalSection
31+
EndGlobal

‎README.md‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# OwnershipGuard
2+
3+
[![NuGet](https://img.shields.io/nuget/v/OwnershipGuard.svg)](https://www.nuget.org/packages/OwnershipGuard/)
4+
5+
OwnershipGuard is an ASP.NET Core library that enforces ownership checks and optional tenant checks before endpoint or MVC action execution. It centralizes resource access validation by route id and helps prevent IDOR and broken access control caused by missing per-endpoint checks.
6+
7+
## Requirements
8+
9+
- .NET 8
10+
- ASP.NET Core 8
11+
12+
## Installation
13+
14+
```bash
15+
dotnet add package OwnershipGuard
16+
```
17+
18+
```xml
19+
<PackageReference Include="OwnershipGuard" Version="x.y.z" />
20+
```
21+
22+
## Quick Start
23+
24+
1. Register services.
25+
26+
```csharp
27+
using System.Security.Claims;
28+
29+
builder.Services.AddOwnershipGuard(options =>
30+
{
31+
options.UserIdClaimType = ClaimTypes.NameIdentifier;
32+
options.TenantIdClaimType = "tenant_id";
33+
options.UseProblemDetailsResponses = true;
34+
options.HideExistenceWhenForbidden = false;
35+
});
36+
```
37+
38+
2. Register descriptors after `builder.Build()`.
39+
40+
```csharp
41+
var app = builder.Build();
42+
var registry = app.Services.GetRequiredService<IOwnershipDescriptorRegistry>();
43+
44+
// String key + ownership check
45+
registry.Register<Note>(
46+
sp => sp.GetRequiredService<YourDbContext>().Notes,
47+
n => n.Id,
48+
n => n.OwnerId);
49+
50+
// Typed key (Guid) + ownership + tenant check
51+
registry.Register<Document, Guid>(
52+
sp => sp.GetRequiredService<YourDbContext>().Documents,
53+
d => d.Id,
54+
d => d.OwnerId,
55+
tenantSelector: d => d.TenantId);
56+
```
57+
58+
3. Apply the check to endpoints.
59+
60+
Minimal API:
61+
62+
```csharp
63+
var documentOwnership = new RequireOwnershipFilter("id", typeof(Document));
64+
app.MapGet("/documents/{id}", ...).AddEndpointFilter(documentOwnership);
65+
app.MapPut("/documents/{id}", ...).AddEndpointFilter(documentOwnership);
66+
```
67+
68+
MVC:
69+
70+
```csharp
71+
[ApiController]
72+
[Route("documents")]
73+
[RequireOwnership("id", typeof(Document))]
74+
public sealed class DocumentsController : ControllerBase
75+
{
76+
[HttpGet("{id}")]
77+
public IActionResult Get(string id) => Ok();
78+
}
79+
```
80+
81+
## Response Behavior
82+
83+
| Status | Condition |
84+
|--------|-----------|
85+
| `400 Bad Request` | Route id is missing or empty, or typed key parsing fails (for `Register<T, TKey>`). |
86+
| `401 Unauthorized` | User claim is missing, or tenant claim is missing when a tenant-aware descriptor is used. |
87+
| `403 Forbidden` | Resource exists but ownership or tenant check fails, and `HideExistenceWhenForbidden` is `false`. |
88+
| `404 Not Found` | Resource does not exist, or ownership/tenant check fails when `HideExistenceWhenForbidden` is `true`. |
89+
| `500 Internal Server Error` | Descriptor is not registered for the requested entity type. |
90+
91+
## Configuration
92+
93+
| Option | Default | Description |
94+
|--------|---------|-------------|
95+
| `UserIdClaimType` | `ClaimTypes.NameIdentifier` | Claim type used to resolve the current user id. |
96+
| `TenantIdClaimType` | `"tenant_id"` | Claim type used to resolve the current tenant id when tenant checks are required. |
97+
| `UseProblemDetailsResponses` | `true` | When `true`, filters return ProblemDetails payloads for errors. When `false`, filters return plain status responses. |
98+
| `HideExistenceWhenForbidden` | `false` | When `true`, failed ownership/tenant checks return `404`; otherwise they return `403`. |
99+
100+
## Optional Query Helpers
101+
102+
```csharp
103+
using OwnershipGuard.EntityFrameworkCore;
104+
105+
var ownedDocs = await db.Documents
106+
.WhereOwnedBy(userId, d => d.OwnerId)
107+
.ToListAsync();
108+
109+
var tenantDocs = await db.Documents
110+
.WhereTenant(tenantId, d => d.TenantId)
111+
.ToListAsync();
112+
```
113+
114+
## Non-Goals
115+
116+
OwnershipGuard does not implement:
117+
118+
- Authentication
119+
- Authorization policy systems (RBAC)
120+
- Rate limiting
121+
- Input validation
122+
123+
## Build, Test, and Pack
124+
125+
```bash
126+
dotnet build OwnershipGuard.sln -c Release
127+
dotnet test -c Release
128+
dotnet pack -c Release src/OwnershipGuard/OwnershipGuard.csproj
129+
```
130+
131+
NuGet publish checklist: [NUGET_PUBLISH.md](NUGET_PUBLISH.md)
132+
133+
## License
134+
135+
MIT. See [LICENSE](LICENSE).

‎global.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"sdk": {
3+
"version": "8.0.100",
4+
"rollForward": "latestMajor"
5+
}
6+
}

‎samples.zip‎

30.4 KB
Binary file not shown.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
namespace OwnershipGuard.DemoApi;
4+
5+
public sealed class AppDbContext : DbContext
6+
{
7+
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
8+
9+
public DbSet<Document> Documents => Set<Document>();
10+
public DbSet<Note> Notes => Set<Note>();
11+
12+
protected override void OnModelCreating(ModelBuilder modelBuilder)
13+
{
14+
modelBuilder.Entity<Document>(e =>
15+
{
16+
e.HasKey(d => d.Id);
17+
e.Property(d => d.OwnerId).HasMaxLength(64);
18+
e.Property(d => d.TenantId).HasMaxLength(64);
19+
e.Property(d => d.Title).HasMaxLength(256);
20+
});
21+
22+
modelBuilder.Entity<Note>(e =>
23+
{
24+
e.HasKey(n => n.Id);
25+
e.Property(n => n.OwnerId).HasMaxLength(64);
26+
e.Property(n => n.Title).HasMaxLength(256);
27+
});
28+
}
29+
}

0 commit comments

Comments
 (0)