It is easy to treat privacy as a field on a record. Add IsPublic, default it to false, and it can feel as if the system now knows who may read a résumé. But a stored value cannot make that decision unless every request checks it against an identified caller.
In my CV Management System prototype, the data model includes an IsPublic flag and the update DTO can change it. Tracing the read paths showed a different reality: the API does not establish a caller identity, and the read and export services load a CV by ID without applying an access policy.
CV MANAGEMENT / ACCESS REVIEW
A field is not a permission checkCurrent request path
- 01HTTP callerNo signed-in identity
- 02Read or export route
/api/cv/{id}·/api/export/{id}/… - 03Load by record IDService → repository → CV
- 04CV data returnedJSON · PDF · DOCX · HTML
IsPublic not enforcedBoundary to add
- 01Identify the callerValidate an authenticated principal
- 02Authorize this recordOwner, tenant, or explicit public policy
- 03Project allowed fieldsDo not serialize the whole entity by default
- 04Export under the same policyTest PDF, DOCX, JSON, and HTML paths
One decision point should guard every representation of the same CV.
f791fa0. The right-hand lane is a proposed policy boundary, not an implemented feature.A field only matters when a request checks it
The project has a useful separation of concerns. Controllers accept HTTP requests, application services handle CV operations, repositories talk to SQLite, and export services create PDF or DOCX output. That gives each part a clearer job and makes the data flow easier to review.
The security boundary has to cross those same layers. In this snapshot, Program.cs does not register an authentication scheme, and the CV and export controllers do not require an authenticated principal. The application calls UseAuthorization(), but no route policy or [Authorize] attribute protects these handlers. The configured CORS policy allows any origin; CORS controls which browser origins may read a response, but it does not prove who the caller is.
The IsPublic property appears in the domain model and update DTO. The ordinary read path does not use it as a gate: GetByIdAsync loads a record and maps it to a response, while GetAllAsync returns repository results without an owner or visibility filter. A public/private label is useful only when the application gives it explicit behavior.
Exports are another way to read the same record
The API exposes separate routes for PDF, DOCX, JSON, and HTML. Each route resolves a CV by its ID and returns another representation of its contents. That means a permission check added only to the regular detail endpoint would leave other read paths to secure separately.
I would put the decision where every route can share it: identify the caller, load only records that caller may access, and then create a response projection for the requested format. A deliberate public view should be a separate policy with a narrow set of fields. Exporting should follow the same policy as viewing, because a downloadable file is still a copy of personal data.
This is also why I would avoid serializing a database entity directly as an export. A résumé can contain email, phone number, employment history, education, and references. The application should decide which fields belong in each public or owner-only view before PDF, DOCX, JSON, or HTML rendering begins.
The boundary I would add before using real records
First, require an authenticated identity for private CV routes. Next, scope repository reads and writes to the owner or tenant from that identity; a caller-supplied record ID is not proof of ownership. Then make public sharing an explicit, opt-in route that returns a purpose-built public DTO, rather than toggling a flag and returning the same object.
The export handlers need the same authorization decision and should record who exported which CV and when. I would also define deletion and retention behavior, restrict CORS to the actual client origins, and add integration tests that attempt to read and export another user’s record. Those tests should exercise every output format, including the case where a CV is marked private.
These are requirements for a future boundary, not features I have implemented in this prototype. Its README describes it as a demo and says to use only synthetic sample records until access control, privacy, retention, and audit requirements are in place. I agree with that constraint: this API should not be exposed as an HR service or receive real applicant data in its current form.
What I checked in the source
I reviewed private source snapshot f791fa0 and ran the isolated .NET test project from a temporary checkout. The test assembly built and ran 24 tests: 23 passed and one failed. The failing test expects UpdateAsync to return null for a missing CV, while the current service throws KeyNotFoundException and the HTTP controller translates that case to a 404. That is a contract mismatch worth fixing; it is not evidence about access control.
There are no controller or integration tests for unauthenticated reads or export access in the test tree. The GitHub Actions workflow also appends || true to restore, build, and test commands, so those steps do not preserve the command’s failure status. I therefore treat the local test run as narrow evidence about the service and validators, not as a security certification or a successful end-to-end verification.
This prototype helped me separate three things that are easy to confuse: a database flag, an HTTP policy, and a safe public representation. They need to agree before a record can be shared. For API-boundary and privacy reviews, contact me.