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();
}
/**
* 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> {
try {
await dbOp((conn) => conn.queryObject("select 1"));
@ -111,17 +141,20 @@ export async function dbOp<T>(
return result;
}
/**
* Example: queryObject('select * from "user" where id = $1', [userId])
*/
export async function queryObject<T>(
sql: string,
args?: QueryArguments,
connection?: PoolClient | Transaction,
): Promise<QueryObjectResult<T> | null> {
console.debug(`queryObject: ${sql}`);
if (!connection) {
return await dbOp(async (connection) => {
return await queryObject(sql, args, connection);
});
} else {
log.debug(`queryObject: ${sql}`);
const result = await connection.queryObject<T>({
camelcase: true,
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,
args?: QueryArguments,
connection?: PoolClient,
): Promise<QueryArrayResult<T> | null> {
console.debug(`queryArray: ${sql}`);
if (!connection) {
return await dbOp(async (connection) => {
return await queryArray<T>(sql, args, connection);
});
} else {
log.debug(`queryArray: ${sql}`);
const result = await connection.queryArray<T>({
text: sql.trim(),
args,
@ -210,7 +246,7 @@ export async function createTeam(
},
transaction?: Transaction,
): Promise<Team> {
console.debug("createTeam tx:", transaction);
log.debug("createTeam tx:", transaction);
if (!transaction) {
return await wrapWithTransaction<Team>(
"createTeam",
@ -234,7 +270,7 @@ export async function createTeam(
}
return team;
} catch (e) {
console.error("Error creating team:", e);
log.error("Error creating team:", e);
throw e;
}
}
@ -253,7 +289,7 @@ export async function wrapWithTransaction<T>(
);
try {
await transaction.begin();
console.debug(
log.debug(
`started ${transactionName} tx with options ${
JSON.stringify(transactionOptions)
}:`,
@ -264,11 +300,11 @@ export async function wrapWithTransaction<T>(
return result;
} catch (e) {
await transaction.rollback();
console.error("Failed to complete transaction:", e);
log.error("Failed to complete transaction:", e);
throw e;
}
} catch (e) {
console.error("Failed to create transaction");
log.error("Failed to create transaction");
throw e;
}
});
@ -304,7 +340,7 @@ export async function createUser(
return user;
} catch (e) {
console.error("Error creating user:", e);
log.error("Error creating user:", e);
throw e;
}
}

View file

@ -1,18 +1,31 @@
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 ContextState } from "@/types.ts";
interface TeamPageProps {
team: Team;
users: User[];
}
export const handler: Handlers<TeamPageProps> = {
export const handler: Handlers<TeamPageProps, ContextState> = {
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 users that are a member of a team to view them
// NOTE: maybe teams can be public...?
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 });
try {
const team = await getTeam({ id });