1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Server: Add support for separate admin instance

This commit is contained in:
Laurent Cozic 2023-07-26 14:36:13 +01:00
parent c718706f9c
commit d78ab16021
9 changed files with 67 additions and 16 deletions

View File

@ -307,7 +307,7 @@ async function main() {
}
appLogger().info('Starting services...');
await startServices(ctx.joplinBase.services);
await startServices(config(), ctx.joplinBase.services);
appLogger().info(`Call this for testing: \`curl ${config().apiBaseUrl}/api/ping\``);

View File

@ -23,6 +23,11 @@ const defaultEnvValues: EnvVariables = {
COOKIES_SECURE: false,
RUNNING_IN_DOCKER: false,
// The admin panel is accessible only if this is an admin instance.
// Additionally, processing services (those defined in setupTaskService.ts)
// only run on the admin instance.
IS_ADMIN_INSTANCE: true,
// Maxiumm allowed drift between NTP time and server time. A few
// milliseconds is normally not an issue unless many clients are modifying
// the same note at the exact same time. But past a certain limit, it might
@ -152,6 +157,8 @@ export interface EnvVariables {
USER_DATA_AUTO_DELETE_ENABLED: boolean;
USER_DATA_AUTO_DELETE_AFTER_DAYS: number;
IS_ADMIN_INSTANCE: boolean;
}
const parseBoolean = (s: string): boolean => {

View File

@ -1,5 +1,6 @@
import config from '../config';
import { ErrorForbidden } from '../utils/errors';
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, koaNext, expectNotThrow, expectHttpError, createUserAndSession } from '../utils/testing/testUtils';
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, koaNext, expectNotThrow, expectHttpError, createUserAndSession, models } from '../utils/testing/testUtils';
import checkAdminHandler from './checkAdminHandler';
describe('checkAdminHandler', () => {
@ -55,4 +56,26 @@ describe('checkAdminHandler', () => {
await expectHttpError(async () => checkAdminHandler(context, koaNext), ErrorForbidden.httpCode);
});
test('should not be able to perform requests if logged in as an admin on a non-admin instance', async () => {
const { session } = await createUserAndSession(1, true);
const prev = config().IS_ADMIN_INSTANCE;
config().IS_ADMIN_INSTANCE = false;
const context = await koaAppContext({
sessionId: session.id,
request: {
method: 'GET',
url: '/login',
},
});
await expectHttpError(async () => checkAdminHandler(context, koaNext), ErrorForbidden.httpCode);
// Should have been logged out too
expect(await models().session().exists(session.id)).toBe(false);
config().IS_ADMIN_INSTANCE = prev;
});
});

View File

@ -1,11 +1,23 @@
import { AppContext, KoaNext } from '../utils/types';
import { isAdminRequest } from '../utils/requestUtils';
import { ErrorForbidden } from '../utils/errors';
import config from '../config';
import webLogout from '../utils/webLogout';
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
const owner = ctx.joplin.owner;
if (isAdminRequest(ctx)) {
if (!ctx.joplin.owner) throw new ErrorForbidden();
if (!ctx.joplin.owner.is_admin) throw new ErrorForbidden();
if (!config().IS_ADMIN_INSTANCE) throw new ErrorForbidden();
if (!owner || !owner.is_admin) throw new ErrorForbidden();
}
// This can happen if an instance is switched from admin to non-admin. In
// that case, the user is still logged in as an admin, but on a non-admin
// instance so we log him out.
if (owner && owner.is_admin && !config().IS_ADMIN_INSTANCE) {
await webLogout(ctx);
throw new ErrorForbidden();
}
return next();

View File

@ -3,16 +3,12 @@ import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import config from '../../config';
import { contextSessionId } from '../../utils/requestUtils';
import { cookieSet } from '../../utils/cookies';
import webLogout from '../../utils/webLogout';
const router = new Router(RouteType.Web);
router.post('logout', async (_path: SubPath, ctx: AppContext) => {
const sessionId = contextSessionId(ctx, false);
cookieSet(ctx, 'sessionId', '');
cookieSet(ctx, 'adminSessionId', '');
await ctx.joplin.models.session().logout(sessionId);
await webLogout(ctx);
return redirect(ctx, `${config().baseUrl}/login`);
});

View File

@ -7,7 +7,6 @@ import TaskService, { RunType, Task } from './TaskService';
const newService = () => {
return new TaskService(Env.Dev, models(), config(), {
share: null,
email: null,
mustache: null,
tasks: null,

View File

@ -18,11 +18,12 @@ async function setupServices(env: Env, models: Models, config: Config): Promise<
tasks: null,
};
output.tasks = await setupTaskService(env, models, config, output),
await output.mustache.loadPartials();
await output.email.checkConfiguration();
if (config.IS_ADMIN_INSTANCE) {
await output.email.checkConfiguration();
output.tasks = await setupTaskService(env, models, config, output);
}
return output;
}

View File

@ -1,5 +1,8 @@
import { Services } from '../services/types';
import { Config } from './types';
export default async function startServices(services: Services) {
void services.tasks.runInBackground();
export default async function startServices(config: Config, services: Services) {
if (config.IS_ADMIN_INSTANCE) {
void services.tasks.runInBackground();
}
}

View File

@ -0,0 +1,10 @@
import { cookieSet } from './cookies';
import { contextSessionId } from './requestUtils';
import { AppContext } from './types';
export default async (ctx: AppContext) => {
const sessionId = contextSessionId(ctx, false);
cookieSet(ctx, 'sessionId', '');
cookieSet(ctx, 'adminSessionId', '');
await ctx.joplin.models.session().logout(sessionId);
};