8000 WIP: Group assignment from JWT claims by EternalDeiwos · Pull Request #2568 · outline/outline · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

WIP: Group assignment from JWT claims #2568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 62 additions & 14 deletions server/commands/accountProvisioner.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import {
AuthenticationProviderDisabledError,
} from "../errors";
import mailer from "../mailer";
import { Collection, Team, User } from "../models";
import { Collection, Team, User, Group, GroupUser } from "../models";
import groupCreator from "./groupCreator";
import teamCreator from "./teamCreator";
import userCreator from "./userCreator";

const Op = Sequelize.Op;

type Props = {|
ip: string,
user: {|
Expand All @@ -24,6 +27,7 @@ type Props = {|
subdomain: string,
avatarUrl?: string,
|},
groups: string[],
authenticationProvider: {|
name: string,
providerId: string,
Expand All @@ -39,6 +43,11 @@ type Props = {|
export type AccountProvisionerResult = {|
user: User,
team: Team,
groups: {|
group: Group,
membership: GroupUser,
isNewGroup: boolean,
|},
isNewTeam: boolean,
isNewUser: boolean,
|};
Expand All @@ -47,12 +56,13 @@ export default async function accountProvisioner({
ip,
user: userParams,
team: teamParams,
groups: groupNames = [],
authenticationProvider: authenticationProviderParams,
authentication: authenticationParams,
}: Props): Promise<AccountProvisionerResult> {
let result;
let teamResult;
try {
result = await teamCreator({
teamResult = await teamCreator({
name: teamParams.name,
domain: teamParams.domain,
subdomain: teamParams.subdomain,
Expand All @@ -63,15 +73,16 @@ export default async function accountProvisioner({
throw new AuthenticationError(err.message);
}

invariant(result, "Team creator result must exist");
const { authenticationProvider, team, isNewTeam } = result;
invariant(teamResult, "Team creator result must exist");
const { authenticationProvider, team, isNewTeam } = teamResult;

if (!authenticationProvider.enabled) {
throw new AuthenticationProviderDisabledError();
}

let userResult;
try {
const result = await userCreator({
userResult = await userCreator({
name: userParams.name,
email: userParams.email,
isAdmin: isNewTeam,
Expand All @@ -84,7 +95,7 @@ export default async function accountProvisioner({
},
});

const { isNewUser, user } = result;
const { isNewUser, user } = userResult;

if (isNewUser) {
await mailer.sendTemplate("welcome", {
Expand All @@ -108,13 +119,6 @@ export default async function accountProvisioner({
await team.provisionFirstCollection(user.id);
}
}

return {
user,
team,
isNewUser,
isNewTeam,
};
} catch (err) {
if (err instanceof Sequelize.UniqueConstraintError) {
const exists = await User.findOne({
Expand All @@ -136,4 +140,48 @@ export default async function accountProvisioner({

throw err;
}

invariant(userResult, "User creator result must exist");
const { user } = userResult;

let groups;
try {
groups = await Promise.all(
groupNames.map(async (name) =>
groupCreator({
name,
ip,
user,
})
)
);
} catch (err) {
// TODO
10000 console.error(err);
}

// Remove group memberships that are not part of the current user info.
// This reconciles the user with whatever groups are passed in with the latest auth.
// At least one group must be specfied to enable this behaviour.
if (groupNames.length) {
try {
await GroupUser.destroy({
where: {
userId: userResult.user.id,
groupId: {
[Op.notIn]: groups.map((group) => group.membership.groupId),
},
},
});
} catch (err) {
// TODO
console.error(err);
}
}

return {
...teamResult,
...userResult,
groups,
};
}
111 changes: 111 additions & 0 deletions server/commands/groupCreator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// @flow
import Sequelize from "sequelize";
import { Event, Group, GroupUser, User } from "../models";
import { sequelize } from "../sequelize";

const Op = Sequelize.Op;

type GroupCreatorResult = {|
group: Group,
membership: GroupUser,
isNewGroup: boolean,
|};

export default async function groupCreator({
name,
ip,
user,
}: {|
name: string,
ip: string,
user: User,
|}): Promise<GroupCreatorResult> {
let isNewGroup = false;
let group = await Group.findOne({
where: {
name: { [Op.iLike]: name },
},
});

// The group does not already exist so we should create it
if (!group) {
let transaction = await sequelize.transaction();

try {
group = await Group.create(
{
name,
teamId: user.teamId,
createdById: user.id,
},
{
transaction,
}
);

await Event.create(
{
name: "groups.create",
actorId: user.id,
teamId: user.teamId,
modelId: group.id,
data: { name },
ip,
},
{
transaction,
}
);

await transaction.commit();

// reload to get default scope
group = await Group.findByPk(group.id);
isNewGroup = true;
} catch (err) {
await transaction.rollback();
throw err;
}
}

// Check for an existing group membership
let membership = await GroupUser.findOne({
where: {
groupId: group.id,
userId: user.id,
},
});

// User is not a member of the group, so we should add them
if (!membership) {
await group.addUser(user, {
through: { createdById: user.id },
});

membership = await GroupUser.findOne({
where: {
groupId: group.id,
userId: user.id,
},
});

await Event.create({
name: "groups.add_user",
userId: user.id,
teamId: user.teamId,
modelId: group.id,
actorId: user.id,
data: { name: user.name },
ip,
});

// reload to get default scope
group = await Group.findByPk(group.id);
}

return {
isNewGroup,
group,
membership,
};
}
8 changes: 8 additions & 0 deletions server/routes/auth/providers/oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET;
const OIDC_AUTH_URI = process.env.OIDC_AUTH_URI;
const OIDC_TOKEN_URI = process.env.OIDC_TOKEN_URI;
const OIDC_USERINFO_URI = process.env.OIDC_USERINFO_URI;
const OIDC_GROUPS_CLAIM = process.env.OIDC_GROUPS_CLAIM || "groups";
const OIDC_SCOPES = process.env.OIDC_SCOPES || "";
const allowedDomains = getAllowedDomains();

Expand Down Expand Up @@ -90,6 +91,12 @@ if (OIDC_CLIENT_ID) {
}

const subdomain = domain.split(".")[0];
let groups = profile[OIDC_GROUPS_CLAIM] || [];

// Ensure groups is an array
if (typeof groups === "string") {
groups = groups.split(",");
}

const result = await accountProvisioner({
ip: req.ip,
Expand All @@ -104,6 +111,7 @@ if (OIDC_CLIENT_ID) {
email: profile.email,
avatarUrl: profile.picture,
},
groups,
authenticationProvider: {
name: providerName,
providerId: domain,
Expand Down
0