(core) updates from grist-core

This commit is contained in:
Paul Fitzpatrick 2023-11-27 04:25:34 -05:00
commit 65a1863015
6 changed files with 158 additions and 29 deletions

View File

@ -6,8 +6,8 @@
* IdP is the "Identity Provider", somewhere users log into, e.g. Okta or Google Apps.
*
* We also use optional attributes for the user's name, for which we accept any of:
* given_name
* family_name
* given_name + family_name
* name
*
* Expected environment variables:
* env GRIST_OIDC_SP_HOST=https://<your-domain>
@ -21,6 +21,17 @@
* The client secret for the application, as registered with the IdP.
* env GRIST_OIDC_IDP_SCOPES
* The scopes to request from the IdP, as a space-separated list. Defaults to "openid email profile".
* env GRIST_OIDC_SP_PROFILE_NAME_ATTR
* The key of the attribute to use for the user's name.
* If omitted, the name will either be the concatenation of "given_name" + "family_name" or the "name" attribute.
* env GRIST_OIDC_SP_PROFILE_EMAIL_ATTR
* The key of the attribute to use for the user's email. Defaults to "email".
* env GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT
* If set to "true", on logout, there won't be any attempt to call the IdP's end_session_endpoint
* (the user will remain logged in in the IdP).
* env GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED
* If set to "true", the user will be allowed to login even if the email is not verified by the IDP.
* Defaults to false.
*
* This version of OIDCConfig has been tested with Keycloak OIDC IdP following the instructions
* at:
@ -43,12 +54,17 @@ import { Sessions } from './Sessions';
import log from 'app/server/lib/log';
import { appSettings } from './AppSettings';
import { RequestWithLogin } from './Authorizer';
import { UserProfile } from 'app/common/LoginSessionAPI';
const CALLBACK_URL = '/oauth2/callback';
export class OIDCConfig {
private _client: Client;
private _redirectUrl: string;
private _namePropertyKey?: string;
private _emailPropertyKey: string;
private _skipEndSessionEndpoint: boolean;
private _ignoreEmailVerified: boolean;
public constructor() {
}
@ -69,6 +85,24 @@ export class OIDCConfig {
envVar: 'GRIST_OIDC_IDP_CLIENT_SECRET',
censor: true,
});
this._namePropertyKey = section.flag('namePropertyKey').readString({
envVar: 'GRIST_OIDC_SP_PROFILE_NAME_ATTR',
});
this._emailPropertyKey = section.flag('emailPropertyKey').requireString({
envVar: 'GRIST_OIDC_SP_PROFILE_EMAIL_ATTR',
defaultValue: 'email',
});
this._skipEndSessionEndpoint = section.flag('endSessionEndpoint').readBool({
envVar: 'GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT',
defaultValue: false,
})!;
this._ignoreEmailVerified = section.flag('ignoreEmailVerified').readBool({
envVar: 'GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED',
defaultValue: false,
})!;
const issuer = await Issuer.discover(issuerUrl);
this._redirectUrl = new URL(CALLBACK_URL, spHost).href;
@ -78,6 +112,10 @@ export class OIDCConfig {
redirect_uris: [ this._redirectUrl ],
response_types: [ 'code' ],
});
if (this._client.issuer.metadata.end_session_endpoint === undefined && !this._skipEndSessionEndpoint) {
throw new Error('The Identity provider does not propose end_session_endpoint. ' +
'If that is expected, please set GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true');
}
log.info(`OIDCConfig: initialized with issuer ${issuerUrl}`);
}
@ -105,6 +143,11 @@ export class OIDCConfig {
);
const userInfo = await this._client.userinfo(tokenSet);
if (!this._ignoreEmailVerified && userInfo.email_verified !== true) {
throw new Error(`OIDCConfig: email not verified for ${userInfo.email}`);
}
const profile = this._makeUserProfileFromUserInfo(userInfo);
log.info(`OIDCConfig: got OIDC response for ${profile.email} (${profile.name}) redirecting to ${targetUrl}`);
@ -140,6 +183,10 @@ export class OIDCConfig {
}
public async getLogoutRedirectUrl(req: express.Request, redirectUrl: URL): Promise<string> {
// For IdPs that don't have end_session_endpoint, we just redirect to the logout page.
if (this._skipEndSessionEndpoint) {
return redirectUrl.href;
}
return this._client.endSessionUrl({
post_logout_redirect_uri: redirectUrl.href
});
@ -167,14 +214,21 @@ export class OIDCConfig {
return codeVerifier;
}
private _makeUserProfileFromUserInfo(userInfo: UserinfoResponse) {
const email = userInfo.email;
private _makeUserProfileFromUserInfo(userInfo: UserinfoResponse): Partial<UserProfile> {
return {
email: String(userInfo[ this._emailPropertyKey ]),
name: this._extractName(userInfo)
};
}
private _extractName(userInfo: UserinfoResponse): string|undefined {
if (this._namePropertyKey) {
return (userInfo[ this._namePropertyKey ] as any)?.toString();
}
const fname = userInfo.given_name ?? '';
const lname = userInfo.family_name ?? '';
return {
email,
name: `${fname} ${lname}`.trim(),
};
return `${fname} ${lname}`.trim() || userInfo.name;
}
}

View File

@ -217,7 +217,13 @@
"Duplicate Table": "Tabelle duplizieren",
"Raw Data Tables": "Rohdaten-Tabellen",
"Table ID copied to clipboard": "Tabellen-ID in die Zwischenablage kopiert",
"You do not have edit access to this document": "Sie haben keinen Bearbeitungszugriff auf dieses Dokument"
"You do not have edit access to this document": "Sie haben keinen Bearbeitungszugriff auf dieses Dokument",
"Edit Record Card": "Karteikarte bearbeiten",
"Rename Table": "Tabelle umbenennen",
"{{action}} Record Card": "{{action}} Karteikarte",
"Record Card": "Karteikarte",
"Remove Table": "Tabelle entfernen",
"Record Card Disabled": "Karteikarte Deaktiviert"
},
"DocHistory": {
"Activity": "Aktivität",
@ -663,7 +669,8 @@
"Insert row above": "Zeile oben einfügen",
"Insert row below": "Zeile unten einfügen",
"Duplicate rows_one": "Zeile duplizieren",
"Duplicate rows_other": "Zeilen duplizieren"
"Duplicate rows_other": "Zeilen duplizieren",
"View as card": "Ansicht als Karte"
},
"SelectionSummary": {
"Copied to clipboard": "In die Zwischenablage kopiert"
@ -1295,5 +1302,13 @@
},
"searchDropdown": {
"Search": "Suchen"
},
"CardContextMenu": {
"Insert card above": "Karte oben einfügen",
"Duplicate card": "Karte duplizieren",
"Insert card below": "Karte unten einfügen",
"Delete card": "Karte löschen",
"Copy anchor link": "Ankerlink kopieren",
"Insert card": "Karte einfügen"
}
}

View File

@ -543,7 +543,8 @@
"Insert row above": "Insertar fila arriba",
"Insert row below": "Insertar fila debajo",
"Duplicate rows_one": "Duplicar fila",
"Duplicate rows_other": "Duplicar filas"
"Duplicate rows_other": "Duplicar filas",
"View as card": "Ver como tarjeta"
},
"ShareMenu": {
"Access Details": "Detalles de Acceso",
@ -718,7 +719,13 @@
"Duplicate Table": "Duplicar tabla",
"Raw Data Tables": "Tablas de datos brutos",
"Table ID copied to clipboard": "ID de tabla copiado al portapapeles",
"Click to copy": "Haga clic para copiar"
"Click to copy": "Haga clic para copiar",
"Edit Record Card": "Editar la ficha del registro",
"Rename Table": "Cambiar el nombre de la tabla",
"{{action}} Record Card": "{{action}} Ficha",
"Record Card": "Ficha de registro",
"Remove Table": "Quitar la tabla",
"Record Card Disabled": "Tarjeta de registro desactivada"
},
"DocPageModel": {
"Add Empty Table": "Agregar tabla vacía",
@ -1285,5 +1292,13 @@
"FloatingPopup": {
"Maximize": "Maximizar",
"Minimize": "Minimizar"
},
"CardContextMenu": {
"Insert card above": "Inserte la tarjeta arriba",
"Duplicate card": "Tarjeta duplicada",
"Insert card below": "Inserte la tarjeta a continuación",
"Delete card": "Borrar la tarjeta",
"Copy anchor link": "Copiar enlace fijado",
"Insert card": "Insertar la tarjeta"
}
}

View File

@ -217,7 +217,13 @@
"Duplicate Table": "Duplicar a Tabela",
"Raw Data Tables": "Tabelas de Dados Primários",
"Table ID copied to clipboard": "ID da Tabela copiada para a área de transferência",
"You do not have edit access to this document": "Você não tem permissão de edição desse documento"
"You do not have edit access to this document": "Você não tem permissão de edição desse documento",
"Edit Record Card": "Editar cartão de registro",
"Rename Table": "Renomear tabela",
"{{action}} Record Card": "{{action}} Cartão de registro",
"Record Card": "Cartão de registro",
"Remove Table": "Remover tabela",
"Record Card Disabled": "Cartão de registro desabilitado"
},
"DocHistory": {
"Activity": "Atividade",
@ -663,7 +669,8 @@
"Insert row above": "Inserir linha acima",
"Insert row below": "Inserir linha abaixo",
"Duplicate rows_one": "Duplicar linha",
"Duplicate rows_other": "Duplicar linhas"
"Duplicate rows_other": "Duplicar linhas",
"View as card": "Ver como cartão"
},
"SelectionSummary": {
"Copied to clipboard": "Copiado para a área de transferência"
@ -1295,5 +1302,13 @@
"FloatingPopup": {
"Maximize": "Maximizar",
"Minimize": "Minimizar"
},
"CardContextMenu": {
"Insert card above": "Inserir cartão acima",
"Duplicate card": "Duplicar o cartão",
"Insert card below": "Inserir cartão abaixo",
"Delete card": "Excluir cartão",
"Copy anchor link": "Copiar link de ancoragem",
"Insert card": "Inserir cartão"
}
}

View File

@ -334,7 +334,13 @@
"Duplicate Table": "Дублировать таблицу",
"Table ID copied to clipboard": "Идентификатор таблицы скопирован в буфер обмена",
"You do not have edit access to this document": "У вас нет доступа к редактированию этого документа",
"Delete {{formattedTableName}} data, and remove it from all pages?": "Удалить {{formattedTableName}} данные, и удалить их со всех страниц?"
"Delete {{formattedTableName}} data, and remove it from all pages?": "Удалить {{formattedTableName}} данные, и удалить их со всех страниц?",
"Edit Record Card": "Редактировать карточку записи",
"Rename Table": "Переименовать Таблицу",
"{{action}} Record Card": "{{action}} Карточка записи",
"Record Card": "Карточка записи",
"Remove Table": "Удалить Таблицу",
"Record Card Disabled": "Карточка записи отключена"
},
"DocHistory": {
"Snapshots": "Снимки",
@ -603,7 +609,8 @@
"Insert row below": "Вставить строку ниже",
"Insert row": "Вставить строку",
"Insert row above": "Вставить строку выше",
"Delete": "Удалить"
"Delete": "Удалить",
"View as card": "Посмотреть как карточку"
},
"RecordLayout": {
"Updating record layout.": "Обновление макета записи."
@ -1231,5 +1238,13 @@
"FloatingPopup": {
"Maximize": "Максимизировать",
"Minimize": "Минимизировать"
},
"CardContextMenu": {
"Insert card above": "Вставить карточку выше",
"Duplicate card": "Дублировать карточку",
"Insert card below": "Вставить карточку ниже",
"Delete card": "Удалить карточку",
"Copy anchor link": "Скопировать якорную ссылку",
"Insert card": "Вставить карточку"
}
}

View File

@ -146,7 +146,7 @@
"Deleted {{at}}": "Izbrisano {{at}}",
"Delete {{name}}": "Izbriši {{name}}",
"Document will be permanently deleted.": "Dokument bo trajno izbrisan.",
"Permanently Delete \"{{name}}\"?": "Trajno izbrisati \"{{name}}\"?",
"Permanently Delete \"{{name}}\"?": "Trajno izbrišem \"{{name}}\"?",
"(The organization needs a paid plan)": "(Organizacija potrebuje plačljiv načrt)",
"Access Details": "Podrobnosti o dostopu",
"All Documents": "Vsi dokumenti",
@ -168,7 +168,7 @@
"Requires edit permissions": "Zahteva dovoljenja za urejanje",
"Other Sites": "Druga spletna mesta",
"Pinned Documents": "Pripeti dokumenti",
"To restore this document, restore the workspace first.": "Če želite obnoviti ta dokument, najprej obnovite delovni prostor.",
"To restore this document, restore the workspace first.": "Če želiš obnoviti ta dokument, najprej obnovi delovni prostor.",
"You are on your personal site. You also have access to the following sites:": "Nahajate se na svojem osebnem spletnem mestu. Prav tako imate dostop do naslednjih spletnih mest:",
"Restore": "Obnovi",
"Move {{name}} to workspace": "Premakni {{name}} v delovni prostor",
@ -260,7 +260,8 @@
"Copy anchor link": "Kopiraj sidrno povezavo",
"Duplicate rows_one": "Podvoji vrstico",
"Duplicate rows_other": "Podvoji vrstice",
"Insert row above": "Vstavi vrstico zgoraj"
"Insert row above": "Vstavi vrstico zgoraj",
"View as card": "Kartični pogled"
},
"Tools": {
"Delete": "Izbriši",
@ -298,7 +299,13 @@
"Duplicate Table": "Podvojena tabela",
"Table ID copied to clipboard": "ID tabele kopiran v odložišče",
"You do not have edit access to this document": "Nimate dostopa za urejanje tega dokumenta",
"Raw Data Tables": "Neobdelana tabela"
"Raw Data Tables": "Neobdelana tabela",
"Edit Record Card": "Uredi evidenčno kartico",
"Rename Table": "Preimenuj Tabelo",
"{{action}} Record Card": "{{action}} Evidenčno Kartico",
"Record Card": "Evidenčna kartica",
"Remove Table": "Odstrani Tabelo",
"Record Card Disabled": "Evidenčna kartica onemogočena"
},
"ViewLayoutMenu": {
"Delete record": "Brisanje zapisa",
@ -590,7 +597,7 @@
"Organization": "Organizacija",
"Replacing the original requires editing rights on the original document.": "Za zamenjavo izvirnika so potrebne pravice za urejanje izvirnega dokumenta.",
"Remove document history (can significantly reduce file size)": "Odstranitev zgodovine dokumenta (lahko znatno zmanjša velikost datoteke)",
"To save your changes, please sign up, then reload this page.": "Če želite shraniti spremembe, se prijavite in nato ponovno naložite to stran.",
"To save your changes, please sign up, then reload this page.": "Če želiš shraniti spremembe, se prijavi in nato ponovno naloži to stran.",
"The original version of this document will be updated.": "Prvotna različica tega dokumenta bo posodobljena.",
"However, it appears to be already identical.": "Vendar se zdi, da je že identična.",
"Update Original": "Posodobitev izvirnika",
@ -642,7 +649,7 @@
"Access rules give you the power to create nuanced rules to determine who can see or edit which parts of your document.": "Pravila dostopa vam omogočajo, da ustvarite podrobna pravila, s katerimi določite, kdo lahko vidi ali ureja posamezne dele dokumenta.",
"Rearrange the fields in your card by dragging and resizing cells.": "Z vlečenjem in spreminjanjem velikosti polj na kartici spremenite njihovo razporeditev.",
"Useful for storing the timestamp or author of a new record, data cleaning, and more.": "Uporabno za shranjevanje časovnega žiga ali avtorja novega zapisa, čiščenje podatkov in drugo.",
"Click the Add New button to create new documents or workspaces, or import data.": "Če želite ustvariti nove dokumente ali delovne prostore ali uvoziti podatke, kliknite gumb Dodaj.",
"Click the Add New button to create new documents or workspaces, or import data.": "Klikni gumb dodaj novega, če želiš ustvariti nove dokumente, delovne prostore ali uvoziti podatke,",
"Nested Filtering": "Vgnezdeno filtriranje",
"Only those rows will appear which match all of the filters.": "Prikazane bodo samo tiste vrstice, ki ustrezajo vsem filtrom.",
"Editing Card Layout": "Urejanje postavitve kartice",
@ -655,11 +662,11 @@
"They allow for one record to point (or refer) to another.": "Omogočajo, da en zapis kaže (ali se sklicuje) na drugega.",
"Reference Columns": "Referenčni stolpci",
"To configure your calendar, select columns for start": {
"end dates and event titles. Note each column's type.": "Če želite konfigurirati svoj koledar, izberite stolpce za začetne/končne datume in naslove dogodkov. Upoštevajte vrsto vsakega stolpca."
"end dates and event titles. Note each column's type.": "Če želiš konfigurirati svoj koledar, izberi stolpce za začetne/končne datume in naslove dogodkov. Upoštevaj vrsto vsakega stolpca."
},
"Calendar": "Koledar",
"Apply conditional formatting to cells in this column when formula conditions are met.": "Uporabi pogojno oblikovanje za celice v tem stolpcu, ko so izpolnjeni pogoji formule.",
"To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Če želite narediti sidrno povezavo, ki uporabnika pripelje do določene celice, kliknite vrstico in pritisnite {{shortcut}}.",
"To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Če želiš narediti sidrno povezavo, ki uporabnika pripelje do določene celice, klikni vrstico in pritisni {{shortcut}}.",
"Unpin to hide the the button while keeping the filter.": "Odpnite, da skrijete gumb in obdržite filter.",
"Apply conditional formatting to rows based on formulas.": "Uporabi pogojno oblikovanje za vrstice na podlagi formul.",
"Click on “Open row styles” to apply conditional formatting to rows.": "Klikni »Odpri sloge vrstic«, da za vrstice uporabiš pogojno oblikovanje.",
@ -707,14 +714,14 @@
"Close": "Zapri",
"Allow anyone with the link to open.": "Omogočite odprtje vsakomur, ki ima povezavo.",
"Invite people to {{resourceType}}": "Povabite ljudi k {{resourceType}}",
"Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Uporabnik podeduje dovoljenja od {{parent}}. Če jih želite odstraniti, nastavite možnost \"Podeduje dostop\" na \"Ne\".",
"Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Uporabnik podeduje dovoljenja od {{parent}}. Če jih želiš odstraniti, nastavi možnost \"Podeduje dostop\" na \"Ne\".",
"Remove my access": "Odstranitev mojega dostopa",
"Public access": "Javni dostop",
"Public Access": "Javni dostop",
"Cancel": "Prekliči",
"Grist support": "Grist podpora",
"You are about to remove your own access to this {{resourceType}}": "Odstranili boste svoj dostop do tega {{resourceType}}",
"User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Uporabnik podeduje dovoljenja od {{parent}}. Če jih želite odstraniti, nastavite možnost \"Podeduje dostop\" na \"Ni\".",
"User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Uporabnik podeduje dovoljenja od {{parent}}. Če jih želiš odstraniti, nastavi možnost \"Podeduje dostop\" na \"Ni\".",
"Guest": "Gost",
"Invite multiple": "Povabite več",
"Confirm": "Potrdite",
@ -813,7 +820,7 @@
"Welcome to Grist!": "Dobrodošli v Gristu!",
"Visit our {{link}} to learn more about Grist.": "Obiščite našo spletno stran {{link}} da izveste več o Grisstu.",
"Sign in": "Prijavi se",
"To use Grist, please either sign up or sign in.": "Če želite uporabljati Grist, se prijavite ali prvič prijavite."
"To use Grist, please either sign up or sign in.": "Če želiš uporabljati Grist, se prijavi ali prvič prijavi."
},
"WelcomeSitePicker": {
"You have access to the following Grist sites.": "Imate dostop do naslednjih Grist spletnih mest .",
@ -1231,5 +1238,13 @@
},
"sendToDrive": {
"Sending file to Google Drive": "Pošiljanje datoteke v Google Drive"
},
"CardContextMenu": {
"Insert card above": "Vstavi kartico zgoraj",
"Duplicate card": "Podvoji kartico",
"Insert card below": "Vstavi kartico spodaj",
"Delete card": "Briši kartico",
"Copy anchor link": "Kopiraj sidrno povezavo",
"Insert card": "Vstavi kartico"
}
}