Skip to content

Commit

Permalink
Merge pull request #61 from ItKlubBozoLagan/feature/organisation-perm…
Browse files Browse the repository at this point in the history
…issions

Feature/organisation permissions
  • Loading branch information
VisenP authored Oct 15, 2024
2 parents 73aa8c6 + 24ca2dd commit 87d0735
Show file tree
Hide file tree
Showing 33 changed files with 641 additions and 254 deletions.
2 changes: 2 additions & 0 deletions apps/backend/src/database/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { migration_add_evaluation_language_to_problem } from "./migrations/0036_
import { migration_add_notifications } from "./migrations/0037_add_notifications";
import { migration_add_join_codes } from "./migrations/0038_add_join_codes";
import { migration_add_mail_preferences } from "./migrations/0039_add_mail_preferences";
import { migration_add_organisations_permissions } from "./migrations/0040_add_organisations_permissions";

export const Database = new ScylloClient<{
users: User;
Expand Down Expand Up @@ -133,6 +134,7 @@ const migrations: Migration<any>[] = [
migration_add_notifications,
migration_add_join_codes,
migration_add_mail_preferences,
migration_add_organisations_permissions,
];

export const initDatabase = async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { OrganisationMemberV3, OrganisationPermissions, OrganisationV1 } from "@kontestis/models";
import { EMPTY_PERMISSIONS, grantPermission } from "permissio";
import { Migration } from "scyllo";

import { Database } from "../Database";

type MigrationType = {
organisation_members: OrganisationMemberV3;
organisations: OrganisationV1;
};

export const migration_add_organisations_permissions: Migration<MigrationType> = async (
database,
log
) => {
await database.raw("ALTER TABLE organisation_members ADD permissions BIGINT");

const organisationMembers = await database.selectFrom("organisation_members", "*", {});

const organisations = await database.selectFrom("organisations", "*", {});

const organisationsById: Record<string, OrganisationV1> = {};

for (const organisation of organisations) {
organisationsById[organisation.id.toString()] = organisation;
}

for (const organisationMember of organisationMembers) {
const organisation = organisationsById[organisationMember.organisation_id.toString()];

await Database.update(
"organisation_members",
{
permissions:
organisationMember.id === organisation?.owner
? grantPermission(EMPTY_PERMISSIONS, OrganisationPermissions.ADMIN)
: EMPTY_PERMISSIONS,
},
{
id: organisationMember.id,
user_id: organisationMember.user_id,
organisation_id: organisationMember.organisation_id,
}
);
}

log("Done");
};
22 changes: 3 additions & 19 deletions apps/backend/src/extractors/extractCluster.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import {
AdminPermissions,
ContestMemberPermissions,
hasAdminPermission,
hasContestPermission,
Snowflake,
} from "@kontestis/models";
import { ContestMemberPermissions, Snowflake } from "@kontestis/models";
import { Request } from "express";
import { StatusCodes } from "http-status-codes";

import { Database } from "../database/Database";
import { SafeError } from "../errors/SafeError";
import { mustHaveContestPermission } from "../preconditions/hasPermission";
import { extractIdFromParameters } from "../utils/extractorUtils";
import { extractContest } from "./extractContest";
import { extractContestMember } from "./extractContestMember";
import { extractProblem } from "./extractProblem";
import { extractUser } from "./extractUser";
import { memoizedRequestExtractor } from "./MemoizedRequestExtractor";

export const extractCluster = async (
Expand All @@ -34,16 +27,7 @@ export const extractCluster = async (
if (Date.now() >= contest.start_time.getTime() + 1000 * contest.duration_seconds)
return cluster;

const user = await extractUser(req);

if (hasAdminPermission(user.permissions, AdminPermissions.VIEW_CONTEST)) return cluster;

const member = await extractContestMember(req, contest.id);

if (
!hasContestPermission(member.contest_permissions, ContestMemberPermissions.VIEW_PRIVATE)
)
throw new SafeError(StatusCodes.FORBIDDEN);
await mustHaveContestPermission(req, ContestMemberPermissions.VIEW_PRIVATE, contest.id);

return cluster;
});
20 changes: 3 additions & 17 deletions apps/backend/src/extractors/extractContest.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import {
AdminPermissions,
ContestMemberPermissions,
hasAdminPermission,
hasContestPermission,
Snowflake,
} from "@kontestis/models";
import { ContestMemberPermissions, Snowflake } from "@kontestis/models";
import { Request } from "express";
import { StatusCodes } from "http-status-codes";

import { Database } from "../database/Database";
import { SafeError } from "../errors/SafeError";
import { mustHaveContestPermission } from "../preconditions/hasPermission";
import { extractIdFromParameters } from "../utils/extractorUtils";
import { extractContestMember } from "./extractContestMember";
import { extractUser } from "./extractUser";
import { memoizedRequestExtractor } from "./MemoizedRequestExtractor";

export const extractContest = (
Expand All @@ -28,14 +21,7 @@ export const extractContest = (

if (contest.public) return contest;

const user = await extractUser(req);

if (hasAdminPermission(user.permissions, AdminPermissions.VIEW_CONTEST)) return contest;

const member = await extractContestMember(req, contest.id);

if (!hasContestPermission(member.contest_permissions, ContestMemberPermissions.VIEW))
throw new SafeError(StatusCodes.NOT_FOUND);
await mustHaveContestPermission(req, ContestMemberPermissions.VIEW, contest.id);

return contest;
});
12 changes: 3 additions & 9 deletions apps/backend/src/extractors/extractContestMember.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ContestMemberPermissions, hasContestPermission, Snowflake } from "@kontestis/models";
import { ContestMemberPermissions, Snowflake } from "@kontestis/models";
import { Request } from "express";
import { StatusCodes } from "http-status-codes";

import { Database } from "../database/Database";
import { SafeError } from "../errors/SafeError";
import { mustHaveContestPermission } from "../preconditions/hasPermission";
import { extractIdFromParameters } from "../utils/extractorUtils";
import { extractUser } from "./extractUser";
import { memoizedRequestExtractor } from "./MemoizedRequestExtractor";
Expand All @@ -21,14 +22,7 @@ export const extractContestMember = (

if (!member) throw new SafeError(StatusCodes.NOT_FOUND);

if (
!hasContestPermission(
member.contest_permissions,
ContestMemberPermissions.VIEW,
user.permissions
)
)
throw new SafeError(StatusCodes.FORBIDDEN);
await mustHaveContestPermission(req, ContestMemberPermissions.VIEW, contestId);

return member;
});
27 changes: 7 additions & 20 deletions apps/backend/src/extractors/extractFinalSubmission.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import {
AdminPermissions,
ContestMemberPermissions,
hasAdminPermission,
hasContestPermission,
Snowflake,
} from "@kontestis/models";
import { ContestMemberPermissions, Snowflake } from "@kontestis/models";
import { Request } from "express";
import { StatusCodes } from "http-status-codes";

import { Database } from "../database/Database";
import { SafeError } from "../errors/SafeError";
import { mustHaveContestPermission } from "../preconditions/hasPermission";
import { extractIdFromParameters } from "../utils/extractorUtils";
import { extractContestMember } from "./extractContestMember";
import { extractUser } from "./extractUser";
import { memoizedRequestExtractor } from "./MemoizedRequestExtractor";

Expand All @@ -33,18 +27,11 @@ export const extractFinalSubmission = async (

if (user.id === finalSubmission.user_id) return finalSubmission;

if (hasAdminPermission(user.permissions, AdminPermissions.VIEW_CONTEST))
return finalSubmission;

const member = await extractContestMember(req, finalSubmission.contest_id);

if (
hasContestPermission(
member.contest_permissions,
ContestMemberPermissions.VIEW_PRIVATE
)
)
return finalSubmission;
await mustHaveContestPermission(
req,
ContestMemberPermissions.VIEW_PRIVATE,
finalSubmission.contest_id
);

throw new SafeError(StatusCodes.NOT_FOUND);
}
Expand Down
22 changes: 4 additions & 18 deletions apps/backend/src/extractors/extractModifiableContest.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
import {
AdminPermissions,
ContestMemberPermissions,
hasAdminPermission,
hasContestPermission,
Snowflake,
} from "@kontestis/models";
import { ContestMemberPermissions, Snowflake } from "@kontestis/models";
import { Request } from "express";
import { StatusCodes } from "http-status-codes";

import { SafeError } from "../errors/SafeError";
import { mustHaveContestPermission } from "../preconditions/hasPermission";
import { extractContest } from "./extractContest";
import { extractContestMember } from "./extractContestMember";
import { extractUser } from "./extractUser";

export const extractModifiableContest = async (req: Request, contestId?: Snowflake) => {
const [user, contest] = await Promise.all([extractUser(req), extractContest(req, contestId)]);
const contest = await extractContest(req, contestId);

if (hasAdminPermission(user.permissions, AdminPermissions.EDIT_CONTEST)) return contest;

const member = await extractContestMember(req, contest.id);

if (!hasContestPermission(member.contest_permissions, ContestMemberPermissions.EDIT))
throw new SafeError(StatusCodes.FORBIDDEN);
await mustHaveContestPermission(req, ContestMemberPermissions.EDIT, contest.id);

return contest;
};
15 changes: 4 additions & 11 deletions apps/backend/src/extractors/extractModifiableOrganisation.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import { AdminPermissions, hasAdminPermission, Snowflake } from "@kontestis/models";
import { OrganisationPermissions, Snowflake } from "@kontestis/models";
import { Request } from "express";
import { StatusCodes } from "http-status-codes";

import { SafeError } from "../errors/SafeError";
import { mustHaveOrganisationPermission } from "../preconditions/hasPermission";
import { extractOrganisation } from "./extractOrganisation";
import { extractUser } from "./extractUser";

export const extractModifiableOrganisation = async (req: Request, organisationId?: Snowflake) => {
const user = await extractUser(req);
const organisation = await extractOrganisation(req, organisationId);

if (
hasAdminPermission(user.permissions, AdminPermissions.EDIT_ORGANISATIONS) ||
user.id === organisation.owner
)
return organisation;
await mustHaveOrganisationPermission(req, OrganisationPermissions.ADMIN, organisation.id);

throw new SafeError(StatusCodes.FORBIDDEN);
return organisation;
};
11 changes: 10 additions & 1 deletion apps/backend/src/extractors/extractOrganisation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,16 @@ export const extractCurrentOrganisation = (req: Request) => {
const organisationId = req.headers[ORG_HEADER];

if (!organisationId || typeof organisationId !== "string" || !/\d+/.test(organisationId))
throw new SafeError(StatusCodes.BAD_REQUEST);
return extractOrganisation(req, DEFAULT_ORGANISATION.id);

return extractOrganisation(req, BigInt(organisationId));
};

export const extractCurrentOrganisationId = (req: Request) => {
const organisationId = req.headers[ORG_HEADER];

if (!organisationId || typeof organisationId !== "string" || !/\d+/.test(organisationId))
return DEFAULT_ORGANISATION.id;

return BigInt(organisationId);
};
27 changes: 27 additions & 0 deletions apps/backend/src/extractors/extractOrganisationMember.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Snowflake } from "@kontestis/models";
import { Request } from "express";
import { StatusCodes } from "http-status-codes";

import { Database } from "../database/Database";
import { SafeError } from "../errors/SafeError";
import { extractIdFromParameters } from "../utils/extractorUtils";
import { extractUser } from "./extractUser";
import { memoizedRequestExtractor } from "./MemoizedRequestExtractor";

export const extractOrganisationMember = (
req: Request,
organisationId: Snowflake = extractIdFromParameters(req, "organisation_id")
) => {
return memoizedRequestExtractor(req, "__organisation_member_" + organisationId, async () => {
const user = await extractUser(req);

const organisationMember = await Database.selectOneFrom("organisation_members", "*", {
organisation_id: organisationId,
user_id: user.id,
});

if (!organisationMember) throw new SafeError(StatusCodes.NOT_FOUND);

return organisationMember;
});
};
26 changes: 7 additions & 19 deletions apps/backend/src/extractors/extractProblem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
AdminPermissions,
ContestMemberPermissions,
hasAdminPermission,
hasContestPermission,
Problem,
Snowflake,
} from "@kontestis/models";
Expand All @@ -11,12 +10,11 @@ import { StatusCodes } from "http-status-codes";

import { Database } from "../database/Database";
import { SafeError } from "../errors/SafeError";
import { hasContestPermission } from "../preconditions/hasPermission";
import { extractIdFromParameters } from "../utils/extractorUtils";
import { R } from "../utils/remeda";
import { extractContest } from "./extractContest";
import { extractContestMember } from "./extractContestMember";
import { extractOptionalUser } from "./extractOptionalUser";
import { extractUser } from "./extractUser";
import { memoizedRequestExtractor } from "./MemoizedRequestExtractor";

export const extractProblem = (
Expand Down Expand Up @@ -57,10 +55,11 @@ export const extractProblem = (
if (
Date.now() >= contest.start_time.getTime() &&
(!optionalMember ||
!hasContestPermission(
optionalMember.contest_permissions,
ContestMemberPermissions.VIEW_PRIVATE
)) &&
!(await hasContestPermission(
req,
ContestMemberPermissions.VIEW_PRIVATE,
contest.id
))) &&
(!optionalUser ||
!hasAdminPermission(optionalUser.permissions, AdminPermissions.VIEW_CONTEST))
)
Expand All @@ -71,18 +70,7 @@ export const extractProblem = (
"solution_code",
]);

const user = await extractUser(req);

if (hasAdminPermission(user.permissions, AdminPermissions.VIEW_CONTEST)) return problem;

const member = await extractContestMember(req, problem.contest_id);

if (
hasContestPermission(
member.contest_permissions,
ContestMemberPermissions.VIEW_PRIVATE
)
)
if (await hasContestPermission(req, ContestMemberPermissions.VIEW_PRIVATE, contest.id))
return problem;

throw new SafeError(StatusCodes.NOT_FOUND);
Expand Down
Loading

0 comments on commit 87d0735

Please sign in to comment.