conductor-oss/conductor · error · AccessForbiddenException

Workflow does not own file: {}

Error message

Workflow does not own file: {}

What it means

Thrown by getOwnedFile when the supplied workflowId is null/blank or does not equal the file record's owning workflowId. Upload-state mutations (confirmUpload, uploadContent, getUploadUrl, multipart ops) may only be performed by the workflow that created the file. Raised as AccessForbiddenException (HTTP 403).

Source

Thrown at core/src/main/java/org/conductoross/conductor/core/storage/FileStorageServiceImpl.java:232

        response.setFileHandleId(FileIdToFileHandleIdConverter.toFileHandleId(fileId));
        response.setUploadStatus(FileUploadStatus.UPLOADED);
        response.setContentHash(info.getContentHash());
        return response;
    }

    @Override
    public void abortMultipartUpload(String workflowId, String fileId, String uploadId) {
        FileModel model = getOwnedFile(workflowId, fileId);
        fileStorage.abortMultipartUpload(model.getStoragePath(), uploadId);
    }

    /** Upload state may only be mutated by the workflow that created the file record. */
    private @NonNull FileModel getOwnedFile(String workflowId, String fileId) {
        FileModel model = getFileModelOrThrow(fileId);
        if (workflowId == null
                || workflowId.isBlank()
                || !workflowId.equals(model.getWorkflowId())) {
            throw new AccessForbiddenException("Workflow does not own file: " + fileId);
        }
        return model;
    }

    /** Downloads and metadata are visible to the owning workflow's full workflow family. */
    private @NonNull FileModel getFamilyAccessibleFile(String workflowId, String fileId) {
        FileModel model = getFileModelOrThrow(fileId);
        if (model.getWorkflowId() == null || model.getWorkflowId().isBlank()) {
            throw new AccessForbiddenException("File has no workflowId: " + fileId);
        }

        Set<String> family = workflowFamilyResolver.getFamily(workflowId);
        if (!family.contains(model.getWorkflowId())) {
            throw new AccessForbiddenException("Workflow cannot access file: " + fileId);
        }
        return model;
    }

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Pass the exact workflowId that owns the file (the one used in createFile).
  2. For family-wide read/download use getDownloadUrl/downloadContent (family-accessible), not the write endpoints.
  3. If a child workflow must write, have the owner create the file and pass the handle down with its workflowId.

Example fix

// before - using the parent workflowId for a file owned by the child
fileStorageService.confirmUpload(parentWfId, fileId); // throws 403

// after - use the owning workflowId
fileStorageService.confirmUpload(ownerWorkflowId, fileId);
Defensive patterns

Strategy: validation

Validate before calling

FileHandle meta = fileStorageService.getFileMetadata(wfId, fileId);
boolean isOwner = wfId != null && !wfId.isBlank() && wfId.equals(meta.getWorkflowId());

Try / catch

try {
    fileStorageService.confirmUpload(ownerWfId, fileId);
} catch (AccessForbiddenException e) {
    // not the owner; cannot mutate
}

Prevention

When it happens

Trigger: Calling confirmUpload/getUploadUrl/uploadContent/multipart endpoints with a workflowId different from the file's owner; passing a blank workflowId; cross-workflow access to another workflow's file for a write operation.

Common situations: Sub-workflow attempting a write on a parent-owned file; client passing a stale or wrong workflow id; cross-tenant access attempt; misrouting a request to the wrong file.

Related errors


AI-assisted analysis of conductor-oss/conductor@cf7c3e4a8a (2026-08-14). Data as JSON: /api/errors/4b6b1a141cb49661. Report an issue: GitHub.