[CORL-155] User Suspending and Banning (#2247)

* feat: suspending, banning, now propogation

* feat: adapting to `now`

* feat: support auth for suspension/banned

* feat: added trace-id to requests

* feat: new mutation api with hooks support

* feat: added user status filtering, current field

* feat: Implement filter by status, adapt to new USER_STATUS type, add lookup helper <3

* fix: typo

* fix: tests

* chore: rename banned status to ban status

* test: feature test + lots of test helper improvements e.g. types

* fix: add translation to ban user modal

* fix: translation

* fix: test
This commit is contained in:
Wyatt Johnson
2019-04-22 22:57:32 +00:00
committed by GitHub
parent b63c00f26f
commit dbbc1af42e
147 changed files with 4609 additions and 1468 deletions
@@ -75,6 +75,9 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) {
throw new Error("logout requires a token on the request, none was found");
}
// Talk is guarenteed at this point.
const { now } = req.talk!;
// Decode the token.
const decoded = jwt.decode(token, {});
if (!decoded) {
@@ -88,7 +91,7 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) {
const { jti, exp }: LogoutToken = validate(LogoutTokenSchema, decoded);
// Compute the number of seconds that the token will be valid for.
const validFor = exp - Date.now() / 1000;
const validFor = exp - now.valueOf() / 1000;
if (validFor > 0) {
// Invalidate the token, the expiry is in the future and it needs to be
// revoked.
@@ -40,7 +40,8 @@ export default class FacebookStrategy extends OAuth2Strategy<
protected async findOrCreateUser(
tenant: Tenant,
integration: Required<GQLFacebookAuthIntegration>,
{ id, photos, emails, displayName }: Profile
{ id, photos, emails, displayName }: Profile,
now = new Date()
) {
// Create the user profile that will be used to lookup the User.
const profile: FacebookProfile = {
@@ -71,14 +72,19 @@ export default class FacebookStrategy extends OAuth2Strategy<
emailVerified = false;
}
user = await insert(this.mongo, tenant, {
username: displayName,
role: GQLUSER_ROLE.COMMENTER,
email,
emailVerified,
avatar,
profiles: [profile],
});
user = await insert(
this.mongo,
tenant,
{
username: displayName,
role: GQLUSER_ROLE.COMMENTER,
email,
emailVerified,
avatar,
profiles: [profile],
},
now
);
}
// TODO: maybe update user details?
@@ -39,7 +39,8 @@ export default class GoogleStrategy extends OAuth2Strategy<
protected async findOrCreateUser(
tenant: Tenant,
integration: Required<GQLGoogleAuthIntegration>,
{ id, photos, emails, displayName }: Profile
{ id, photos, emails, displayName }: Profile,
now = new Date()
) {
// Create the user profile that will be used to lookup the User.
const profile: GoogleProfile = {
@@ -70,14 +71,19 @@ export default class GoogleStrategy extends OAuth2Strategy<
emailVerified = false;
}
user = await insert(this.mongo, tenant, {
username: displayName,
role: GQLUSER_ROLE.COMMENTER,
email,
emailVerified,
avatar,
profiles: [profile],
});
user = await insert(
this.mongo,
tenant,
{
username: displayName,
role: GQLUSER_ROLE.COMMENTER,
email,
emailVerified,
avatar,
profiles: [profile],
},
now
);
}
// TODO: maybe update user details?
@@ -33,7 +33,8 @@ export interface Verifier<T = Token> {
verify: (
tokenString: string,
token: T,
tenant: Tenant
tenant: Tenant,
now: Date
) => Promise<Readonly<User> | null>;
/**
@@ -58,7 +59,7 @@ export class JWTStrategy extends Strategy {
];
}
private async verify(tokenString: string, tenant: Tenant) {
private async verify(tokenString: string, tenant: Tenant, now = new Date()) {
const token: Token = jwt.decode(tokenString);
if (!token || typeof token === "string") {
throw new TokenInvalidError(tokenString, "token could not be decoded");
@@ -67,7 +68,7 @@ export class JWTStrategy extends Strategy {
// Try to verify the token.
for (const verifier of this.verifiers) {
if (verifier.supports(token, tenant)) {
return verifier.verify(tokenString, token, tenant);
return verifier.verify(tokenString, token, tenant, now);
}
}
@@ -87,13 +88,13 @@ export class JWTStrategy extends Strategy {
return this.pass();
}
const { tenant } = req.talk!;
const { now, tenant } = req.talk!;
if (!tenant) {
return this.error(new TenantNotFoundError(req.hostname));
}
try {
const user = await this.verify(token, tenant);
const user = await this.verify(token, tenant, now);
if (!user) {
return this.pass();
}
@@ -57,7 +57,8 @@ export default abstract class OAuth2Strategy<
protected abstract findOrCreateUser(
tenant: Tenant,
integration: Required<T>,
profile: Profile
profile: Profile,
now: Date
): Promise<User | undefined>;
protected verifyCallback = async (
@@ -70,6 +71,7 @@ export default abstract class OAuth2Strategy<
try {
// Talk is defined at this point.
const tenant = req.talk!.tenant!;
const now = req.talk!.now;
// Get the integration.
const integration = this.getIntegration(tenant.auth.integrations);
@@ -78,7 +80,8 @@ export default abstract class OAuth2Strategy<
const user = await this.findOrCreateUser(
tenant,
integration as Required<T>,
profile
profile,
now
);
return done(null, user);
@@ -137,7 +137,8 @@ export async function findOrCreateOIDCUser(
mongo: Db,
tenant: Tenant,
integration: OIDCAuthIntegration,
token: OIDCIDToken
token: OIDCIDToken,
now = new Date()
): Promise<Readonly<User> | null> {
// Unpack/validate the token content.
const {
@@ -174,14 +175,19 @@ export async function findOrCreateOIDCUser(
const username = preferred_username || nickname || name;
// Create the new user, as one didn't exist before!
user = await insert(mongo, tenant, {
username,
role: GQLUSER_ROLE.COMMENTER,
email,
emailVerified: email_verified,
avatar: picture,
profiles: [profile],
});
user = await insert(
mongo,
tenant,
{
username,
role: GQLUSER_ROLE.COMMENTER,
email,
emailVerified: email_verified,
avatar: picture,
profiles: [profile],
},
now
);
}
// TODO: (wyattjoh) possibly update the user profile if the remaining details mismatch?
@@ -194,7 +200,8 @@ export function findOrCreateOIDCUserWithToken(
tenant: Tenant,
client: JwksClient,
integration: OIDCAuthIntegration,
token: string
token: string,
now: Date
) {
return new Promise<Readonly<User> | null>((resolve, reject) => {
logger.trace({ tenantID: tenant.id }, "verifying oidc id_token");
@@ -219,7 +226,8 @@ export function findOrCreateOIDCUserWithToken(
mongo,
tenant,
integration,
decoded as OIDCIDToken
decoded as OIDCIDToken,
now
);
return resolve(user);
} catch (err) {
@@ -303,8 +311,9 @@ export default class OIDCStrategy extends Strategy {
return done(new Error("no id_token in params"));
}
// Grab the tenant out of the request, as we need some more details.
const { tenant } = req.talk!;
// Grab the tenant out of the request, as we need some more details. Talk
// is guaranteed at this point.
const { now, tenant } = req.talk!;
if (!tenant) {
// TODO: return a better error.
return done(new Error("tenant not found"));
@@ -330,7 +339,8 @@ export default class OIDCStrategy extends Strategy {
tenant,
client,
integration,
id_token
id_token,
now
);
return done(null, user || undefined);
} catch (err) {
@@ -30,7 +30,12 @@ export class OIDCVerifier implements Verifier<OIDCIDToken> {
this.cache = new TenantCacheAdapter(tenantCache);
}
public async verify(tokenString: string, token: OIDCIDToken, tenant: Tenant) {
public async verify(
tokenString: string,
token: OIDCIDToken,
tenant: Tenant,
now: Date
) {
// Ensure that the integration is enabled.
const integration = getEnabledIntegration(tenant.auth.integrations.oidc);
@@ -52,7 +57,8 @@ export class OIDCVerifier implements Verifier<OIDCIDToken> {
tenant,
client,
integration,
tokenString
tokenString,
now
);
}
@@ -42,7 +42,8 @@ export async function findOrCreateSSOUser(
mongo: Db,
tenant: Tenant,
integration: GQLSSOAuthIntegration,
token: SSOToken
token: SSOToken,
now = new Date()
) {
if (!token.user) {
// TODO: (wyattjoh) replace with better error.
@@ -71,13 +72,18 @@ export async function findOrCreateSSOUser(
// FIXME: (wyattjoh) implement rules! Not all users should be able to create an account via this method.
// Create the new user, as one didn't exist before!
user = await insert(mongo, tenant, {
username,
role: GQLUSER_ROLE.COMMENTER,
email,
avatar,
profiles: [profile],
});
user = await insert(
mongo,
tenant,
{
username,
role: GQLUSER_ROLE.COMMENTER,
email,
avatar,
profiles: [profile],
},
now
);
}
// TODO: (wyattjoh) possibly update the user profile if the remaining details mismatch?
@@ -122,7 +128,12 @@ export class SSOVerifier implements Verifier<SSOToken> {
return tenant.auth.integrations.sso.enabled && isSSOToken(token);
}
public async verify(tokenString: string, token: SSOToken, tenant: Tenant) {
public async verify(
tokenString: string,
token: SSOToken,
tenant: Tenant,
now = new Date()
) {
const integration = tenant.auth.integrations.sso;
if (!integration.enabled) {
// TODO: (wyattjoh) return a better error.
@@ -140,6 +151,6 @@ export class SSOVerifier implements Verifier<SSOToken> {
algorithms: ["HS256"], // TODO: (wyattjoh) investigate replacing algorithm.
});
return findOrCreateSSOUser(this.mongo, tenant, integration, token);
return findOrCreateSSOUser(this.mongo, tenant, integration, token, now);
}
}
+13 -2
View File
@@ -1,3 +1,5 @@
import uuid from "uuid/v1";
import { TenantNotFoundError } from "talk-server/errors";
import TenantCache from "talk-server/services/tenant/cache";
import { RequestHandler } from "talk-server/types/express";
@@ -12,9 +14,18 @@ export const tenantMiddleware = ({
passNoTenant = false,
}: MiddlewareOptions): RequestHandler => async (req, res, next) => {
try {
// Set Talk on the request.
if (!req.talk) {
req.talk = {};
const id = uuid();
// Write the ID on the request.
res.set("X-Trace-ID", id);
// The only call to `new Date()` as a part of the request process. This
// is passed around the request to ensure constant-time actions.
const now = new Date();
// Set Talk on the request.
req.talk = { id, now };
}
// Set the Talk Tenant Cache on the request.