-
Notifications
You must be signed in to change notification settings - Fork 68
feat(rls): add owner-write RLS flow and move dashboard RLS config t… #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d1a30ab
c7723f4
487a3b0
864904e
7fd6ace
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -25,6 +25,27 @@ const validateUsersSchema = (schema) => { | |||||||||||||||||||
| return !!(hasEmail && hasPassword); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const getDefaultRlsForCollection = (collectionName, schema = []) => { | ||||||||||||||||||||
| const normalizedName = String(collectionName || '').toLowerCase(); | ||||||||||||||||||||
| const keys = Array.isArray(schema) ? schema.map(f => f?.key).filter(Boolean) : []; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let ownerField = 'userId'; | ||||||||||||||||||||
| if (normalizedName === 'users') { | ||||||||||||||||||||
| ownerField = '_id'; | ||||||||||||||||||||
| } else if (keys.includes('userId')) { | ||||||||||||||||||||
| ownerField = 'userId'; | ||||||||||||||||||||
| } else if (keys.includes('ownerId')) { | ||||||||||||||||||||
| ownerField = 'ownerId'; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return { | ||||||||||||||||||||
| enabled: false, | ||||||||||||||||||||
| mode: 'owner-write-only', | ||||||||||||||||||||
| ownerField, | ||||||||||||||||||||
| requireAuthForWrite: true | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -124,10 +145,19 @@ module.exports.getSingleProject = async (req, res) => { | |||||||||||||||||||
| if (col.name === 'users' && col.model) { | ||||||||||||||||||||
| return { | ||||||||||||||||||||
| ...col, | ||||||||||||||||||||
| model: col.model.filter(m => m.key !== 'password') | ||||||||||||||||||||
| model: col.model.filter(m => m.key !== 'password'), | ||||||||||||||||||||
| rls: col.rls || { | ||||||||||||||||||||
| enabled: false, | ||||||||||||||||||||
| mode: 'owner-write-only', | ||||||||||||||||||||
| ownerField: '_id', | ||||||||||||||||||||
| requireAuthForWrite: true | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return col; | ||||||||||||||||||||
| return { | ||||||||||||||||||||
| ...col, | ||||||||||||||||||||
| rls: col.rls || getDefaultRlsForCollection(col.name, col.model) | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
Comment on lines
145
to
+160
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's significant code duplication here for setting default RLS configurations. This logic is also slightly different from the You can refactor this to remove duplication and reuse the helper function, making the code more maintainable. const sanitizedCol = { ...col };
if (sanitizedCol.name === 'users' && sanitizedCol.model) {
sanitizedCol.model = sanitizedCol.model.filter(m => m.key !== 'password');
}
sanitizedCol.rls = sanitizedCol.rls || getDefaultRlsForCollection(sanitizedCol.name, sanitizedCol.model);
return sanitizedCol; |
||||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -324,7 +354,11 @@ module.exports.createCollection = async (req, res) => { | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| project.collections.push({ name: collectionName, model: schema }); | ||||||||||||||||||||
| project.collections.push({ | ||||||||||||||||||||
| name: collectionName, | ||||||||||||||||||||
| model: schema, | ||||||||||||||||||||
| rls: getDefaultRlsForCollection(collectionName, schema) | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| await project.save(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| await deleteProjectById(projectId); | ||||||||||||||||||||
|
|
@@ -930,10 +964,19 @@ module.exports.toggleAuth = async (req, res) => { | |||||||||||||||||||
| if (col.name === 'users' && col.model) { | ||||||||||||||||||||
| return { | ||||||||||||||||||||
| ...col, | ||||||||||||||||||||
| model: col.model.filter(m => m.key !== 'password') | ||||||||||||||||||||
| model: col.model.filter(m => m.key !== 'password'), | ||||||||||||||||||||
| rls: col.rls || { | ||||||||||||||||||||
| enabled: false, | ||||||||||||||||||||
| mode: 'owner-write-only', | ||||||||||||||||||||
| ownerField: '_id', | ||||||||||||||||||||
| requireAuthForWrite: true | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return col; | ||||||||||||||||||||
| return { | ||||||||||||||||||||
| ...col, | ||||||||||||||||||||
| rls: col.rls || getDefaultRlsForCollection(col.name, col.model) | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
Comment on lines
964
to
+979
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to const sanitizedCol = { ...col };
if (sanitizedCol.name === 'users' && sanitizedCol.model) {
sanitizedCol.model = sanitizedCol.model.filter(m => m.key !== 'password');
}
sanitizedCol.rls = sanitizedCol.rls || getDefaultRlsForCollection(sanitizedCol.name, sanitizedCol.model);
return sanitizedCol; |
||||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -945,4 +988,66 @@ module.exports.toggleAuth = async (req, res) => { | |||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||
| res.status(500).json({ error: err.message }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // PATCH REQ - UPDATE COLLECTION RLS (V1) | ||||||||||||||||||||
| module.exports.updateCollectionRls = async (req, res) => { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const { projectId, collectionName } = req.params; | ||||||||||||||||||||
| const { enabled, mode, ownerField, requireAuthForWrite } = req.body || {}; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const project = await Project.findOne({ _id: projectId, owner: req.user._id }); | ||||||||||||||||||||
| if (!project) return res.status(404).json({ error: "Project not found" }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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 modelKeys = (collection.model || []).map(f => f.key); | ||||||||||||||||||||
| const nextOwnerField = ownerField || collection?.rls?.ownerField || 'userId'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
||||||||||||||||||||
| // Restrict use of '_id' as ownerField to the 'users' collection only. | |
| if (nextOwnerField === '_id' && collection.name !== 'users') { | |
| return res.status(400).json({ | |
| error: "Invalid owner field", | |
| message: "ownerField '_id' is only allowed for the 'users' collection" | |
| }); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
else ifblock is redundant becauseownerFieldis already initialized to'userId'. You can safely remove these lines to simplify the code and improve readability.