Skip to content

Commit

Permalink
Merge pull request #40 from icefoganalytics/issue-38/part-2/data-owne…
Browse files Browse the repository at this point in the history
…r-add-ability-to-define-access-constraints

Part 2: Data Owner: Add Ability To Define Access Constraints
  • Loading branch information
klondikemarlen authored Feb 19, 2024
2 parents 0c0611d + 5a00c72 commit 662761b
Show file tree
Hide file tree
Showing 55 changed files with 1,858 additions and 141 deletions.
36 changes: 33 additions & 3 deletions api/package-lock.json

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

2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"jwks-rsa": "^3.1.0",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"qs": "^6.11.2",
"sequelize": "^6.35.2",
"slugify": "^1.6.6",
"tedious": "^16.6.1",
Expand All @@ -40,6 +41,7 @@
"@types/jest": "^29.5.11",
"@types/lodash": "^4.14.202",
"@types/luxon": "^3.4.2",
"@types/qs": "^6.9.11",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
Expand Down
9 changes: 9 additions & 0 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ import express, { type Request, type Response } from "express"
import cors from "cors"
import path from "path"
import helmet from "helmet"
import qs from "qs"

import { FRONTEND_URL } from "@/config"
import router from "@/router"

export const app = express()

// For parsing nulls out of query strings
// See web/src/api/http-client.ts
app.set("query parser", (params: string) => {
return qs.parse(params, {
strictNullHandling: true,
})
})
app.use(express.json()) // for parsing application/json
app.use(express.urlencoded({ extended: true })) // for parsing application/x-www-form-urlencoded
app.use(
Expand Down
43 changes: 43 additions & 0 deletions api/src/controllers/access-requests-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { WhereOptions } from "sequelize"

import { AccessRequest } from "@/models"
import { TableSerializer } from "@/serializers/access-requests"

import BaseController from "@/controllers/base-controller"

export class AccessRequestsController extends BaseController {
async index() {
const where = this.query.where as WhereOptions<AccessRequest>

const totalCount = await AccessRequest.count({ where })
const accessRequests = await AccessRequest.findAll({
where,
include: [
{
association: "requestor",
include: [
{
association: "groupMembership",
include: ["department"],
},
],
},
"accessGrant",
],
limit: this.pagination.limit,
offset: this.pagination.offset,
order: [["requestor", "firstName", "ASC"]],
})

try {
const serializedAccessRequests = TableSerializer.perform(accessRequests, this.currentUser)
return this.response.json({ accessRequests: serializedAccessRequests, totalCount })
} catch (error) {
return this.response
.status(500)
.json({ message: `Failed to serialize access requests: ${error}` })
}
}
}

export default AccessRequestsController
46 changes: 46 additions & 0 deletions api/src/controllers/access-requests/approve-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { isNil } from "lodash"

import { AccessRequest } from "@/models"
import { AccessRequestsPolicy } from "@/policies"
import { ApproveService } from "@/services/access-requests"
import { type AccessRequestWithDataset } from "@/policies/access-requests-policy"

import BaseController from "@/controllers/base-controller"

export class ApproveController extends BaseController {
async create() {
const accessRequest = await this.loadAccessRequest()
if (isNil(accessRequest)) {
return this.response.status(404).json({ message: "Access request not found." })
}

const policy = this.buildPolicy(accessRequest)
if (!policy.update()) {
return this.response
.status(403)
.json({ message: "You are not authorized to approve access requests on this dataset." })
}

try {
const updatedAccessRequest = await ApproveService.perform(accessRequest, this.currentUser)
return this.response.status(200).json({ accessRequest: updatedAccessRequest })
} catch (error) {
return this.response.status(422).json({ message: `Access request approval failed: ${error}` })
}
}

private async loadAccessRequest(): Promise<AccessRequestWithDataset | null> {
const { accessRequestId } = this.request.params
const accessRequest = await AccessRequest.findByPk(accessRequestId, { include: ["dataset"] })
if (isNil(accessRequest?.dataset)) return null

// TODO: figure out how to make this type cast unneccessary
return accessRequest as AccessRequestWithDataset
}

private buildPolicy(accessRequest: AccessRequestWithDataset) {
return new AccessRequestsPolicy(this.currentUser, accessRequest)
}
}

export default ApproveController
51 changes: 51 additions & 0 deletions api/src/controllers/access-requests/deny-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { isNil } from "lodash"

import { AccessRequest } from "@/models"
import { AccessRequestsPolicy } from "@/policies"
import { AccessRequestWithDataset } from "@/policies/access-requests-policy"
import { DenyService } from "@/services/access-requests"

import BaseController from "@/controllers/base-controller"

export class DenyController extends BaseController {
async create() {
const accessRequest = await this.loadAccessRequest()
if (isNil(accessRequest)) {
return this.response.status(404).json({ message: "Access request not found." })
}

const policy = this.buildPolicy(accessRequest)
if (!policy.update()) {
return this.response
.status(403)
.json({ message: "You are not authorized to deny access requests on this dataset." })
}

const permittedAttributes = policy.permitAttributes(this.request.body)
try {
const updatedAccessRequest = await DenyService.perform(
accessRequest,
permittedAttributes,
this.currentUser
)
return this.response.status(200).json({ accessRequest: updatedAccessRequest })
} catch (error) {
return this.response.status(422).json({ message: `Access request denial failed: ${error}` })
}
}

private async loadAccessRequest(): Promise<AccessRequestWithDataset | null> {
const { accessRequestId } = this.request.params
const accessRequest = await AccessRequest.findByPk(accessRequestId, { include: ["dataset"] })
if (isNil(accessRequest?.dataset)) return null

// TODO: figure out how to make this type cast unneccessary
return accessRequest as AccessRequestWithDataset
}

private buildPolicy(accessRequest: AccessRequestWithDataset) {
return new AccessRequestsPolicy(this.currentUser, accessRequest)
}
}

export default DenyController
3 changes: 3 additions & 0 deletions api/src/controllers/access-requests/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { ApproveController } from "./approve-controller"
export { DenyController } from "./deny-controller"
export { RevokeController } from "./revoke-controller"
46 changes: 46 additions & 0 deletions api/src/controllers/access-requests/revoke-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { isNil } from "lodash"

import { AccessRequest } from "@/models"
import { AccessRequestsPolicy } from "@/policies"
import { AccessRequestWithDataset } from "@/policies/access-requests-policy"
import { RevokeService } from "@/services/access-requests"

import BaseController from "@/controllers/base-controller"

export class RevokeController extends BaseController {
async create() {
const accessRequest = await this.loadAccessRequest()
if (isNil(accessRequest)) {
return this.response.status(404).json({ message: "Access request not found." })
}

const policy = this.buildPolicy(accessRequest)
if (!policy.update()) {
return this.response
.status(403)
.json({ message: "You are not authorized to revoke access requests on this dataset." })
}

try {
const updatedAccessRequest = await RevokeService.perform(accessRequest, this.currentUser)
return this.response.status(200).json({ accessRequest: updatedAccessRequest })
} catch (error) {
return this.response.status(422).json({ message: `Access request revokal failed: ${error}` })
}
}

private async loadAccessRequest(): Promise<AccessRequestWithDataset | null> {
const { accessRequestId } = this.request.params
const accessRequest = await AccessRequest.findByPk(accessRequestId, { include: ["dataset"] })
if (isNil(accessRequest?.dataset)) return null

// TODO: figure out how to make this type cast unneccessary
return accessRequest as AccessRequestWithDataset
}

private buildPolicy(accessRequest: AccessRequestWithDataset) {
return new AccessRequestsPolicy(this.currentUser, accessRequest)
}
}

export default RevokeController
2 changes: 2 additions & 0 deletions api/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { AccessGrantsController } from "./access-grants-controller"
export { AccessRequestsController } from "./access-requests-controller"
export { CurrentUserController } from "./current-user-controller"
export { DatasetFieldsController } from "./dataset-fields-controller"
export { DatasetsController } from "./datasets-controller"
Expand All @@ -8,6 +9,7 @@ export { TagsController } from "./tags-controller"
export { UserGroupsController } from "./user-groups-controller"
export { UsersController } from "./users-controller"

export * as AccessRequests from "./access-requests"
export * as Users from "./users"
export * as UserGroups from "./user-groups"
export * as QaScenarios from "./qa-scenarios"
Loading

0 comments on commit 662761b

Please sign in to comment.