[CORL-754] SSO Key Management (#2732)

* feat: initial impl

* feat: clean up redis when keys are deleted

* fix: remove test timeout value
This commit is contained in:
Wyatt Johnson
2020-03-19 20:57:49 +00:00
committed by GitHub
parent 1503b735e5
commit 2dc63d3a27
14 changed files with 1083 additions and 664 deletions
+6 -1
View File
@@ -1,9 +1,11 @@
import DataLoader from "dataloader";
import GraphContext from "coral-server/graph/context";
import { GQLDiscoveredOIDCConfiguration } from "coral-server/graph/schema/__generated__/types";
import { retrieveLastUsedAtTenantSSOKeys } from "coral-server/models/tenant";
import { discoverOIDCConfiguration } from "coral-server/services/tenant";
import { GQLDiscoveredOIDCConfiguration } from "coral-server/graph/schema/__generated__/types";
export default (ctx: GraphContext) => ({
discoverOIDCConfiguration: new DataLoader<
string,
@@ -17,4 +19,7 @@ export default (ctx: GraphContext) => ({
cache: !ctx.disableCaching,
}
),
retrieveSSOKeyLastUsedAt: new DataLoader((kids: string[]) =>
retrieveLastUsedAtTenantSSOKeys(ctx.redis, ctx.tenant.id, kids)
),
});
@@ -3,13 +3,16 @@ import { Tenant } from "coral-server/models/tenant";
import {
createAnnouncement,
createWebhookEndpoint,
deactivateSSOKey,
deleteAnnouncement,
deleteSSOKey,
deleteWebhookEndpoint,
disableFeatureFlag,
disableWebhookEndpoint,
enableFeatureFlag,
enableWebhookEndpoint,
regenerateSSOKey,
rotateSSOKey,
rotateWebhookEndpointSecret,
update,
updateWebhookEndpoint,
@@ -18,10 +21,13 @@ import {
import {
GQLCreateAnnouncementInput,
GQLCreateWebhookEndpointInput,
GQLDeactivateSSOKeyInput,
GQLDeleteSSOKeyInput,
GQLDeleteWebhookEndpointInput,
GQLDisableWebhookEndpointInput,
GQLEnableWebhookEndpointInput,
GQLFEATURE_FLAG,
GQLRotateSSOKeyInput,
GQLRotateWebhookEndpointSecretInput,
GQLUpdateSettingsInput,
GQLUpdateWebhookEndpointInput,
@@ -43,6 +49,12 @@ export const Settings = ({
update(mongo, redis, tenantCache, config, tenant, input.settings),
regenerateSSOKey: (): Promise<Tenant | null> =>
regenerateSSOKey(mongo, redis, tenantCache, tenant, now),
rotateSSOKey: ({ inactiveIn }: GQLRotateSSOKeyInput) =>
rotateSSOKey(mongo, redis, tenantCache, tenant, inactiveIn, now),
deactivateSSOKey: ({ kid }: GQLDeactivateSSOKeyInput) =>
deactivateSSOKey(mongo, redis, tenantCache, tenant, kid, now),
deleteSSOKey: ({ kid }: GQLDeleteSSOKeyInput) =>
deleteSSOKey(mongo, redis, tenantCache, tenant, kid),
enableFeatureFlag: (flag: GQLFEATURE_FLAG) =>
enableFeatureFlag(mongo, redis, tenantCache, tenant, flag),
disableFeatureFlag: (flag: GQLFEATURE_FLAG) =>
+4 -4
View File
@@ -19,12 +19,12 @@ import {
import { scrape } from "coral-server/services/stories/scraper";
import {
GQLAddExpertInput,
GQLAddStoryExpertInput,
GQLCloseStoryInput,
GQLCreateStoryInput,
GQLMergeStoriesInput,
GQLOpenStoryInput,
GQLRemoveExpertInput,
GQLRemoveStoryExpertInput,
GQLRemoveStoryInput,
GQLScrapeStoryInput,
GQLUpdateStoryInput,
@@ -78,8 +78,8 @@ export const Stories = (ctx: GraphContext) => ({
scrape(ctx.mongo, ctx.config, ctx.tenant.id, input.id),
updateStoryMode: async (input: GQLUpdateStoryModeInput) =>
updateStoryMode(ctx.mongo, ctx.tenant, input.storyID, input.mode),
addStoryExpert: async (input: GQLAddExpertInput) =>
addStoryExpert: async (input: GQLAddStoryExpertInput) =>
addStoryExpert(ctx.mongo, ctx.tenant, input.storyID, input.userID),
removeStoryExpert: async (input: GQLRemoveExpertInput) =>
removeStoryExpert: async (input: GQLRemoveStoryExpertInput) =>
removeStoryExpert(ctx.mongo, ctx.tenant, input.storyID, input.userID),
});
+12 -2
View File
@@ -1,7 +1,5 @@
import { GQLMutationTypeResolver } from "coral-server/graph/schema/__generated__/types";
// TODO: (wyattjoh) add rate limiting to these edges
export const Mutation: Required<GQLMutationTypeResolver<void>> = {
editComment: async (source, { input }, ctx) => ({
comment: await ctx.mutators.Comments.edit(input),
@@ -81,6 +79,18 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
settings: await ctx.mutators.Settings.regenerateSSOKey(),
clientMutationId: input.clientMutationId,
}),
rotateSSOKey: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.rotateSSOKey(input),
clientMutationId: input.clientMutationId,
}),
deactivateSSOKey: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.deactivateSSOKey(input),
clientMutationId: input.clientMutationId,
}),
deleteSSOKey: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.deleteSSOKey(input),
clientMutationId: input.clientMutationId,
}),
createStory: async (source, { input }, ctx) => ({
story: await ctx.mutators.Stories.create(input),
clientMutationId: input.clientMutationId,
@@ -0,0 +1,8 @@
import * as settings from "coral-server/models/settings";
import { GQLSecretTypeResolver } from "coral-server/graph/schema/__generated__/types";
export const Secret: GQLSecretTypeResolver<settings.Secret> = {
lastUsedAt: async ({ kid }, args, ctx) =>
ctx.loaders.Auth.retrieveSSOKeyLastUsedAt.load(kid),
};
+2
View File
@@ -37,6 +37,7 @@ import { Profile } from "./Profile";
import { Query } from "./Query";
import { RecentCommentHistory } from "./RecentCommentHistory";
import { RejectCommentPayload } from "./RejectCommentPayload";
import { Secret } from "./Secret";
import { Settings } from "./Settings";
import { SlackConfiguration } from "./SlackConfiguration";
import { SSOAuthIntegration } from "./SSOAuthIntegration";
@@ -89,6 +90,7 @@ const Resolvers: GQLResolver = {
RecentCommentHistory,
RejectCommentPayload,
SSOAuthIntegration,
Secret,
Story,
StorySettings,
Subscription,
+289 -101
View File
@@ -65,6 +65,13 @@ rate enforces a rate limit on requests made by the user.
"""
directive @rate(max: Int = 1, seconds: Int!, key: String) on FIELD_DEFINITION
"""
deprecated indicates that a field should not be used in the future.
"""
directive @deprecated(
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
################################################################################
## Custom Scalar Types
################################################################################
@@ -483,6 +490,40 @@ type LocalAuthIntegration {
## SSOAuthIntegration
##########################
type Secret {
"""
kid is the identifier for the key used when verifying tokens issued by the
provider.
"""
kid: String!
"""
secret is the actual underlying secret used to verify the tokens with.
"""
secret: String!
"""
createdAt is the date that the key was created at.
"""
createdAt: Time!
"""
lastUsedAt is the time that the
"""
lastUsedAt: Time
"""
rotatedAt is the time that the token was rotated out.
"""
rotatedAt: Time
"""
inactiveAt is the date that the token can no longer be used to validate
tokens.
"""
inactiveAt: Time
}
"""
SSOAuthIntegration is an AuthIntegration that provides a secret to the admins
of a tenant, where they can sign a SSO payload with it to provide to the
@@ -504,15 +545,24 @@ type SSOAuthIntegration {
"""
targetFilter: AuthenticationTargetFilter!
"""
keys are the different SSOKey's used by this Tenant.
"""
keys: [Secret!]! @auth(roles: [ADMIN])
"""
key is the secret that is used to sign tokens.
"""
key: String @auth(roles: [ADMIN])
key: String
@auth(roles: [ADMIN])
@deprecated(reason: "field is deprecated in favour of `keys`")
"""
keyGeneratedAt is the Time that the key was effective from.
"""
keyGeneratedAt: Time @auth(roles: [ADMIN])
keyGeneratedAt: Time
@auth(roles: [ADMIN])
@deprecated(reason: "field is deprecated in favour of `keys`")
}
##########################
@@ -1234,21 +1284,6 @@ enum WEBHOOK_EVENT_NAME {
STORY_CREATED
}
"""
TODO: merge with SSOKey with PR #2732
"""
type Secret {
"""
secret is the actual underlying secret used to verify the tokens with.
"""
secret: String!
"""
createdAt is the date that the key was created at.
"""
createdAt: Time!
}
type WebhookEndpoint {
"""
id is the unique identifier for this specific endpoint.
@@ -1342,6 +1377,28 @@ type Announcement {
content: String!
}
type Site {
"""
id is the identifier of the Site.
"""
id: ID!
"""
name is the name of the Site.
"""
name: String!
"""
allowedOrigins are the allowed origins for embeds.
"""
allowedOrigins: [String!]!
"""
createdAt is when the site was created.
"""
createdAt: Time!
}
"""
Settings stores the global settings for a given Tenant.
"""
@@ -5855,6 +5912,64 @@ type EnableFeatureFlagPayload {
flags: [FEATURE_FLAG!]!
}
#########################
## rotateSSOKey
#########################
input RotateSSOKeyInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
inactiveIn is the number of seconds that the current active SSOKey should be
kept active (allow signed tokens signed with this secret) before rejecting
them.
"""
inactiveIn: Int!
}
type RotateSSOKeyPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
settings is the Settings that the SSO key was regenerated on.
"""
settings: Settings
}
#########################
## deactivateSSOKey
#########################
input DeactivateSSOKeyInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
kid is the ID of the SSOKey being deactivated.
"""
kid: ID!
}
type DeactivateSSOKeyPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
settings is the Settings that the SSO key was regenerated on.
"""
settings: Settings
}
#########################
# disableFeatureFlag
#########################
@@ -5884,10 +5999,10 @@ type DisableFeatureFlagPayload {
}
#########################
# Add / Remove Expert
# addStoryExpert
#########################
input AddExpertInput {
input AddStoryExpertInput {
"""
clientMutationId is required for Relay support.
"""
@@ -5904,7 +6019,7 @@ input AddExpertInput {
userID: ID!
}
type AddExpertPayload {
type AddStoryExpertPayload {
"""
clientMutationId is required for Relay support.
"""
@@ -5916,7 +6031,11 @@ type AddExpertPayload {
story: Story!
}
input RemoveExpertInput {
#########################
# removeStoryExpert
#########################
input RemoveStoryExpertInput {
"""
clientMutationId is required for Relay support.
"""
@@ -5933,7 +6052,7 @@ input RemoveExpertInput {
userID: ID!
}
type RemoveExpertPayload {
type RemoveStoryExpertPayload {
"""
clientMutationId is required for Relay support.
"""
@@ -5945,6 +6064,38 @@ type RemoveExpertPayload {
story: Story!
}
#########################
## deleteSSOKey
#########################
input DeleteSSOKeyInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
kid is the ID of the SSOKey being deleted.
"""
kid: ID!
}
type DeleteSSOKeyPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
settings is the Settings that the SSO key was regenerated on.
"""
settings: Settings
}
#########################
## updateStoryMode
#########################
input UpdateStoryModeInput {
"""
storyID is the story id to enable Q&A on.
@@ -5963,15 +6114,110 @@ input UpdateStoryModeInput {
}
type UpdateStoryModePayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
story is the resultant story that Q&A was enabled on.
"""
story: Story!
}
##################
## createSite
##################
input CreateSite {
"""
name is the name of the Site.
"""
name: String!
"""
allowedOrigins are the allowed origins for embeds.
"""
allowedOrigins: [String!]!
}
input CreateSiteInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
site is the input for the Site to create.
"""
site: CreateSite!
}
type CreateSitePayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
site is the Site that was newly created.
"""
site: Site!
}
##################
## updateSite
##################
input UpdateSite {
"""
name is the name of the Site.
"""
name: String
"""
url is the Site URL, seen in email communications.
"""
url: String
"""
contactEmail is the contact email for the Site, seen in email communications.
"""
contactEmail: String
"""
allowedOrigins are the allowed origins for embeds.
"""
allowedOrigins: [String!]
}
input UpdateSiteInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the Site to update.
"""
id: ID!
"""
site is the updates for the Site.
"""
site: UpdateSite!
}
type UpdateSitePayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
site is the newly updated Site.
"""
site: Site!
}
##################
@@ -6012,6 +6258,25 @@ type Mutation {
"""
regenerateSSOKey(input: RegenerateSSOKeyInput!): RegenerateSSOKeyPayload!
@auth(roles: [ADMIN])
@deprecated(reason: "deprecated in favour of `rotateSSOKey`")
"""
rotateSSOKey can be used to rotate a given active SSOKey.
"""
rotateSSOKey(input: RotateSSOKeyInput!): RotateSSOKeyPayload!
@auth(roles: [ADMIN])
"""
deactivateSSOKey will deactivate a given deactivated SSOKey.
"""
deactivateSSOKey(input: DeactivateSSOKeyInput!): DeactivateSSOKeyPayload!
@auth(roles: [ADMIN])
"""
deleteSSOKey will delete a given inactive SSOKey.
"""
deleteSSOKey(input: DeleteSSOKeyInput!): DeleteSSOKeyPayload!
@auth(roles: [ADMIN])
"""
createCommentReaction will create a Reaction authored by the current logged in
@@ -6416,13 +6681,13 @@ type Mutation {
"""
addStoryExpert adds an expert to a story.
"""
addStoryExpert(input: AddExpertInput!): AddExpertPayload!
addStoryExpert(input: AddStoryExpertInput!): AddStoryExpertPayload!
@auth(roles: [ADMIN, MODERATOR])
"""
removeStoryExpert removes an expert from a story.
"""
removeStoryExpert(input: RemoveExpertInput!): RemoveExpertPayload!
removeStoryExpert(input: RemoveStoryExpertInput!): RemoveStoryExpertPayload!
@auth(roles: [ADMIN, MODERATOR])
}
@@ -6608,80 +6873,3 @@ type Subscription {
commentFeatured(storyID: ID!): CommentFeaturedPayload!
@auth(roles: [MODERATOR, ADMIN])
}
type Site {
"""
id is the identifier of the Site.
"""
id: ID!
"""
name is the name of the Site.
"""
name: String!
"""
allowedOrigins are the allowed origins for embeds.
"""
allowedOrigins: [String!]!
"""
createdAt is when the site was created.
"""
createdAt: Time!
}
input CreateSite {
"""
name is the name of the Site.
"""
name: String!
"""
allowedOrigins are the allowed origins for embeds.
"""
allowedOrigins: [String!]!
}
input UpdateSite {
"""
name is the name of the Site.
"""
name: String
"""
url is the Site URL, seen in email communications.
"""
url: String
"""
contactEmail is the contact email for the Site, seen in email communications.
"""
contactEmail: String
"""
allowedOrigins are the allowed origins for embeds.
"""
allowedOrigins: [String!]
}
input CreateSiteInput {
clientMutationId: String!
site: CreateSite!
}
type CreateSitePayload {
clientMutationId: String!
site: Site!
}
input UpdateSiteInput {
clientMutationId: String!
site: UpdateSite!
id: ID!
}
type UpdateSitePayload {
clientMutationId: String!
site: Site!
}