[CORL-416] Disable Live Updates (#2391)

* feat: initial implementation

* fix: docs
This commit is contained in:
Wyatt Johnson
2019-07-05 23:10:19 +00:00
committed by GitHub
parent da1fa9c9fc
commit e7745a85aa
41 changed files with 626 additions and 28 deletions
+8
View File
@@ -181,6 +181,14 @@ const config = convict({
env: "DISABLE_TENANT_CACHING",
arg: "disableTenantCaching",
},
disable_live_updates: {
doc:
"Disables subscriptions for the comment stream for all stories across all tenants",
format: Boolean,
default: false,
env: "DISABLE_LIVE_UPDATES",
arg: "disableLiveUpdates",
},
disable_mongodb_autoindexing: {
doc: "Disables the creation of new MongoDB indexes",
format: Boolean,
+8
View File
@@ -643,3 +643,11 @@ export class InviteRequiresEmailAddresses extends CoralError {
});
}
}
export class LiveUpdatesDisabled extends CoralError {
constructor() {
super({
code: ERROR_CODES.LIVE_UPDATES_DISABLED,
});
}
}
+1
View File
@@ -48,4 +48,5 @@ export const ERROR_TRANSLATIONS: Record<ERROR_CODES, string> = {
JWT_REVOKED: "error-jwtRevoked",
INVITE_TOKEN_EXPIRED: "error-inviteTokenExpired",
INVITE_REQUIRES_EMAIL_ADDRESSES: "error-inviteRequiresEmailAddresses",
LIVE_UPDATES_DISABLED: "error-liveUpdatesDisabled",
};
@@ -8,9 +8,10 @@ export const Settings = ({
redis,
tenantCache,
tenant,
config,
}: TenantContext) => ({
update: (input: GQLUpdateSettingsInput): Promise<Tenant | null> =>
update(mongo, redis, tenantCache, tenant, input.settings),
update(mongo, redis, tenantCache, config, tenant, input.settings),
regenerateSSOKey: (): Promise<Tenant | null> =>
regenerateSSOKey(mongo, redis, tenantCache, tenant),
});
@@ -0,0 +1,24 @@
import { isUndefined } from "lodash";
import { GQLLiveConfigurationTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
import * as settings from "coral-server/models/settings";
export type LiveConfigurationInput = settings.LiveConfiguration;
export const LiveConfiguration: GQLLiveConfigurationTypeResolver<
LiveConfigurationInput
> = {
configurable: (source, args, ctx) =>
Boolean(!ctx.config.get("disable_live_updates")),
enabled: (source, args, ctx) => {
if (ctx.config.get("disable_live_updates")) {
return false;
}
if (isUndefined(source.enabled)) {
return ctx.tenant.live.enabled;
}
return source.enabled;
},
};
@@ -5,6 +5,7 @@ import { GQLStorySettingsTypeResolver } from "../schema/__generated__/types";
export const StorySettings: GQLStorySettingsTypeResolver<
story.StorySettings
> = {
live: s => s.live || {},
moderation: (s, input, ctx) => s.moderation || ctx.tenant.moderation,
premodLinksEnable: (s, input, ctx) =>
s.premodLinksEnable || ctx.tenant.premodLinksEnable,
@@ -22,6 +22,7 @@ import { FeatureCommentPayload } from "./FeatureCommentPayload";
import { Flag } from "./Flag";
import { GoogleAuthIntegration } from "./GoogleAuthIntegration";
import { Invite } from "./Invite";
import { LiveConfiguration } from "./LiveConfiguration";
import { ModerationQueue } from "./ModerationQueue";
import { ModerationQueues } from "./ModerationQueues";
import { Mutation } from "./Mutation";
@@ -60,6 +61,7 @@ const Resolvers: GQLResolver = {
Flag,
GoogleAuthIntegration,
Invite,
LiveConfiguration,
ModerationQueue,
ModerationQueues,
Mutation,
@@ -1086,6 +1086,12 @@ type Settings {
"""
locale: LOCALES!
"""
live provides configuration options related to live updates for stories on
this site.
"""
live: LiveConfiguration!
"""
moderation is the moderation mode for all Stories on the site.
"""
@@ -2061,7 +2067,27 @@ type StoryMetadata {
section: String
}
"""
LiveConfiguration provides configuration options related to live updates.
"""
type LiveConfiguration {
"""
configurable when false indicates that live updates cannot be modified.
"""
configurable: Boolean!
"""
enabled when true will allow live updates.
"""
enabled: Boolean!
}
type StorySettings {
"""
live provides configuration options related to live updates on this Story.
"""
live: LiveConfiguration!
"""
moderation determines whether or not this is a PRE or POST moderated story.
"""
@@ -2921,6 +2947,12 @@ input StoryConfigurationInput {
SettingsInput is the partial type of the Settings type for performing mutations.
"""
input SettingsInput {
"""
live provides configuration options related to live updates for stories on
this site.
"""
live: LiveConfigurationInput
"""
allowedDomains is the list of domains that stories can come from.
"""
@@ -3401,10 +3433,25 @@ input StoryMessageBoxInput {
content: String
}
"""
LiveConfigurationInput provides configuration options related to live updates.
"""
input LiveConfigurationInput {
"""
enabled when true will allow live updates.
"""
enabled: Boolean
}
"""
UpdateStorySettings is the input required to update a Story's Settings.
"""
input UpdateStorySettings {
"""
live provides configuration options related to live updates on this Story.
"""
live: LiveConfigurationInput
"""
moderation determines whether or not this is a PRE or POST moderated story.
"""
@@ -24,6 +24,7 @@ import {
import {
CoralError,
InternalError,
LiveUpdatesDisabled,
TenantNotFoundError,
} from "coral-server/errors";
import {
@@ -35,6 +36,7 @@ import { getOperationMetadata } from "coral-server/graph/common/extensions/helpe
import logger from "coral-server/logger";
import { extractTokenFromRequest } from "coral-server/services/jwt";
import { userIsStaff } from "coral-server/models/user/helpers";
import TenantContext, { TenantContextOptions } from "../context";
type OnConnectFn = (
@@ -121,6 +123,17 @@ export function onConnect(options: OnConnectOptions): OnConnectFn {
}
}
// Check to see if live updates are disabled on the server, if they are,
// we can block the websocket request here for non-staff users.
if (options.config.get("disable_live_updates")) {
// TODO: (wyattjoh) if the story settings can only disable, and not
// enable live updates (as it takes precedence over global settings)
// then we can add a check for `!tenant.live.enabled` here too.
if (!opts.user || !userIsStaff(opts.user)) {
throw new LiveUpdatesDisabled();
}
}
// Extract the users clientID from the request.
const clientID = extractClientID(connectionParams);
if (clientID) {
@@ -129,7 +142,11 @@ export function onConnect(options: OnConnectOptions): OnConnectFn {
return new TenantContext(opts);
} catch (err) {
logger.error({ err }, "could not setup websocket connection");
if (err instanceof LiveUpdatesDisabled) {
logger.info({ err }, "websocket connection rejected");
} else {
logger.error({ err }, "could not setup websocket connection");
}
if (!(err instanceof CoralError)) {
err = new InternalError(err, "could not setup websocket connection");
+4
View File
@@ -3,6 +3,7 @@ import {
GQLAuth,
GQLFacebookAuthIntegration,
GQLGoogleAuthIntegration,
GQLLiveConfiguration,
GQLLocalAuthIntegration,
GQLMODERATION_MODE,
GQLOIDCAuthIntegration,
@@ -10,7 +11,10 @@ import {
GQLSSOAuthIntegration,
} from "coral-server/graph/tenant/schema/__generated__/types";
export type LiveConfiguration = Omit<GQLLiveConfiguration, "configurable">;
export interface GlobalModerationSettings {
live: LiveConfiguration;
moderation: GQLMODERATION_MODE;
premodLinksEnable: boolean;
}
+4 -1
View File
@@ -18,6 +18,7 @@ import {
createIndexFactory,
} from "coral-server/models/helpers/indexing";
import Query from "coral-server/models/helpers/query";
import { GlobalModerationSettings } from "coral-server/models/settings";
import { TenantResource } from "coral-server/models/tenant";
import {
@@ -33,7 +34,9 @@ function collection<T = Story>(mongo: Db) {
return mongo.collection<Readonly<T>>("stories");
}
export type StorySettings = DeepPartial<GQLStorySettings>;
export type StorySettings = DeepPartial<
Pick<GQLStorySettings, "messageBox"> & GlobalModerationSettings
>;
export type StoryMetadata = GQLStoryMetadata;
+5
View File
@@ -81,6 +81,11 @@ export async function createTenant(
// Default to post moderation.
moderation: GQLMODERATION_MODE.POST,
// Default to enabled.
live: {
enabled: true,
},
communityGuidelines: {
enabled: false,
content: "",
@@ -35,7 +35,4 @@ export const commentLength: IntermediateModerationPhase = ({
// Reject if the comment is too long or too short.
testCharCount(tenant, length);
if (story.settings) {
testCharCount(story.settings, length);
}
};
@@ -5,18 +5,13 @@ import {
IntermediatePhaseResult,
} from "coral-server/services/comments/pipeline";
const testDisabledCommenting = (settings: Partial<Settings>) =>
const testDisabledCommenting = (settings: Settings) =>
settings.disableCommenting && settings.disableCommenting.enabled;
export const commentingDisabled: IntermediateModerationPhase = ({
story,
tenant,
}): IntermediatePhaseResult | void => {
// Check to see if the story has closed commenting.
if (
testDisabledCommenting(tenant) ||
(story.settings && testDisabledCommenting(story.settings))
) {
if (testDisabledCommenting(tenant)) {
throw new CommentingDisabledError();
}
};
@@ -1,3 +1,4 @@
import { DeepPartial } from "coral-common/types";
import {
GQLCOMMENT_FLAG_REASON,
GQLCOMMENT_STATUS,
@@ -11,7 +12,7 @@ import {
} from "coral-server/services/comments/pipeline";
const testPremodLinksEnable = (
settings: Partial<GlobalModerationSettings>,
settings: DeepPartial<GlobalModerationSettings>,
comment: Pick<Comment, "metadata">
) =>
settings.premodLinksEnable && comment.metadata && comment.metadata.linkCount;
@@ -1,3 +1,4 @@
import { DeepPartial } from "coral-common/types";
import {
GQLCOMMENT_STATUS,
GQLMODERATION_MODE,
@@ -8,7 +9,7 @@ import {
IntermediatePhaseResult,
} from "coral-server/services/comments/pipeline";
const testModerationMode = (settings: Partial<GlobalModerationSettings>) =>
const testModerationMode = (settings: DeepPartial<GlobalModerationSettings>) =>
settings.moderation === GQLMODERATION_MODE.PRE;
// This phase checks to see if the settings have premod enabled, if they do,
+16 -3
View File
@@ -1,8 +1,13 @@
import { Redis } from "ioredis";
import { isUndefined } from "lodash";
import { Db } from "mongodb";
import { URL } from "url";
import { discover } from "coral-server/app/middleware/passport/strategies/oidc/discover";
import { Config } from "coral-server/config";
import { TenantInstalledAlreadyError } from "coral-server/errors";
import { GQLSettingsInput } from "coral-server/graph/tenant/schema/__generated__/types";
import logger from "coral-server/logger";
import {
createTenant,
CreateTenantInput,
@@ -11,9 +16,6 @@ import {
updateTenant,
} from "coral-server/models/tenant";
import { discover } from "coral-server/app/middleware/passport/strategies/oidc/discover";
import { TenantInstalledAlreadyError } from "coral-server/errors";
import logger from "coral-server/logger";
import TenantCache from "./cache";
export type UpdateTenant = GQLSettingsInput;
@@ -22,9 +24,20 @@ export async function update(
mongo: Db,
redis: Redis,
cache: TenantCache,
config: Config,
tenant: Tenant,
input: UpdateTenant
): Promise<Tenant | null> {
// If the environment variable for disabling live updates is provided, then
// ensure we don't permit changes to the database model.
if (
config.get("disable_live_updates") &&
input.live &&
!isUndefined(input.live.enabled)
) {
delete input.live.enabled;
}
const updatedTenant = await updateTenant(mongo, tenant.id, input);
if (!updatedTenant) {
return null;