LibreSign / LibreSign/libresign

Finalize signer geolocation backend with IP-based location

Open
#8,086 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

backend feature-request php
Dominant language
PHP
Stars
818
Forks
146
Avg merge
11h 31m
Merged PRs (30d)
326

Description

Context

LibreSign already has device-reported signer geolocation from #6960.

That implementation added the first location source before the complete geolocation model was known.

We now also need approximate location derived from the signer IP address.

This issue defines the final backend model for signer geolocation before this feature becomes stable.

It includes:

  • cleanup of the current device geolocation contract;
  • backend support for IP-based geolocation;
  • GeoIP database configuration and validation;
  • policy snapshot behavior for existing and new signature requests;
  • the API contract required by #8087;
  • the required runtime dependency in LibreSign/3rdparty.

The frontend is handled by #8087.

Blocks:

  • #8087 — frontend support for IP-based signer geolocation.

Goal

Provide one signer geolocation domain with two independent location sources:

geolocation
├── device
└── ip

The two sources must not overwrite each other.

Device location is reported by the signer device.

IP location is resolved by LibreSign on the server from the client IP.

Both sources may exist for the same signer.

The backend must not choose one source as the true location.

Device location may be more precise, but it is not proof of physical presence.

IP-based location is approximate and may be affected by VPNs, proxies, mobile networks and similar infrastructure.

Policies

Use two independent policies.

Device geolocation

Rename the current policy:

signer_geolocation

to:

signer_device_geolocation

Keep the existing values and behavior:

  • disabled
  • optional
  • required

Keep:

  • system scope;
  • group scope;
  • user scope;
  • inheritance;
  • delegation;
  • per-signer requirement when the effective policy is optional.

When the policy is:

  • disabled: device location cannot be required;
  • optional: the requester may require it for selected signers;
  • required: all signers must provide device location.
IP geolocation

Add:

signer_ip_geolocation

Values:

  • disabled
  • enabled

Default:

  • disabled

Use the same policy scopes and delegation model used by device geolocation.

There is no requester per-signer override for IP geolocation.

When enabled for a signature request, LibreSign attempts GeoIP lookup for every signer when they sign.

Policy Workbench relationship

Do not create a third master policy only to group these two values.

The two policies must remain independent in the backend.

#8087 should present them together under the same Signer geolocation area in the Policy Workbench.

This keeps policy inheritance independent while still providing one clear configuration area.

Freeze policy values per signature request

Geolocation policy behavior must be frozen when the signature request or envelope is created and its policy snapshot is generated.

The current effective policy controls only new signature requests.

For an existing signature request, runtime behavior must come from the policy value stored in its policy_snapshot, not from the current effective policy.

Policy disabled

When a new signature request is created while a geolocation policy is disabled:

  • store the disabled state in the policy snapshot;
  • do not collect data for that geolocation source;
  • do not allow that source to become active later for the same signature request because the effective policy changed.

If the effective policy becomes enabled later, this existing signature request must remain disabled for that source.

Policy enabled

When a new signature request is created while a geolocation policy is enabled:

  • store the enabled or effective state in the policy snapshot;
  • use that stored value for the complete lifetime of the signature request.

If the effective policy becomes disabled later, this existing signature request must continue using the stored enabled state.

New signature requests created after the policy is disabled must use the disabled state.

Existing requests without a snapshot value

An existing signature request that does not contain a geolocation policy in its policy_snapshot must not inherit a policy that was enabled later.

For an existing signature request:

  • use the stored snapshot value when present;
  • when the policy is absent from the snapshot, treat that geolocation source as disabled.

Do not fall back to the current effective policy for an existing signature request.

The current effective policy may be used only when creating a new signature request and generating its policy snapshot.

Device geolocation

For signer_device_geolocation, the policy snapshot defines whether device geolocation can be used.

When the effective mode is optional, keep the final per-signer requirement frozen in:

deviceGeolocationRequirement

Later policy changes must not change that stored signer requirement.

IP geolocation

For signer_ip_geolocation, the policy snapshot defines whether IP geolocation must be collected when each signer signs.

If the snapshot has IP geolocation enabled, collection must continue even if the effective policy becomes disabled later.

If the snapshot has IP geolocation disabled, enabling the effective policy later must not start GeoIP collection for that existing signature request.

Device geolocation contract cleanup

Device geolocation is not yet a stable production contract, so update its names to make the source explicit.

Rename the requester input:

geolocationRequired

to:

deviceGeolocationRequired

Rename the frozen signer metadata:

geolocationRequirement

to:

deviceGeolocationRequirement

Rename the signing input:

geolocation

to:

deviceGeolocation

Only client-provided device location may be accepted in deviceGeolocation.

The client must never submit IP geolocation data.

Stored signer metadata

Store signer location under one geolocation object.

Example:

{
  "deviceGeolocationRequirement": "required",
  "geolocation": {
    "device": {
      "status": "collected",
      "latitude": -23.5505,
      "longitude": -46.6333,
      "accuracy": 25,
      "timestamp": 1700000000000
    },
    "ip": {
      "status": "resolved",
      "sourceIp": "200.100.50.25",
      "countryCode": "BR",
      "country": "Brazil",
      "regionCode": "SP",
      "region": "São Paulo",
      "city": "São Paulo",
      "latitude": -23.55,
      "longitude": -46.63,
      "accuracyRadius": 20
    }
  }
}

accuracyRadius is expressed in kilometers.

Only store location attributes that are present in the GeoIP database result.

Do not infer missing:

  • country;
  • region;
  • city;
  • coordinates;
  • accuracy values.

Fields generated by LibreSign for audit and lookup state, such as status, sourceIp and reason, are still stored as defined in this issue.

Do not store postal address information.

When coordinates are stored and the database provides accuracyRadius, store it together with the coordinates.

Device and IP location must never replace each other.

Relationship with collect_metadata

Do not refactor the existing collect_metadata policy in this issue.

It keeps its current behavior for generic request metadata:

  • remote-address;
  • user-agent.

GeoIP is a separate audit feature.

When signer_ip_geolocation is enabled in the signature request policy snapshot, the source IP used for the lookup must be stored in:

geolocation.ip.sourceIp

This must happen even when collect_metadata is disabled.

This does not enable collect_metadata.

This also must not start storing the User-Agent.

Expected behavior:

collect_metadata = false, GeoIP disabled

Do not store:

  • remote-address;
  • user-agent;
  • geolocation.ip.
collect_metadata = true, GeoIP disabled

Keep the current behavior:

  • store remote-address;
  • store user-agent;
  • do not store geolocation.ip.
collect_metadata = false, GeoIP enabled

Store:

  • geolocation.ip, including sourceIp.

Do not store generic:

  • remote-address;
  • user-agent.
collect_metadata = true, GeoIP enabled

Keep the current request metadata and also store geolocation.ip.

The same IP may exist as:

  • remote-address;
  • geolocation.ip.sourceIp.

This is acceptable because the fields have different audit meaning.

GeoIP runtime dependency

Use the official geoip2/geoip2 PHP package for MaxMind database access.

Runtime PHP dependencies shipped with LibreSign are managed in the LibreSign/3rdparty repository.

Add the GeoIP dependency there and follow the existing 3rdparty process:

  • declare the dependency in LibreSign/3rdparty;
  • update its Composer lock file;
  • regenerate the scoped Composer dependencies with PHP-Scoper;
  • make sure the library works with the OCA\Libresign\Vendor namespace used by LibreSign;
  • update the 3rdparty submodule in the LibreSign implementation PR.

Do not add the runtime dependency only to the main LibreSign composer.json.

The dependency must follow the same update and maintenance process as the other PHP runtime libraries shipped through LibreSign/3rdparty.

The LibreSign PR must be tested with the exact 3rdparty commit that contains the GeoIP dependency before it is merged.

The rest of the LibreSign backend should not depend directly on the upstream package namespace.

Keep MaxMind-specific access behind the internal GeoIP lookup service.

GeoIP database

For this first implementation, support local MaxMind City databases:

  • GeoLite2 City;
  • GeoIP2 City.

Other providers and database types are out of scope.

Store the full database path as instance-level LibreSign app configuration using IAppConfig.

Use:

geoip_database_path

This is technical instance configuration.

It is not a policy and must not have group or user scopes.

Do not automatically download or update GeoIP databases.

GeoIP lookup must use the configured local database.

Do not send signer IP addresses to MaxMind or another external GeoIP service.

Nextcloud GeoBlocker reference

The Nextcloud GeoBlocker app may be used as a reference for the operational model of using an administrator-configured local MaxMind database.

Reference:

HomeITAdmin/nextcloud_geoblocker

LibreSign must not depend on that app.

Do not copy its lookup or validation model directly.

In particular:

  • do not load geoip2.phar;
  • do not search for the library next to the database file;
  • do not validate the database by checking a fixed public IP;
  • do not create support for multiple GeoIP providers in this issue.

LibreSign should use its own dependency, service and testing architecture.

The issue must remain fully implementable without reading the GeoBlocker source.

Database validation

Validate the configured database without using a lookup against a hard-coded public IP.

Check that:

  • a path is configured;
  • the file exists;
  • the file is readable;
  • the database can be opened;
  • the database type is supported.

Use database metadata provided by the MaxMind reader when possible.

Expose one of these states:

  • not_configured
  • not_found
  • not_readable
  • invalid_database
  • unsupported_database
  • ready

When available, also expose:

  • database type;
  • database build time;
  • file modification time.

Invalid GeoIP configuration must not break normal LibreSign requests.

Do not expose internal exception messages, stack traces or unnecessary filesystem details through the API.

Admin API

Provide an admin-only API for #8087.

Read configuration

GET /api/v1/admin/geoip

Example:

{
  "path": "/usr/share/GeoIP/GeoLite2-City.mmdb",
  "status": "ready",
  "databaseType": "GeoLite2-City",
  "buildEpoch": 1700000000,
  "modifiedAt": "2026-09-01T10:00:00+00:00"
}

Optional database metadata may be omitted when unavailable.

Save configuration

POST /api/v1/admin/geoip

Input:

{
  "path": "/usr/share/GeoIP/GeoLite2-City.mmdb"
}

Return the same configuration structure used by the GET endpoint.

An empty path clears the configuration.

Allow a path to be saved even when the file is not available yet.

The returned status must describe the current state.

Only administrators may read or update this configuration.

This API must expose only GeoIP configuration and status.

It must not expose signer IP addresses or signer geolocation metadata.

Client IP source

Use the client address already resolved by Nextcloud through:

IRequest::getRemoteAddress()

Do not parse these headers directly:

  • X-Forwarded-For;
  • Forwarded;
  • similar proxy headers.

Do not add a second trusted proxy configuration in LibreSign.

Nextcloud remains responsible for resolving the client address.

GeoIP lookup flow

GeoIP is passive audit collection.

It must not require new signer input or permission.

When the signer sends the signing request:

  1. load the existing signature request and related file;
  2. read the frozen signer_ip_geolocation value from its policy_snapshot;
  3. if the snapshot value is missing or disabled, do not perform GeoIP work;
  4. get the client IP from the current HTTP request;
  5. check if the GeoIP database is ready;
  6. resolve the IP using the local database;
  7. normalize the result;
  8. add the result to geolocation.ip;
  9. continue through the existing signing flow;
  10. persist the same metadata for synchronous or asynchronous signing.

Do not consult the current effective policy to decide GeoIP behavior for an existing signature request.

The lookup must happen before an asynchronous signing job is queued.

Do not perform the lookup inside the background worker.

The worker does not represent the original signer HTTP connection.

GeoIP result

Successful lookup:

{
  "status": "resolved",
  "sourceIp": "200.100.50.25",
  "countryCode": "BR",
  "country": "Brazil",
  "regionCode": "SP",
  "region": "São Paulo",
  "city": "São Paulo",
  "latitude": -23.55,
  "longitude": -46.63,
  "accuracyRadius": 20
}

Valid IP with no matching database record:

{
  "status": "not_found",
  "sourceIp": "200.100.50.25"
}

Lookup unavailable:

{
  "status": "unavailable",
  "sourceIp": "200.100.50.25",
  "reason": "database_not_ready"
}

Supported unavailable reasons:

  • database_not_ready
  • address_unavailable
  • lookup_failed

If no client address is available, omit sourceIp.

Do not store internal exception messages in signer metadata.

Technical details may be written to the server log, but signer IP addresses, coordinates and complete geolocation payloads must not be added to log messages or log context.

Failure behavior

GeoIP is additional audit evidence.

It must never block signing.

Signing continues when:

  • the database is not configured;
  • the database file is missing;
  • the database is invalid;
  • the client address is unavailable;
  • the address is not found;
  • the lookup fails.

When the signature request policy snapshot has GeoIP enabled, store the lookup status where possible.

This allows API and frontend consumers to distinguish:

  • GeoIP not enabled for the signature request;
  • lookup unavailable;
  • address not found;
  • resolved location.

Development-state terminology

Signer geolocation and GeoIP have not been released yet.

Do not treat the current implementation as a legacy contract.

During this work:

  • update the current implementation directly to the final model;
  • do not add aliases for current development field names;
  • do not add fallback policy names for the current development state;
  • do not add compatibility layers for unreleased geolocation contracts;
  • do not keep current metadata shapes only because they already exist in main;
  • do not describe unreleased geolocation code, policies, APIs or data structures as legacy.

There is only one intended geolocation contract after this issue: the final contract defined here.

Upgrade compatibility

Signer geolocation has not been released as a stable LibreSign feature.

Do not add migrations only to preserve intermediate geolocation states created during development.

If a current unreleased migration already contains geolocation-related behavior, update that migration so fresh installations and supported upgrades use the final contract defined by this issue directly.

Do not add another migration only to preserve a development database that already executed an earlier version of that unreleased migration.

Developers using such a database may need to recreate or reset their development environment.

Existing data from released LibreSign versions must remain unchanged.

Tests must cover the supported installation and upgrade paths using the final migration state.

Do not keep current development API names, policy names or metadata structures only for development compatibility.

API output

Expose geolocation from stored signer metadata.

Do not rebuild historical audit data from the current policy.

The existing file list and validation APIs must expose, when available:

{
  "deviceGeolocationRequirement": "required",
  "geolocation": {
    "device": {},
    "ip": {}
  }
}

Only include sources that exist.

Keep the current metadata filtering behavior.

Do not expose the complete internal SignRequest.metadata object.

Geolocation data must follow the same authorization rules as the signer metadata exposed by the existing audit and validation APIs.

For existing signature requests, API behavior related to whether a geolocation source is active must follow the stored policy snapshot, not the current effective policy.

Update:

  • ResponseDefinitions;
  • OpenAPI definitions;
  • generated TypeScript OpenAPI types.

After this issue is complete, #8087 must not require a new backend field, endpoint or business rule.

Implementation structure

Keep the GeoIP code separated from controllers and from the signing engine.

Use small backend services for:

  • GeoIP configuration and database status;
  • GeoIP lookup;
  • result normalization;
  • signer geolocation orchestration.

The HTTP/signing layer should only decide when collection is needed and pass normalized metadata through the existing persistence flow.

Keep MaxMind-specific code behind a LibreSign lookup boundary.

Do not spread the scoped MaxMind reader usage through controllers or signing services.

Reuse an opened database reader during the same request when possible.

Do not create a generic multiple-provider framework until another provider is needed.

Do not create a separate GeoIP persistence layer, history or cache containing signer IP addresses or geolocation results.

Use the existing signer metadata persistence defined by this issue.

Tests

Prefer real behavior for GeoIP database and lookup tests.

Use a small public MaxMind test .mmdb fixture.

Keep any required license or attribution with the fixture.

Do not use:

  • Internet access;
  • a production GeoLite2 database;
  • an external GeoIP service.

Keep mocks mainly at framework boundaries such as:

  • IRequest;
  • app configuration;
  • existing service collaborators when needed.

Do not mock the MaxMind reader in tests that verify real database parsing or lookup.

Policy tests

Cover:

  • device policy keeps disabled, optional and required;
  • IP policy defaults to disabled;
  • system resolution;
  • group resolution;
  • user resolution;
  • inheritance;
  • delegation;
  • policy snapshot creation;
  • later policy changes do not affect existing signature requests.

Add regression coverage for both geolocation sources:

  • create a signature request while the policy is disabled, enable it later, and confirm the existing request remains disabled;
  • create a signature request while the policy is enabled, disable it later, and confirm the existing request keeps the stored behavior;
  • confirm new signature requests created after a policy change use the new effective policy;
  • for an existing signature request without the policy in policy_snapshot, confirm the source remains disabled even when the current effective policy is enabled;
  • confirm runtime behavior for an existing signature request never falls back to the current effective policy.

For device geolocation, also cover that the frozen deviceGeolocationRequirement remains unchanged after later policy changes.

Device regression tests

Keep or update existing coverage for:

  • optional per-signer device requirement;
  • required device location;
  • server-side validation;
  • device collection independent from collect_metadata;
  • device location persistence.
Database tests

Cover:

  • no configured path;
  • missing file;
  • unreadable file where supported;
  • invalid database;
  • unsupported database type;
  • valid City database;
  • database metadata;
  • API responses do not expose internal exception messages or stack traces.
Lookup tests

Cover:

  • successful IPv4 lookup;
  • IPv6 when available in the fixture;
  • address not found;
  • unavailable client address;
  • unavailable database;
  • lookup failure;
  • normalization of country, region, city, coordinates and accuracy radius.
Signing tests

Cover:

  • GeoIP disabled in the policy snapshot performs no lookup;
  • missing GeoIP policy in the policy snapshot performs no lookup;
  • GeoIP enabled in the policy snapshot stores sourceIp;
  • successful result is stored;
  • lookup failure does not block signing;
  • collect_metadata = false with GeoIP enabled stores GeoIP source IP but not generic remote-address or user-agent;
  • collect_metadata = true keeps its current behavior;
  • device and IP sources coexist;
  • one source never overwrites the other;
  • synchronous signing persists the expected result;
  • asynchronous signing uses data collected from the original HTTP request and does not resolve GeoIP inside the worker;
  • disabling the current effective GeoIP policy after request creation does not stop collection for a request whose snapshot has GeoIP enabled;
  • enabling the current effective GeoIP policy after request creation does not start collection for a request whose snapshot has GeoIP disabled or missing.
API authorization tests

Cover:

  • GeoIP administration endpoints are restricted to administrators;
  • signer geolocation follows the same access rules as existing signer audit and validation metadata;
  • unauthorized users cannot retrieve signer IP addresses or geolocation metadata;
  • the admin GeoIP configuration API never returns signer data.
Logging tests

Where practical, cover failure paths and confirm signer IP addresses, coordinates and complete geolocation payloads are not added to log context.

Installation and upgrade tests

Cover:

  • fresh installation uses the final policy names and geolocation contract defined in this issue;
  • supported upgrade from the latest released LibreSign version results in the same final contract;
  • unrelated signer metadata is preserved;
  • unrelated file metadata is preserved;
  • unrelated policy data is preserved.

Do not add tests for intermediate geolocation contracts that were never released.

3rdparty integration tests

Verify that:

  • the GeoIP dependency is available through the scoped LibreSign/3rdparty autoloader;
  • the LibreSign implementation can perform a real lookup using the scoped package shipped by the app.

Use data providers where they reduce duplicated cases.

Mutation testing

Cover new isolated GeoIP backend logic with Infection.

For small isolated classes:

  • no mutation errors;
  • aim for no escaped mutants.

Do not exclude new production code only to make mutation testing pass.

Quality gates

The implementation must pass the existing backend checks:

  • PHPUnit;
  • Behat/integration tests;
  • Psalm;
  • PHPCS;
  • Infection;
  • OpenAPI generation and validation.

The related LibreSign/3rdparty change must also pass its existing CI and dependency generation checks.

Done when

  • Device geolocation uses the final source-specific contract.
  • signer_device_geolocation and signer_ip_geolocation are independently resolved.
  • Existing device geolocation behavior still works with the final contract.
  • Both geolocation policies are frozen when the signature request policy snapshot is generated.
  • Existing signature requests use their stored snapshot and never fall back to the current effective policy.
  • Existing requests without a geolocation policy in the snapshot treat that source as disabled.
  • Enabling a geolocation policy affects only new signature requests.
  • Disabling a geolocation policy does not change requests already created with that source enabled.
  • GeoIP database configuration and validation are implemented.
  • The admin API provides the complete GeoIP configuration state required by #8087.
  • GeoIP uses the client IP resolved by Nextcloud.
  • GeoIP lookup is local and does not send signer IP addresses to external services.
  • The source IP is stored as part of GeoIP audit evidence.
  • Enabling GeoIP does not enable or change collect_metadata.
  • Device and IP geolocation can coexist.
  • No location source overwrites another.
  • GeoIP lookup happens before synchronous and asynchronous signing paths split.
  • GeoIP failures never block signing.
  • Signer IP addresses, coordinates and complete geolocation payloads are not added to normal application logs.
  • Geolocation data follows the same authorization rules as existing signer audit and validation metadata.
  • The GeoIP admin API exposes configuration and status only, never signer data.
  • Stored location data is exposed through the existing audit/validation APIs.
  • OpenAPI and generated types describe the final backend contract.
  • Current unreleased geolocation migrations, if present, use the final contract directly.
  • No compatibility layer is added only for an intermediate development state.
  • Fresh installation and supported upgrade paths are covered by automated tests.
  • The GeoIP PHP dependency is shipped through LibreSign/3rdparty.
  • The LibreSign submodule points to the reviewed 3rdparty commit containing the dependency.
  • Runtime behavior is covered by automated tests.
  • #8087 can implement the complete frontend without adding new backend behavior.

Out of scope

This issue does not implement:

  • frontend GeoIP settings;
  • Policy Workbench frontend changes;
  • audit frontend changes;
  • browser geolocation UI;
  • compatibility support only for intermediate development states;
  • GeoIP database upload;
  • automatic GeoIP database download or update;
  • external GeoIP services;
  • multiple GeoIP providers;
  • separate GeoIP analytics or history;
  • GeoIP-based signing restrictions;
  • risk or fraud scoring;
  • rules that compare device and IP location;
  • map visualization;
  • refactoring or splitting the existing collect_metadata policy.

Frontend support is handled by #8087.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the existing device geolocation implementation from #6960 and tracing policy snapshots through signature-request creation and signing. Then inspect the admin API entry points for GET/POST /api/v1/admin/geoip and the LibreSign/3rdparty dependency process. Done means independent device and IP sources, frozen policy behavior, validated local database configuration, and the specified admin API contract.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
api, backend, databases, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.