Repository navigation
v4.7.0
Upgrading from 4.6 takes four steps, in this order.
- Update the console and the renderer first. This release answers with API contract 7
(X-Api-Contract-Version: 7), delivery contract 7 and Pages contract 2. Deploy barakoBrew 1.8.0
or later and barakoPress 0.12.0 or later before the API; older ones refuse to run against it. - Stop the API. Three migrations ship under
migrations/4.7.0/. - Run
db-migrate, thendb-assert.membership-unique-user-tenant.sqladds a unique index on
(user, tenant) for memberships. If two memberships already share a user and a tenant it stops,
changes nothing and says how many pairs there are; the query that lists them is in the file's
header. Remove the extra rows and run it again. The twotenant-policy-restorefiles change
nothing unlessTenancy:DatabaseEnforcementis on. - Start 4.7.0.
scripts/upgrade-check.sh --list prints the files and the rollbacks in the order they run. To roll
back to 4.6, stop 4.7.0 and run the rollbacks, newest first, before starting the older image. The
policy restore rollbacks change nothing on purpose.
Breaking
- The contracts move to 7, and Pages to 2. The admin contract (
X-Api-Contract-Version) and the delivery contract (X-Delivery-Contract-Version) move from 6 to 7, and the Pages bodycontractmoves from 1 to 2. On the admin side, a retry with a used Idempotency-Key gets the first response back (or a 422 for a different request) instead of a 409, and writes now refuse what they used to accept: an email that is not a bare address, a field sent under two spellings, two content type fields whose names differ only in case, a connector setting whose name looks like a credential, and an update, push, rollback, transition or create that the caller's rule would not grant for the entry as it will be stored. On the delivery side, a slug field that is not Public no longer addresses entries, and a reference to an entry delivery would not serve (a draft, an entry that is not Public, or one that no longer exists) is left out ofdata(a single reference loses its key, a list keeps only the ids that are served). Pages resolve follows the same reference rule. A console or renderer that checks the range needs a release that accepts 7 (and Pages 2) before this API is deployed.
Added
- A signed-in caller can ask which roles and capabilities it holds.
GET /api/meanswers the
caller's user id, username, the tenant, its stored roles there (global roles and the active
membership's, by id and name) and every capability those roles carry, with*reported as
stored. It is the reading the API decides Sensitive and Hidden access by, so a console no longer
has to guess from role names. The tenant is the token's own. The route takes no parameters, is
sentCache-Control: no-store, and refuses an API key with 403. Additive (#1107). - A renderer can read a deliverable type's field roles and route template anonymously.
GET /api/public/types/{type}/descriptionanswers the type'srouteTemplateand, for each
Public field, its name, type, role and editor hint, so a renderer can build titles, links, a
sitemap and a feed from the type's own declaration. Sensitive and Hidden fields and tokens are
not listed, and no sensitivity, rule or default is returned. A type that is not publicly
deliverable is a 404, the same as an unknown one. Cached for one minute and varying by
X-Tenant. The route has three segments under/api/public/, and every route a content type is
read on has at most two, so a type of any name,typesincluded, is read exactly as before.
Additive (#1108). - The credential name words are published.
GET /api/meta/describeanswers
credentialNamePartsto every signed-in caller: the words that make a setting key or a workflow
action parameter read as a credential. It is read from the list the API checks with, so the
console can mask its inputs by it instead of keeping a copy. Additive (#1113). - Dead-lettered and cancelled jobs were never deleted. Set
Jobs:DeadLetterRetentionDays(90 is
the recommended value, at most 3650) to turn on an hourly sweep that deletes them once they have
been given up on for longer than that. It is off by default, so a deployment keeps every dead
letter as before until it opts in. A job that dead-letters now records when it gave up in
completedAt. - The job queue published no metrics. It now publishes
barakocms_jobs_attempts_totalby
outcome, andbarakocms_jobs_due,barakocms_jobs_oldest_due_age_secondsand
barakocms_jobs_dead_lettered, counted at most everyJobs:MetricsIntervalSeconds(30 by
default, 0 for off). See docs/background-jobs.md.
Changed
-
A retry with a used
Idempotency-Keygot a 409 instead of the response it missed. A client
whose connection dropped after the write committed learned that it had succeeded and never learned
the id it created. The same request from the same authenticated caller now gets the first
response replayed: its status code,Content-Type,Locationand body, plus
Idempotent-Replayed: true, and the handler does not run again. The same key with a different
method, path, query string or body is answered 422. A key whose first request is still running
stays a 409. A response overIdempotency:MaxStoredResponseBytes(default 65536) is not kept, and
a retry of it stays a 409 saying so. Routes that answer with a credential (sign-in, refresh, OTP and
MFA verify, tenant switch, MFA setup and enable, API key, preview token and share link create),
routes that take a password (register, password change, password set) and share link opening
never store or replay a response, store no request hash, and keep the 409; an endpoint opts in
with[NoIdempotentReplay]. Unauthenticated callers keep deduplication only: the same request
again is a 409, a different one under the key is a 422, and no response is stored. Requests are
compared by an HMAC, and stored bodies are sealed for their own record, both under keys derived
for this use from the stored-secret key material. Only
a 2xx is kept; any other status releases the key, which now includes 3xx. An API key is now its own
caller, so it no longer shares keys with the user it acts for. This is a status change on every
POST,PUTandPATCH, covered byApiContract.Version7. Keys are honoured for
Idempotency:KeyHours(default 24, 1 to 720) and are free again after that, checked on the
request rather than waiting for the hourly sweep that used to be the only expiry. A deployment
that relies on keys lasting longer setsIdempotency:KeyHours, up to 720 (30 days). Expired
records are deleted by their own hourly sweep, which runs again after a minute and logs a warning
when it could not finish. Seedocs/idempotency.md. -
A reset socket or one 503 cost a workflow a whole durable attempt. Outbound calls from the
WebhookandRequestactions, and the rest of the outbound HTTP client's calls, are now tried
again inside the attempt when the failure is transient (a connection that did not open, 408, 429
with itsRetry-After, 502, 503, 504), with a timeout on each try and a breaker per tenant and
host, using Carom. A call that may have reached the server is resent only when the receiver can
recognise it (an idempotent method, anIdempotency-Key, or a webhook's delivery id). A 429 never
counts toward a breaker, so one tenant's quota on a shared host does not refuse calls for the
others. An open breaker fails the attempt as retryable and names only the host. Email is not
retried inside the attempt; an optionalEmailSendTimeoutSeconds, off by default, records a send
that runs past it as unknown rather than retrying it. Settings are underWorkflows:Outbound, and
the host refuses to start when they let the slowest action take more than 80% of the shortest
lease. The outbound client's own timeout is now the retry budget plus 30 seconds, 74 seconds by
default, down from 100. This replaces the standard .NET resilience handler on that client, which
also retried a 500 and a POST after a timeout, soMicrosoft.Extensions.Http.Resilienceis no
longer a dependency. (#703) -
The upgrade file list is read from
migrations/.scripts/upgrade-check.sh --listprints a
psqlline for each file, core files first, then module files, then the rollback newest first,
in the order CI applies them, and leaves out every folder no newer thanFROM_VERSION.
docs/upgrading-to-4.0.mdnow sends operators there instead of keeping its own copy of the
list, and a new migration needs no edit to the script or the guide. The upgrade lines the guide
printed ran some module files before core files; the order is now the one CI proves. -
BarakoCMS.Testingcarries the core's version. It was 4.3.0 beside a 4.6.0 core.
scripts/check-module-versions.shnow fails when the two differ, and the module template's
defaultTestingVersionfollows it. -
The stale pull request refresher pushes with
REFRESH_PR_TOKEN. A branch it updated with
GITHUB_TOKENgot no CI run. The workflow now reads a token from theREFRESH_PR_TOKEN
repository secret and fails, naming the secret, when it is not set. -
A content type could declare two fields whose names differ only in case. Creating a type, a
blueprint and a portability import now refuse it, naming both spellings, since every
reader finds a field by its name ignoring case. A type that already stores such a pair can still
be changed.
Fixed
- Test teardown stops each projection coordinator first. The integration fixture stops the
coordinator of every host it started before any host stops, so the host's own stop finds it
already stopped. AnObjectDisposedExceptionfrom that stop is written to stderr instead of
failing every test in the collection; anything else still fails it. - A browser on another origin saw a rate limited request as a network error. The limiter answers
before the CORS middleware runs, so its 429 had no allow header. It now carries the CORS headers
the origin is allowed (a tenant domain still without credentials), and aRetry-After, which the
CORS policy now exposes to script. The fixed window limiter reports its whole window there rather
than the time left in it, so the wait is never too short and can be longer than needed. A
preflight is still counted by the limiter. - One workflow action that could not be built failed every step of every run. The runner, the
Conditional action, the workflow engine and the action registry built every registered action
together, so one constructor that threw failed all of them, and saving, validating and listing
workflows answered 500. Each action is now built on its own
when that happens: steps using the other actions run, and a step whose action is missing while
some could not be built fails with the class and exception type named, retried up to the normal
attempt limit. The exception's message is not logged or stored. - Tests that build their own host leave FastEndpoints usable.
JobWorkerSchemaOrderTestsput
FastEndpoints' process-wide resolver back after disposing each host, so a later test that issues
a token outside a request no longer fails. MODULES.md says what a module's tests should do about
the same statics. - One person holds one membership per tenant. Memberships carry a unique index on the user and
the tenant, andmigrations/4.7.0/membership-unique-user-tenant.sqladds it to an existing
database. Where two rows already share a user and a tenant the file refuses, changes nothing and
says how many pairs there are; its header has the query that lists them. Two requests adding the
same person to a tenant at once could store two rows, and the roles the person held then depended
on which row a read found first. Now the second request is answered 409 and stores nothing. - A top-level page cannot take a reserved slug whatever its slug field's sensitivity
(BarakoCMS.Pages 4.3.1). The check on write reads the slug the way core's authoring checks do,
so a slug field marked Sensitive is still checked. - The tenant policy is put back where an upgrade file left a table without it.
migrations/4.7.0/tenant-policy-restore.sql(core) and
migrations/4.7.0/forms-tenant-policy-restore.sql(shipped by Forms 4.5.0, so it runs after the
Forms files that create its tables) read whether the database enforces tenancy with row level
security from its other tables. Where it does, each of the six tables an earlier upgrade file
creates (share links, sourcing policies, collection syncs and the three Forms tables) that lacks
marten_tenant_isolationgets the same policy, copied from a table that has it, with row level
security on. WithTenancy:DatabaseEnforcementoff they change nothing. - The UpdateField workflow action stored any text in an email or choice field. It now checks
the value as an entry write does and fails the step for good when it is refused, without naming
the value. A multiple choice cannot be set from one text value. A Field spelled in another case
than the stored key now replaces that value instead of adding a second key.
Security
-
Image variants are made with SkiaSharp instead of SixLabors.ImageSharp. BarakoCMS.Files 4.5.0
resizes PNG, JPEG and WebP with SkiaSharp 4.153.1 (MIT, over BSD-licensed Skia) and its Linux
build that needs only libc and libstdc++, on x64 and arm64. The width ladder, never upscaling,
keeping the original's format and the pixel limit read from the header before decoding are
unchanged. What a variant looks like changes in these ways:- EXIF is no longer copied into a variant, so camera details and location stay with the original.
- A photo with an EXIF orientation tag is turned upright in the variant's pixels instead.
- An animated WebP is served whole at full size rather than resized. The pixel limit still runs on
its header first. - An animated PNG's variant is a still of its first frame.
- JPEG and lossy WebP variants are written at quality 75 rather than at a quality estimated from
the source. A lossless WebP stays lossless.
ImageSharpResizerstill compiles and now delegates toSkiaImageResizer; it is marked obsolete
for removal in barakoCMS 5.0. The suite image keeps only the Skia native library for its own
architecture. That library bundles code under several licences, and their notices ship in the
suite image under/app/licenses. Resizing also declines bytes that are not the stored file's
declared type, or not PNG, JPEG or WebP, before any decoder sees them. A host that takes BarakoCMS.Files from NuGet and publishes without a runtime
identifier gets Skia's native library for every platform it supports, several hundred megabytes
with the Windows debug symbols; publish with-r linux-x64(or your target) to keep one. -
A job's stored error could hold a full URL. A URL in the exception message is now cut to its
scheme and host before the error is stored, andGET /api/jobsapplies the same cut to errors
stored before this release. -
Semantic search returns a slug only when the type marks the slug field Public (BarakoCMS.AI
4.3.2). The module uses the same slug rule as delivery, and a hit's slug and title are read off
the entry under the type's current fields at search time rather than taken from the index, so
an entry indexed while its slug field was Public does not return it after the field is marked
Sensitive. -
A refused choice value was repeated back in the 400. The message now names the field and the
values it accepts, never the value received. A choice the entry already holds is not checked
again on an edit, so a stored value outside the current options, including one in a field the
caller cannot see and did not send, no longer blocks saving the rest of the entry. -
A connector setting could hold a credential in plain text.
POST /api/connectorsand
PUT /api/connectors/{slug}now refuse a setting whose name reads as a credential with a 400
pointing atsecrets, which are encrypted and never returned. The settings the auth modes read
(TokenUrlamong them) are still accepted. -
A Create rule's conditions were not applied to a create.
POST /api/contents, a collection
push creating an entry andPOST /api/import/contentnow check the Create rule against the entry
as it will be stored, with the caller as its creator, so a rule limited to one branch cannot
create an entry in another. The first two answer 403; an import reports the row as refused. A
Create rule with no conditions grants as before, and one on$createdByequal to
$CURRENT_USERstill lets the caller create. A role whose Create rule holds conditions now has
them applied, which can refuse creates that used to be accepted. -
authorizationandbearernow read as credential names. A setting key or a workflow
parameter holding either word is treated like one holdingtokenorpassword: a setting under
it can only be cleared, and a workflow parameter under it is encrypted at startup and not
returned.authon its own is not a credential word, so a name likeAuthoris unaffected. -
A delivered reference names only entries delivery would serve. On the anonymous routes (the
list, search, the read by slug and its preview, the feed, the event stream, share links and the
tenant profile) and inIPublicContentProjector.ProjectAsync, a reference field keeps an id only
when it names an entry in the same tenant that is Published, document Public and of a publicly
deliverable type, the testincludealready applied. A single reference to any other entry is
left out of the entry, and a list keeps the ids that pass, in stored order. An entry resolved
throughincludehas its own references checked the same way. The check reads the targets once
per response. On the list and search, afilter[field][eq|ne|has]on a reference field that
names any other entry answers as if no entry held that id, which is what the delivered data
shows. If the event stream cannot read the targets, it sends the commit's changes with no
reference ids in them and still sends its unpublish events. -
Delivery uses a slug field only when the type marks it Public. A field of type
slugmarked
Sensitive is not the type's slug on the anonymous routes: it is not served as the top-level
slug,GET /api/public/{type}/{slug}does not look entries up by it, and the feed and sitemap do
not build links from it. The type then has no slug route, as a type with no slug field never had.
Signed-in reads by slug, the uniqueness check on write and collection pushes still use the field. -
An email field took a display name or header form such as
Name <a@b.co>. It now takes a
bare address only: angle brackets, quotes, parentheses, commas, semicolons, colons, square
brackets, backslashes and control or format characters are refused, and so is an address longer
than 254 characters. This refuses some writes that used to be accepted. A value already stored is
still read as it is, and an edit that sends it back unchanged is accepted. -
A field sent under two spellings had only the first one checked. Both were stored, so a reader
taking the other spelling got a value nothing had checked. A write to a field of the type under
more than one spelling, ignoring case, is now refused with a 400 on create, update, collection
push, rollback and transition, and as a row error on bulk import, as file fields already were.
This refuses some writes that used to be accepted. -
A public form submission read only the first spelling of a field sent twice.
POST /api/public/forms/{slug}now refuses a field sent under more than one spelling, ignoring
case, with a 400 on that field, as a signed-in write does. BarakoCMS.Forms goes to 4.5.0. -
An update was checked against the entry as stored only.
PUT /api/contents/{id}, a
collection push, a rollback and a transition carrying values now also check the rule that
granted the write against the entry as it will be stored, so a rule with conditions, such as a
branch field or$CURRENT_USER, holds after the write as well as before it. A write that would
leave the entry outside the rule answers 403 and stores nothing.