Skip to content

Commit

Permalink
feat: authenticationProviders API endpoints (outline#1962)
Browse files Browse the repository at this point in the history
  • Loading branch information
tommoor authored Mar 26, 2021
1 parent 626c94e commit e00a437
Show file tree
Hide file tree
Showing 19 changed files with 671 additions and 354 deletions.
3 changes: 2 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ Interested in more documentation on the API routes? Check out the [API documenta
server
├── api - All API routes are contained within here
│ └── middlewares - Koa middlewares specific to the API
├── auth - Authentication providers, in the form of passport.js strategies
├── auth - Authentication logic
│ └── providers - Authentication providers export passport.js strategies and config
├── commands - We are gradually moving to the command pattern for new write logic
├── config - Database configuration
├── emails - Transactional email templates
Expand Down
17 changes: 1 addition & 16 deletions server/api/auth.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
// @flow
import path from "path";
import Router from "koa-router";
import { find } from "lodash";
import { parseDomain, isCustomSubdomain } from "../../shared/utils/domains";
import { signin } from "../../shared/utils/routeHelpers";
import providers from "../auth/providers";
import auth from "../middlewares/authentication";
import { Team } from "../models";
import { presentUser, presentTeam, presentPolicies } from "../presenters";
import { isCustomDomain } from "../utils/domains";
import { requireDirectory } from "../utils/fs";

const router = new Router();
let providers = [];

requireDirectory(path.join(__dirname, "..", "auth")).forEach(
([{ config }, id]) => {
if (config && config.enabled) {
providers.push({
id,
name: config.name,
authUrl: signin(id),
});
}
}
);

function filterProviders(team) {
return providers
Expand Down
85 changes: 85 additions & 0 deletions server/api/authenticationProviders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// @flow
import Router from "koa-router";
import allAuthenticationProviders from "../auth/providers";
import auth from "../middlewares/authentication";
import { AuthenticationProvider, Event } from "../models";
import policy from "../policies";
import { presentAuthenticationProvider, presentPolicies } from "../presenters";

const router = new Router();
const { authorize } = policy;

router.post("authenticationProviders.info", auth(), async (ctx) => {
const { id } = ctx.body;
ctx.assertUuid(id, "id is required");

const user = ctx.state.user;
const authenticationProvider = await AuthenticationProvider.findByPk(id);
authorize(user, "read", authenticationProvider);

ctx.body = {
data: presentAuthenticationProvider(authenticationProvider),
policies: presentPolicies(user, [authenticationProvider]),
};
});

router.post("authenticationProviders.update", auth(), async (ctx) => {
const { id, isEnabled } = ctx.body;
ctx.assertUuid(id, "id is required");
ctx.assertPresent(isEnabled, "isEnabled is required");

const user = ctx.state.user;
const authenticationProvider = await AuthenticationProvider.findByPk(id);
authorize(user, "update", authenticationProvider);

const enabled = !!isEnabled;
if (enabled) {
await authenticationProvider.enable();
} else {
await authenticationProvider.disable();
}

await Event.create({
name: "authenticationProviders.update",
data: { enabled },
modelId: id,
teamId: user.teamId,
actorId: user.id,
ip: ctx.request.ip,
});

ctx.body = {
data: presentAuthenticationProvider(authenticationProvider),
policies: presentPolicies(user, [authenticationProvider]),
};
});

router.post("authenticationProviders.list", auth(), async (ctx) => {
const user = ctx.state.user;
authorize(user, "read", user.team);

const teamAuthenticationProviders = await user.team.getAuthenticationProviders();
const otherAuthenticationProviders = allAuthenticationProviders.filter(
(p) =>
!teamAuthenticationProviders.find((t) => t.name === p.id) &&
p.enabled &&
// email auth is dealt with separetly right now, although it definitely
// wants to be here in the future – we'll need to migrate more data though
p.id !== "email"
);

ctx.body = {
data: {
authenticationProviders: [
...teamAuthenticationProviders.map(presentAuthenticationProvider),
...otherAuthenticationProviders.map((p) => ({
name: p.id,
isEnabled: false,
isConnected: false,
})),
],
},
};
});

export default router;
156 changes: 156 additions & 0 deletions server/api/authenticationProviders.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// @flow
import TestServer from "fetch-test-server";
import uuid from "uuid";
import app from "../app";
import { buildUser, buildAdmin, buildTeam } from "../test/factories";
import { flushdb } from "../test/support";

const server = new TestServer(app.callback());

beforeEach(() => flushdb());
afterAll(() => server.close());

describe("#authenticationProviders.info", () => {
it("should return auth provider", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const authenticationProviders = await team.getAuthenticationProviders();

const res = await server.post("/api/authenticationProviders.info", {
body: {
id: authenticationProviders[0].id,
token: user.getJwtToken(),
},
});
const body = await res.json();

expect(res.status).toEqual(200);
expect(body.data.name).toBe("slack");
expect(body.data.isEnabled).toBe(true);
expect(body.data.isConnected).toBe(true);
expect(body.policies[0].abilities.read).toBe(true);
expect(body.policies[0].abilities.update).toBe(false);
});

it("should require authorization", async () => {
const team = await buildTeam();
const user = await buildUser();
const authenticationProviders = await team.getAuthenticationProviders();

const res = await server.post("/api/authenticationProviders.info", {
body: {
id: authenticationProviders[0].id,
token: user.getJwtToken(),
},
});
expect(res.status).toEqual(403);
});

it("should require authentication", async () => {
const team = await buildTeam();
const authenticationProviders = await team.getAuthenticationProviders();

const res = await server.post("/api/authenticationProviders.info", {
body: {
id: authenticationProviders[0].id,
},
});
expect(res.status).toEqual(401);
});
});

describe("#authenticationProviders.update", () => {
it("should not allow admins to disable when last authentication provider", async () => {
const team = await buildTeam();
const user = await buildAdmin({ teamId: team.id });
const authenticationProviders = await team.getAuthenticationProviders();

const res = await server.post("/api/authenticationProviders.update", {
body: {
id: authenticationProviders[0].id,
isEnabled: false,
token: user.getJwtToken(),
},
});

expect(res.status).toEqual(400);
});

it("should allow admins to disable", async () => {
const team = await buildTeam();
const user = await buildAdmin({ teamId: team.id });
await team.createAuthenticationProvider({
name: "google",
providerId: uuid.v4(),
});
const authenticationProviders = await team.getAuthenticationProviders();

const res = await server.post("/api/authenticationProviders.update", {
body: {
id: authenticationProviders[0].id,
isEnabled: false,
token: user.getJwtToken(),
},
});
const body = await res.json();

expect(res.status).toEqual(200);
expect(body.data.name).toBe("slack");
expect(body.data.isEnabled).toBe(false);
expect(body.data.isConnected).toBe(true);
});

it("should require authorization", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const authenticationProviders = await team.getAuthenticationProviders();

const res = await server.post("/api/authenticationProviders.update", {
body: {
id: authenticationProviders[0].id,
isEnabled: false,
token: user.getJwtToken(),
},
});
expect(res.status).toEqual(403);
});

it("should require authentication", async () => {
const team = await buildTeam();
const authenticationProviders = await team.getAuthenticationProviders();

const res = await server.post("/api/authenticationProviders.update", {
body: {
id: authenticationProviders[0].id,
isEnabled: false,
},
});
expect(res.status).toEqual(401);
});
});

describe("#authenticationProviders.list", () => {
it("should return enabled and available auth providers", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });

const res = await server.post("/api/authenticationProviders.list", {
body: { token: user.getJwtToken() },
});
const body = await res.json();

expect(res.status).toEqual(200);
expect(body.data.authenticationProviders.length).toBe(2);
expect(body.data.authenticationProviders[0].name).toBe("slack");
expect(body.data.authenticationProviders[0].isEnabled).toBe(true);
expect(body.data.authenticationProviders[0].isConnected).toBe(true);
expect(body.data.authenticationProviders[1].name).toBe("google");
expect(body.data.authenticationProviders[1].isEnabled).toBe(false);
expect(body.data.authenticationProviders[1].isConnected).toBe(false);
});

it("should require authentication", async () => {
const res = await server.post("/api/authenticationProviders.list");
expect(res.status).toEqual(401);
});
});
2 changes: 2 additions & 0 deletions server/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import validation from "../middlewares/validation";
import apiKeys from "./apiKeys";
import attachments from "./attachments";
import auth from "./auth";
import authenticationProviders from "./authenticationProviders";
import collections from "./collections";
import documents from "./documents";
import events from "./events";
Expand Down Expand Up @@ -45,6 +46,7 @@ api.use(editor());

// routes
router.use("/", auth.routes());
router.use("/", authenticationProviders.routes());
router.use("/", events.routes());
router.use("/", users.routes());
router.use("/", collections.routes());
Expand Down
Loading

0 comments on commit e00a437

Please sign in to comment.