-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
WIP: File improvements #7088
WIP: File improvements #7088
Conversation
Codecov Report
@@ Coverage Diff @@
## master #7088 +/- ##
==========================================
- Coverage 93.66% 83.92% -9.75%
==========================================
Files 169 169
Lines 12499 12656 +157
==========================================
- Hits 11707 10621 -1086
- Misses 792 2035 +1243
Continue to review full report at Codecov.
|
classLevelPermissions: { | ||
find: { | ||
'*': true, | ||
}, |
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.
I think here we should disable the find permission for security reasons because not every developers will take the time to ensure that the parent object acls are transfered on the file ACL.
Current configuration will be less secure than the current one if no ACL configured. May be we should add a default behavior: Parse.File automatically add ACL from parent objet to itself.
This way Parse.File will be only reachable based on the parent object.
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.
Good call!
src/Controllers/FilesController.js
Outdated
const references = fileObject.references || []; | ||
if (!references.includes({ objectId: object.objectId, className })) { | ||
references.push({ objectId: object.objectId, className }); | ||
toSave = true; | ||
} |
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.
If i understand, reference is an Array of Pointer. Here it could be a bottle neck if a file has too many references :)
(Like a admin upload an avatar, and all the user base use it). May be reference could be only a simple native DB counter (transaction activated) to keep how many objects use it.
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.
Also references update should be ACID, otherwise we could start to have invalid reference
count/array.
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.
I have updated this on the latest revision, pls let me know if I understood you correctly. Thank you so much for you feedback
src/Controllers/FilesController.js
Outdated
if (url.length > 1) { | ||
appendToken = `${url[1]}&`; | ||
} | ||
const token = randomString(25); | ||
appendToken += `token=${token}`; | ||
file.url = `${url[0]}?${appendToken}`; | ||
const expiry = new Date(new Date().getTime() + 30 * 60000); | ||
tokens.push({ | ||
token, | ||
expiry, | ||
user: auth.user, | ||
}); | ||
toSave = true; | ||
} |
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.
i'm not sure to understand why we have tokens here, are you open to create a quick mind map on how you see the new File system implementation business logic ?
Free download and free to use, useful for code architecture: https://www.xmind.net/fr/xmind2020/ 😄
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.
If we use token may be we should prefer a stateless system like jsonwebtoken
with a derived hash of the master key as the secret.
(I use this kind of system for the webauthn JWT), it will avoid token management into our code.
const getJwtSecret = config => {
const hash = crypto.createHash('sha512');
hash.update(config.masterKey, 'utf-8');
// Security:
// sha512 return 128 chars, we can keep only 64 chars since it represent 6,61E98 combinations
// using the hash allow to reduce risk of compromising the master key
// if brute force is attempted on the JWT
return hash.digest().toString('hex').slice(64);
};
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.
So I am thinking:
If a Parse.Object is fetched with a Parse.File in it, we want to:
- Check if the
req.user
has correct ACL to viewFile
- If they do, attach a "token" to the
req.file.url
. Create a DB reference that linksreq.user
with the token, with an expiry time. - When there is a
GET
request on the file, lookup the Token. If no token exists, throw file not found, as no Parse.Object has been inflated with a Parse.File. - Call beforeFind on Parse.File using
token.user
(saved from step 2, so that the file trigger can know the auth) - If beforeFind throws, return an error. Otherwise, res.end with the data.
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.
Check if the req.user has correct ACL to view File
When there is a GET request on the file
Here i see 2 things:
- Security during Parse.Query operation, managed by classic database controller permission check
- Before returning the file url, create a JWT with real file url signed by a derived masterkey, replace the real url with JWT url
- The browser will use the temporary jwt file name ex: https://my-parse.com/file/HereTheJWT
- Parse decode the JWT, get the real file URL, then stream the real file content into response
Here we do not need to re-check the user permission on browser GET request since the JWT could be super short lived (1-2min), and will be always generated ONLY if user has permission to get the object file.
Here a limitation of token system and also jwt file url: It will disable the file browser cache.
But currently it's not suitable to force User to set up a Parse Session Token cookie.
Note: If a file object is PUBLICLY READABLE, do not generate the JWT and return the url directly. It will allow developers to get file without initiating a query. (ex: avatar account choice at login time).
What do you think about that ? @dblythy
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.
The only problem with that is attaching request.user
in the new beforeFind(Parse.File)
trigger. I understand what you mean by the JWT token, but if we're going to have a beforeFind(Parse.File)
trigger run before the GET request, it would be nice to attach which user / masterKey the token is associated with, even for 'public' files (req.user == null
)
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.
I have added a broader explanation in the comments below, let me know any thoughts
Ok so I hope this helps @Moumouls. Here is a conceptual explanation of the code. There are two fundamental features: File Tokens, and File references. Let's begin with File Tokens. There is no way to know who performed a get request on a URL. How this code gets around that:
Ok, so now there is a link with a database store and a file object. Now, after an object is queried and a file is found, we need to provide a token to connect the user who made the query with the user who will ultimately use that object to perform a GET request on file.url. Let's assume that
Creating tokens happens for "public" files too, so cloud code can have full control of which users called what via File References As pointers have to all be of the same class, I have created _FileReferences. Simply, when an object is mutated or saved, the code loops through and looks for files in If query a Parse.Object with a file but the _File references relation doesn't contain the reference to the Parse.Object, add it in. This is so over time the references grow with existing datasets. |
Thanks @dblythy for the explanation. Here we are sure of one thing: we need token in file url. If we use token (DB or JWT) with temporary access (1-2min) generated at query response time (like: Then on the file GET route (triggered by the browser) we only need to validate the token (since permission was already checked and beforeFind triggered at query time) Also i'm pretty sure that having token for public files will lead to some issue for developers who transmits URLs to other services (documents, avatars, etc...). Public file should be always accessible without a token to avoid blocking developers on their usage. Here a quick xmind on how i see the implementation currently: Tell me @dblythy if you see something strange or if i miss something 😉 |
So the I like this approach. We could also attach The only caveats:
|
In the new implementation since we will use Line 808 in 05f5aa0
Here i'm sure that we need to avoid a too complex implementation and use all systems already present in Parse Server for all other classes as much as possible 😃 The only custom thing that we need to implement correctly is the token system. 💯
Not sure to totally understand this one
Do you have a quick example ? |
Closing for now but will come back around to build this feature in the future. |
New Pull Request Checklist
Issue Description
Works towards adding:
-ACL for Parse.File using temporary tokens
-Allows
beforeSave(Parse.File)
andafterSave(Parse.File)
-Creates
beforeFind(Parse.File)
andafterFind(Parse.File)
to allow editing the file contents depending on the request (you can return a different Parse.File if you'd like). Uses temporary tokens to associatereq.user
.-Creates _File class for references -> adds and removes references on object save. Eventually will be used to cleanup "orphan" files
-Created _File schema to allow preventing file upload via schema
Draft PR to see your thoughts, and might need collaboration in fixing the schema stuff that is commented (@Moumouls if you're interested in helping 🎉)
Related issue: #6780
TODOs before merging