Skip to content

Commit

Permalink
Merge pull request #280 from esek/feature/accesslogs
Browse files Browse the repository at this point in the history
Feature/accesslogs
  • Loading branch information
pontussjostedt authored Nov 3, 2024
2 parents 3b24d33 + c3d8e1c commit 4f20fad
Show file tree
Hide file tree
Showing 14 changed files with 1,291 additions and 336 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Alla märkbara ändringar ska dokumenteras i denna fil.
Baserat på [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
och följer [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.14.0]
- log changes to individualAccess and postAccess

## [1.13.0]
- Verify user logic in user api, resolver and graph.

Expand Down
2 changes: 2 additions & 0 deletions codegen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ generates:
Proposal: ../mappers#ProposalResponse
Nomination: ../mappers#NominationResponse
Hehe: ../mappers#HeheResponse
AccessLogPost: ../mappers#AccessLogPostResponse
AccessLogIndividualAccess: ../mappers#AccessLogIndividualAccessResponse
ApiKey: ../mappers#ApiKeyResponse
schema: "./src/schemas/*.graphql"
plugins:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ekorre-ts",
"version": "1.13.0",
"version": "1.14.0",
"description": "E-Sektionens backend",
"main": "src/index.ts",
"scripts": {
Expand Down Expand Up @@ -31,7 +31,8 @@
"Axel Froborg <[email protected]>",
"Marcus Lindell <[email protected]>",
"Eric Weidow <[email protected]>",
"Axel Andersson <[email protected]>"
"Axel Andersson <[email protected]>",
"Pontus Sjöstedt <[email protected]>"
],
"bugs": {
"email": "[email protected]"
Expand Down Expand Up @@ -102,4 +103,4 @@
"supertest": "^6.3.3",
"ts-jest": "^29.0.3"
}
}
}
114 changes: 72 additions & 42 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,51 @@ model PrismaIndividualAccess {
@@map("individual_accesses")
}

model PrismaIndividualAccessLog {
id Int @id @default(autoincrement())
grantor PrismaUser @relation(name: "PrismaIndividualAccessLogToPrismaUserGrantor", fields: [refGrantor], references: [username], onDelete: Cascade)
refGrantor String @map("ref_grantor")
target PrismaUser @relation(name: "PrismaIndividualAccessLogToPrismaUserTarget", fields: [refTarget], references: [username], onDelete: Cascade)
refTarget String @map("ref_target")
resourceType PrismaResourceType @map("resource_type")
resource String
isActive Boolean @map("is_active")
timestamp DateTime @default(now())
@@map("individual_accesses_log")
}

model PrismaUser {
username String @id
access PrismaIndividualAccess[] @relation(name: "PrismaIndividualAccessToPrismaUser")
articles PrismaArticle[] @relation(name: "PrismaArticleToAuthor")
editedArticles PrismaArticle[] @relation(name: "PrismaArticleToLastUpdateBy")
elections PrismaElection[] @relation(name: "PrismaElectionToPrismaUser")
nominations PrismaNomination[] @relation(name: "PrismaNominationToPrismaUser")
proposals PrismaProposal[] @relation(name: "PrismaProposalToPrismaUser")
files PrismaFile[] @relation(name: "PrismaFileToPrismaUser")
heHes PrismaHehe[] @relation(name: "PrismaHeheToPrismaUser")
postHistory PrismaPostHistory[] @relation(name: "PrismaPostHistoryToPrismaUser")
emergencyContacts PrismaEmergencyContact[] @relation(name: "PrismaEmergencyContactToPrismaUser")
passwordResets PrismaPasswordReset[] @relation(name: "PrismaPasswordResetToPrismaUser")
apiKeys PrismaApiKey[] @relation(name: "PrismaApiKeyToPrismaUser")
loginProviders PrismaLoginProvider[] @relation(name: "PrismaUserToPrismaLoginProvider")
passwordHash String @map("password_hash")
passwordSalt String @map("password_salt")
firstName String @map("first_name")
lastName String @map("last_name")
class String
photoUrl String? @map("photo_url")
email String @unique
phone String?
address String?
zipCode String? @map("zip_code")
website String?
luCard String? @unique
dateJoined DateTime @default(now()) @map("date_joined")
verifyInfo PrismaVerifyInfo? @relation(name: "PrismaVerifyInfoToPrismaUser")
username String @id
access PrismaIndividualAccess[] @relation(name: "PrismaIndividualAccessToPrismaUser")
grantedIndividualAccessLog PrismaIndividualAccessLog[] @relation(name: "PrismaIndividualAccessLogToPrismaUserGrantor")
grantedPostAccessLog PrismaPostAccessLog[] @relation(name: "PrismaPostAccessLogToPrismaUserGrantor")
targetIndividualAccessLog PrismaIndividualAccessLog[] @relation(name: "PrismaIndividualAccessLogToPrismaUserTarget")
articles PrismaArticle[] @relation(name: "PrismaArticleToAuthor")
editedArticles PrismaArticle[] @relation(name: "PrismaArticleToLastUpdateBy")
elections PrismaElection[] @relation(name: "PrismaElectionToPrismaUser")
nominations PrismaNomination[] @relation(name: "PrismaNominationToPrismaUser")
proposals PrismaProposal[] @relation(name: "PrismaProposalToPrismaUser")
files PrismaFile[] @relation(name: "PrismaFileToPrismaUser")
heHes PrismaHehe[] @relation(name: "PrismaHeheToPrismaUser")
postHistory PrismaPostHistory[] @relation(name: "PrismaPostHistoryToPrismaUser")
emergencyContacts PrismaEmergencyContact[] @relation(name: "PrismaEmergencyContactToPrismaUser")
passwordResets PrismaPasswordReset[] @relation(name: "PrismaPasswordResetToPrismaUser")
apiKeys PrismaApiKey[] @relation(name: "PrismaApiKeyToPrismaUser")
loginProviders PrismaLoginProvider[] @relation(name: "PrismaUserToPrismaLoginProvider")
passwordHash String @map("password_hash")
passwordSalt String @map("password_salt")
firstName String @map("first_name")
lastName String @map("last_name")
class String
photoUrl String? @map("photo_url")
email String @unique
phone String?
address String?
zipCode String? @map("zip_code")
website String?
luCard String? @unique
dateJoined DateTime @default(now()) @map("date_joined")
verifyInfo PrismaVerifyInfo? @relation(name: "PrismaVerifyInfoToPrismaUser")
@@index([firstName, lastName])
@@map("users")
Expand All @@ -62,21 +78,35 @@ model PrismaPostAccess {
@@map("post_accesses")
}

model PrismaPostAccessLog {
id Int @id @default(autoincrement())
grantor PrismaUser @relation(name: "PrismaPostAccessLogToPrismaUserGrantor", fields: [refGrantor], references: [username], onDelete: Cascade)
refGrantor String @map("ref_grantor")
target PrismaPost @relation(name: "PrismaPostAccessLogToPrismaPostTarget", fields: [refTarget], references: [id], onDelete: Cascade)
refTarget Int @map("ref_target")
resourceType PrismaResourceType @map("resource_type")
resource String
isActive Boolean @map("is_active")
timestamp DateTime @default(now())
@@map("post_accesses_log")
}

model PrismaPost {
id Int @id @default(autoincrement())
access PrismaPostAccess[] @relation(name: "PrismaPostToPrismaPostAccess")
electables PrismaElectable[] @relation(name: "PrismaElectableToPrismaPost")
nominations PrismaNomination[] @relation(name: "PrismaNominationToPrismaPost")
history PrismaPostHistory[] @relation(name: "PrismaPostToPrismaPostHistory")
sortPriority Int @default(0)
postname String @unique
email String?
utskott PrismaUtskott
description String
spots Int
postType PrismaPostType @map("post_type")
active Boolean @default(true)
interviewRequired Boolean @default(false) @map("interview_required")
id Int @id @default(autoincrement())
access PrismaPostAccess[] @relation(name: "PrismaPostToPrismaPostAccess")
targetAccessLog PrismaPostAccessLog[] @relation(name: "PrismaPostAccessLogToPrismaPostTarget")
electables PrismaElectable[] @relation(name: "PrismaElectableToPrismaPost")
nominations PrismaNomination[] @relation(name: "PrismaNominationToPrismaPost")
history PrismaPostHistory[] @relation(name: "PrismaPostToPrismaPostHistory")
sortPriority Int @default(0)
postname String @unique
email String?
utskott PrismaUtskott
description String
spots Int
postType PrismaPostType @map("post_type")
active Boolean @default(true)
interviewRequired Boolean @default(false) @map("interview_required")
// Posts are either searched by name or by utskott,
// name handled by primare key (@id)
Expand Down
144 changes: 133 additions & 11 deletions src/api/access.api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { PostAPI } from '@/api/post.api';
import { ServerError } from '@/errors/request.errors';
import { Logger } from '@/logger';
import { AccessEntry } from '@/models/access';
import { AccessEntry, AccessLogEntry } from '@/models/access';
import { devGuard } from '@/util';
import { AccessInput, Door, Feature } from '@generated/graphql';
import {
Prisma,
PrismaApiKeyAccess,
PrismaIndividualAccess,
PrismaIndividualAccessLog,
PrismaPostAccess,
PrismaPostAccessLog,
PrismaResourceType,
} from '@prisma/client';

Expand Down Expand Up @@ -76,6 +78,71 @@ export class AccessAPI {
return access;
}

/**
*
* @param incoming old values
* @param current new values
* @returns A record describing which elements are not in both arrays with the value true or false depending on
* if it's only in the incoming or current array
*/
private getArrDiff(incoming: string[], current: string[]): Record<string, boolean> {
const differences: Record<string, boolean> = {};

const incomingSet = new Set(incoming);
const currentSet = new Set(current);

for (const item of incoming) {
if (!currentSet.has(item)) {
differences[item] = true;
}
}

for (const item of current) {
if (!incomingSet.has(item)) {
differences[item] = false;
}
}

return differences;
}

/**
*
* @param grantor The user giving out permission
* @param target The user/post which is getting their access changed
* @param newAccess
* @param oldAccess
* @returns getArrDiff calculated for each feature type with info about the grantor and target
*/
private getAllInputAccessDiff<
T extends number | string,
N extends AccessEntry,
O extends AccessEntry,
>(grantor: string, target: T, newAccess: N[], oldAccess: O[]): AccessLogEntry<T>[] {
const log = Object.values(PrismaResourceType).flatMap((resourceType) => {
const oldResource = oldAccess
.filter((access) => access.resourceType == resourceType)
.map((access) => access.resource);
const newResource = newAccess
.filter((access) => access.resourceType == resourceType)
.map((access) => access.resource);

const resourceDiff = this.getArrDiff(newResource, oldResource);

const logDiff = Object.entries(resourceDiff).map(([resource, isActive]) => ({
refGrantor: grantor,
refTarget: target,
resourceType: resourceType,
resource: resource,
isActive: isActive,
}));

return logDiff as AccessLogEntry<T>[];
});

return log;
}

/**
* Set the individual access for an user
*
Expand All @@ -84,7 +151,11 @@ export class AccessAPI {
* @param username Username for the user
* @param newAccess The new individual access for this user
*/
async setIndividualAccess(username: string, newAccess: AccessInput): Promise<boolean> {
async setIndividualAccess(
username: string,
newAccess: AccessInput,
grantor: string,
): Promise<boolean> {
const { doors, features } = newAccess;
const access: Prisma.PrismaIndividualAccessUncheckedCreateInput[] = [];

Expand Down Expand Up @@ -119,12 +190,23 @@ export class AccessAPI {
data: access,
});

// Ensure deletion and creation is made in one swoop,
// so access is not deleted if old one is bad
const [, res] = await prisma.$transaction([deleteQuery, createQuery]);
const individualAccessDiff = this.getAllInputAccessDiff(
grantor,
username,
access,
await this.getIndividualAccess(username),
);
const logDiffQuery = prisma.prismaIndividualAccessLog.createMany({
data: individualAccessDiff,
});

const [, res] = await prisma.$transaction([deleteQuery, createQuery, logDiffQuery]);

logger.info(`Updated access for user ${username} by ${grantor}`);
logger.debug(
`Updated access for user ${username} to ${Logger.pretty(newAccess)} by ${grantor}`,
);

logger.info(`Updated access for user ${username}`);
logger.debug(`Updated access for user ${username} to ${Logger.pretty(newAccess)}`);
return res.count === access.length;
}

Expand Down Expand Up @@ -188,7 +270,7 @@ export class AccessAPI {
* @param postId The ID for the user for which acces is to be changed
* @param newAccess The new access for this post
*/
async setPostAccess(postId: number, newAccess: AccessInput): Promise<boolean> {
async setPostAccess(postId: number, newAccess: AccessInput, grantor: string): Promise<boolean> {
const { doors, features } = newAccess;
const access: Prisma.PrismaPostAccessUncheckedCreateInput[] = [];

Expand Down Expand Up @@ -223,15 +305,37 @@ export class AccessAPI {
data: access,
});

const postAccessDiff = this.getAllInputAccessDiff(
grantor,
postId,
access,
await this.getPostAccess(postId),
);
const logDiffQuery = prisma.prismaPostAccessLog.createMany({
data: postAccessDiff,
});

// Ensure deletion and creation is made in one swoop,
// so access is not deleted if old one is bad
const [, res] = await prisma.$transaction([deleteQuery, createQuery]);
const [, res] = await prisma.$transaction([deleteQuery, createQuery, logDiffQuery]);

logger.info(`Updated access for post with id ${postId}`);
logger.debug(`Updated access for post with id ${postId} to ${Logger.pretty(newAccess)}`);
logger.info(`Updated access for post with id ${postId} by ${grantor}`);
logger.debug(
`Updated access for post with id ${postId} to ${Logger.pretty(newAccess)} by ${grantor}`,
);
return res.count === access.length;
}

async getAllPostLogs(): Promise<PrismaPostAccessLog[]> {
const values = await prisma.prismaPostAccessLog.findMany({});
return values;
}

async getAllIndividualAccessLogs(): Promise<PrismaIndividualAccessLog[]> {
const values = await prisma.prismaIndividualAccessLog.findMany({});
return values;
}

/**
* Gets a users entire access, including inherited access from posts,
* and is within post access cooldown (users retains post access some time after
Expand Down Expand Up @@ -309,4 +413,22 @@ export class AccessAPI {
},
});
}

/**
* Used for testing
* Will clear every post accesslog
*/
async clearPostAccessLog() {
devGuard('Tried to clear post accesslogs in production!');
await prisma.prismaPostAccessLog.deleteMany();
}

/**
* Used for testing
* Will clear every individual accesslog
*/
async clearIndividualAccessLog() {
devGuard('Tried to clear individual accesslogs in production!');
await prisma.prismaIndividualAccessLog.deleteMany();
}
}
Loading

0 comments on commit 4f20fad

Please sign in to comment.