mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) updates from grist-core
This commit is contained in:
commit
48a8af83fc
@ -256,7 +256,6 @@ APP_STATIC_URL | url prefix for static resources
|
||||
APP_STATIC_INCLUDE_CUSTOM_CSS | set to "true" to include custom.css (from APP_STATIC_URL) in static pages
|
||||
APP_UNTRUSTED_URL | URL at which to serve/expect plugin content.
|
||||
GRIST_ADAPT_DOMAIN | set to "true" to support multiple base domains (careful, host header should be trustworthy)
|
||||
GRIST_ALLOWED_HOSTS | comma-separated list of permitted domains origin for requests (e.g. my.site,another.com)
|
||||
GRIST_APP_ROOT | directory containing Grist sandbox and assets (specifically the sandbox and static subdirectories).
|
||||
GRIST_BACKUP_DELAY_SECS | wait this long after a doc change before making a backup
|
||||
GRIST_BOOT_KEY | if set, offer diagnostics at /boot/GRIST_BOOT_KEY
|
||||
|
@ -18,7 +18,7 @@ import {makeId} from 'app/server/lib/idUtils';
|
||||
import log from 'app/server/lib/log';
|
||||
import {IPermitStore, Permit} from 'app/server/lib/Permit';
|
||||
import {AccessTokenInfo} from 'app/server/lib/AccessTokens';
|
||||
import {allowHost, getOriginUrl, isEnvironmentAllowedHost, optStringParam} from 'app/server/lib/requestUtils';
|
||||
import {allowHost, getOriginUrl, optStringParam} from 'app/server/lib/requestUtils';
|
||||
import * as cookie from 'cookie';
|
||||
import {NextFunction, Request, RequestHandler, Response} from 'express';
|
||||
import {IncomingMessage} from 'http';
|
||||
@ -271,7 +271,7 @@ export async function addRequestUser(
|
||||
// custom-domain owner could hijack such sessions.
|
||||
const allowedOrg = getAllowedOrgForSessionID(mreq.sessionID);
|
||||
if (allowedOrg) {
|
||||
if (allowHost(req, allowedOrg.host) || isEnvironmentAllowedHost(allowedOrg.host)) {
|
||||
if (allowHost(req, allowedOrg.host)) {
|
||||
customHostSession = ` custom-host-match ${allowedOrg.host}`;
|
||||
} else {
|
||||
// We need an exception for internal forwarding from home server to doc-workers. These use
|
||||
|
@ -8,7 +8,6 @@ import {RequestWithGrist} from 'app/server/lib/GristServer';
|
||||
import log from 'app/server/lib/log';
|
||||
import {Permit} from 'app/server/lib/Permit';
|
||||
import {Request, Response} from 'express';
|
||||
import _ from 'lodash';
|
||||
import {Writable} from 'stream';
|
||||
|
||||
// log api details outside of dev environment (when GRIST_HOSTED_VERSION is set)
|
||||
@ -87,7 +86,7 @@ export function trustOrigin(req: Request, resp: Response): boolean {
|
||||
const origin = req.get('origin');
|
||||
if (!origin) { return true; } // Not a CORS request.
|
||||
if (process.env.GRIST_HOST && req.hostname === process.env.GRIST_HOST) { return true; }
|
||||
if (!allowHost(req, new URL(origin)) && !isEnvironmentAllowedHost(new URL(origin))) { return false; }
|
||||
if (!allowHost(req, new URL(origin))) { return false; }
|
||||
|
||||
// For a request to a custom domain, the full hostname must match.
|
||||
resp.header("Access-Control-Allow-Origin", origin);
|
||||
@ -104,14 +103,14 @@ export function allowHost(req: Request, allowedHost: string|URL) {
|
||||
const allowedUrl = (typeof allowedHost === 'string') ? new URL(`${proto}://${allowedHost}`) : allowedHost;
|
||||
if (mreq.isCustomHost) {
|
||||
// For a request to a custom domain, the full hostname must match.
|
||||
return actualUrl.hostname === allowedUrl.hostname;
|
||||
return actualUrl.hostname === allowedUrl.hostname;
|
||||
} else {
|
||||
// For requests to a native subdomains, only the base domain needs to match.
|
||||
const allowedDomain = parseSubdomain(allowedUrl.hostname);
|
||||
const actualDomain = parseSubdomain(actualUrl.hostname);
|
||||
return (!_.isEmpty(actualDomain) ?
|
||||
return actualDomain.base ?
|
||||
actualDomain.base === allowedDomain.base :
|
||||
allowedUrl.hostname === actualUrl.hostname);
|
||||
actualUrl.hostname === allowedUrl.hostname;
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,13 +118,6 @@ export function matchesBaseDomain(domain: string, baseDomain: string) {
|
||||
return domain === baseDomain || domain.endsWith("." + baseDomain);
|
||||
}
|
||||
|
||||
export function isEnvironmentAllowedHost(url: string|URL) {
|
||||
const urlHost = (typeof url === 'string') ? url : url.hostname;
|
||||
return (process.env.GRIST_ALLOWED_HOSTS || "").split(",").some(domain =>
|
||||
domain && matchesBaseDomain(urlHost, domain)
|
||||
);
|
||||
}
|
||||
|
||||
export function isParameterOn(parameter: any): boolean {
|
||||
return gutil.isAffirmative(parameter);
|
||||
}
|
||||
|
@ -579,7 +579,9 @@
|
||||
"You do not have write access to this site": "Sie haben keinen Schreibzugriff auf diese Seite",
|
||||
"Download full document and history": "Vollständiges Dokument und Geschichte herunterladen",
|
||||
"Remove all data but keep the structure to use as a template": "Entfernen Sie alle Daten, behalten Sie aber die Struktur als Vorlage bei",
|
||||
"Remove document history (can significantly reduce file size)": "Dokumentverlauf entfernen (kann die Dateigröße deutlich reduzieren)"
|
||||
"Remove document history (can significantly reduce file size)": "Dokumentverlauf entfernen (kann die Dateigröße deutlich reduzieren)",
|
||||
"Download document": "Dokument herunterladen",
|
||||
"Download": "Download"
|
||||
},
|
||||
"NTextBox": {
|
||||
"false": "falsch",
|
||||
@ -1439,5 +1441,15 @@
|
||||
},
|
||||
"FormPage": {
|
||||
"There was an error submitting your form. Please try again.": "Beim Absenden Ihres Formulars ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut."
|
||||
},
|
||||
"DateRangeOptions": {
|
||||
"Last 30 days": "Letzte 30 Tage",
|
||||
"Last 7 days": "Letzte 7 Tage",
|
||||
"Last Week": "Letzte Woche",
|
||||
"Next 7 days": "Nächste 7 Tage",
|
||||
"This month": "Diesen Monat",
|
||||
"This week": "Diese Woche",
|
||||
"This year": "Dieses Jahr",
|
||||
"Today": "Heute"
|
||||
}
|
||||
}
|
||||
|
@ -475,7 +475,9 @@
|
||||
"You do not have write access to this site": "No tiene acceso de escritura a este sitio",
|
||||
"Download full document and history": "Descargar documento completo e historial",
|
||||
"Remove all data but keep the structure to use as a template": "Elimine todos los datos pero mantenga la estructura para usarla como plantilla",
|
||||
"Remove document history (can significantly reduce file size)": "Eliminar el historial del documento (puede reducir significativamente el tamaño del archivo)"
|
||||
"Remove document history (can significantly reduce file size)": "Eliminar el historial del documento (puede reducir significativamente el tamaño del archivo)",
|
||||
"Download": "Descargar",
|
||||
"Download document": "Descargar el documento"
|
||||
},
|
||||
"NTextBox": {
|
||||
"false": "falso",
|
||||
@ -1429,5 +1431,15 @@
|
||||
"FormSuccessPage": {
|
||||
"Form Submitted": "Formulario enviado",
|
||||
"Thank you! Your response has been recorded.": "¡Muchas gracias! Su respuesta ha quedado registrada."
|
||||
},
|
||||
"DateRangeOptions": {
|
||||
"Last 30 days": "Últimos 30 días",
|
||||
"Last 7 days": "Últimos 7 días",
|
||||
"Last Week": "Última semana",
|
||||
"Next 7 days": "Próximos 7 días",
|
||||
"This month": "Este mes",
|
||||
"This week": "Esta semana",
|
||||
"This year": "Este año",
|
||||
"Today": "Hoy"
|
||||
}
|
||||
}
|
||||
|
@ -38,8 +38,8 @@
|
||||
"Insert row below": "下に行を挿入",
|
||||
"Delete": "削除",
|
||||
"Copy anchor link": "リンクをコピー",
|
||||
"Duplicate rows_one": "重複行",
|
||||
"Duplicate rows_other": "重複行",
|
||||
"Duplicate rows_one": "行を複製",
|
||||
"Duplicate rows_other": "行を複製",
|
||||
"Insert row above": "上に行を挿入"
|
||||
},
|
||||
"Drafts": {
|
||||
@ -316,27 +316,27 @@
|
||||
"team site": "チームサイト",
|
||||
"Create a team to share with more people": "より多くの人と共有するためにチームを作る",
|
||||
"guest": "ゲスト",
|
||||
"Public access: ": "パブリックアクセス: ",
|
||||
"Public access: ": "公開: ",
|
||||
"Team member": "チームメンバー",
|
||||
"Manage members of team site": "チームサイトのメンバーの管理",
|
||||
"Off": "Off",
|
||||
"Save & ": "保存 ",
|
||||
"Outside collaborator": "外部コラボレーター",
|
||||
"{{collaborator}} limit exceeded": "{{collaborator}} 制限超過",
|
||||
"User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.": "ユーザは{{parent})}からパーミッションを継承します。削除するには、'Inherit access' オプションを 'None' に設定します。",
|
||||
"User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.": "ユーザは{{parent})}からパーミッションを継承します。削除するには、'アクセス権の継承' オプションを 'None' に設定します。",
|
||||
"Your role for this {{resourceType}}": "この{{resourceType}}のあなたの役割",
|
||||
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.": "一旦自分のアクセス権を削除してしまうと、{{resourceType}} に十分なアクセス権を持つ他の誰かの援助がない限り、元に戻すことはできません。",
|
||||
"Close": "閉じる",
|
||||
"Allow anyone with the link to open.": "誰でもリンクを開くことができるようにする。",
|
||||
"Invite people to {{resourceType}}": "{{resourceType}} に招待する",
|
||||
"Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "パブリックアクセスは{{parent}} から継承されます。 削除するには、'Inherit access' オプションを 'None' に設定します。",
|
||||
"Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "公開設定は{{parent}} から継承されます。 削除するには、'アクセス権の継承' オプションを 'None' に設定します。",
|
||||
"Remove my access": "アクセスを削除",
|
||||
"Public access": "パブリック・アクセス",
|
||||
"Public Access": "パブリック・アクセス",
|
||||
"Public access": "公開",
|
||||
"Public Access": "公開",
|
||||
"Cancel": "キャンセル",
|
||||
"Grist support": "Gristサポート",
|
||||
"You are about to remove your own access to this {{resourceType}}": "この {{resourceType}} への自分のアクセス権を削除しようとしています",
|
||||
"User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "ユーザは{{parent}} からパーミッションを継承します。 削除するには、'Inherit access' オプションを 'None' に設定します。",
|
||||
"User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "ユーザは{{parent}} からパーミッションを継承します。 削除するには、'アクセス権の継承' オプションを 'None' に設定します。",
|
||||
"Guest": "ゲスト",
|
||||
"Invite multiple": "複数招待",
|
||||
"Confirm": "確認",
|
||||
@ -465,13 +465,13 @@
|
||||
"Copy": "コピー",
|
||||
"Delete {{count}} columns_one": "列の削除",
|
||||
"Delete {{count}} columns_other": "{{count}}列削除",
|
||||
"Duplicate rows_one": "重複行",
|
||||
"Duplicate rows_one": "行を複製",
|
||||
"Insert row above": "上に行を挿入",
|
||||
"Delete {{count}} rows_other": "{{count}}行削除",
|
||||
"Clear values": "値をクリア",
|
||||
"Clear cell": "セルをクリア",
|
||||
"Comment": "コメント",
|
||||
"Duplicate rows_other": "重複行",
|
||||
"Duplicate rows_other": "行を複製",
|
||||
"Reset {{count}} columns_one": "列をリセット",
|
||||
"Insert column to the right": "右側に列を挿入",
|
||||
"Filter by this value": "この値でフィルタ",
|
||||
|
@ -579,7 +579,9 @@
|
||||
"You do not have write access to this site": "Você não tem acesso de gravação a este site",
|
||||
"Download full document and history": "Baixe documento completo e histórico",
|
||||
"Remove all data but keep the structure to use as a template": "Remova todos os dados, mas mantenha a estrutura para usar como um modelo",
|
||||
"Remove document history (can significantly reduce file size)": "Remova o histórico do documento (pode reduzir significativamente o tamanho do arquivo)"
|
||||
"Remove document history (can significantly reduce file size)": "Remova o histórico do documento (pode reduzir significativamente o tamanho do arquivo)",
|
||||
"Download": "Baixar",
|
||||
"Download document": "Baixar documento"
|
||||
},
|
||||
"NTextBox": {
|
||||
"false": "falso",
|
||||
@ -1439,5 +1441,15 @@
|
||||
"FormSuccessPage": {
|
||||
"Form Submitted": "Formulário enviado",
|
||||
"Thank you! Your response has been recorded.": "Obrigado! Sua resposta foi registrada."
|
||||
},
|
||||
"DateRangeOptions": {
|
||||
"Last 30 days": "Últimos 30 dias",
|
||||
"Last 7 days": "Últimos 7 dias",
|
||||
"Last Week": "Semana passada",
|
||||
"Next 7 days": "Próximo 7 dias",
|
||||
"This month": "Este mês",
|
||||
"This week": "Esta semana",
|
||||
"This year": "Este ano",
|
||||
"Today": "Hoje"
|
||||
}
|
||||
}
|
||||
|
@ -1377,5 +1377,15 @@
|
||||
"FormSuccessPage": {
|
||||
"Form Submitted": "Obrazec oddan",
|
||||
"Thank you! Your response has been recorded.": "Hvala ti! Tvoj odgovor je bil zabeležen."
|
||||
},
|
||||
"DateRangeOptions": {
|
||||
"Last 30 days": "Zadnjih 30 dni",
|
||||
"Last 7 days": "Zadnjih 7 dni",
|
||||
"Last Week": "Zadnji teden",
|
||||
"Next 7 days": "Naslednjih 7 dni",
|
||||
"This month": "Ta mesec",
|
||||
"This week": "Ta teden",
|
||||
"This year": "To leto",
|
||||
"Today": "Danes"
|
||||
}
|
||||
}
|
||||
|
@ -4865,23 +4865,6 @@ function testDocApi() {
|
||||
});
|
||||
|
||||
describe("Allowed Origin", () => {
|
||||
it('should allow only example.com', async () => {
|
||||
async function checkOrigin(origin: string, allowed: boolean) {
|
||||
const resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/data`,
|
||||
{...chimpy, headers: {...chimpy.headers, "Origin": origin}}
|
||||
);
|
||||
assert.equal(resp.headers['access-control-allow-credentials'], allowed ? 'true' : undefined);
|
||||
assert.equal(resp.status, allowed ? 200 : 403);
|
||||
}
|
||||
|
||||
await checkOrigin("https://www.toto.com", false);
|
||||
await checkOrigin("https://badexample.com", false);
|
||||
await checkOrigin("https://bad.com/example.com/toto", false);
|
||||
await checkOrigin("https://example.com/path", true);
|
||||
await checkOrigin("https://example.com:3000/path", true);
|
||||
await checkOrigin("https://good.example.com/toto", true);
|
||||
});
|
||||
|
||||
it("should respond with correct CORS headers", async function () {
|
||||
const wid = await getWorkspaceId(userApi, 'Private');
|
||||
const docId = await userApi.newDoc({name: 'CorsTestDoc'}, wid);
|
||||
|
@ -49,7 +49,6 @@ export class TestServer {
|
||||
GRIST_PORT: '0',
|
||||
GRIST_DISABLE_S3: 'true',
|
||||
REDIS_URL: process.env.TEST_REDIS_URL,
|
||||
GRIST_ALLOWED_HOSTS: `example.com,localhost`,
|
||||
GRIST_TRIGGER_WAIT_DELAY: '100',
|
||||
// this is calculated value, some tests expect 4 attempts and some will try 3 times
|
||||
GRIST_TRIGGER_MAX_ATTEMPTS: '4',
|
||||
|
Loading…
Reference in New Issue
Block a user