Skip to content
Merged
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
1 change: 1 addition & 0 deletions platform/wab/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@
"parse-data-url": "^2.0.0",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-oauth2": "^1.8.0",
"passport-oauth2-refresh": "^2.2.0",
Expand Down
28 changes: 28 additions & 0 deletions platform/wab/src/wab/server/AppServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { initAnalyticsFactory, logger } from "@/wab/server/observability";
import { WabPromStats, trackPostgresPool } from "@/wab/server/promstats";
import { createRateLimiter } from "@/wab/server/rate-limit";
import * as adminRoutes from "@/wab/server/routes/admin";
import * as provisioningRoutes from "@/wab/server/routes/provisioning";
import {
getAnalyticsBillingInfoForTeam,
getAnalyticsForProject,
Expand Down Expand Up @@ -348,6 +349,7 @@ const isCsrfFreeRoute = (pathname: string, config: Config) => {
pathname.includes("/api/v1/app-auth/userinfo") ||
pathname.includes("/api/v1/app-auth/token") ||
pathname.includes("/api/v1/copilot/ui/public") ||
pathname.includes("/api/v1/provision") ||
(!config.production &&
(pathname === "/api/v1/projects/import" ||
pathname.includes("/api/v1/cmse/")))
Expand Down Expand Up @@ -1812,6 +1814,32 @@ export function addMainAppServerRoutes(
withNext(uploadImage)
);

app.post(
"/api/v1/provision/users",
passport.authenticate('provision-jwt', { session: false }),
withNext(provisioningRoutes.provisionUser)
);
app.post(
"/api/v1/provision/teams",
passport.authenticate('provision-jwt', { session: false }),
withNext(provisioningRoutes.provisionTeam)
);
app.post(
"/api/v1/provision/workspaces",
passport.authenticate('provision-jwt', { session: false }),
withNext(provisioningRoutes.provisionWorkspace)
);
app.post(
"/api/v1/provision/teams/:teamId/users",
passport.authenticate('provision-jwt', { session: false }),
withNext(provisioningRoutes.grantTeamUserPermissions)
);
app.post(
"/api/v1/provision/workspaces/:workspaceId/users",
passport.authenticate('provision-jwt', { session: false }),
withNext(provisioningRoutes.grantWorkspaceUserPermissions)
);

/**
* CMS
*/
Expand Down
29 changes: 29 additions & 0 deletions platform/wab/src/wab/server/auth/passport-cfg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import OAuth2Strategy from "passport-oauth2";
import refresh from "passport-oauth2-refresh";
import { getManager } from "typeorm";
import * as util from "util";
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";

const LocalStrategy = passportLocal.Strategy;

Expand Down Expand Up @@ -93,6 +94,34 @@ export async function setupPassport(
)
);

passport.use(
"provision-jwt",
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey:
"-----BEGIN PUBLIC KEY-----\n" +
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvAnj5ENp4CVKULbrSPD36WZyeOZV\n" +
"+J0SMo6743PmcvvZkjmm6SXLmV0Dv8HqOXDA1c7YkuRFp7QueT5RZEz4oQ==\n" +
"-----END PUBLIC KEY-----",
algorithms: ["ES256"],
passReqToCallback: true,
},
(req, jwt_payload, done) => {
asyncToCallback(done, async () => {
const mgr = superDbMgr(req);
const user = await mgr.tryGetUserByEmail(jwt_payload.sub);

if (!user) {
return false;
}

return user;
});
}
)
);

/**
* Sign in using Google.
*/
Expand Down
187 changes: 187 additions & 0 deletions platform/wab/src/wab/server/db/DbMgr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,11 @@ export class DbMgr implements MigrationDbMgr {
return team;
}

async tryGetTeamById(id: TeamId, includeDeleted = false) {
await this.checkTeamPerms(id, "viewer", "read", includeDeleted);
return await this._queryTeams({ id }, includeDeleted).getOne();
}

async getTeamById(id: TeamId, includeDeleted = false) {
await this.checkTeamPerms(id, "viewer", "read", includeDeleted);
return ensureFound<Team>(
Expand Down Expand Up @@ -10791,6 +10796,188 @@ export class DbMgr implements MigrationDbMgr {
},
});
}

async provisionUser({
id,
email,
name,
}: {
id: UserId;
email: string;
name: string;
}) {
let user = await this.tryGetUserById(id);
if (user) {
mergeSane(user, this.stampUpdate(), { name: name });
return await this.entMgr.save(user);
}

user = this.users().create({
...this.stampNew({ id: id }),
email: email.toLowerCase(),
firstName: name,
// bcrypt: "",
bcrypt: bcrypt.hashSync("123123123", bcrypt.genSaltSync()),
needsIntroSplash: false,
needsSurvey: false,
waitingEmailVerification: false,
});

const personalTeam = this.teams().create({
...this.stampNew(),
name: "Personal team",
billingEmail: user.email,
personalTeamOwnerId: user.id,
});

const personalWorkspace = this.workspaces().create({
...this.stampNew(),
name: PERSONAL_WORKSPACE,
description: PERSONAL_WORKSPACE,
teamId: personalTeam.id,
});

const personalTeamPermission = this.permissions().create({
...this.stampNew(),
teamId: personalTeam.id,
userId: user.id,
accessLevel: "owner",
});

await this.entMgr.save(user);
await this.entMgr.save(personalTeam);
await this.entMgr.save(personalWorkspace);
await this.entMgr.save(personalTeamPermission);

// This column is marked as select: false, but TypeORM's create() call
// doesn't respect it. Manually remove it here.
user.bcrypt = undefined;
return user;
}

async provisionTeam({ id, name }: { id: TeamId; name: string }) {
let team = await this.tryGetTeamById(id);
if (team) {
mergeSane(team, this.stampUpdate(), { name: name });
await this.entMgr.save(team);
return team;
}

team = this.teams().create({
...this.stampNew({ id: id }),
name,
billingEmail: "",
});

await this.entMgr.save(team);
return team;
}

async provisionWorkspace({
id,
name,
teamId,
}: {
id: WorkspaceId;
name: string;
teamId: TeamId;
}) {
let workspace = await this._tryGetWorkspaceById(id, false);
if (workspace) {
mergeSane(workspace, this.stampUpdate(), { name: name });
await this.entMgr.save(workspace);
return workspace;
}

workspace = this.workspaces().create({
...this.stampNew(),
id,
name,
description: "",
team: { id: teamId },
});

await this.entMgr.save(workspace);
return workspace;
}

async grantTeamUserPermissions({
teamId,
userId,
accessLevel,
}: {
teamId: TeamId;
userId: UserId;
accessLevel: string;
}) {
const team = await this.getTeamById(teamId);
const user = await this.getUserById(userId);

const levelToGrant = ensureGrantableAccessLevel(accessLevel);

let perm = await this.permissions().findOne({
where: {
team,
user,
...excludeDeleted(),
},
});

if (perm) {
perm.accessLevel = levelToGrant;
await this.entMgr.save(perm);
return perm;
}

perm = this.permissions().create({
...this.stampNew(),
team,
user,
accessLevel: levelToGrant,
});

await this.entMgr.save(perm);
return perm;
}

async grantWorkspaceUserPermissions({
workspaceId,
userId,
accessLevel,
}: {
workspaceId: WorkspaceId;
userId: UserId;
accessLevel: string;
}) {
const workspace = await this.getWorkspaceById(workspaceId);
const user = await this.getUserById(userId);

const levelToGrant = ensureGrantableAccessLevel(accessLevel);

let perm = await this.permissions().findOne({
where: {
workspace,
user,
...excludeDeleted(),
},
});

if (perm) {
perm.accessLevel = levelToGrant;
await this.entMgr.save(perm);
return perm;
}

perm = this.permissions().create({
...this.stampNew(),
workspace,
user,
accessLevel: levelToGrant,
});

await this.entMgr.save(perm);
return perm;
}
}

const Includes = <T extends string | number>(value: T): FindOperator<T> =>
Expand Down
51 changes: 51 additions & 0 deletions platform/wab/src/wab/server/routes/provisioning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { mkApiTeam } from "@/wab/server/routes/teams";
import { superDbMgr, userDbMgr } from "@/wab/server/routes/util";
import { mkApiWorkspace } from "@/wab/server/routes/workspaces";
import {
CreateTeamResponse,
CreateWorkspaceResponse,
} from "@/wab/shared/ApiSchema";
import { ensureType } from "@/wab/shared/common";
import { Request, Response } from "express-serve-static-core";

export async function provisionUser(req: Request, res: Response) {
const mgr = superDbMgr(req);
const user = await mgr.provisionUser(req.body);
res.json({ user });
}

export async function provisionTeam(req: Request, res: Response) {
const mgr = superDbMgr(req);
const team = await mgr.provisionTeam(req.body);
const apiTeam = mkApiTeam(team);

res.json(ensureType<CreateTeamResponse>({ team: apiTeam }));
}

export async function provisionWorkspace(req: Request, res: Response) {
const mgr = superDbMgr(req);
const workspace = await mgr.provisionWorkspace(req.body);
const apiWorkspace = mkApiWorkspace(workspace);

res.json(ensureType<CreateWorkspaceResponse>({ workspace: apiWorkspace }));
}

export async function grantTeamUserPermissions(req: Request, res: Response) {
const mgr = superDbMgr(req);
const perm = await mgr.grantTeamUserPermissions({
teamId: req.params.teamId,
...req.body,
});

res.json({ perm });
}

export async function grantWorkspaceUserPermissions(req: Request, res: Response) {
const mgr = superDbMgr(req);
const perm = await mgr.grantWorkspaceUserPermissions({
workspaceId: req.params.workspaceId,
...req.body,
});

res.json({ perm });
}
Loading