First authz check

This commit is contained in:
Daniel Flanagan 2022-11-10 11:41:46 -06:00
parent 773439d2e0
commit e667f39852
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
3 changed files with 60 additions and 11 deletions

0
access.ts Normal file
View file

View file

@ -38,6 +38,36 @@ export function initDatabaseConnectionPool({ url }: PostgresConfig) {
testDbConnection(); testDbConnection();
} }
/**
* Checks that a certain SQL predicate fetches a row that indeed exists.
*
* sqlSnippet should assume it comes after a 'select * from'. For example: '"user" where id = 1'.
*/
async function rowExists(
sqlSnippet: string,
args: unknown[],
): Promise<boolean> {
const result = await queryArray<[boolean]>(
`select exists(select 1 from ${sqlSnippet});`,
args,
);
if (result && result.rows.length > 0) {
log.info("rowExists result:", result.rows);
return !!(result.rows[0][0]);
}
return false;
}
export async function isUserInTeam(
userId: string,
teamId: string,
): Promise<boolean> {
return await rowExists("team_user where user_id = $1 and team_id = $2", [
userId,
teamId,
]);
}
export async function testDbConnection(): Promise<boolean> { export async function testDbConnection(): Promise<boolean> {
try { try {
await dbOp((conn) => conn.queryObject("select 1")); await dbOp((conn) => conn.queryObject("select 1"));
@ -111,17 +141,20 @@ export async function dbOp<T>(
return result; return result;
} }
/**
* Example: queryObject('select * from "user" where id = $1', [userId])
*/
export async function queryObject<T>( export async function queryObject<T>(
sql: string, sql: string,
args?: QueryArguments, args?: QueryArguments,
connection?: PoolClient | Transaction, connection?: PoolClient | Transaction,
): Promise<QueryObjectResult<T> | null> { ): Promise<QueryObjectResult<T> | null> {
console.debug(`queryObject: ${sql}`);
if (!connection) { if (!connection) {
return await dbOp(async (connection) => { return await dbOp(async (connection) => {
return await queryObject(sql, args, connection); return await queryObject(sql, args, connection);
}); });
} else { } else {
log.debug(`queryObject: ${sql}`);
const result = await connection.queryObject<T>({ const result = await connection.queryObject<T>({
camelcase: true, camelcase: true,
text: sql.trim(), text: sql.trim(),
@ -132,17 +165,20 @@ export async function queryObject<T>(
} }
} }
export async function queryArray<T extends []>( /**
* Example: queryArray('select * from "user" where id = $1', [userId])
*/
export async function queryArray<T extends unknown[]>(
sql: string, sql: string,
args?: QueryArguments, args?: QueryArguments,
connection?: PoolClient, connection?: PoolClient,
): Promise<QueryArrayResult<T> | null> { ): Promise<QueryArrayResult<T> | null> {
console.debug(`queryArray: ${sql}`);
if (!connection) { if (!connection) {
return await dbOp(async (connection) => { return await dbOp(async (connection) => {
return await queryArray<T>(sql, args, connection); return await queryArray<T>(sql, args, connection);
}); });
} else { } else {
log.debug(`queryArray: ${sql}`);
const result = await connection.queryArray<T>({ const result = await connection.queryArray<T>({
text: sql.trim(), text: sql.trim(),
args, args,
@ -210,7 +246,7 @@ export async function createTeam(
}, },
transaction?: Transaction, transaction?: Transaction,
): Promise<Team> { ): Promise<Team> {
console.debug("createTeam tx:", transaction); log.debug("createTeam tx:", transaction);
if (!transaction) { if (!transaction) {
return await wrapWithTransaction<Team>( return await wrapWithTransaction<Team>(
"createTeam", "createTeam",
@ -234,7 +270,7 @@ export async function createTeam(
} }
return team; return team;
} catch (e) { } catch (e) {
console.error("Error creating team:", e); log.error("Error creating team:", e);
throw e; throw e;
} }
} }
@ -253,7 +289,7 @@ export async function wrapWithTransaction<T>(
); );
try { try {
await transaction.begin(); await transaction.begin();
console.debug( log.debug(
`started ${transactionName} tx with options ${ `started ${transactionName} tx with options ${
JSON.stringify(transactionOptions) JSON.stringify(transactionOptions)
}:`, }:`,
@ -264,11 +300,11 @@ export async function wrapWithTransaction<T>(
return result; return result;
} catch (e) { } catch (e) {
await transaction.rollback(); await transaction.rollback();
console.error("Failed to complete transaction:", e); log.error("Failed to complete transaction:", e);
throw e; throw e;
} }
} catch (e) { } catch (e) {
console.error("Failed to create transaction"); log.error("Failed to create transaction");
throw e; throw e;
} }
}); });
@ -304,7 +340,7 @@ export async function createUser(
return user; return user;
} catch (e) { } catch (e) {
console.error("Error creating user:", e); log.error("Error creating user:", e);
throw e; throw e;
} }
} }

View file

@ -1,18 +1,31 @@
import { Handlers, PageProps } from "$fresh/server.ts"; import { Handlers, PageProps } from "$fresh/server.ts";
import { getTeam, getTeamUsers } from "@/db/mod.ts"; import { getTeam, getTeamUsers, isUserInTeam } from "@/db/mod.ts";
import { type Team, type User } from "@/types.ts"; import { type Team, type User } from "@/types.ts";
import { type ContextState } from "@/types.ts";
interface TeamPageProps { interface TeamPageProps {
team: Team; team: Team;
users: User[]; users: User[];
} }
export const handler: Handlers<TeamPageProps> = { export const handler: Handlers<TeamPageProps, ContextState> = {
async GET(request, context) { async GET(request, context) {
if (!context.state.user?.id) {
// unauthenticated requests may not view teams
return await context.renderNotFound();
}
// TODO: implement this with row-level security?
// TODO: do I just use supabase at this point?
// TODO: only allow logged-in users to view teams (and most resources!) // TODO: only allow logged-in users to view teams (and most resources!)
// TODO: only allow users that are a member of a team to view them // TODO: only allow users that are a member of a team to view them
// NOTE: maybe teams can be public...? // NOTE: maybe teams can be public...?
const { id } = context.params; const { id } = context.params;
if (!await isUserInTeam(context.state.user?.id, id)) {
// users that are not a member of a team may not view it
return await context.renderNotFound();
}
console.debug({ request, context }); console.debug({ request, context });
try { try {
const team = await getTeam({ id }); const team = await getTeam({ id });