Skip to content

Sync policy text fields cannot be saved: values are sent as strings where the API schema requires integers #19

Description

@toolonghere

Type: bug

Description

Under Domains → <domain> → SYNC POLICY, the three text fields cannot be saved. Pressing
SAVE returns Bad request. Checkboxes and sliders on the same form save without problems.

Affected fields:

Label in the UI Key
Password expiration (days) devpwexpiration
Inactivity (seconds) before device locks itself maxinacttimedevlock
Password history devpwhistory

The API rejects the request:

PATCH /api/v1/system/domains/1? -> 400
{"errors":["Value {'domainname': 'example.com', 'domainStatus': 0, 'orgID': 0, 'maxUser': 100,
 ..., 'syncPolicy': {'devpwenabled': 1, 'devpwhistory': '0'}, 'chat': False} not valid for
 schema of type SchemaType.ANY: (<ValidationError: \"'0' is not of type integer\">,)"],
 "message":"Bad Request"}

Note 'devpwhistory': '0' — a string. The API schema is right to refuse it;
res/config.yaml declares devpwhistory, devpwexpiration and maxinacttimedevlock as
type: integer, default: 0.

Steps to reproduce

  1. Domains → <any domain> → SYNC POLICY
  2. Change Password history (any value, including typing back the default 0)
  3. SAVEBad request

Cause

ChangeEvent.target.value is always a string. The checkbox and slider handlers coerce to
numbers, the text handler does not.

containers/DomainDetails.tsx:

230:  const handleSyncChange = (field: string) => (event: ChangeEvent) => {
231:    const { syncPolicy } = state;
232:    setState({
233:      ...state,
234:      syncPolicy: {
235:        ...syncPolicy,
236:        [field]: event.target.value,     // <-- string
237:      },

compared with the two handlers next to it, which produce numbers:

handleSyncCheckboxChange: [field]: newValue ? 1 : 0
handleSlider:             [field]: newVal

utils.tsx then normalises the policy before diffing, but || 0 only replaces
undefined/null/"". A non-empty string passes straight through, and "0" is truthy:

199: export function getPolicyDiff(defaultPolicy: Partial<SyncPolicy>, syncPolicy: Partial<SyncPolicy>): Partial<SyncPolicy> {
200:   const formattedPolicy: Partial<SyncPolicy> = {
201:     ...syncPolicy,
202:     devpwhistory: syncPolicy.devpwhistory || 0,
203:     devpwexpiration: syncPolicy.devpwexpiration || 0,
204:     maxinacttimedevlock: syncPolicy.maxinacttimedevlock || 0,
205:   };
206:   const result: Record<string, any> = {};
207:   for(const [key, value] of Object.entries(defaultPolicy)) {
208:     if (formattedPolicy[key as keyof SyncPolicy] !== value) {
209:       result[key] = formattedPolicy[key as keyof SyncPolicy];

Line 208 is worth a second look: the comparison is strict, so "0" !== 0 is true. Typing the
default value back into the field is enough to put it in the diff — and therefore enough to fail
the save. The user sees a rejection for a change that amounts to nothing.

components/SyncPolicies.tsx:73 wires the text fields to that handler, so all three share the
fault:

 69:    value: syncPolicy[field],
 ...
 73:    onChange: handleChange(field),

The same handler exists at user level with the same defect —
containers/UserDetails.tsx:621, [field]: event.target.value — so
Users → <user> → SYNC POLICY is affected identically.

Suggested fix

Coerce where the value enters state, in both containers:

   const handleSyncChange = (field: string) => (event: ChangeEvent) => {
     const { syncPolicy } = state;
     setState({
       ...state,
       syncPolicy: {
         ...syncPolicy,
-        [field]: event.target.value,
+        [field]: event.target.value === "" ? "" : Number(event.target.value),
       },
     });

Keeping "" as-is lets the field be cleared while typing; getPolicyDiff already maps it to 0.

Alternatively, or in addition, make the normalisation in utils.tsx do the conversion it looks
like it is doing:

-    devpwhistory: syncPolicy.devpwhistory || 0,
-    devpwexpiration: syncPolicy.devpwexpiration || 0,
-    maxinacttimedevlock: syncPolicy.maxinacttimedevlock || 0,
+    devpwhistory: Number(syncPolicy.devpwhistory) || 0,
+    devpwexpiration: Number(syncPolicy.devpwexpiration) || 0,
+    maxinacttimedevlock: Number(syncPolicy.maxinacttimedevlock) || 0,

That second hunk alone fixes the reported failure and also removes the spurious diff entry on
line 208. Doing both keeps the state itself correctly typed, which matters because
SyncPolicy declares these fields as numbers — TypeScript does not catch the mismatch today
because ChangeEvent is untyped at that point.

Workaround

grommunio-admin domain modify has no syncPolicy option, so the only way to set one of the
three fields is to PATCH the API directly with a correctly typed value.

Not a duplicate of #11

#11 (closed, fixed April 2026) is the same
class of defect — the web client sending values the API schema refuses — but a different
instance: it was None for non-nullable fields when creating a domain. This one is a string
where an integer is required when updating one, and it originates in handleSyncChange /
getPolicyDiff, which #11 did not touch. The failure is still reproducible on the versions below.

All 18 issues and pull requests in grommunio/admin-web were fetched and searched locally for
syncPolicy, sync policy, devpw, integer, maxinact and policy, open and closed alike.
#11 is the only hit, and only on the words "bad request".

Versions

grommunio-admin-web  5.0.0.62.0ad249e-1+203.1
grommunio-admin-api  1.20.43.mbb64403-1+64.1

Ubuntu 24.04, community repo. Source read from the shipped source maps in
/usr/share/grommunio-admin-web/static/js/*.map, so line numbers are from the released build.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions