mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
* `GRIST_ANON_PLAYGROUND`: When set to 'false' deny anonymous users access to the home page * `GRIST_FORCE_LOGIN`: Much like `GRIST_ANON_PLAYGROUND` but don't support anonymous access at all (features like sharing docs publicly requires authentication) --------- Co-authored-by: Florent FAYOLLE <florent.fayolle@beta.gouv.fr>
This commit is contained in:
parent
dc5ddc27b0
commit
5ff79703b4
@ -286,7 +286,8 @@ GRIST_SERVERS | the types of server to setup. Comma separated values which may c
|
||||
GRIST_SESSION_COOKIE | if set, overrides the name of Grist's cookie
|
||||
GRIST_SESSION_DOMAIN | if set, associates the cookie with the given domain - otherwise defaults to GRIST_DOMAIN
|
||||
GRIST_SESSION_SECRET | a key used to encode sessions
|
||||
GRIST_FORCE_LOGIN | when set to 'true' disables anonymous access
|
||||
GRIST_ANON_PLAYGROUND | When set to 'false' deny anonymous users access to the home page
|
||||
GRIST_FORCE_LOGIN | Much like GRIST_ANON_PLAYGROUND but don't support anonymous access at all (features like sharing docs publicly requires authentication)
|
||||
GRIST_SINGLE_ORG | set to an org "domain" to pin client to that org
|
||||
GRIST_TEMPLATE_ORG | set to an org "domain" to show public docs from that org
|
||||
GRIST_HELP_CENTER | set the help center link ref
|
||||
|
@ -190,7 +190,7 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
||||
public createLeftPane(leftPanelOpen: Observable<boolean>) {
|
||||
return cssLeftPanel(
|
||||
dom.maybe(this.gristDoc, (activeDoc) => [
|
||||
addNewButton(leftPanelOpen,
|
||||
addNewButton({ isOpen: leftPanelOpen },
|
||||
menu(() => addMenu(this.importSources, activeDoc, this.isReadonly.get()), {
|
||||
placement: 'bottom-start',
|
||||
// "Add New" menu should have the same width as the "Add New" button that opens it.
|
||||
|
@ -5,14 +5,27 @@ import {dom, DomElementArg, Observable, styled} from "grainjs";
|
||||
|
||||
const t = makeT(`AddNewButton`);
|
||||
|
||||
export function addNewButton(isOpen: Observable<boolean> | boolean = true, ...args: DomElementArg[]) {
|
||||
export function addNewButton(
|
||||
{
|
||||
isOpen,
|
||||
isDisabled = false,
|
||||
}: {
|
||||
isOpen: Observable<boolean> | boolean,
|
||||
isDisabled?: boolean
|
||||
},
|
||||
...args: DomElementArg[]
|
||||
) {
|
||||
return cssAddNewButton(
|
||||
cssAddNewButton.cls('-open', isOpen),
|
||||
cssAddNewButton.cls('-disabled', isDisabled),
|
||||
// Setting spacing as flex items allows them to shrink faster when there isn't enough space.
|
||||
cssLeftMargin(),
|
||||
cssAddText(t("Add New")),
|
||||
dom('div', {style: 'flex: 1 1 16px'}),
|
||||
cssPlusButton(cssPlusIcon('Plus')),
|
||||
cssPlusButton(
|
||||
cssPlusButton.cls('-disabled', isDisabled),
|
||||
cssPlusIcon('Plus')
|
||||
),
|
||||
dom('div', {style: 'flex: 0 1 16px'}),
|
||||
...args,
|
||||
);
|
||||
@ -47,6 +60,11 @@ export const cssAddNewButton = styled('div', `
|
||||
background-color: ${theme.controlPrimaryHoverBg};
|
||||
--circle-color: ${theme.addNewCircleHoverBg};
|
||||
}
|
||||
|
||||
&-disabled, &-disabled:hover {
|
||||
color: ${theme.controlDisabledFg};
|
||||
background-color: ${theme.controlDisabledBg}
|
||||
}
|
||||
`);
|
||||
const cssLeftMargin = styled('div', `
|
||||
flex: 0 1 24px;
|
||||
@ -72,6 +90,9 @@ const cssPlusButton = styled('div', `
|
||||
border-radius: 14px;
|
||||
background-color: var(--circle-color);
|
||||
text-align: center;
|
||||
&-disabled {
|
||||
background-color: ${theme.controlDisabledBg};
|
||||
}
|
||||
`);
|
||||
const cssPlusIcon = styled(icon, `
|
||||
background-color: ${theme.addNewCircleFg};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {getLoginOrSignupUrl, getLoginUrl, getSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {HomeModel} from 'app/client/models/HomeModel';
|
||||
import {productPill} from 'app/client/ui/AppHeader';
|
||||
import * as css from 'app/client/ui/DocMenuCss';
|
||||
@ -12,6 +12,7 @@ import {cssLink} from 'app/client/ui2018/links';
|
||||
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {Computed, dom, DomContents, styled} from 'grainjs';
|
||||
|
||||
const t = makeT('HomeIntro');
|
||||
@ -112,10 +113,36 @@ function makePersonalIntro(homeModel: HomeModel, user: FullUser) {
|
||||
];
|
||||
}
|
||||
|
||||
function makeAnonIntroWithoutPlayground(homeModel: HomeModel) {
|
||||
return [
|
||||
(!isFeatureEnabled('helpCenter') ? null : cssIntroLine(t("Visit our {{link}} to learn more about Grist.", {
|
||||
link: helpCenterLink()
|
||||
}), testId('welcome-text-no-playground'))),
|
||||
cssIntroLine(t("To use Grist, please either sign up or sign in.")),
|
||||
cssBtnGroup(
|
||||
cssBtn(t("Sign up"), cssButton.cls('-primary'), testId('intro-sign-up'),
|
||||
dom.on('click', () => location.href = getSignupUrl())
|
||||
),
|
||||
cssBtn(t("Sign in"), testId('intro-sign-in'),
|
||||
dom.on('click', () => location.href = getLoginUrl())
|
||||
)
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
function makeAnonIntro(homeModel: HomeModel) {
|
||||
const welcomeToGrist = css.docListHeader(t("Welcome to Grist!"), testId('welcome-title'));
|
||||
|
||||
if (!getGristConfig().enableAnonPlayground) {
|
||||
return [
|
||||
welcomeToGrist,
|
||||
...makeAnonIntroWithoutPlayground(homeModel)
|
||||
];
|
||||
}
|
||||
|
||||
const signUp = cssLink({href: getLoginOrSignupUrl()}, t("Sign up"));
|
||||
return [
|
||||
css.docListHeader(t("Welcome to Grist!"), testId('welcome-title')),
|
||||
welcomeToGrist,
|
||||
cssIntroLine(t("Get started by exploring templates, or creating your first Grist document.")),
|
||||
cssIntroLine(t("{{signUp}} to save your work. ", {signUp}),
|
||||
(!isFeatureEnabled('helpCenter') ? null : t("Visit our {{link}} to learn more.", { link: helpCenterLink() })),
|
||||
|
@ -30,16 +30,17 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
|
||||
const creating = observable<boolean>(false);
|
||||
const renaming = observable<Workspace|null>(null);
|
||||
const isAnonymous = !home.app.currentValidUser;
|
||||
const canCreate = !isAnonymous || getGristConfig().enableAnonPlayground;
|
||||
|
||||
return cssContent(
|
||||
dom.autoDispose(creating),
|
||||
dom.autoDispose(renaming),
|
||||
addNewButton(leftPanelOpen,
|
||||
menu(() => addMenu(home, creating), {
|
||||
addNewButton({ isOpen: leftPanelOpen, isDisabled: !canCreate },
|
||||
canCreate ? menu(() => addMenu(home, creating), {
|
||||
placement: 'bottom-start',
|
||||
// "Add New" menu should have the same width as the "Add New" button that opens it.
|
||||
stretchToSelector: `.${cssAddNewButton.className}`
|
||||
}),
|
||||
}) : null,
|
||||
dom.cls('behavioral-prompt-add-new'),
|
||||
testId('dm-add-new'),
|
||||
),
|
||||
|
@ -604,6 +604,9 @@ export interface GristLoadConfig {
|
||||
// If set, enable anonymous sharing UI elements.
|
||||
supportAnon?: boolean;
|
||||
|
||||
// If set, enable anonymous playground.
|
||||
enableAnonPlayground?: boolean;
|
||||
|
||||
// If set, allow selection of the specified engines.
|
||||
// TODO: move this list to a separate endpoint.
|
||||
supportEngines?: EngineCode[];
|
||||
|
@ -170,6 +170,7 @@ export class DocWorkerApi {
|
||||
const canView = expressWrap(this._assertAccess.bind(this, 'viewers', false));
|
||||
// check document exists (not soft deleted) and user can edit it
|
||||
const canEdit = expressWrap(this._assertAccess.bind(this, 'editors', false));
|
||||
const checkAnonymousCreation = expressWrap(this._checkAnonymousCreation.bind(this));
|
||||
const isOwner = expressWrap(this._assertAccess.bind(this, 'owners', false));
|
||||
// check user can edit document, with soft-deleted documents being acceptable
|
||||
const canEditMaybeRemoved = expressWrap(this._assertAccess.bind(this, 'editors', true));
|
||||
@ -1234,7 +1235,7 @@ export class DocWorkerApi {
|
||||
*
|
||||
* TODO: unify this with the other document creation and import endpoints.
|
||||
*/
|
||||
this._app.post('/api/docs', expressWrap(async (req, res) => {
|
||||
this._app.post('/api/docs', checkAnonymousCreation, expressWrap(async (req, res) => {
|
||||
const userId = getUserId(req);
|
||||
|
||||
let uploadId: number|undefined;
|
||||
@ -1452,6 +1453,17 @@ export class DocWorkerApi {
|
||||
return await this._dbManager.increaseUsage(getDocScope(req), limit, {delta: 1});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow document creation for anonymous users if GRIST_ANONYMOUS_CREATION is set to false.
|
||||
*/
|
||||
private async _checkAnonymousCreation(req: Request, res: Response, next: NextFunction) {
|
||||
const isAnonPlayground = isAffirmative(process.env.GRIST_ANON_PLAYGROUND ?? true);
|
||||
if (isAnonymousUser(req) && !isAnonPlayground) {
|
||||
throw new ApiError('Anonymous document creation is disabled', 403);
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
private async _assertAccess(role: 'viewers'|'editors'|'owners'|null, allowRemoved: boolean,
|
||||
req: Request, res: Response, next: NextFunction) {
|
||||
const scope = getDocScope(req);
|
||||
|
@ -850,10 +850,11 @@ export class FlexServer implements GristServer {
|
||||
baseDomain: this._defaultBaseDomain,
|
||||
});
|
||||
|
||||
const isForced = appSettings.section('login').flag('forced').readBool({
|
||||
const forceLogin = appSettings.section('login').flag('forced').readBool({
|
||||
envVar: 'GRIST_FORCE_LOGIN',
|
||||
});
|
||||
const forcedLoginMiddleware = isForced ? this._redirectToLoginWithoutExceptionsMiddleware : noop;
|
||||
|
||||
const forcedLoginMiddleware = forceLogin ? this._redirectToLoginWithoutExceptionsMiddleware : noop;
|
||||
|
||||
const welcomeNewUser: express.RequestHandler = isSingleUserMode() ?
|
||||
(req, res, next) => next() :
|
||||
|
@ -56,6 +56,7 @@ export function makeGristConfig(options: MakeGristConfigOptons): GristLoadConfig
|
||||
helpCenterUrl: process.env.GRIST_HELP_CENTER || "https://support.getgrist.com",
|
||||
pathOnly,
|
||||
supportAnon: shouldSupportAnon(),
|
||||
enableAnonPlayground: isAffirmative(process.env.GRIST_ANON_PLAYGROUND ?? true),
|
||||
supportEngines: getSupportedEngineChoices(),
|
||||
features: getFeatures(),
|
||||
pageTitleSuffix: configuredPageTitleSuffix(),
|
||||
|
52
test/nbrowser/HomeIntroWithoutPlaygound.ts
Normal file
52
test/nbrowser/HomeIntroWithoutPlaygound.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import {assert, driver} from 'mocha-webdriver';
|
||||
import * as gu from 'test/nbrowser/gristUtils';
|
||||
import {setupTestSuite} from 'test/nbrowser/testUtils';
|
||||
|
||||
describe('HomeIntroWithoutPlayground', function() {
|
||||
this.timeout(40000);
|
||||
setupTestSuite({samples: true});
|
||||
gu.withEnvironmentSnapshot({'GRIST_ANON_PLAYGROUND': false});
|
||||
|
||||
describe("Anonymous on merged-org", function() {
|
||||
it('should show welcome page with signin and signup buttons and "add new" button disabled', async function () {
|
||||
// Sign out
|
||||
const session = await gu.session().personalSite.anon.login();
|
||||
|
||||
// Open doc-menu
|
||||
await session.loadDocMenu('/');
|
||||
|
||||
assert.equal(await driver.find('.test-welcome-title').getText(), 'Welcome to Grist!');
|
||||
assert.match(
|
||||
await driver.find('.test-welcome-text-no-playground').getText(),
|
||||
/Visit our Help Center.*about Grist./
|
||||
);
|
||||
|
||||
// Check the sign-up and sign-in buttons.
|
||||
const getSignUp = async () => await driver.findContent('.test-intro-sign-up', 'Sign up');
|
||||
const getSignIn = async () => await driver.findContent('.test-intro-sign-in', 'Sign in');
|
||||
// Check that these buttons take us to a Grist login page.
|
||||
for (const getButton of [getSignUp, getSignIn]) {
|
||||
const button = await getButton();
|
||||
await button.click();
|
||||
await gu.checkLoginPage();
|
||||
await driver.navigate().back();
|
||||
await gu.waitForDocMenuToLoad();
|
||||
}
|
||||
});
|
||||
|
||||
it('should not allow creating new documents', async function () {
|
||||
// Sign out
|
||||
const session = await gu.session().personalSite.anon.login();
|
||||
|
||||
// Open doc-menu
|
||||
await session.loadDocMenu('/');
|
||||
|
||||
// Check that add-new button is disabled
|
||||
assert.equal(await driver.find('.test-dm-add-new').matches('[class*=-disabled]'), true);
|
||||
|
||||
// Check that add-new menu is not displayed
|
||||
await driver.find('.test-dm-add-new').doClick();
|
||||
assert.equal(await driver.find('.test-dm-new-doc').isPresent(), false);
|
||||
});
|
||||
});
|
||||
});
|
@ -120,6 +120,24 @@ describe('DocApi', function () {
|
||||
testDocApi();
|
||||
});
|
||||
|
||||
describe('With GRIST_ANON_PLAYGROUND disabled', async () => {
|
||||
setup('anon-playground', async () => {
|
||||
const additionalEnvConfiguration = {
|
||||
ALLOWED_WEBHOOK_DOMAINS: `example.com,localhost:${webhooksTestPort}`,
|
||||
GRIST_DATA_DIR: dataDir,
|
||||
GRIST_ANON_PLAYGROUND: 'false'
|
||||
};
|
||||
home = docs = await TestServer.startServer('home,docs', tmpDir, suitename, additionalEnvConfiguration);
|
||||
homeUrl = serverUrl = home.serverUrl;
|
||||
hasHomeApi = true;
|
||||
});
|
||||
|
||||
it('should not allow anonymous users to create new docs', async () => {
|
||||
const resp = await axios.post(`${serverUrl}/api/docs`, null, nobody);
|
||||
assert.equal(resp.status, 403);
|
||||
});
|
||||
});
|
||||
|
||||
// the way these tests are written, non-merged server requires redis.
|
||||
if (process.env.TEST_REDIS_URL) {
|
||||
describe("should work with a home server and a docworker", async () => {
|
||||
|
Loading…
Reference in New Issue
Block a user