-
Notifications
You must be signed in to change notification settings - Fork 2
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
Feat/is 21 repo privatisation #806
Conversation
94cb9f0
to
478aca0
Compare
queryInterface.addColumn( | ||
"deployments", // name of Source model | ||
"encryption_iv", // name of column we're adding | ||
{ | ||
type: Sequelize.TEXT, | ||
allowNull: true, | ||
transaction: t, | ||
} | ||
), | ||
queryInterface.addColumn( | ||
"deployments", // name of Source model | ||
"password_date", // name of column we're adding | ||
{ | ||
type: Sequelize.DATE, | ||
allowNull: true, | ||
transaction: t, | ||
} |
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.
could i get more information/context on what encryption_iv + password_date
implies? i was under the impression that we'd only need encrypted_password
?
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.
encryption_iv
is the initialisation vector used for encryption - this is needed so that similar patterns of text when encrypted don't give rise to similar ciphertexts! This information needs to be stored to retrieve the original unencrypted value
password_date
is an additional field which will be helpful for a future feature - we eventually want to be able to tell when a repo password has not been changed for a certain amount of time, so this is useful for that, though it's currently unused
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.
some comments; also, should we update 1pw env vars?
const deploymentInfo = await this.deploymentsService.getDeploymentInfoFromSiteId( | ||
id | ||
) | ||
if (deploymentInfo.isErr()) return deploymentInfo |
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.
why not just throw here rather than returning and throwing? also, can't help but feel that using neverthrow
in .js
files is asking for trouble .-.
@@ -77,11 +80,58 @@ class SettingsRouter { | |||
return next() | |||
} | |||
|
|||
async getRepoPassword(req, res, next) { |
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.
nit: maybe can prefix with underscore/ignore the unused stuff
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.
attachReadRouteHandlerWrapper(this.getRepoPassword) | ||
) | ||
router.post( | ||
"/repoPassword", | ||
attachRollbackRouteHandlerWrapper(this.updateRepoPassword) |
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.
probably should kebab-case
sir
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.
res.status(200).send("OK") | ||
return next() |
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.
is there a difference between this and return res.status(200).send("OK")
?
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.
Yes for these cases - we have a notification middleware which can potentially trigger, so we need to move on to the next router instead of returning immediately
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.
skimmed
return errAsync(`Deployment for ${repoName} does not exist`) | ||
const { id, hostingId: appId } = deploymentInfo | ||
let updateAppInput | ||
if (!enablePassword) { |
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.
could we just branch here and hide the rest of the logic into 2 separate methods?
very confusing to figure out where/what is used, especially because the purpose of the two is diametrically opposed (1 sets password, 1 removes)
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.
Yeah that makes sense, f13e3f2
src/utils/crypto-utils.ts
Outdated
|
||
import { config } from "@config/config" | ||
|
||
const ALGORITHM = "aes-256-cbc" |
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.
const ALGORITHM = "aes-256-cbc" | |
const DECRYPTION_ALGORITHM = "aes-256-cbc" |
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.
changed in d1f2659!
src/utils/crypto-utils.ts
Outdated
const decipher = createDecipheriv( | ||
ALGORITHM, | ||
SECRET_KEY, | ||
Buffer.from(iv, "hex") | ||
) | ||
let decrypted = decipher.update(encryptedPassword, "hex", "utf8") | ||
decrypted += decipher.final("utf8") | ||
return decrypted |
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.
sorry, but this is abit confusing to me - it seems to me that we are decrypting first and thereafter, converting from hex
to utf8
before calling a finalizer of utf8
and appending it to the updated value (in effect, decrypted === decipher<utf8> * 2)
isn't it?)
am i misunderstanding something? shouldn't it be either
// update -> final
decipher.update(...)
return decipher.final(...)
// just return straight
return decipher.final(...)
why have a repeated output string?
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 is the usage as given by the documentation - i'm not sure what to call the intermediate steps either tbh, so I used their naming. But .update alone doesn't actually provide the final decrypted string, and .final is needed to cut off the decipher usage, but doesn't actually provide any decryption
src/utils/crypto-utils.ts
Outdated
|
||
const ALGORITHM = "aes-256-cbc" | ||
|
||
export const decryptPassword = (encryptedPassword: string, iv: string) => { |
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.
highly suggest typing + branding the return and enforcing this on downstream callers. doesn't have to be here if the logic here is correct.
// type-def of decrypted password, prevents assigning string to it
type DecryptedPassword = Brand<string, "DecryptedPassword">
export const decryptPassword = (): DecryptedPassword
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.
Updated in cea7e0e!
src/validators/RequestSchema.js
Outdated
const UpdateRepoPasswordRequestSchema = Joi.object().keys({ | ||
encryptedPassword: Joi.string(), | ||
iv: Joi.string(), | ||
enablePassword: Joi.boolean(), | ||
}) |
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.
is this all or nothing? ie, do we accept partial inputs like encryptedPassword + iv
only
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.
No partial input - always specify whether we're enabling or disabling the password
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.
Hey mostly lgtm, but I have some questions regarding the implementation of this.
- does FE generate the IV?
- Do we regenerate the IV everytime the user wishes to change the password?
- did we consider only FE not knowing the IV and secret key? Ie, we send unencrypted pass over secure HTTPS, but this way only BE is aware of the symmetric encryption + has the relevant keys to achieve this functionality?
doc: "Secret key used to encrypt password", | ||
env: "SITE_PASSWORD_SECRET_KEY", | ||
format: String, | ||
default: "", |
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.
Sanity check here: should this not be required string
here? Having an empty string here should not be a sane value for decryption right?
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 catch, 0c5622f
@@ -43,6 +50,37 @@ class SettingsService { | |||
} | |||
} | |||
|
|||
async getEncryptedPassword(sessionData) { |
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.
nit: could we enforce a return type here so that it doesnt resolve to Promise<any>
?
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.
It's a js file, I don't think we can enforce the return type :(
const deploymentInfo = await this.deploymentsService.getDeploymentInfoFromSiteId( | ||
id | ||
) | ||
if (deploymentInfo.isErr()) return deploymentInfo |
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.
Sanity check here, shouldnt we reuturn some sort of error here instead?
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 returns the wrapped error from getDeploymentInfoFromSiteId
- we're not modifying it here
) | ||
|
||
if (passwordRes.isErr()) { | ||
throw passwordRes.error |
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.
sanity check here: should we return some sort of 404 rather than thorwing an error here?
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.
Had a chat with Chin about this - given that it's difficult to step through to retrieve all the possible errors here now, I'm leaning towards leaving this as throw
and fixing it after the mvp is out (by refactoring to ts) - do you have concerns about this?
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.
fixing it after the mvp
let's add a ticket for the refactor
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.
@alexanderleegs am ok with this, but are we sure that there is is no errors meant for internal consumption thrown back to the end user?
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.
Verified offline - the thrown isomer errors have user facing error messages, and any other error gets caught by our error handler to throw a generic error and message
const { userWithSiteSessionData } = res.locals | ||
|
||
const { error } = UpdateRepoPasswordRequestSchema.validate(req.body) | ||
if (error) throw new BadRequestError(error.message) |
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.
Similar comment as above, should we be returning 404 rather than throwing here?
Buffer.from(iv, "hex") | ||
) | ||
let decrypted = decipher.update(encryptedPassword, "hex", "utf8") | ||
decrypted += decipher.final("utf8") |
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.
why do we need to do an update + final here ah?
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.
See above on Chin's comment!
@@ -115,6 +118,83 @@ class DeploymentsService { | |||
.map(() => amplifyInfo) | |||
}) | |||
} | |||
|
|||
getDeploymentInfoFromSiteId = async (repoId: string) => { |
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.
could we rename repoId
to siteId
? we have a repos
and a sites
table, might be better to make a distinction here
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 catch, forgot to rename the var - d255ec4
iv: string, | ||
enablePassword: boolean | ||
) => { | ||
const deploymentInfo = await this.repository.findOne({ |
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.
hey, this can be made simpler right?
eg:
const deploymentInfo = await this.repository.findOne({
where: {
name: repoName,
},
})
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.
repoName only exists in Repo
though, so we need to do the joins!
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.
[nit]: rename for repository
to deploymentRepository
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.
@@ -88,6 +96,28 @@ class DeploymentClient { | |||
JEKYLL_ENV: branchName === "master" ? "production" : "staging", | |||
}, | |||
}) | |||
|
|||
generateUpdatePasswordInput = ( |
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.
nit: could we separate out the concerns of with two functions of
generateUpdatePasswordInput
and
generateDeletePasswordInput
?
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.
Sure, changed in 592ab87
) | ||
|
||
if (passwordRes.isErr()) { | ||
throw passwordRes.error |
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.
fixing it after the mvp
let's add a ticket for the refactor
if (siteInfo.isErr()) { | ||
// Missing site indicating netlify site - return special result | ||
return okAsync({ | ||
password: "", |
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.
would it be possible to return this without the password when isAmplifySite is false? the password only applies if isAmplifySite is true right?
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.
Yeah that makes sense, have updated it in af96a20
src/utils/crypto-utils.ts
Outdated
encryptedPassword: string, | ||
iv: string | ||
): DecryptedPassword => { | ||
const SECRET_KEY = Buffer.from( |
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.
shouldn't this be a argument to the method too? decryptPassword is a generic util to decrypt an encrypted pass with all necessary parameters given to 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.
Sure, have made the change to both decrypt and encrypt in 67a47f2
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.
sadly cicd failign ya
iv: string, | ||
enablePassword: boolean | ||
) => { | ||
const deploymentInfo = await this.repository.findOne({ |
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.
[nit]: rename for repository
to deploymentRepository
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.
@alexanderleegs not sure why but there are sadly alot of extra commits
Could I get your help to rebase this?
[Style] When we are making stacked diffs, shall we we only merge them in when the parent PR is approved? abit hard to reason about a larger pr in one sitting :(
CHANGELOG.md
Outdated
- fix(test cases): fix failing tests [`c843865`](https://github.com/isomerpages/isomercms-backend/commit/c84386549a28602e1af10cd2eda9f056212afaa7) | ||
|
||
#### [v0.24.0](https://github.com/isomerpages/isomercms-backend/compare/v0.23.1...v0.24.0) | ||
#### [v0.24.1](https://github.com/isomerpages/isomercms-backend/compare/v0.23.1...v0.24.1) |
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.
hey i think these got committed by accident?
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.
@alexanderleegs i think need to rebase
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.
Yup fixed on rebase!
* chore: update site fixtures * test: add githubService tests * test: add settings router test * Fix: settings tests * chore: update env vars for tests * chore: update fixtures * fix: requestSchema * fix: use writehandler instead of rollback handler * chore: update tests to use new dto * feat: add crypto-utils test * feat: add integration test * fix: update updatePassword schema * fix: update crypto-utils tests * chore: remove unused imports * chore: update names of imported fixtures * feat: add new test case for invalid req * chore: update const name * fix: test input for new dto * chore: add validator * chore: update tests * nit: update test names
98a17a0
to
58fa9d5
Compare
Sorry I thought the base PR was also approved already! |
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.
mostly ok pending 2 concerns:
- this pr contains throw .error. Have we made sure that the errors thrown back to the users are not meant for internal consumption? Eg, an axios error from github should not be returned to the user.
- This is a stacked diff, have we made sanity checks on the child diff? Specifically ensuring that
- all non-email sites dont have access to this
- all the email sites is able to privatise their repos?
) | ||
|
||
if (passwordRes.isErr()) { | ||
throw passwordRes.error |
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.
@alexanderleegs am ok with this, but are we sure that there is is no errors meant for internal consumption thrown back to the end user?
|
||
async changeRepoPrivacy(sessionData, shouldMakePrivate) { | ||
const { siteName, isomerUserId } = sessionData | ||
const endpoint = `${siteName}` |
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.
sanity check here, wouldnt endpoint refer to repos/<repoName>
and siteName
refer to just repoName
? should we just use the variable siteName
instead?
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 is referring to the endpoint still! The actual endpoint to call is /repos/<orgname>/<repoName>
, but our axios instance prefixes with /repos/<orgname>/
, so the remaining endpoint is just <repoName>
(referred to as siteName
here
* chore: add new env var for netlify access token * feat: add netlify privatisation * chore: update .env.test * chore: add return type * chore: updates stop_builds type * chore: cast error * chore: move netlify conditional out separately * fix: check for error type * chore: shift and rename file * Fix: refactor axios-utils and add isAxiosError * refactor: pass only password to updateNetlifySite * fix: use isAxiosError * fix: revert to require * chore: swap to @utils * chore: modify method name * chore: rename netlifyService to netlifyApi * Fix/restrict privatisation to email login (#840) * fix: add verifyIsEmailUser wrapper to password endpoints * chore: update tests
Problem
This PR introduces a repo privatisation feature. To be reviewed in conjunction with PR #1316 on the isomercms-frontend repo.
This PR adds new fields to the
sites
anddeployments
table -isPrivate
andencryptedPassword
,encryptionIv
,passwordDate
respectively. The new endpoints have been added to the settings router and related services, with the deployment modification logic added toDeploymentsService
.Tests
See frontend for related tests - for private repos, the
is_private
param for thesites
entry should betrue
and theencrypted_password
,encryption_iv
andpassword_date
indeployments
should contain the appropriate entriesDeploy Notes
Migrations will need to be run on staging/prod before merging this PR.
New environment variables:
SITE_PASSWORD_SECRET_KEY
: Encryption key for stored passwords, should match the equivalent key on the backendNETLIFY_ACCESS_TOKEN
: this is the access token we use to access our account. We should be using the one generated for isomeradmin