Skip to content

Commit

Permalink
Merge pull request #4328 from coralproject/feat/CORL-2892-allow-addit…
Browse files Browse the repository at this point in the history
…ional-origins

[CORL-2892]: allow additional origins for oembed api calls
  • Loading branch information
tessalt authored Sep 26, 2023
2 parents 106f9a6 + 4167907 commit 296634a
Show file tree
Hide file tree
Showing 15 changed files with 167 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { AdvancedConfigContainer_settings } from "coral-admin/__generated__/Adva
import AMPConfig from "./AMPConfig";
import CommentStreamLiveUpdatesContainer from "./CommentStreamLiveUpdatesContainer";
import CustomCSSConfig from "./CustomCSSConfig";
import EmbeddedCommentRepliesConfig from "./EmbeddedCommentRepliesConfig";
import EmbeddedCommentsConfig from "./EmbeddedCommentsConfig";
import ForReviewQueueConfig from "./ForReviewQueueConfig";
import StoryCreationConfig from "./StoryCreationConfig";

Expand All @@ -31,7 +31,7 @@ const AdvancedConfigContainer: React.FunctionComponent<Props> = ({
return (
<HorizontalGutter size="double" data-testid="configure-advancedContainer">
<CustomCSSConfig disabled={submitting} />
<EmbeddedCommentRepliesConfig disabled={submitting} />
<EmbeddedCommentsConfig disabled={submitting} />
<CommentStreamLiveUpdatesContainer
disabled={submitting}
settings={settings}
Expand All @@ -47,7 +47,7 @@ const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment AdvancedConfigContainer_settings on Settings {
...CustomCSSConfig_formValues @relay(mask: false)
...EmbeddedCommentRepliesConfig_formValues @relay(mask: false)
...EmbeddedCommentsConfig_formValues @relay(mask: false)
...CommentStreamLiveUpdates_formValues @relay(mask: false)
...StoryCreationConfig_formValues @relay(mask: false)
...CommentStreamLiveUpdatesContainer_settings
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Localized } from "@fluent/react/compat";
import React, { FunctionComponent } from "react";
import { graphql } from "react-relay";

import { FormField, HelperText, Label } from "coral-ui/components/v2";

import ConfigBox from "../../ConfigBox";
import Header from "../../Header";
import OnOffField from "../../OnOffField";
import Subheader from "../../Subheader";
import AllowedOriginsTextarea from "../Sites/AllowedOriginsTextarea";

// eslint-disable-next-line no-unused-expressions
graphql`
fragment EmbeddedCommentsConfig_formValues on Settings {
embeddedComments {
allowReplies
oEmbedAllowedOrigins
}
}
`;

interface Props {
disabled: boolean;
}

const EmbeddedCommentsConfig: FunctionComponent<Props> = ({ disabled }) => (
<ConfigBox
data-testid="embedded-comments-config"
title={
<Localized id="configure-advanced-embeddedComments">
<Header htmlFor="configure-advanced-embeddedComments">
Embedded comments
</Header>
</Localized>
}
>
<FormField>
<Localized id="configure-advanced-embeddedCommentReplies-label">
<Label>Allow replies to embedded comments</Label>
</Localized>
<Localized id="configure-advanced-embeddedCommentReplies-explanation">
<HelperText>
When enabled, a reply button will appear with each embedded comment to
encourage additional discussion on that specific comment or story.
</HelperText>
</Localized>
<OnOffField name="embeddedComments.allowReplies" disabled={disabled} />
</FormField>
<Localized id="configure-advanced-embeddedComments-subheader">
<Subheader>For sites using oEmbed</Subheader>
</Localized>
<FormField>
<Localized id="configure-advanced-oembedAllowedOrigins-label">
<Label>oEmbed permitted domains</Label>
</Localized>
<Localized id="configure-advanced-oembedAllowedOrigins-description">
<HelperText>
Domains that are permitted to make calls to the oEmbed API (ex.
http://localhost:3000, https://staging.domain.com,
https://domain.com).
</HelperText>
</Localized>
<AllowedOriginsTextarea
name="embeddedComments.oEmbedAllowedOrigins"
disabled={disabled}
/>
</FormField>
</ConfigBox>
);

export default EmbeddedCommentsConfig;
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ import ValidationMessage from "../../ValidationMessage";
import styles from "./AllowedOriginsTextarea.css";

interface Props {
name: string;
defaultValue?: ReadonlyArray<string>;
disabled?: boolean;
}

const AllowedOriginsTextarea: FunctionComponent<Props> = ({ defaultValue }) => (
const AllowedOriginsTextarea: FunctionComponent<Props> = ({
name,
defaultValue,
disabled = false,
}) => (
<Field
name="allowedOrigins"
name={name}
parse={parseStringList}
format={formatStringList}
validate={validateStrictURLList}
Expand All @@ -32,6 +38,7 @@ const AllowedOriginsTextarea: FunctionComponent<Props> = ({ defaultValue }) => (
autoCapitalize="off"
spellCheck={false}
fullwidth
disabled={disabled}
/>
<ValidationMessage meta={meta} />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const CreateSiteForm: FunctionComponent<Props> = ({ onCreate }) => {
</HelperText>
</Localized>
</FormFieldHeader>
<AllowedOriginsTextarea />
<AllowedOriginsTextarea name="allowedOrigins" />
</FormField>
{submitError && (
<CallOut fullWidth color="error">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ const EditSiteForm: FunctionComponent<Props> = ({
</HelperText>
</Localized>
</FormFieldHeader>
<AllowedOriginsTextarea defaultValue={site.allowedOrigins} />
<AllowedOriginsTextarea
defaultValue={site.allowedOrigins}
name="allowedOrigins"
/>
</FormField>
<FormField>
<FormFieldHeader>
Expand Down
44 changes: 42 additions & 2 deletions client/src/core/client/admin/test/configure/advanced.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ it("renders configure advanced", async () => {
const { configureContainer } = await createTestRenderer();
expect(within(configureContainer).getByLabelText("Custom CSS")).toBeDefined();
expect(
within(configureContainer).getByText("Embedded comment replies")
within(configureContainer).getByText("Embedded comments")
).toBeDefined();
expect(
within(configureContainer).getByText("Comment stream live updates")
Expand Down Expand Up @@ -223,7 +223,7 @@ it("change embedded comments allow replies", async () => {
});

const embeddedCommentReplies = within(advancedContainer).getByTestId(
"embedded-comment-replies-config"
"embedded-comments-config"
);

const offField = within(embeddedCommentReplies).getByText("Off");
Expand All @@ -241,3 +241,43 @@ it("change embedded comments allow replies", async () => {
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
});

it("change oembed permitted domains", async () => {
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(
variables.settings.embeddedComments?.oEmbedAllowedOrigins
).toEqual(["http://localhost:8080"]);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const { advancedContainer, saveChangesButton } = await createTestRenderer({
resolvers,
});

const oembedAllowedOriginsConfig = within(advancedContainer).getByTestId(
"embedded-comments-config"
);

const allowedOriginsTextArea = within(oembedAllowedOriginsConfig).getByRole(
"textbox"
);

userEvent.type(allowedOriginsTextArea, "http://");

userEvent.click(saveChangesButton);

expect(within(advancedContainer).getByText("Invalid URL"));

userEvent.type(allowedOriginsTextArea, "localhost:8080");

userEvent.click(saveChangesButton);

await waitFor(() => {
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
});
1 change: 1 addition & 0 deletions client/src/core/client/admin/test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export const settings = createFixture<GQLSettings>({
emailDomainModeration: [],
embeddedComments: {
allowReplies: true,
oEmbedAllowedOrigins: [],
},
flairBadges: {
flairBadgesEnabled: false,
Expand Down
7 changes: 6 additions & 1 deletion locales/en-US/admin.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -905,12 +905,17 @@ configure-advanced-customCSS-containsFontFace =
URL to a custom CSS stylesheet that contains all @font-face
definitions needed by above stylesheet.
configure-advanced-embeddedCommentReplies = Embedded comment replies
configure-advanced-embeddedComments = Embedded comments
configure-advanced-embeddedComments-subheader = For sites using oEmbed
configure-advanced-embeddedCommentReplies-explanation = When enabled, a reply button
will appear with each embedded comment to encourage additional discussion on that
specific comment or story.
configure-advanced-embeddedCommentReplies-label = Allow replies to embedded comments
configure-advanced-oembedAllowedOrigins-header = oEmbed permitted domains
configure-advanced-oembedAllowedOrigins-description = Domains that are permitted to make calls to the oEmbed API (ex. http://localhost:3000, https://staging.domain.com, https://domain.com).
configure-advanced-oembedAllowedOrigins-label = oEmbed permitted domains
configure-advanced-permittedDomains = Permitted domains
configure-advanced-permittedDomains-description =
Domains where your { -product-name } instance is allowed to be embedded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { AppOptions } from "..";
import { getRequesterOrigin } from "../helpers";

export const commentEmbedWhitelisted =
({ mongo }: Pick<AppOptions, "mongo">): RequestHandler =>
({ mongo }: Pick<AppOptions, "mongo">, oembedAPI = false): RequestHandler =>
async (req, res, next) => {
// First try to get the commentID from the query params
let { commentID } = req.query;
Expand Down Expand Up @@ -37,6 +37,13 @@ export const commentEmbedWhitelisted =
origin = req.header("Origin");
}
if (origin) {
// if oEmbed API call, we also check oEmbed allowed origins on tenant
if (
oembedAPI &&
tenant.embeddedComments?.oEmbedAllowedOrigins.includes(origin)
) {
return next();
}
if (site.allowedOrigins.includes(origin)) {
return next();
}
Expand Down
2 changes: 1 addition & 1 deletion server/src/core/server/app/router/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function createAPIRouter(app: AppOptions, options: RouterOptions) {
router.get("/oembed", cspSiteMiddleware(app), oembedHandler(app));
router.get(
"/services/oembed",
commentEmbedWhitelisted(app),
commentEmbedWhitelisted(app, true),
cors(createCommentEmbedCorsOptionsDelegate(app.mongo)),
oembedProviderHandler(app)
);
Expand Down
9 changes: 7 additions & 2 deletions server/src/core/server/graph/resolvers/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ export const Settings: GQLSettingsTypeResolver<Tenant> = {
return deprecated;
},
embeddedComments: (
{ embeddedComments = { allowReplies: true } },
{ embeddedComments = { allowReplies: true, oEmbedAllowedOrigins: [] } },
args,
ctx
) => embeddedComments,
) => {
return {
allowReplies: embeddedComments.allowReplies ?? true,
oEmbedAllowedOrigins: embeddedComments.oEmbedAllowedOrigins ?? [],
};
},
flairBadges: ({
flairBadges = { flairBadgesEnabled: false, badges: [] },
}) => {
Expand Down
9 changes: 8 additions & 1 deletion server/src/core/server/graph/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1673,6 +1673,10 @@ EmbeddedCommentsConfiguration specifies the configuration for embedded comments.
"""
type EmbeddedCommentsConfiguration {
allowReplies: Boolean
"""
oEmbedAllowedOrigins are the allowed origins for oEmbed API calls.
"""
oEmbedAllowedOrigins: [String!]! @auth(roles: [ADMIN, MODERATOR])
}

"""
Expand Down Expand Up @@ -1710,7 +1714,6 @@ type BadgeConfiguration {
FlairBadgeConfiguration specifies the configuration for flair badges, including
whether they are enabled and any configured image urls.
"""

type FlairBadge {
name: String!
url: String!
Expand Down Expand Up @@ -5568,6 +5571,10 @@ EmbeddedCommentsConfigurationInput specifies the configuration for comment embed
"""
input EmbeddedCommentsConfigurationInput {
allowReplies: Boolean
"""
oEmbedAllowedOrigins are the allowed origins for oEmbed API calls.
"""
oEmbedAllowedOrigins: [String!]
}

"""
Expand Down
1 change: 1 addition & 0 deletions server/src/core/server/models/tenant/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ export async function createTenant(
emailDomainModeration: [],
embeddedComments: {
allowReplies: true,
oEmbedAllowedOrigins: [],
},
flairBadges: {
flairBadgesEnabled: false,
Expand Down
4 changes: 4 additions & 0 deletions server/src/core/server/test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ export const createTenantFixture = (
flattenReplies: false,
disableDefaultFonts: false,
emailDomainModeration: [],
embeddedComments: {
allowReplies: true,
oEmbedAllowedOrigins: [],
},
};

return merge(fixture, defaults);
Expand Down

0 comments on commit 296634a

Please sign in to comment.