Securing your files and folders

Securing your files and folders

Contents

Security rules

Access to files and folders is secured via the File Security section in Fliplet Studio. Rules control who can read, create (upload), update, and delete files — enforced server-side for all app token requests. Rules can be configured through the Studio UI wizard without writing any code.

Key behaviors:

  • Denied by default — access is denied when no rules exist on a resource or any of its ancestors. Note that apps come with a preset rule granting public read and logged-in upload access, which can be changed in Studio
  • First-match wins — rules are evaluated top to bottom; the first rule that grants access is used and evaluation stops. If no rules match, access is denied
  • Inheritance chain — rules are resolved by walking up the resource hierarchy until rules are found:
    1. The file or folder’s own rules
    2. Parent folder → grandparent folder → … (up the folder tree)
    3. The app’s root media access rules (final fallback)
    4. No rules found anywhere → access denied

    A resource with its own rules completely replaces inherited rules (no merging). The system uses only the first set of rules it finds in this chain.

  • Action-based — rules specify which operations they permit: read, create, update, delete

Access rule structure

File security rules follow the same conventions as Data Source security rules, using the same allow schema, condition operators, and session handling. Each rule specifies who can access files and what operations they can perform.

Property Type Required Description
type Array of strings Yes Operations this rule applies to: "read", "create", "update", "delete". Note: "create" is only valid on folder rules (not file rules)
allow String or Object Yes Who can access: "all", "loggedIn", { "user": {...} }, { "tokens": [...] }, or { "dataSource": {...} }
enabled Boolean No Whether the rule is active (defaults to true)
appId Array of numbers No Restrict this rule to specific app IDs (applies to all apps if omitted)
name String No Descriptive label for identifying the rule in Studio
script String No Custom JavaScript code for advanced security logic. When present, overrides allow and type — see Custom security rules
stop Boolean No If true and the rule does not match the request, evaluation stops immediately (explicit deny). Defaults to false. See Explicit deny with stop

A maximum of 20 rules can be configured per file or folder.

Defining who can access

The allow property supports the same modes as Data Source rules. User filters support three operators (equals, notequals, contains) and can reference session data using Handlebars syntax:

[
  {
    "type": ["read", "create", "update", "delete"],
    "allow": { "user": { "Role": { "equals": "Admin" } } },
    "enabled": true
  },
  {
    "type": ["read"],
    "allow": {
      "user": {
        "Department": { "equals": "Engineering" },
        "Status": { "notequals": "Inactive" }
      }
    },
    "enabled": true
  },
  {
    "type": ["read", "create"],
    "allow": "loggedIn",
    "enabled": true
  }
]

To grant access to specific API tokens, use the tokens mode with an array of Fliplet API token IDs:

{
  "type": ["read"],
  "allow": { "tokens": [42857] },
  "enabled": true
}

For the full allow reference and Handlebars templating details, see Data Source security rules.

Explicit deny with stop

Normally, when a rule does not match the current request it is skipped and evaluation continues with the next rule. Setting stop: true on a rule changes this behavior: if the rule does not match, evaluation halts immediately and access is denied.

This lets you create an explicit deny — a rule that says “if you don’t pass this check, stop here; don’t even look at the remaining rules.”

Example: only non-suspended logged-in users should be able to read files, even though a broader loggedIn rule exists further down:

[
  {
    "type": ["read"],
    "allow": {
      "user": { "Status": { "notequals": "Suspended" } }
    },
    "stop": true,
    "enabled": true
  },
  {
    "type": ["read"],
    "allow": "loggedIn",
    "enabled": true
  }
]

A non-suspended user matches the first rule and is granted access. A suspended user fails the first rule — because stop is true, evaluation halts and access is denied before the second loggedIn rule is reached.

Note: The stop property is specific to file security rules. It is not available on Data Source security rules.

Data source ownership rules

File rules support an additional allow mode not available on Data Source rules: { "dataSource": {...} }. This grants access when a data source entry references the file and optionally matches user criteria. This mode is only available on file rules (not folder rules).

[
  {
    "type": ["read"],
    "allow": {
      "dataSource": {
        "id": 123,
        "fileColumn": "Attachments",
        "where": {
          "Owner": "{{user.[Email]}}"
        }
      }
    },
    "enabled": true
  }
]
Property Type Required Description
id Number Yes The data source ID to query
fileColumn String Yes The column name that contains file references (checked for the file’s ID via substring match)
where Object No Additional filter criteria applied to matching entries. Supports Handlebars templates for session data. If omitted, any entry referencing the file grants access

Full example: department document library

This example shows a folder structure where public files are accessible to everyone, department folders are restricted to logged-in users in the matching department, and only admins can upload or delete.

Folder structure:

/public/
  welcome.pdf
/engineering/
  architecture.pdf
  roadmap.xlsx
/marketing/
  brand-guide.pdf

Rules on the /public/ folder:

[
  {
    "type": ["read"],
    "allow": "all",
    "enabled": true
  }
]

Rules on the /engineering/ folder:

[
  {
    "type": ["read", "create", "update", "delete"],
    "allow": { "user": { "Role": { "equals": "Admin" } } },
    "enabled": true
  },
  {
    "type": ["read"],
    "allow": {
      "user": {
        "Department": { "equals": "Engineering" }
      }
    },
    "enabled": true
  }
]

The second rule uses a literal value ("Engineering") for the department check. Unlike Data Source rules which have require for query scoping, file security rules can only filter by the user’s identity via allow.user. Using a Handlebars self-reference like "equals": "{{user.[Department]}}" would compare the user’s department against itself — always true, granting access to any logged-in user regardless of department.

Rules on the /marketing/ folder would follow the same pattern with "Department": { "equals": "Marketing" }.

Access outcomes:

User Role Department /public/welcome.pdf read /engineering/roadmap.xlsx read /engineering/ upload
Anonymous Granted Denied Denied
Bob User Engineering Granted Granted Denied
Carol User Marketing Granted Denied Denied
Alice Admin Engineering Granted Granted Granted

Rules are evaluated top to bottom — the admin rule matches first for Alice, the department rule matches for Bob. Carol is denied because her department does not match the literal value “Engineering”. Files in /engineering/ inherit the folder’s rules.

API calls that succeed

Bob reading files from /engineering/ — Bob is logged in (Role: User, Department: Engineering). The second rule matches because his department equals “Engineering”:

// JS API — list files in the engineering folder
Fliplet.Media.Folders.get({ folderId: 2 }).then(function (response) {
  // response.files contains architecture.pdf and roadmap.xlsx
  console.log('Engineering files:', response.files);
});
// REST API equivalent
GET /v1/media?folderId=2
Auth-token: <Bob's app token>

// Response (200 OK):
{
  "files": [
    { "id": 10, "name": "architecture.pdf", "contentType": "application/pdf", ... },
    { "id": 11, "name": "roadmap.xlsx", "contentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ... }
  ],
  "folders": []
}

Alice uploading to /engineering/ — Alice is logged in (Role: Admin). The first rule matches because her role equals “Admin”, granting create access:

// JS API — upload a file to the engineering folder
var formData = new FormData();
formData.append('files[0]', fileInput.files[0]);

Fliplet.Media.Files.upload({
  data: formData,
  folderId: 2 // engineering folder
}).then(function (files) {
  console.log('Uploaded:', files);
});
// REST API equivalent
POST /v1/media/files?folderId=2
Auth-token: <Alice's app token>
Content-Type: multipart/form-data

// Response (201 Created):
{
  "files": [{ "id": 12, "name": "new-doc.pdf", ... }]
}

Anonymous reading from /public/ — No login required. The rule uses "allow": "all":

// JS API — list files in the public folder
Fliplet.Media.Folders.get({ folderId: 1 }).then(function (response) {
  // response.files contains welcome.pdf
});
// REST API — read file contents directly
GET /v1/media/files/9/contents/welcome.pdf
// No auth token needed — the rule grants public read access

// Response: file stream (200 OK)

API calls that fail

When a request is denied, the API responds with HTTP status 401 and a JSON error body (note: Data Source denials return HTTP 400 instead):

{
  "error": "file.access",
  "message": "You do not have permission to access this file"
}

In the JS API, the promise is rejected with this error.

Carol trying to read /engineering/ files — Carol is logged in (Role: User, Department: Marketing). The admin rule does not match (wrong role), and the department rule does not match (Marketing ≠ Engineering):

// JS API — Carol tries to read a file from engineering
Fliplet.Media.Folders.get({ folderId: 2 }).catch(function (error) {
  // error: "You do not have permission to access this file"
});
// REST API equivalent
GET /v1/media?folderId=2
Auth-token: <Carol's app token>

// Response (401 Unauthorized):
{ "error": "file.access", "message": "You do not have permission to access this file" }

Bob trying to upload to /engineering/ — Bob is logged in (Role: User, Department: Engineering). He can read files, but the only rule granting create access requires Role: Admin:

// JS API — Bob tries to upload
Fliplet.Media.Files.upload({
  data: formData,
  folderId: 2
}).catch(function (error) {
  // error: "You do not have permission to create files here"
});
// REST API equivalent
POST /v1/media/files?folderId=2
Auth-token: <Bob's app token>
Content-Type: multipart/form-data

// Response (401 Unauthorized):
{ "error": "file.access", "message": "You do not have permission to create files here" }

Anonymous trying to read /engineering/ files — No login. Neither rule matches: the admin rule requires a user session, and the department rule requires a logged-in user with a matching department:

// REST API
GET /v1/media/files/10/contents/architecture.pdf
// No auth token

// Response (401 Unauthorized):
{ "error": "file.access", "message": "You do not have permission to access this file" }

Private media files and app bundles

Files that do not have an unconditional public read rule (i.e., "allow": "all" for "read") are treated as private. This affects how files are included in app bundles:

  • Public files are included in the app bundle as normal and are available offline
  • Private files are excluded from the bundle — only metadata (file ID, name, content type) is included
  • At runtime, the app must make authenticated API requests to access private file contents (e.g., via Fliplet.Media.Files.get() or the /v1/media/files/:id/contents endpoint)
  • Private files are not available offline unless your app explicitly fetches and caches them at runtime

This partitioning ensures that secured files are never exposed in the publicly downloadable app bundle.

Custom security rules

For advanced logic beyond what the standard rule properties support, you can write custom JavaScript security rules. This follows the same model as custom Data Source security rules — the script is evaluated at runtime in a sandboxed environment.

Important: When a rule has a custom script, the script is the sole determinant of access. Standard rule fields like allow and type are ignored — the script runs regardless of login status or operation type. Your script must perform its own identity and operation checks (e.g., if (!user) return { granted: false };). If the script does not return a value (e.g., an unhandled operation type falls through without a return), access is denied by default.

When writing a custom rule, these variables are available in the script context:

  • type (String) — the operation being attempted: read, create, update, or delete
  • user (Object) — the authenticated user’s session data, or undefined if not logged in. For data-source passport sessions (e.g., Email Verification, Fliplet Login), this contains the flat column values from the user’s row in the authentication data source (e.g., user.Email, user.Role). For SAML2 sessions, it contains the assertion attributes.
  • file (Object) — the resource being accessed. This is the plain Sequelize model of either a file or a folder, depending on the operation. See The file object for available properties.
  • DataSources (Function) — server-side library for reading data from other data sources. See Reading data from other Data Sources.

Script execution has a 3-second timeout. Async operations (like DataSources queries) are supported via await.

Here is an example covering multiple scenarios:

// Check the operation type
if (type === 'read') {
  // Any logged-in user can read
  if (user) {
    return { granted: true };
  }

  return { granted: false, message: 'You must be logged in to access this file' };
}

if (type === 'create') {
  // Only editors can upload
  if (user && user.Role === 'Editor') {
    return { granted: true };
  }

  return { granted: false, message: 'Only editors can upload files' };
}

if (type === 'update' || type === 'delete') {
  // Only the original uploader can modify or delete.
  // file.userId is the Fliplet user ID of who uploaded the file.
  // user.id is the authenticated user's Fliplet user ID (not a data source column).
  if (user && file && file.userId === user.id) {
    return { granted: true };
  }

  return { granted: false, message: 'You can only modify files you uploaded' };
}

// Deny by default if none of the conditions above matched
return { granted: false };

Granting access

Return { granted: true } to allow access, or { granted: false } to deny. You can include a message when denying for debugging purposes (e.g., { granted: false, message: 'Only managers can access report files' }).

Important: You must return an object — bare boolean values (e.g., return true) are not supported and will be treated as a denial. Always use return { granted: true } or return { granted: false }.

The file object

The file variable in custom scripts contains the plain Sequelize model of the resource being accessed — either a file or a folder. The available properties depend on the resource type.

For files:

{
  id: 456,
  name: "team-photo.jpg",
  contentType: "image/jpeg",
  size: [640, 480],           // Image dimensions [width, height] — only populated for images
  mediaFolderId: 12,         // ID of the parent folder
  appId: 789,
  userId: 42,                // ID of the user who uploaded the file
  isEncrypted: false,
  metadata: {},
  accessRules: null,         // This file's own rules (if any)
  createdAt: "2025-06-15T10:30:00Z",
  updatedAt: "2025-06-15T10:30:00Z"
}

For folders (when the operation targets a folder, e.g., create for uploading into a folder):

{
  id: 12,
  name: "2025",
  parentId: 5,               // ID of the parent folder
  appId: 789,
  metadata: {},
  accessRules: null,
  createdAt: "2025-01-10T08:00:00Z",
  updatedAt: "2025-01-10T08:00:00Z"
}

Important: The file object does not include related records (folder details, app name, uploader email). To look up related data, use the DataSources library or match on the available IDs (userId, mediaFolderId, appId).

For create operations (file upload), file contains the metadata available from the upload request (e.g., name, contentType). Fields like id and createdAt are not yet available.

Restrict file types on upload

if (type === 'create') {
  var allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];

  if (file && allowedTypes.indexOf(file.contentType) === -1) {
    return { granted: false, message: 'Only JPEG, PNG, and PDF files are allowed' };
  }

  return { granted: true };
}

// This rule does not grant access to other operation types

Reading data from other Data Sources

Custom rules can query other data sources using the DataSources server-side library. The API is identical to data source custom rules — refer to that page for the full find and findOne reference, supported query operators, and usage examples.

These reads run at server level and bypass all security rules on the target data source. Connect using the data source ID (number) or name (string):

if (type === 'create') {
  if (!user) {
    return { granted: false, message: 'You must be logged in to upload files' };
  }

  var permission = await DataSources('Permissions').findOne({
    where: { Email: user.Email, CanUpload: 'Yes' }
  });

  if (permission) {
    return { granted: true };
  }

  return { granted: false, message: 'You do not have upload permissions' };
}

Use DataSources('Name') (data source name) instead of DataSources(123) (ID) in custom scripts. Data source names are preserved during app clone, so name-based lookups continue to work without manual remapping.

REST API endpoints

To manage access rules programmatically, see the Media access rules API for endpoints to get and set rules on files, folders, and apps.