-
Notifications
You must be signed in to change notification settings - Fork 252
fix(variables): validate variable keys before writing them #3155
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
1d4cfad
0feeb2a
e10ae01
de9b9a5
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 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |||||||||||
| import { Icon, Layout, Selector, Tooltip, Typography, Upload } from '@appwrite.io/pink-svelte'; | ||||||||||||
| import { parse } from '$lib/helpers/envfile'; | ||||||||||||
| import { removeFile } from '$lib/helpers/files'; | ||||||||||||
| import { validateVariables } from '$lib/helpers/variables'; | ||||||||||||
|
|
||||||||||||
| export let show = false; | ||||||||||||
| export let variables: Partial<Models.Variable>[]; | ||||||||||||
|
|
@@ -35,19 +36,20 @@ | |||||||||||
| if (!Object.keys(uploaded).length) { | ||||||||||||
| throw new Error('No variables found'); | ||||||||||||
| } | ||||||||||||
| const entries = Object.entries(uploaded); | ||||||||||||
| // Drop the valueless entries first. They are never written, so an | ||||||||||||
| // invalid key on one of them must not reject the whole file. | ||||||||||||
| const entries = Object.entries(uploaded).filter(([, value]) => !!value); | ||||||||||||
|
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.
When an uploaded
Suggested change
Knowledge Base Used: Functions and Sites Prompt To Fix With AIThis is a comment left during a code review.
Path: src/lib/components/variables/importVariablesModal.svelte
Line: 41
Comment:
**Empty imports silently succeed**
When an uploaded `.env` file contains only valueless entries, this filter removes every entry after the raw parse has passed the earlier emptiness check. Validation and iteration then succeed on an empty array and the modal closes without importing anything or reporting `No variables found`.
```suggestion
const entries = Object.entries(uploaded).filter(([, value]) => !!value);
if (!entries.length) {
throw new Error('No variables found');
}
```
**Knowledge Base Used:** [Functions and Sites](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/appwrite/console/-/docs/project-functions-sites.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||||||||||||
|
|
||||||||||||
| for (const [key, value] of entries) { | ||||||||||||
| if (value.length > 8192) { | ||||||||||||
| throw new Error(`Variable ${key} is longer than 8192 allowed characters`); | ||||||||||||
| } | ||||||||||||
| const validationError = validateVariables( | ||||||||||||
| entries.map(([key, value]) => ({ key, value })) | ||||||||||||
| ); | ||||||||||||
|
greptile-apps[bot] marked this conversation as resolved.
|
||||||||||||
| if (validationError) { | ||||||||||||
| throw new Error(validationError); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| entries | ||||||||||||
| .filter(([, value]) => !!value) | ||||||||||||
| .forEach(([key, value]) => { | ||||||||||||
| variables.push({ key, value, secret }); | ||||||||||||
| }); | ||||||||||||
| entries.forEach(([key, value]) => { | ||||||||||||
| variables.push({ key, value, secret }); | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| show = false; | ||||||||||||
| } catch (e) { | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { expect, test } from 'vitest'; | ||
| import { | ||
| getVariableKeyError, | ||
| isValidVariableKey, | ||
| validateVariables, | ||
| VARIABLE_KEY_MAX_LENGTH, | ||
| VARIABLE_VALUE_MAX_LENGTH | ||
| } from '$lib/helpers/variables'; | ||
|
|
||
| test('accept keys that are valid environment variable names', () => { | ||
| expect(isValidVariableKey('APP_TEST')).toBe(true); | ||
| expect(isValidVariableKey('_PRIVATE')).toBe(true); | ||
| expect(isValidVariableKey('key1')).toBe(true); | ||
| expect(isValidVariableKey('a'.repeat(VARIABLE_KEY_MAX_LENGTH))).toBe(true); | ||
| }); | ||
|
|
||
| test('reject keys that cannot be used as environment variable names', () => { | ||
| expect(isValidVariableKey('MY-KEY')).toBe(false); | ||
| expect(isValidVariableKey('MY.KEY')).toBe(false); | ||
| expect(isValidVariableKey('MY KEY')).toBe(false); | ||
| expect(isValidVariableKey('9KEY')).toBe(false); | ||
| expect(isValidVariableKey('KÉY')).toBe(false); | ||
| expect(isValidVariableKey('KEY\t')).toBe(false); | ||
| expect(isValidVariableKey('')).toBe(false); | ||
| expect(isValidVariableKey('a'.repeat(VARIABLE_KEY_MAX_LENGTH + 1))).toBe(false); | ||
| }); | ||
|
|
||
| test('report a missing key separately from an invalid one', () => { | ||
| expect(getVariableKeyError('')).toEqual('Variable key is required'); | ||
| expect(getVariableKeyError('MY-KEY')).toContain('is invalid'); | ||
| expect(getVariableKeyError('a'.repeat(VARIABLE_KEY_MAX_LENGTH + 1))).toContain('longer than'); | ||
| expect(getVariableKeyError('APP_TEST')).toBeNull(); | ||
| }); | ||
|
|
||
| test('validate a list of variables and name the offending key', () => { | ||
| expect(validateVariables([{ key: 'APP_TEST', value: 'value' }])).toBeNull(); | ||
| expect(validateVariables([{ key: 'APP_TEST', value: '' }])).toBeNull(); | ||
|
|
||
| expect( | ||
| validateVariables([ | ||
| { key: 'APP_TEST', value: 'value' }, | ||
| { key: 'MY-KEY', value: 'value' } | ||
| ]) | ||
| ).toContain('MY-KEY'); | ||
|
|
||
| expect( | ||
| validateVariables([{ key: 'APP_TEST', value: 'v'.repeat(VARIABLE_VALUE_MAX_LENGTH + 1) }]) | ||
| ).toContain('APP_TEST'); | ||
| }); |
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.
Wouldn’t this also validate keys that are already stored? 🤔 If a project contains a legacy key (examplews MY-KEY), updating only its value would fail here before sdkUpdateVariable(..., undefined, ...) is ever reached. Should we only validate keys for newly added variables and keep validating values for existing ones?
cc @greptile
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, but the concern is scoped to the wrong component. In
createVariableModal.svelte,validateVariablesis only called onnewVariables— the rows the user is currently entering in the form. The pre-existingvariablesprop is never passed through validation here, so legacy stored keys are safe.The concern is real in
updateVariableModal.sveltethough. There,pairis initialized fromselectedVar(the stored variable), andvalidateVariables([pair])runs unconditionally on submit — it doesn't check whetherpair.keyactually changed. So if a user opens the update modal on a legacyMY-KEYvariable to change only the value, the key validation fires and blocks them beforesdkUpdateVariable(withundefinedkey) is ever reached. That's the breakage the PR description says it fixes, but looking at the actual code inupdateVariableModal.sveltethe guard isn't there.The fix would be to skip key validation when the key is unchanged:
But
validateVariables→getVariableKeyErrorreturns'Variable key is required'for anundefinedkey, sovalidateVariableswould also need to treatkey === undefinedas "skip key check" (i.e. only validate value). Worth addressing before merge — as-is,updateVariableModal.sveltestill breaks value-only edits on any pre-existing invalid key.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.
Checked both create modals and both update modals — no change needed here, and the split you describe is what is already implemented.
Create modals (
lib/components/variables/createVariableModal.svelte,project-[region]-[project]/createVariableModal.svelte):validateVariablesonly ever receivesnewVariables, the rows being typed into the form. The pre-existingvariablesprop is never passed through it, so stored keys are untouched.Update modal (
project-[region]-[project]/updateVariablesModal.svelte) — this is the one reachable from the post-create variables table, and it already guards on the key having changed:so a value-only edit on a legacy
MY-KEYvalidates the value only, and dispatcheskey: undefinedso the API keeps the stored key.lib/components/variables/updateVariableModal.svelte(the singular one, which greptile pointed at) validates unconditionally, but as you noted in the other thread it is only reachable throughEnvironmentVariablesin the two create-flow configuration components, where the key is always user-entered.