Skip to main content

Ruby library (premium)

All notable changes to the habarico-enterprise gem are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[7.2.0] — 2026-10-19​

Changed​

  • BREAKING: Requires the free library habarico >= 7.2.0, installed from rubygems.org. The version now follows the free libraries' shared 7.2 line. (HAB-1034)

[1.0.0] — 2026-10-10​

Added​

  • Initial habarico-enterprise gem, built on the extension points added to the free gem in the same change. Adding it to the Gemfile registers every feature; nothing has to be wired up by hand. A lib/habarico-enterprise.rb shim makes Bundler.require find the entry point despite the hyphen in the gem name — without it the gem would load silently as a no-op. (HAB-812)
  • HabaricoEnterprise::Engine — a Rails::Engine so Rails picks up this gem's app/ directory. Without it the controllers behind the contributed routes are never autoloadable and every request to /search or /{resource}/aggregate/schema fails with uninitialized constant. (HAB-812)
  • Global spotlight search, moved from the free gem: GET /search, HabaricoEnterprise::SearchService, HabaricoEnterprise::Search::SearchHighlighter, SearchResourceSchema and its generator, plus the global_fulltext_search_enabled opt-in it adds to the resource DSL and to the global resource schema. The list endpoint's meta.highlights comes from here too, through Habarico::Config.search_highlighter_class. (HAB-812)
  • Change history, moved from the free gem's habarico/contrib/paper_trail: habarico_enterprise/paper_trail — VersionController, VersionSerializer, DiffRenderer, VersionResourceSchemaGenerator, the PaperTrail::Version model extensions and 13 locale files. Opt-in: needs the paper_trail and diff_match_patch gems. (HAB-812)
  • can_read_history on the resource DSL — declares who may read a tracked resource's change history, with the same bool-or-callable contract as can_read. Left unset, the history follows the resource's can_read. (HAB-903)
  • Chunked/direct uploads, moved from the free gem's habarico/contrib/uploads: habarico_enterprise/uploads — the tus and S3 multipart providers, Uploads::Config, Uploads::Providers, HabaricoEnterprise::UploadOptions, the initiate/complete/abort endpoints (HabaricoEnterprise::UploadsController) and the tusd helper scripts in scripts/. The three endpoints are now drawn by RoutingService.register_route_block, beside schema at the admin-site root, instead of having to be mounted by hand as in the free contrib — a host upgrading should delete its post "uploads/…" lines from config/routes.rb. Accordingly Config::S3.initiate_url became optional: left nil it resolves the drawn route. Registered through Eluvia::Uploads.finalizer, Habarico::FieldOptions (the upload schema key) and Habarico::FieldOptionsGenerator (which fills it), so a free-only install emits no options.upload at all. Opt-in — require "habarico_enterprise/uploads" — because it registers a process-wide upload finalizer and needs configuration; the s3 provider additionally needs the host app's own aws-sdk-s3. Matches the Python split, where chunked uploads are paid. (HAB-812, HAB-583)
  • options.max_size / options.accept on file_attr fields - the client-side half of the upload rules, derived from the same active_storage_validations declarations initiate reads and checked on a picked file before the upload starts, so a file that would be refused anyway never leaves the browser. Emitted for the base64 path too, since the validations describe the field rather than the transport; Config.max_upload_size joins max_size only when the s3 provider serves the field, since Providers::S3#initiate is the only place that enforces it. A Regexp content_type: yields no accept at all rather than a partial list that would be stricter than the server, and the hard blocklist is deliberately not reflected in accept either - so an absent or permissive accept never means the server will accept the file. Both keys are declared on Habarico::FieldOptions by this gem, the same way upload is, and need the matching keys in contract/openapi.yaml. (HAB-942)
  • Aggregations and the dashboard, moved from the free gem: habarico_enterprise/aggregations — the /{resource}/aggregate endpoint and its schema route, AggregateService, AggregateSchema / AggregateFieldSchema and their generator, AggregateFieldDefinition, the aggregate_* / dashboard_enabled DSL options with their original defaults, and validate_aggregate_fields!. Registered through Definition, Habarico::Controller, RoutingService.register_resource_route_block and GlobalResourceSchema. (HAB-812)
  • can_read_aggregations on the resource DSL — declares who may reach a resource's aggregate endpoint and its schema, with the same bool-or-callable contract as can_read. Left unset, the aggregations follow the resource's can_read. (HAB-914)

Changed​

  • BREAKING: the gem moves to version 1, the major of the free habarico gem it extends, and requires habarico >= 1.0.0. The two gems share their major from here on, so the version of the free gem names the paid one that goes with it. (HAB-1019)
  • max_size / accept on a file_attr's schema options are now derived by the free habarico gem itself (Habarico::FileValidations) rather than only here - a free-only install gets both options too, not just the S3/tus one. Validations.for_model / .accept_for are now re-exports of the free gem's functions; Validations::Rules / ::NONE likewise alias Habarico::FileValidations::Rules / ::NONE. apply_field_limits no longer takes rules and no longer touches accept at all - it only folds the project-wide Config.max_upload_size ceiling into whatever options.max_size the free gem already derived. No behaviour change for a project using habarico-enterprise - initiate, the signed target token, and the schema options it advertises are unaffected. (HAB-966)

Security​

  • The S3 initiate endpoint trusted the client-supplied content_type outright and used it unchanged in the presigned URL's signature and create_multipart_upload; finalize's copy_object then carried it forward into the final, served object with no re-check - so a client could make the browser later render text/html/image/svg+xml inline (stored XSS). initiate now rejects text/html, application/xhtml+xml and image/svg+xml by default, and additionally enforces the content_type: and size: validations the host declared on the file_attr's underlying_attachment with active_storage_validations - a per-field content-type allowlist and size ceiling, the latter applied alongside Config.max_upload_size (the stricter of the two wins). active_storage_validations stays outside the gemspec, like aws-sdk-s3: a host that does not add it simply declares no rules and only the blocklist applies. A generated initiate_url now carries an opaque signed ?field= token holding that field's rules, derived once when the schema is built - the signature is what makes carrying values safe, and the same values are advertised as the field's max_size / accept options in the same response, so the two cannot disagree. Providers::S3#initiate takes a new rules: keyword - the field's limits the claimed content_type/size are measured against - and Providers::S3#field_schema takes rules: in place of the model_label/field_name it never used. (HAB-942)
  • The text/html/application/xhtml+xml/image/svg+xml block above is a default, not an unconditional wall: a field whose content_type: validation names one of them exactly (e.g. content_type: 'image/svg+xml', or the :svg shorthand) now opts that field back in - a deliberate, field-scoped declaration, unlike a wildcard or Regexp match (image/*, %r{\Aimage/.*\z}), which still does not override the block, since a pattern is not a considered decision about SVG specifically. Serving an opted-in file safely (forced download, a sandboxed origin, CSP, sanitisation) stays the host's own responsibility. The default itself is now also a project-wide setting - Config.dangerous_content_types - rather than a hardcoded constant, so a host can replace or empty it entirely; emptying it removes the floor for every field with no restriction of its own, not just one naming a type exactly, so it carries the same serving-side responsibility at a larger scale. (HAB-966)
  • Unbounded size in the S3 multipart initiate — the provider presigned one URL per part with no ceiling, so a claimed size of 10^16 asked it for ~1.9 billion presigned URLs and took the worker down with it. initiate now refuses, before anything is created in S3, any upload needing more than Providers::S3::MAX_PARTS (10 000, S3's own multipart limit) parts, plus any negative size — all three as 422 Unprocessable Entity, the status the Python library already answers such a request with. The new Uploads::Config.max_upload_size (default nil) lets a host cap uploads further; left unset, the effective ceiling is MAX_PARTS * chunk_size — ~48.8 GiB at the default chunk size, which S3 could not have completed anyway. The refusals are translated, and the gem now has a catalog of its own: lib/habarico_enterprise/locales/*.yml in 13 languages, loaded from the entry point, covering the upload messages plus the aggregation errors and the search yes/no labels that until now only had English fallbacks. (HAB-904)
  • The S3 finalize step built the final upload key from the client-supplied filename with no sanitization, unlike initiate in the same provider, which already sanitizes it via ActiveStorage::Filename. finalize now sanitizes the same way, and uses the sanitized name for both the S3 key and the attached blob's filename metadata. (HAB-911)
  • BREAKING: The aggregate schema endpoint ignored permissions entirely. GET /{resource}/aggregate checked can?(:read), but GET /{resource}/aggregate/schema checked nothing — HabaricoEnterprise::Aggregations.resource_schema received the controller's context and discarded it (the parameter was named _context), handing anyone who could reach the route the aggregate field whitelist, the labels, the enum choices, the references and the resource's whole filter schema. Both routes now resolve the new can_read_aggregations, falling back to the resource's can_read when it is not declared: the schema endpoint answers 404, which is what it already answers for a resource without the opt-in, and the data endpoint keeps answering 403. aggregate_enabled in the global resource schema (GET /schema) is emitted as false for a user who may not aggregate, so the frontend does not render a widget it cannot fill. Aggregations.resource_schema now requires a Habarico::Context and raises Eluvia::Errors::StandardError without one, the way Habarico::SchemaService already does — a host calling it with a resource name alone must pass a context. A host relying on the aggregate endpoints being reachable without can_read must declare can_read_aggregations true on those resources. (HAB-914)
  • The versions resource no longer grants blanket read access. Reading a version now requires the permission of the tracked resource resolved from its item_type — the new can_read_history when declared, the resource's can_read otherwise — and every versions query is narrowed to readable item types, with the tracked resource's policy scope, scopes and default filter applied to the item ids. Versions of models without a registered resource are no longer readable, and fetching a version outside these permissions returns 404. (HAB-903)
  • Stored XSS in the change history diff — field values were inserted into <ins>/<del> tags without HTML escaping, and the frontend renders the diff as rich text. A user with write access to a tracked field could store a payload that executes when an admin views the record's history. Values are now escaped the same way SearchHighlighter already does. (HAB-897)
  • BREAKING: Unbounded limit in the global spotlight search - GET /search accepted any number of digits and applied it per resource, so one request materialized up to limit records of every resource with global_fulltext_search_enabled and ran the highlighter over each of them. limit is now validated by the new Search::Limit.resolve: it must be an integer between 1 and Search::Config.max_limit (default 100), and anything else - non-numeric, zero, negative, or over the ceiling - is refused with 422, the status an over-wide aggregation already answers with. Only an absent (or empty) limit falls back to Search::Config.default_limit (default 5), which replaces the hardcoded value. Previously such a request silently returned 5 results, so a client sending a malformed limit now gets an error instead of data. GlobalSearchController resolves the param before calling SearchService#get_search_result, which now takes the resolved integer. (HAB-913)
  • SearchService#get_search_result built its scope straight from definition.model.all and ignored the current user entirely, so the global spotlight search returned records a user had no can?(:read) permission for, records excluded by policy_scope or scopes, and records hidden by default_filter (e.g. soft-deleted rows or other tenants' data). It now mirrors index: a resource without read permission is skipped entirely, and policy_scope, scopes and default_filter are applied before the fulltext match. (HAB-898)