Closes the data-leak holes that survived the move of these controllers
from Yavsc.Api to Yavsc.Blogs. Circles are personal — a circle and its
membership should never be visible, modifiable, or deletable by anyone
other than its owner.
BlogAclApiController:
- GetBlogACL() was returning the full table; now filters by
Allowed.OwnerId == caller's uid, with an Include(a => a.Allowed)
so EF Core can push the filter into SQL instead of materialising
the whole table.
- Other endpoints (GetById, Put, Post, Delete) already enforced
ownership; left as is.
CircleApiController:
- GetCircle() (no id) now filters by OwnerId.
- GetCircle(id) now requires c.Id == id && c.OwnerId == uid;
returns 404 (not 403) on miss to avoid leaking the existence of
someone else's circle.
- PutCircle verifies the existing record is owned by the caller,
then forces circle.OwnerId = uid on the body (the client's value
is ignored). Returns ChallengeResult when the caller doesn't own
the record.
- PostCircle forces circle.OwnerId = uid (was trusting the body).
- DeleteCircle now filters by OwnerId; 404 on miss.
All checks use the same source of truth (User.FindFirstValue(
ClaimTypes.NameIdentifier)) that the existing BlogAclApiController
authz code already relies on.
These two controllers belong to the Blogs subsystem (their routes
/api/blogacl and /api/circle are blog-domain concerns, not the
generic Api surface). Moving them next to BlogApiController keeps
related code together and prepares the PostIt client to consume
them through the same BlogsApiUrl base address as the existing
BlogApiClient.
Mechanical changes only:
- Namespace Yavsc.Controllers -> Yavsc.Blogs.Controllers
- Drop unused 'using Yavsc.Helpers;' (no symbol in the new
compilation unit depends on it; the build confirms it was
dead since the controllers were first written)
- Fix typo in CircleApiController route: 'api/cirle' -> 'api/circle'
(any client trying to call the documented route was hitting 404)
No functional changes to authorization or query shape. The known
security gaps in these controllers (GetBlogACL and GetCircle
return unfiltered collections, DeleteCircle has no ownership
check) are deliberately left untouched in this commit and will
be addressed in a follow-up.
Le run #102 (re-publication du tag 1.0.6 après le fix jq + bump image v2)
a passé le PATCH /releases/10706 (jq a bien extrait l'id racine, plus
de 404), mais l'upload d'asset a planté avec un 400 "Missing 'name'
parameter".
Cause : sur l'appel curl de l'upload d'asset, l'argument `?name=...`
était passé en argument positionnel entre `--data-binary @file` et
l'URL. curl l'interprète comme un second fichier d'input (un fichier
nommé '?name=...'), pas comme un query param, et l'API Forgejo ne
voit jamais le name.
Fix : concaténer `?name=PostIt.Android.apk` à l'URL directement.
L'API Forgejo accepte le name en query string sur POST /releases/{id}/assets.
La section [1.0.6] - stable du CHANGELOG mentionnait encore
debian12-dotnet10-android36-v1 et ne décrivait pas le fix du PATCH
release qui tombait en 404 à cause du sed greedy + JSON minifié.
Mets à jour avant de relancer la publication de la release
1.0.6 (workflow_dispatch), pour que le body publié reflète l'état
réel de l'infra (image v2 avec jq) et du workflow.
L'image runner pazof/yavsc-build-env installe jq (>= 1.7) à partir
de debian12-dotnet10-android36-v2 (Dockerfile du repo
dotnet-android-build-image, commit e06f096 "adds jq"). On en
profite pour supprimer json_escape et json_field à base de sed,
qui étaient fragiles :
* sed est greedy par défaut : sur du JSON minifié d'une seule
ligne (ce que renvoie l'API Forgejo de cette instance pour
/releases/tags/<tag>), la regex s/.*"id".../\1/p attrape la
DERNIÈRE occurrence de "id":<digits> sur la ligne, qui est
l'id de l'auteur de la release (1, premier user du repo),
pas l'id de la release (10706).
* Le head -3 ajouté en PR #30 ne tient pas sur du JSON minifié :
il n'isole rien et le sed greedy continue à capturer
l'id de l'auteur.
* PATCH /releases/1 tombait alors en 404 "The target couldn't
be found" (cf. run échoué du 2026-08-17 04:05 sur le tag
1.0.6).
jq résout les deux problèmes en une fois :
* jq -r '.id' retourne le champ id racine, pas l'id imbriqué
dans author.
* jq -n --arg body "$RELEASE_BODY" '{body: $body, prerelease:
$prerelease}' construit un body JSON proprement échappé
(backslashes, guillemets, newlines, caractères de contrôle
Unicode) sans avoir à le reproduire à la main.
Effet de bord : les bodies PATCH et POST sont écrits dans
/tmp/patch.json et /tmp/post.json puis passés à curl via
--data-binary @<file> au lieu d'une variable shell. Plus de
problème de quoting en chaîne shell, plus de collision avec
les espaces ou les caractères spéciaux du body.
Pré-requis côté runner : image pazof/yavsc-build-env:debian12-
dotnet10-android36-v2 (avec jq) + maj du label correspondant
dans la config du runner Forgejo.
L'API Forgejo renvoie pour /releases/tags/<tag> un objet JSON
pretty-printed où l'id racine (release.id, ex. 10706) est sur la
première ligne, mais l'objet author contient aussi un id (souvent 1
pour le premier user du repo). L'ancienne regex sed matchait la
première occurrence globale de "id" dans le fichier, donc elle
retombait sur author.id=1 et le PATCH /releases/1 tombait en 404
'The target couldn't be found'.
Fix : on pipe le fichier dans 'head -3' pour ne matcher que les
premières lignes (couvre largement le préambule de l'objet release).
Si Forgejo renvoie du JSON minifié (une seule ligne), head -3
renvoie toute la ligne et la regex matche le premier id (la racine,
parce que les champs auteur sont après les champs racine).
L'image runner pazof/yavsc-build-env n'a pas python3 (ni jq, ni
node). Le step de publication Forgejo utilisait python3 pour générer
les bodies JSON (POST /releases, PATCH /releases/{id}) et pour
extraire le 'id' de la réponse.
Fix : deux fonctions bash :
- json_escape : escaping JSON des chaînes (\\, \", \n, \r, \t)
- json_field : extraction d'un champ scalaire d'un fichier JSON via sed
Suffisant pour les bodies qu'on envoie (tag_name, name, body,
prerelease) et les champs qu'on lit (id).
L'image runner pazof/yavsc-build-env a le SDK .NET 10 et le workload
Android, mais PAS le binaire 'docker' ni de daemon Docker. Le
'Build de l'image Docker' du workflow plantait avec 'docker: command
not found'.
Fix : on exécute directement les commandes dotnet du Dockerfile
(restore + build Yavsc.Org/Api/Blogs + build PostIt.Android -r
android-arm64), puis on copie l'APK depuis le chemin de sortie
standard bin/Release/net10.0-android/android-arm64/.
Note : le Dockerfile reste la voie canonique pour les builds en
local et via GitHub Actions (qui a docker). Ce fix concerne
uniquement le workflow Forgejo Actions où le runner n'a pas Docker.
The previous awk extracted the section body but excluded the title
line (## [TAG] - channel), so the '* - $CHANNEL*' pattern never
matched. Fix: include the title line in the extracted body, verify
the channel suffix on the title, then strip the title before passing
the body to the release API.
The runner's docker label points at pazof/yavsc-build-env, a Debian
image without Node.js. Any action like actions/checkout@v7,
actions/upload-artifact@v7, rasterstate/forgejo-release-action, etc.
fails at container start with 'executable file not found in /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/home/paul/.dotnet/tools:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:/home/paul/.nvm/versions/node/v22.23.0/bin:/home/paul/.local/bin:/home/paul/.npm-global/bin:/home/paul/bin:/home/paul/.nix-profile/bin'.
This workflow is rewritten in pure bash:
- replace actions/checkout with explicit git clone + checkout (full
history + tags so GitVersion.MsBuild is happy);
- merge the two jobs into one (no inter-job artifacts needed since
everything shares the runner's filesystem);
- replace rasterstate/forgejo-release-action with direct calls to the
Forgejo REST API (/api/v1/repos/.../releases, .../assets), with
python3 used to build and parse JSON bodies (jq not guaranteed in
the runner image).
Auth: ${{ secrets.GITHUB_TOKEN }} (runner-provided). The
rasterstate action or any other Node-based action can be reinstated
later if the runner image is swapped for one with Node installed.
Repo-level secrets creation is broken on this Forgejo instance
(InsertEncryptedSecret fails with UTF-8 byte-sequence error, likely
a text-vs-bytea column type on the secret table). The fix is in
upstream Forgejo v16; until then, ${{ secrets.GITHUB_TOKEN }} (auto-
provided by the runner, scoped to contents: write for the current
repo) keeps the release workflow operational without any UI setup.
When the instance is upgraded and the secret table is migrated,
revert this commit to switch back to ${{ secrets.RELEASE_TOKEN }}
for least-privilege.
Adds .forgejo/workflows/release.yml: triggered by tag push or
workflow_dispatch, it validates the tag/CHANGELOG parity (stable /
preview / unstable), builds the PostIt Android APK via the existing
Dockerfile (--target build-env), and publishes a Forgejo release with
the APK as an asset via rasterstate/forgejo-release-action@v1.
Mirrors the validate-release logic of .github/workflows/docker-publish-android.yml
so the two channels (Forgejo source-of-truth + GitHub mirror) stay
consistent. Authentication uses ${{ secrets.RELEASE_TOKEN }}, a Forgejo
PAT scoped to write:repository configured in the repository's Actions
secrets.
ConfigureBillingService() walks AppDomain.CurrentDomain.GetAssemblies()
and calls Assembly.GetTypes() on each. If any of the loaded assemblies
has a type that fails to resolve (a flaky dependency, an AddOn with a
broken reference, a test dependency that's been rewritten after compile),
GetTypes() throws ReflectionTypeLoadException (or, less commonly,
FileNotFoundException / TypeLoadException for the assembly itself).
In CI on the forgejo-runner (and especially in test discovery under
xunit v3), one such assembly is loaded somewhere between test runs and
silently throws. The exception is not handled, so:
1. Collections are Cleared at the top of ConfigureBillingService().
2. The reflection loop throws before reaching the
RegisterBilling<HairCutQuery/HairMultiCutQuery/RdvQuery> calls.
3. BillingService.Billing ends up empty (Count = 0).
4. The second ConfigureBillingService() call sees the same assembly
loaded (xunit v3 keeps the AppDomain warm for the whole suite),
throws identically, and the test
Yavsc.BillingServiceTests.ConfigureBillingService_CanBeCalledTwiceWithoutThrowing
fails with 'Assert.Equal() Failure: Expected 3, Actual 0'.
Fix: catch ReflectionTypeLoadException and use the partial
.Types() list (the successfully-resolved subset), and use a
broader catch (with continue) for any other assembly-level
load failure. The lost user-settings types are not material;
they are derived from ApplicationDbContext in a separate loop
right after, and the RegisterBilling<>() calls that populate
BillingService.Billing run last, after both reflective phases
have completed best-effort.
The test still passes locally because the local test environment
loads a clean set of assemblies; only the CI runner (with its
extra test-time tooling) hits this path.
Patch is even (6) and bare, so this is classified as 'stable' by
the validate-release job in .github/workflows/docker-publish-android.yml.
Move the Unreleased section up by inserting [1.0.6] below it, with
a list of changes that landed on this release:
- Self-hosted Forgejo Actions runner now drives CI on yavsc,
using pazof/yavsc-build-env:debian12-dotnet10-android36-v1
pulled from Docker Hub.
- .forgejo/workflows/buildAndTest.yml builds without
actions/checkout (image has no Node) and uses NuGet.config
for the isn.pschneider.fr feed.
- Dockerfile / Dockerfile.backend drop the redundant
'dotnet nuget add source' step that broke the APK build on
GitHub Actions.
The project-level NuGet.config (added in 94012c51) lists the isn feed
so 'dotnet restore' picks it up without an inline 'dotnet nuget add
source' step.
The inline add source was duplicating NuGet.config and causing build
failures in GitHub Actions:
- The --allow-insecure-connections flag did not match the actual
HTTPS deployment of isn.pschneider.fr (Letsencrypt-issued cert,
not self-signed), making the step fail with 'exit code 1'.
- docker build --target build-env (used by
.github/workflows/docker-publish-android.yml) hit this on every
run.
Both Dockerfile and Dockerfile.backend had the same redundant step;
both removed. 'dotnet restore' still finds the feed via NuGet.config
at /src/NuGet.config (copied in by 'COPY . .').
The project-level NuGet.config (added in 94012c51) lists the isn feed
so 'dotnet restore' picks it up without an inline 'dotnet nuget add
source' step.
The inline add source was duplicating NuGet.config and causing build
failures in GitHub Actions:
- The --allow-insecure-connections flag did not match the actual
HTTPS deployment of isn.pschneider.fr (Letsencrypt-issued cert,
not self-signed), making the step fail with 'exit code 1'.
- docker build --target build-env (used by
.github/workflows/docker-publish-android.yml) hit this on every
run.
Both Dockerfile and Dockerfile.backend had the same redundant step;
both removed. 'dotnet restore' still finds the feed via NuGet.config
at /src/NuGet.config (copied in by 'COPY . .').
GitVersion.MsBuild fails on shallow clones ('Repository is a shallow
clone. Git repositories must contain the full history.') because it
walks the git log to compute the SemVer version.
Drop --depth 1 from both the PR ref fetch and the submodule update
so the runner's working tree has full history. The repo is small
enough that the cost is negligible.
The yavsc solution depends on HigginsSoft.IdentityServer8.* 8.1.0-alpha.*,
which is only published on the internal feed https://isn.pschneider.fr.
Public nuget.org has 8.0.4 as the nearest version, so every project that
uses IdentityServer8 (Yavsc.Org, Yavsc.Api, Yavsc.Blogs, Yavsc.Server,
cli, Yavsc.Org.Tests, Yavsc.Blogs.Tests) fails with NU1102 on restore.
Both feeds are reachable anonymously, so listing isn first and
nuget.org second in a project-level config restores everything without
credentials. The CI runner on forgejo now sees the same sources as a
local clone.
forgejo-runner v13 does not interpolate ${{ runner.workspace }}
in working-directory: (or ignores the field entirely for docker
containers), so the container tried to chdir to '/_src' (literally)
which does not exist.
The image WORKDIR is /src, so clone directly into /src/_src and cd
into it at the start of each step. Adds an echo of the checkout
SHA + branch state for visibility in the log.
The runner container does not have an SSH client, and even if it
did, no key is configured for it. Forgejo Actions must reach the
submodule over HTTPS with anonymous read access (which is now
enabled on the Forgejo instance).
Use 'git submodule sync --recursive' on the developer side after
checkout to propagate the URL change to .git/modules/.
In pull_request context, GITHUB_REF_NAME is the PR number ('17'),
not the source branch. Cloning --branch 17 fails with
'Could not find remote branch 17 to clone'.
Use GITHUB_REF (refs/pull/N/head in PR context, refs/heads/<branch>
in push context) and fetch + checkout FETCH_HEAD. workflow_dispatch
falls back to the default branch.
The pazof/yavsc-build-env:debian12-dotnet10-android36-v1 image only
ships .NET 10 SDK + Android SDK + JDK 17, no Node. actions/checkout@v6
requires Node, so the job failed with 'exec: node not found'.
Replace actions/checkout with a direct git clone over HTTPS (Forgejo
anonymous is enabled), and init submodules recursively.
Also drop the bogus docker://image:tag runs-on: matcher, use just 'docker'
to match the runner's declared label name.
Le workflow buildAndTest tournait sur un runner nu debian-latest avec
setup-dotnet@v5 pour la SDK 10.0.x. Restore échouait car cet
environnement n'a ni la source NuGet interne (isn.pschneider.fr) ni
les workloads Android configurés, contrairement à l'image
pazof/yavsc-build-env utilisée par le Dockerfile.
Bascule le job sur un runner labelisé docker avec l'image
debian12-dotnet10-android36-v1 directement. Le step setup-dotnet
devient inutile (l'image a déjà la SDK 10.0), le restore partage
la même config que le Dockerfile.
Refs l'image cible par ARG BUILD_ENV_TAG=debian12-dotnet10-android36-v1.
Le repo cible net10.0 partout (csproj, TFM), mais le workflow CI
Forgejo buildAndTest installait une SDK 9.0.x. Aligne sur 10.0.x
pour que la CI build avec une SDK qui connaît le TFM net10.0.
Pas de global.json ajouté : la SDK est résolue à l'installation
de l'image runner, le repo reste agnostique de la version exacte.