{"record":{"id":"4e135b66549f8edc","repo":"juicedata/juicefs","slug":"invalid-acl-mask-is-required-and-cannot-be-delete","errorCode":null,"errorMessage":"Invalid ACL: mask is required and cannot be deleted.","messagePattern":"Invalid ACL: mask is required and cannot be deleted\\.","errorType":"validation","errorClass":"AclException","httpStatus":null,"severity":"error","filePath":"sdk/java/src/main/java/io/juicefs/utils/AclTransformation.java","lineNumber":231,"sourceCode":"    // union of group class permissions in each scope.\n    for (AclEntry entry : aclBuilder) {\n      scopeFound.add(entry.getScope());\n      if (entry.getType() == GROUP || entry.getName() != null) {\n        FsAction scopeUnionPerms = unionPerms.get(entry.getScope());\n        if (scopeUnionPerms == null) {\n          scopeUnionPerms = FsAction.NONE;\n        }\n        unionPerms.put(entry.getScope(), scopeUnionPerms.or(entry.getPermission()));\n      }\n      if (entry.getName() != null) {\n        maskNeeded.add(entry.getScope());\n      }\n    }\n    // Add mask entry if needed in each scope.\n    for (AclEntryScope scope : scopeFound) {\n      if (!providedMask.containsKey(scope) && maskNeeded.contains(scope) && maskDirty.contains(scope)) {\n        // Caller explicitly removed mask entry, but it's required.\n        throw new AclException(\"Invalid ACL: mask is required and cannot be deleted.\");\n      } else if (providedMask.containsKey(scope) && (!scopeDirty.contains(scope) || maskDirty.contains(scope))) {\n        // Caller explicitly provided new mask, or we are preserving the existing\n        // mask in an unchanged scope.\n        aclBuilder.add(providedMask.get(scope));\n      } else if (maskNeeded.contains(scope) || providedMask.containsKey(scope)) {\n        // Otherwise, if there are maskable entries present, or the ACL\n        // previously had a mask, then recalculate a mask automatically.\n        aclBuilder.add(new AclEntry.Builder().setScope(scope).setType(MASK).setPermission(unionPerms.get(scope)).build());\n      }\n    }\n  }\n\n  private static void copyDefaultsIfNeeded(List<AclEntry> aclBuilder) {\n    Collections.sort(aclBuilder, ACL_ENTRY_COMPARATOR);\n    ScopedAclEntries scopedEntries = new ScopedAclEntries(aclBuilder);\n    if (!scopedEntries.getDefaultEntries().isEmpty()) {\n      List<AclEntry> accessEntries = scopedEntries.getAccessEntries();\n      List<AclEntry> defaultEntries = scopedEntries.getDefaultEntries();","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/juicedata/juicefs/blob/c9a67b23e8e08ec23ec331aa6f1675e2319e921c/sdk/java/src/main/java/io/juicefs/utils/AclTransformation.java#L213-L249","documentation":"calculateMasks validates the effective-mask invariant: whenever the ACL contains maskable entries (named users/groups, group mask) a mask entry is required. This is thrown when the caller's operation would delete the mask in a scope that needs one without supplying a replacement — POSIX ACL semantics forbid a maskless ACL with maskable entries.","triggerScenarios":"filterAclEntriesByAclSpec/replaceAclEntries with a spec that removes the mask entry while named user/group entries remain; merging in a named entry but the caller-supplied spec explicitly dropped 'mask::'.","commonSituations":"Filtering an ACL with 'mask::' in the spec to 'clean up'; hand-writing a replace spec omitting mask; tools migrating ACLs that dropped mask entries.","solutions":["Keep or re-add the mask entry (e.g. 'mask::rwx' or the desired effective mask) in the spec.","If the mask should be recomputed automatically, don't mention mask in the filter/replace spec at all — let calculateMasks generate it.","Remove the maskable named entries too, if the intent was to drop everything needing a mask."],"exampleFix":"// before\nfilterAclEntriesByAclSpec(path, \"mask::\"); // deletes required mask\n// after\nfilterAclEntriesByAclSpec(path, \"user:alice:\"); // remove a named entry; mask is regenerated\n// or explicitly\nreplaceAclEntries(path, \"user::rwx,user:alice:rwx,group::r-x,mask::rwx,other::r--\");","handlingStrategy":"validation","validationCode":"if has_maskable_entries(entries) and mask_entry_missing(resulting_entries):\n    raise ValueError(\"spec would drop the required mask; add mask:: or remove mask from the spec\")","typeGuard":null,"tryCatchPattern":"try {\n  filterAclEntriesByAclSpec(path, spec);\n} catch (AclException e) {\n  if (e.getMessage().contains(\"mask is required\")) {\n    spec = removeMaskFromSpec(spec); // let the lib recompute the mask\n    filterAclEntriesByAclSpec(path, spec);\n  } else throw e;\n}","preventionTips":["Never put mask:: in a filter/remove spec; let the library recalculate it.","When hand-writing replace specs, always include a mask:: entry if named users/groups exist.","Recompute the mask explicitly (mask::rwx) after adding maskable entries."],"tags":["acl","java","mask"],"backgroundTag":"invalid-state-transition","analyzedSha":"c9a67b23e8e08ec23ec331aa6f1675e2319e921c","analyzedAt":"2026-09-06T17:55:48.476Z","contentChangedAt":"2026-09-06T17:55:48.476Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}