diff --git a/README.md b/README.md index cbd8e0de..4df7edb3 100644 --- a/README.md +++ b/README.md @@ -92,14 +92,15 @@ Understanding which key to use—and when—prevents the most common integration ## 🛡️ Row-Level Security (RLS) -RLS lets you safely allow frontend clients to write data without exposing your secret key. When enabled on a collection, `pk_live` writes are gated by user ownership. +RLS lets you safely allow frontend clients to write data without exposing your secret key. When enabled on a collection, `pk_live` writes are gated by user ownership and reads can be scoped by mode. **How it works:** -1. Enable RLS for a collection in the Dashboard (mode: `owner-write-only`). -2. Choose the **owner field** — the document field that stores the authenticated user's ID (e.g., `userId`). -3. The client must send a valid user JWT in the `Authorization: Bearer ` header. -4. urBackend enforces that the JWT's `userId` matches the document's owner field. +1. Enable RLS for a collection in the Dashboard and choose a mode. +2. Use `public-read` for content anyone can view, or `private` for owner-only access. (`owner-write-only` is treated as `public-read` for legacy projects.) +3. Choose the **owner field** — the document field that stores the authenticated user's ID (e.g., `userId`). +4. The client must send a valid user JWT in the `Authorization: Bearer ` header for `pk_live` writes and for `private` reads. +5. urBackend enforces that the JWT's `userId` matches the document's owner field. **Example — user creates a post:** diff --git a/apps/dashboard-api/src/controllers/project.controller.js b/apps/dashboard-api/src/controllers/project.controller.js index bfdca16b..6bcd757b 100644 --- a/apps/dashboard-api/src/controllers/project.controller.js +++ b/apps/dashboard-api/src/controllers/project.controller.js @@ -120,7 +120,7 @@ const getDefaultRlsForCollection = (collectionName, schema = []) => { return { enabled: false, - mode: "owner-write-only", + mode: "public-read", ownerField, requireAuthForWrite: true, }; @@ -1305,9 +1305,10 @@ module.exports.updateCollectionRls = async (req, res) => { const collection = project.collections.find(c => c.name === collectionName); if (!collection) return res.status(404).json({ error: "Collection not found" }); - const validMode = mode || collection?.rls?.mode || 'owner-write-only'; - if (validMode !== 'owner-write-only') { - return res.status(400).json({ error: "Unsupported RLS mode. Only 'owner-write-only' is allowed in V1." }); + const validMode = mode || collection?.rls?.mode || 'public-read'; + const allowedModes = new Set(['public-read', 'private', 'owner-write-only']); + if (!allowedModes.has(validMode)) { + return res.status(400).json({ error: "Unsupported RLS mode. Allowed: public-read, private, owner-write-only (legacy)." }); } const modelKeys = (collection.model || []) @@ -1363,4 +1364,4 @@ module.exports.updateCollectionRls = async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } -} \ No newline at end of file +} diff --git a/apps/public-api/src/__tests__/authorizeReadOperation.test.js b/apps/public-api/src/__tests__/authorizeReadOperation.test.js new file mode 100644 index 00000000..6f9a8bd7 --- /dev/null +++ b/apps/public-api/src/__tests__/authorizeReadOperation.test.js @@ -0,0 +1,141 @@ +'use strict'; + +const authorizeReadOperation = require('../middlewares/authorizeReadOperation'); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeProject(rlsOverrides = {}) { + return { + _id: 'proj_1', + collections: [ + { + name: 'posts', + rls: { + enabled: true, + mode: 'public-read', + ownerField: 'userId', + requireAuthForWrite: true, + ...rlsOverrides, + }, + }, + ], + }; +} + +function makeReq(overrides = {}) { + return { + keyRole: 'publishable', + params: { collectionName: 'posts' }, + project: makeProject(), + authUser: null, + rlsFilter: undefined, + ...overrides, + }; +} + +function makeRes() { + const res = { + statusCode: null, + body: null, + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + res.status.mockImplementation((code) => { + res.statusCode = code; + return res; + }); + res.json.mockImplementation((data) => { + res.body = data; + return res; + }); + return res; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('authorizeReadOperation middleware', () => { + let next; + + beforeEach(() => { + next = jest.fn(); + }); + + test('secret key bypass sets empty filter', async () => { + const req = makeReq({ keyRole: 'secret' }); + const res = makeRes(); + + await authorizeReadOperation(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + expect(req.rlsFilter).toEqual({}); + }); + + test('returns 404 when collection is not found', async () => { + const req = makeReq({ params: { collectionName: 'unknown' } }); + const res = makeRes(); + + await authorizeReadOperation(req, res, next); + + expect(res.statusCode).toBe(404); + expect(res.body.error).toBe('Collection not found'); + expect(next).not.toHaveBeenCalled(); + }); + + test('rls disabled allows public read', async () => { + const req = makeReq({ project: makeProject({ enabled: false }) }); + const res = makeRes(); + + await authorizeReadOperation(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + expect(req.rlsFilter).toEqual({}); + }); + + test('public-read allows read without auth', async () => { + const req = makeReq({ authUser: null }); + const res = makeRes(); + + await authorizeReadOperation(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + expect(req.rlsFilter).toEqual({}); + }); + + test('owner-write-only is treated as public-read', async () => { + const req = makeReq({ project: makeProject({ mode: 'owner-write-only' }) }); + const res = makeRes(); + + await authorizeReadOperation(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + expect(req.rlsFilter).toEqual({}); + }); + + test('private mode requires auth', async () => { + const req = makeReq({ project: makeProject({ mode: 'private' }) }); + const res = makeRes(); + + await authorizeReadOperation(req, res, next); + + expect(res.statusCode).toBe(401); + expect(res.body.error).toBe('Authentication required'); + expect(next).not.toHaveBeenCalled(); + }); + + test('private mode sets owner filter when authed', async () => { + const req = makeReq({ + project: makeProject({ mode: 'private', ownerField: 'userId' }), + authUser: { userId: 'user_abc' }, + }); + const res = makeRes(); + + await authorizeReadOperation(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + expect(req.rlsFilter).toEqual({ userId: 'user_abc' }); + }); +}); diff --git a/apps/public-api/src/__tests__/data.controller.read.test.js b/apps/public-api/src/__tests__/data.controller.read.test.js new file mode 100644 index 00000000..eafd9d35 --- /dev/null +++ b/apps/public-api/src/__tests__/data.controller.read.test.js @@ -0,0 +1,104 @@ +'use strict'; + +const mockFind = jest.fn(); +const mockAnd = jest.fn(); +const mockFindOne = jest.fn(); +const mockQueryLean = jest.fn().mockResolvedValue([]); + +// mockAnd returns an object with .lean() so features.query.lean() works after .and() +mockAnd.mockReturnValue({ lean: mockQueryLean }); + +const mockQueryEngine = jest.fn((query) => { + const engine = { + query, + filter() { return engine; }, + sort() { return engine; }, + paginate() { return engine; }, + }; + return engine; +}); + +jest.mock('@urbackend/common', () => ({ + sanitize: (v) => v, + Project: {}, + getConnection: jest.fn().mockResolvedValue({}), + getCompiledModel: jest.fn(() => ({ + find: (...args) => { + mockFind(...args); + return { and: mockAnd, lean: mockQueryLean }; + }, + findOne: (...args) => { + mockFindOne(...args); + return { lean: jest.fn().mockResolvedValue({ _id: 'doc_1' }) }; + }, + })), + QueryEngine: mockQueryEngine, + validateData: jest.fn(), + validateUpdateData: jest.fn(), +})); + +const { getAllData, getSingleDoc } = require('../controllers/data.controller'); + +function makeReq(overrides = {}) { + return { + params: { collectionName: 'posts', id: '507f1f77bcf86cd799439011' }, + project: { + _id: 'proj_1', + resources: { db: { isExternal: false } }, + collections: [{ name: 'posts', model: [] }], + }, + query: {}, + rlsFilter: {}, + ...overrides, + }; +} + +function makeRes() { + const res = { + statusCode: null, + body: null, + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + res.status.mockImplementation((code) => { + res.statusCode = code; + return res; + }); + res.json.mockImplementation((data) => { + res.body = data; + return res; + }); + return res; +} + +describe('data.controller read RLS filters', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('getAllData applies rlsFilter to find()', async () => { + const req = makeReq({ rlsFilter: { userId: 'user_1' } }); + const res = makeRes(); + + await getAllData(req, res); + + expect(mockFind).toHaveBeenCalledWith(); + expect(mockAnd).toHaveBeenCalledWith([{ userId: 'user_1' }]); + expect(res.json).toHaveBeenCalled(); + }); + + test('getSingleDoc applies rlsFilter to findOne()', async () => { + const req = makeReq({ rlsFilter: { userId: 'user_1' } }); + const res = makeRes(); + + await getSingleDoc(req, res); + + expect(mockFindOne).toHaveBeenCalledWith({ + $and: [ + { _id: '507f1f77bcf86cd799439011' }, + { userId: 'user_1' }, + ], + }); + expect(res.json).toHaveBeenCalled(); + }); +}); diff --git a/apps/public-api/src/controllers/data.controller.js b/apps/public-api/src/controllers/data.controller.js index 99bdcfa0..4bdb70ad 100644 --- a/apps/public-api/src/controllers/data.controller.js +++ b/apps/public-api/src/controllers/data.controller.js @@ -102,8 +102,15 @@ module.exports.getAllData = async (req, res) => { project.resources.db.isExternal, ); + const baseFilter = req.rlsFilter && typeof req.rlsFilter === 'object' ? req.rlsFilter : {}; const features = new QueryEngine(Model.find(), req.query) - .filter() + .filter(); + + if (Object.keys(baseFilter).length > 0) { + features.query = features.query.and([baseFilter]); + } + + features .sort() .paginate(); @@ -140,7 +147,8 @@ module.exports.getSingleDoc = async (req, res) => { project.resources.db.isExternal, ); - const doc = await Model.findById(id).lean(); + const baseFilter = req.rlsFilter && typeof req.rlsFilter === 'object' ? req.rlsFilter : {}; + const doc = await Model.findOne({ $and: [{ _id: id }, baseFilter] }).lean(); if (!doc) return res.status(404).json({ error: "Document not found." }); res.json(doc); diff --git a/apps/public-api/src/middlewares/authorizeReadOperation.js b/apps/public-api/src/middlewares/authorizeReadOperation.js new file mode 100644 index 00000000..0554887a --- /dev/null +++ b/apps/public-api/src/middlewares/authorizeReadOperation.js @@ -0,0 +1,47 @@ +module.exports = async (req, res, next) => { + try { + if (req.keyRole === 'secret') { + req.rlsFilter = {}; + return next(); + } + + const { collectionName } = req.params; + const project = req.project; + const collectionConfig = project.collections.find(c => c.name === collectionName); + + if (!collectionConfig) { + return res.status(404).json({ error: 'Collection not found' }); + } + + const rls = collectionConfig.rls || {}; + if (!rls.enabled) { + req.rlsFilter = {}; + return next(); + } + + const modeRaw = rls.mode || 'public-read'; + const mode = modeRaw === 'owner-write-only' ? 'public-read' : modeRaw; + + if (mode === 'private') { + if (!req.authUser?.userId) { + return res.status(401).json({ + error: 'Authentication required', + message: 'Provide a valid user Bearer token for private reads.' + }); + } + + const ownerField = rls.ownerField || 'userId'; + req.rlsFilter = { [ownerField]: req.authUser.userId }; + return next(); + } + + if (mode === 'public-read') { + req.rlsFilter = {}; + return next(); + } + + return res.status(403).json({ error: 'Unsupported RLS mode' }); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}; diff --git a/apps/public-api/src/middlewares/authorizeWriteOperation.js b/apps/public-api/src/middlewares/authorizeWriteOperation.js index 7bb8c748..d6069ab7 100644 --- a/apps/public-api/src/middlewares/authorizeWriteOperation.js +++ b/apps/public-api/src/middlewares/authorizeWriteOperation.js @@ -31,7 +31,9 @@ module.exports = async (req, res, next) => { }); } - if ((rls.mode || 'owner-write-only') !== 'owner-write-only') { + const modeRaw = rls.mode || 'public-read'; + const allowedModes = new Set(['public-read', 'private', 'owner-write-only']); + if (!allowedModes.has(modeRaw)) { return res.status(403).json({ error: 'Unsupported RLS mode' }); } diff --git a/apps/public-api/src/routes/data.js b/apps/public-api/src/routes/data.js index 07a0655b..a67b9a12 100644 --- a/apps/public-api/src/routes/data.js +++ b/apps/public-api/src/routes/data.js @@ -3,6 +3,7 @@ const router = express.Router(); const verifyApiKey = require('../middlewares/verifyApiKey'); const resolvePublicAuthContext = require('../middlewares/resolvePublicAuthContext'); const authorizeWriteOperation = require('../middlewares/authorizeWriteOperation'); +const authorizeReadOperation = require('../middlewares/authorizeReadOperation'); const projectRateLimiter = require('../middlewares/projectRateLimiter'); const blockUsersCollectionDataAccess = require('../middlewares/blockUsersCollectionDataAccess'); const { insertData, getAllData, getSingleDoc, updateSingleData, deleteSingleDoc } = require("../controllers/data.controller") @@ -13,11 +14,11 @@ router.post('/:collectionName', verifyApiKey, blockUsersCollectionDataAccess, re // GET REQ ALL DATA -router.get('/:collectionName', verifyApiKey, blockUsersCollectionDataAccess, projectRateLimiter, getAllData); +router.get('/:collectionName', verifyApiKey, blockUsersCollectionDataAccess, resolvePublicAuthContext, projectRateLimiter, authorizeReadOperation, getAllData); // GET REQ SINGLE DATA -router.get('/:collectionName/:id', verifyApiKey, blockUsersCollectionDataAccess, projectRateLimiter, getSingleDoc); +router.get('/:collectionName/:id', verifyApiKey, blockUsersCollectionDataAccess, resolvePublicAuthContext, projectRateLimiter, authorizeReadOperation, getSingleDoc); // DELETE REQ SINGLE DATA diff --git a/apps/web-dashboard/src/pages/Database.jsx b/apps/web-dashboard/src/pages/Database.jsx index 04a5cd06..85efdbc3 100644 --- a/apps/web-dashboard/src/pages/Database.jsx +++ b/apps/web-dashboard/src/pages/Database.jsx @@ -59,6 +59,7 @@ export default function Database() { }); const [showFilterMenu, setShowFilterMenu] = useState(false); const [rlsEnabled, setRlsEnabled] = useState(false); + const [rlsMode, setRlsMode] = useState("public-read"); const [rlsOwnerField, setRlsOwnerField] = useState("userId"); const [isSavingRls, setIsSavingRls] = useState(false); const [isRlsDialogOpen, setIsRlsDialogOpen] = useState(false); @@ -109,13 +110,14 @@ export default function Database() { } else { derivedOwnerField = schemaKeys[0] || ''; } + const resolvedMode = c.rls?.mode === 'owner-write-only' ? 'public-read' : (c.rls?.mode || 'public-read'); return { ...c, - rls: c.rls || { - enabled: false, - mode: "owner-write-only", - ownerField: derivedOwnerField, - requireAuthForWrite: true + rls: { + enabled: typeof c.rls?.enabled === 'boolean' ? c.rls.enabled : false, + mode: resolvedMode, + ownerField: c.rls?.ownerField || derivedOwnerField, + requireAuthForWrite: typeof c.rls?.requireAuthForWrite === 'boolean' ? c.rls.requireAuthForWrite : true } }; }); @@ -191,7 +193,11 @@ export default function Database() { const fallbackOwner = activeCollection.name === 'users' ? '_id' : (modelKeys.includes('userId') ? 'userId' : (modelKeys.includes('ownerId') ? 'ownerId' : 'userId')); + const resolvedMode = activeCollection?.rls?.mode === 'owner-write-only' + ? 'public-read' + : (activeCollection?.rls?.mode || 'public-read'); setRlsEnabled(!!activeCollection?.rls?.enabled); + setRlsMode(resolvedMode); setRlsOwnerField(activeCollection?.rls?.ownerField || fallbackOwner); }, [activeCollection]); @@ -203,7 +209,7 @@ export default function Database() { `/api/projects/${projectId}/collections/${activeCollection.name}/rls`, { enabled: rlsEnabled, - mode: "owner-write-only", + mode: rlsMode, ownerField: rlsOwnerField, requireAuthForWrite: true } @@ -211,7 +217,7 @@ export default function Database() { const updatedRls = res.data?.collection?.rls || { enabled: rlsEnabled, - mode: "owner-write-only", + mode: rlsMode, ownerField: rlsOwnerField, requireAuthForWrite: true }; @@ -766,7 +772,7 @@ export default function Database() {

- Collection: {activeCollection.name} | Mode: owner-write-only + Collection: {activeCollection.name} | Mode: {rlsMode}

+

+ {rlsMode === "private" + ? "When enabled, publishable-key access is restricted to the owner for both reads and writes." + : "When enabled, publishable-key writes are restricted to the owner. Reads remain available according to the selected access mode."} +

+ +
+ + +

+ Use public-read for posts/blogs that should be readable by anyone. Use private for personal settings and user-owned data. +

+
diff --git a/docs/database.md b/docs/database.md index e2c9b1d7..2a76e9c5 100644 --- a/docs/database.md +++ b/docs/database.md @@ -19,7 +19,7 @@ Replace `:collectionName` with the name of your collection (e.g., `posts`, `comm By default, write operations require your **secret key**. -If you enable **RLS (owner-write-only)** for a collection from the dashboard, publishable-key writes are also allowed but must include a valid user token: +If you enable **RLS** for a collection from the dashboard, publishable-key writes are also allowed but must include a valid user token: - `x-api-key: pk_live_...` - `Authorization: Bearer ` @@ -27,6 +27,9 @@ If you enable **RLS (owner-write-only)** for a collection from the dashboard, pu Under RLS, writes are permitted only for documents owned by the authenticated user (based on configured `ownerField`). If the `ownerField` is missing in a `POST` payload, urBackend can auto-fill it from the authenticated user id. +RLS read modes: +`public-read` lets anyone read, while `private` restricts reads to the owner's documents and requires a valid user token. + ```javascript await fetch('https://api.ub.bitbros.in/api/data/posts', { method: 'POST', diff --git a/docs/introduction.md b/docs/introduction.md index 14eb4d98..4a8432d6 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -40,9 +40,10 @@ Every project gets two keys with different trust levels. ## Row-Level Security (RLS) -RLS allows `pk_live` clients (e.g., a browser app) to write data while ensuring users can only modify their own documents. +RLS allows `pk_live` clients (e.g., a browser app) to write data while ensuring users can only modify their own documents. It also controls read access when enabled. -**RLS is configured per collection** in the Dashboard. The current mode is `owner-write-only`. +**RLS is configured per collection** in the Dashboard. Modes: +`public-read` (anyone reads, owner writes) and `private` (owner reads and writes). `owner-write-only` is treated as `public-read` for legacy projects. ### How RLS enforces ownership @@ -54,6 +55,9 @@ When a `pk_live` request arrives for a write operation: 4. For **updates/deletes (PUT/PATCH/DELETE)**: The existing document's owner field is fetched and compared against the JWT's `userId`. Mismatches return `403`. 5. Attempts to change the owner field in a PATCH/PUT body are also rejected (`403 Owner field immutable`). +For reads: +`public-read` allows anyone to read, while `private` requires a valid user token and restricts results to the owner's documents. + ### RLS behavior matrix | Request | Key | Token | Outcome | diff --git a/docs/rls-implementation-roadmap.md b/docs/rls-implementation-roadmap.md index f12b94a2..56dc1945 100644 --- a/docs/rls-implementation-roadmap.md +++ b/docs/rls-implementation-roadmap.md @@ -26,9 +26,12 @@ Implication: ### 1.1 What V1 Will Support For each collection, enable an RLS mode: -- `owner-write-only` policy: - - Publishable key writes are allowed only if authenticated end-user is writing their own document. +- `public-read` policy: + - Anyone can read. + - Publishable key writes are allowed only if the authenticated end-user is writing their own document. - Ownership is checked by a configured field (e.g. `userId`, `ownerId`, `_id` for users collection). +- `private` policy: + - Only the owner can read and write (requires a valid user token). Write ops covered in V1: - `POST` @@ -37,7 +40,8 @@ Write ops covered in V1: - `DELETE` Read behavior in V1: -- Keep existing behavior unchanged initially (to reduce risk). +- `public-read` keeps existing behavior for reads. +- `private` filters reads by owner. ### 1.2 Data Model Changes @@ -47,7 +51,7 @@ Suggested shape: ```js collection.rls = { enabled: Boolean, - mode: "owner-write-only", + mode: "public-read", ownerField: "userId", requireAuthForWrite: true } @@ -96,7 +100,7 @@ For `PUT/PATCH/DELETE`: In web dashboard: - Add collection-level RLS section: - Enable/disable RLS toggle. - - Mode selector (only `owner-write-only` in V1). + - Mode selector (`public-read` or `private` in V1). - Owner field selector (from schema fields). - Save config. @@ -242,7 +246,7 @@ Implementation approach: ### Regression checks - Existing dashboard admin flows unaffected. -- Read endpoints unaffected in V1. +- Public-read collections keep existing read behavior. --- @@ -276,7 +280,6 @@ Add request logs for denied writes with: To keep V1 low-risk: - No complex expression parser. -- No read-side row filtering yet. - No multi-policy conflict resolution engine. --- diff --git a/examples/social-demo/COLLECTIONS_GUIDE.md b/examples/social-demo/COLLECTIONS_GUIDE.md index e823335d..ada9abe2 100644 --- a/examples/social-demo/COLLECTIONS_GUIDE.md +++ b/examples/social-demo/COLLECTIONS_GUIDE.md @@ -63,7 +63,7 @@ createdAt | Date | No | Date.now **RLS settings for `posts` (required):** - enabled: `true` -- mode: `owner-write-only` +- mode: `public-read` - ownerField: `userId` - requireAuthForWrite: `true` @@ -93,7 +93,7 @@ updatedAt | Date | No | No | Date.now **RLS settings for `profiles` (required):** - enabled: `true` -- mode: `owner-write-only` +- mode: `public-read` - ownerField: `userId` - requireAuthForWrite: `true` @@ -119,7 +119,7 @@ createdAt | Date | No | Date.now **RLS settings for `comments` (required):** - enabled: `true` -- mode: `owner-write-only` +- mode: `public-read` - ownerField: `userId` - requireAuthForWrite: `true` @@ -142,7 +142,7 @@ createdAt | Date | No | Date.now **RLS settings for `likes` (required):** - enabled: `true` -- mode: `owner-write-only` +- mode: `public-read` - ownerField: `userId` - requireAuthForWrite: `true` @@ -164,7 +164,7 @@ createdAt | Date | No | Date.now **RLS settings for `follows` (required):** - enabled: `true` -- mode: `owner-write-only` +- mode: `public-read` - ownerField: `userId` - requireAuthForWrite: `true` diff --git a/examples/social-demo/README.md b/examples/social-demo/README.md index 2a9b9441..c392e44b 100644 --- a/examples/social-demo/README.md +++ b/examples/social-demo/README.md @@ -140,7 +140,7 @@ This demo is now **PK-first** and aligned with the latest public APIs: For each writable collection (`posts`, `comments`, `likes`, `follows`, `profiles`): - `enabled: true` -- `mode: owner-write-only` +- `mode: public-read` - `ownerField: userId` - `requireAuthForWrite: true` diff --git a/packages/common/src/models/Project.js b/packages/common/src/models/Project.js index 13312271..36a558e5 100644 --- a/packages/common/src/models/Project.js +++ b/packages/common/src/models/Project.js @@ -35,8 +35,8 @@ const collectionSchema = new mongoose.Schema({ enabled: { type: Boolean, default: false }, mode: { type: String, - enum: ["owner-write-only"], - default: "owner-write-only", + enum: ["public-read", "private", "owner-write-only"], + default: "public-read", }, ownerField: { type: String, default: "userId" }, requireAuthForWrite: { type: Boolean, default: true },