Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin Roles and Access - Part 2/3 #103

Merged
merged 6 commits into from
May 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/admins/admins.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { AdminsService } from './admins.service'
import { AdminsRepository } from './admins.repository'
import { SharedModule } from '../shared/shared.module'
import { AdminsController } from './admins.controller'
import { OrganizationsModule } from '../organizations/organizations.module'

@Module({
imports: [SharedModule],
imports: [SharedModule, OrganizationsModule],
providers: [AdminsService, AdminsRepository],
exports: [AdminsService],
controllers: [AdminsController],
Expand Down
6 changes: 5 additions & 1 deletion src/admins/admins.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { Test, TestingModule } from '@nestjs/testing'
import { AdminsService } from './admins.service'
import { AdminsRepository } from './admins.repository'
import { OrganizationsService } from '../organizations/organizations.service'

describe('AdminsService', () => {
let service: AdminsService
const adminsRepository = { findAll: () => ['test'] }
const organizationsService = {}

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AdminsService, AdminsRepository],
providers: [AdminsService, AdminsRepository, OrganizationsService],
})
.overrideProvider(AdminsRepository)
.useValue(adminsRepository)
.overrideProvider(OrganizationsService)
.useValue(organizationsService)
.compile()

service = module.get<AdminsService>(AdminsService)
Expand Down
64 changes: 53 additions & 11 deletions src/admins/admins.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,23 @@ import { AdminsRepository } from './admins.repository'
import { CreateAdminDto, CreateAdminRequestDto } from './dto/create-admin.dto'
import { Admin } from './classes/admin.class'
import * as firebaseAdmin from 'firebase-admin'
import { AdminRole, canUserCreateSuperAdmin, getSuperAdminACLKey } from '../shared/acl'
import {
AdminRole,
canUserCreateSuperAdmin,
getSuperAdminACLKey,
getOrganizationAdminACLKey,
canUserCreateOrganizationAdmin,
canUserCreateNationalAdmin,
} from '../shared/acl'
import { RequestAdminUser } from '../shared/interfaces'
import { OrganizationsService } from '../organizations/organizations.service'

@Injectable()
export class AdminsService {
constructor(private adminsRepository: AdminsRepository) {}
constructor(
private adminsRepository: AdminsRepository,
private organizationsService: OrganizationsService
) {}

async createOneAdminUser(
requestAdminUser: RequestAdminUser,
Expand All @@ -30,18 +41,55 @@ export class AdminsService {
throw new ConflictException('An admin with this email already exists')
}

let generatedUserAccessKey: string
// Start preparing the create admin object. It will be passed to the repo function.
const createAdminDto: CreateAdminDto = new CreateAdminDto()
createAdminDto.email = createAdminRequest.email
createAdminDto.addedByAdminUserId = requestAdminUser.uid
createAdminDto.addedByAdminEmail = requestAdminUser.email
createAdminDto.userAdminRole = createAdminRequest.adminRole

// Check if the user has access to create new user with desired adminRole in the payload.
// Also, determine what accessKey will be added to the new created admin.
switch (createAdminRequest.adminRole) {
case AdminRole.superAdminRole:
if (!canUserCreateSuperAdmin(requestAdminUser.userAccessKey)) {
throw new UnauthorizedException('Insufficient access to create this adminRole')
}
generatedUserAccessKey = getSuperAdminACLKey()
createAdminDto.userAccessKey = getSuperAdminACLKey()
break

case AdminRole.nationalAdminRole:
if (!canUserCreateNationalAdmin(requestAdminUser.userAccessKey)) {
throw new UnauthorizedException('Insufficient access to create this adminRole')
}
throw new UnauthorizedException('WIP. nationalAdminRole not supported yet')

case AdminRole.prefectureAdminRole:
throw new UnauthorizedException('WIP. prefectureAdminRole not supported yet')

case AdminRole.organizationAdminRole:
if (
!canUserCreateOrganizationAdmin(
requestAdminUser.userAccessKey,
createAdminRequest.organizationId
)
) {
throw new UnauthorizedException('Insufficient access to create this adminRole')
}
// Check if organizationId is valid
const isOrganizationCodeValid = await this.organizationsService.isOrganizationCodeValid(
createAdminRequest.organizationId
)
if (!isOrganizationCodeValid) {
throw new BadRequestException('Invalid organizationId value')
}

createAdminDto.userAccessKey = getOrganizationAdminACLKey(createAdminRequest.organizationId)
createAdminDto.organizationId = createAdminRequest.organizationId
break

default:
throw new UnauthorizedException('WIP. adminRole not supported yet')
throw new BadRequestException('Invalid adminRole value')
}

let firebaseUserRecord: firebaseAdmin.auth.UserRecord
Expand All @@ -55,13 +103,7 @@ export class AdminsService {
throw new BadRequestException(error.message)
}

const createAdminDto: CreateAdminDto = new CreateAdminDto()
createAdminDto.adminUserId = firebaseUserRecord.uid
createAdminDto.email = createAdminRequest.email
createAdminDto.addedByAdminUserId = requestAdminUser.uid
createAdminDto.addedByAdminEmail = requestAdminUser.email
createAdminDto.userAdminRole = createAdminRequest.adminRole
createAdminDto.userAccessKey = generatedUserAccessKey

console.log('createAdminDto : ', createAdminDto)

Expand Down
16 changes: 16 additions & 0 deletions src/admins/dto/create-admin.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ export class CreateAdminDto {
@IsNotEmpty()
userAccessKey: string

@ApiPropertyOptional({
description: 'Optional, needed when admin role is ORGANIZATION_ADMIN_ROLE',
})
@ValidateIf((o) => o.userAdminRole === AdminRole.organizationAdminRole)
@IsString()
@IsNotEmpty()
organizationId: string

@ApiPropertyOptional({
description: 'Optional, needed when admin role is PREFECTURE_ADMIN_ROLE',
})
@ValidateIf((o) => o.userAdminRole === AdminRole.prefectureAdminRole)
@IsString()
@IsNotEmpty()
prefectureId: string

@ApiProperty()
@IsString()
@IsNotEmpty()
Expand Down
43 changes: 29 additions & 14 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { validateOrReject } from 'class-validator'
import { LoginNormalUserRequestDto } from './dto/login-normal-user.dto'
import { FirebaseService } from '../shared/firebase/firebase.service'
import { RequestAdminUser } from '../shared/interfaces'
import { Admin } from '../admins/classes/admin.class'

@Injectable()
export class AuthService {
Expand Down Expand Up @@ -38,22 +39,9 @@ export class AuthService {
if (adminObj.email !== requestAdminUser.email) {
throw new ForbiddenException('Email in access token does not match with admin in firestore')
}
if (!adminObj.userAdminRole || !adminObj.userAccessKey) {
throw new ForbiddenException('Admin in firestore does not have role and access')
}

// If custom claim does not exist, then add it because above validation has passed.
if (
!requestAdminUser.isAdminUser ||
!requestAdminUser.userAdminRole ||
!requestAdminUser.userAccessKey
) {
await this.firebaseService.UpsertCustomClaims(requestAdminUser.uid, {
isAdminUser: true,
userAdminRole: adminObj.userAdminRole,
userAccessKey: adminObj.userAccessKey,
})
}
await this.upsertAdminCustomClaims(requestAdminUser, adminObj)
}

/**
Expand All @@ -78,4 +66,31 @@ export class AuthService {

await this.usersService.createOneUser(createUserDto, createUserProfileDto)
}

/**
* Upsert admin custom claims to JWT if they don't already exist.
* Also validate if the admin role and key exist in the firestore entry.
* @param requestAdminUser: RequestAdminUser
* @param admin: Admin
*/
private async upsertAdminCustomClaims(
requestAdminUser: RequestAdminUser,
admin: Admin
): Promise<void> {
if (!admin.userAdminRole || !admin.userAccessKey) {
throw new ForbiddenException('Admin in firestore does not have role and access')
}

if (
!requestAdminUser.isAdminUser ||
!requestAdminUser.userAdminRole ||
!requestAdminUser.userAccessKey
) {
await this.firebaseService.UpsertCustomClaims(requestAdminUser.uid, {
isAdminUser: true,
userAdminRole: admin.userAdminRole,
userAccessKey: admin.userAccessKey,
})
}
}
}
9 changes: 8 additions & 1 deletion src/organizations/organizations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
UpdateOrganizationRequestDto,
} from './dto/create-organization.dto'
import { OrganizationsService } from './organizations.service'
import { RequestAdminUser } from '../shared/interfaces'

@ApiTags('organization')
@ApiBearerAuth()
Expand Down Expand Up @@ -68,12 +69,18 @@ export class OrganizationsController {
@ApiOkResponse({ type: Organization })
@Get('/organizations/:organizationId')
async getOrganizationById(
@Request() req,
@Param('organizationId') organizationId: string
): Promise<Organization> {
const organization = await this.organizationsService.findOneOrganizationById(organizationId)
const requestAdminUser: RequestAdminUser = req.user
const organization = await this.organizationsService.findOneOrganizationById(
requestAdminUser,
organizationId
)
if (!organization) {
throw new NotFoundException('Could not find organization with this id')
}

return organization
}

Expand Down
18 changes: 14 additions & 4 deletions src/organizations/organizations.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, BadRequestException } from '@nestjs/common'
import { Injectable, BadRequestException, UnauthorizedException } from '@nestjs/common'
import { OrganizationsRepository } from './organizations.repository'
import {
CreateOrganizationRequestDto,
Expand All @@ -10,7 +10,9 @@ import {
getSuperAdminACLKey,
getNationalAdminACLKey,
getOrganizationAdminACLKey,
canUserAccessResource,
} from '../shared/acl'
import { RequestAdminUser } from '../shared/interfaces'

@Injectable()
export class OrganizationsService {
Expand Down Expand Up @@ -38,9 +40,17 @@ export class OrganizationsService {
return this.organizationsRepository.findAll(userAccessKey)
}

async findOneOrganizationById(organizationId: string): Promise<Organization> {
// TODO @yashmurty : Fetch resource and perform ACL check.
return this.organizationsRepository.findOneById(organizationId)
async findOneOrganizationById(
requestAdminUser: RequestAdminUser,
organizationId: string
): Promise<Organization> {
const organization = await this.organizationsRepository.findOneById(organizationId)
// Fetch resource and perform ACL check.
if (!canUserAccessResource(requestAdminUser.userAccessKey, organization)) {
throw new UnauthorizedException('User does not have access on this resource')
}

return organization
}

async updateOneOrganization(
Expand Down
25 changes: 22 additions & 3 deletions src/shared/acl/acl.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
prefectureAdminKey,
organizationAdminKey,
} from './acl.constants'
import { UnauthorizedException } from '@nestjs/common'
import { ResourceWithACL } from './acl.class'

/**
* getXXXAdminACLKey function returns the keys that need to be added to
Expand All @@ -28,10 +30,27 @@ export function getOrganizationAdminACLKey(organizationId: string) {
}

/**
* canUserEditResource function is used to check if a user can view/edit a resource
* canUserAccessResource function is used to check if a user can view/edit a resource
*/
export function canUserEditResource(userAccessKey: string, accessControlList: string[]): boolean {
// WIP
export function canUserAccessResource(userAccessKey: string, resource: ResourceWithACL): boolean {
if (!userAccessKey) {
throw new UnauthorizedException('Could not check access without userAccessKey')
}
if (!resource) {
throw new UnauthorizedException('Could not check access on empty resource')
}
if (!resource.accessControlList) {
throw new UnauthorizedException('Could not check access without accessControlList')
}
if (!resource.accessControlList.length) {
throw new UnauthorizedException('Could not check access on empty accessControlList')
}

// If accessControlList contains the userAccessKey, return true.
if (resource.accessControlList.includes(userAccessKey)) {
return true
}

return false
}

Expand Down