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

feat(api): adds multitenancy support #795

Merged
merged 2 commits into from
Feb 11, 2023
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
573 changes: 326 additions & 247 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,4 @@
"optionalDependencies": {
"fsevents": "^2.3.2"
}
}
}
4 changes: 2 additions & 2 deletions src/DataProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ export class DataProcessor {
this.setConnectionParams(clientId)
activation.service.start()
if (devices[clientId].activationStatus) {
activation.service.send({ type: 'ACTIVATED', clientId, isActivated: true })
activation.service.send({ type: 'ACTIVATED', tenantId: clientMsg.tenantId, clientId, isActivated: true })
} else {
activation.service.send({ type: 'ACTIVATION', clientId })
activation.service.send({ type: 'ACTIVATION', tenantId: clientMsg.tenantId, clientId })
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/DomainCredentialManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ export class DomainCredentialManager implements IDomainCredentialManager {
/**
* @description get the provisioning cert for a given domain
* @param {string} domainSuffix
* @param {string} tenantId
* @returns {AMTDomain} returns domain object
*/
async getProvisioningCert (domainSuffix: string): Promise<AMTDomain> {
const domain = await this.amtDomains.getDomainByDomainSuffix(domainSuffix)
async getProvisioningCert (domainSuffix: string, tenantId: string): Promise<AMTDomain> {
const domain = await this.amtDomains.getDomainByDomainSuffix(domainSuffix, tenantId)
this.logger.debug(`domain : ${JSON.stringify(domain)}`)

if (domain?.provisioningCert) {
Expand All @@ -46,10 +47,11 @@ export class DomainCredentialManager implements IDomainCredentialManager {
/**
* @description Checks if the AMT domain exists or not
* @param {string} domainSuffix
* @param {string} tenantId
* @returns {boolean} returns true if domain exists otherwise false.
*/
public async doesDomainExist (domainSuffix: string): Promise<boolean> {
if (await this.amtDomains.getDomainByDomainSuffix(domainSuffix)) {
public async doesDomainExist (domainSuffix: string, tenantId: string): Promise<boolean> {
if (await this.amtDomains.getDomainByDomainSuffix(domainSuffix, tenantId)) {
return true
} else {
return false
Expand Down
14 changes: 8 additions & 6 deletions src/Index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,20 @@ mqtt.connectBroker()
const dbFactory = new DbCreatorFactory()

export const loadCustomMiddleware = async function (): Promise<express.RequestHandler[]> {
const pathToCustomMiddleware = './src/middleware/custom'
const pathToCustomMiddleware = path.join(__dirname, './middleware/custom/')
const middleware: express.RequestHandler[] = []
const doesExist = existsSync(pathToCustomMiddleware)
const isDirectory = lstatSync(pathToCustomMiddleware).isDirectory()
if (doesExist && isDirectory) {
const files = readdirSync(pathToCustomMiddleware)
for (const file of files) {
const pathToMiddleware = path.join('../src/middleware/custom/', file)
console.log(pathToMiddleware)
const customMiddleware = await import(pathToMiddleware)
if (customMiddleware?.default != null) {
middleware.push(customMiddleware.default)
if (path.extname(file) === '.js') {
const pathToMiddleware = path.join(pathToCustomMiddleware, file.substring(0, file.lastIndexOf('.')))
log.info('Loading custom middleware: ' + file)
const customMiddleware = await import(pathToMiddleware)
if (customMiddleware?.default != null) {
middleware.push(customMiddleware.default)
}
}
}
}
Expand Down
30 changes: 15 additions & 15 deletions src/ProfileManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export class ProfileManager implements IProfileManager {
* @param {string} profileName profile to look up
* @returns {string} returns the activation to be performed
*/
public async getActivationMode (profileName: string): Promise<string> {
const profile = await this.getAmtProfile(profileName)
public async getActivationMode (profileName: string, tenantId: string): Promise<string> {
const profile = await this.getAmtProfile(profileName, tenantId)
let activation: string

if (profile?.activation) {
Expand All @@ -50,8 +50,8 @@ export class ProfileManager implements IProfileManager {
* @param {string} profile of cira config
* @returns {string} returns the config for CIRA for a given profile
*/
public async getCiraConfiguration (profileName: string): Promise<CIRAConfig> {
const profile = await this.getAmtProfile(profileName)
public async getCiraConfiguration (profileName: string, tenantId: string): Promise<CIRAConfig> {
const profile = await this.getAmtProfile(profileName, tenantId)
let ciraConfig: CIRAConfig

if (profile?.ciraConfigName && profile.ciraConfigObject) {
Expand All @@ -69,8 +69,8 @@ export class ProfileManager implements IProfileManager {
* @param {string} profileName profile name of amt password
* @returns {string} returns the amt password for a given profile
*/
public async getAmtPassword (profileName: string): Promise<string> {
const profile: AMTConfiguration = await this.getAmtProfile(profileName)
public async getAmtPassword (profileName: string, tenantId: string): Promise<string> {
const profile: AMTConfiguration = await this.getAmtProfile(profileName, tenantId)
let amtPassword: string
if (profile) {
if (profile.generateRandomPassword) {
Expand Down Expand Up @@ -102,8 +102,8 @@ export class ProfileManager implements IProfileManager {
* @param {string} profileName profile name of amt password
* @returns {string} returns the amt password for a given profile
*/
public async getMEBxPassword (profileName: string): Promise<string> {
const profile: AMTConfiguration = await this.getAmtProfile(profileName)
public async getMEBxPassword (profileName: string, tenantId: string): Promise<string> {
const profile: AMTConfiguration = await this.getAmtProfile(profileName, tenantId)
let mebxPassword: string
if (profile) {
if (profile.generateRandomMEBxPassword) {
Expand Down Expand Up @@ -137,8 +137,8 @@ export class ProfileManager implements IProfileManager {
* @param {string} profileName profile name of MPS password
* @returns {string} returns the MPS password for a given profile
*/
public async getMPSPassword (profileName: string): Promise<string> {
const profile: AMTConfiguration = await this.getAmtProfile(profileName)
public async getMPSPassword (profileName: string, tenantId: string): Promise<string> {
const profile: AMTConfiguration = await this.getAmtProfile(profileName, tenantId)
let mpsPassword: string

if (profile?.ciraConfigObject) {
Expand Down Expand Up @@ -166,15 +166,15 @@ export class ProfileManager implements IProfileManager {
* @param {string} profile
* @returns {AMTConfiguration} returns AMTConfig object if profile exists otherwise null.
*/
public async getAmtProfile (profile: string): Promise<AMTConfiguration> {
public async getAmtProfile (profile: string, tenantId: string): Promise<AMTConfiguration> {
try {
if (!profile) {
return null
}
const amtProfile: AMTConfiguration = await this.amtConfigurations.getByName(profile)
const amtProfile: AMTConfiguration = await this.amtConfigurations.getByName(profile, tenantId)
// If the CIRA Config associated with profile, retrieves from DB
if (amtProfile?.ciraConfigName != null) {
amtProfile.ciraConfigObject = await this.amtConfigurations.getCiraConfigForProfile(amtProfile.ciraConfigName)
amtProfile.ciraConfigObject = await this.amtConfigurations.getCiraConfigForProfile(amtProfile.ciraConfigName, tenantId)
}
// If the TLS Config associated with profile, retrieves from DB
if (amtProfile.tlsMode != null && amtProfile.tlsSigningAuthority) {
Expand All @@ -196,8 +196,8 @@ export class ProfileManager implements IProfileManager {
* @param {string} profile
* @returns {boolean} returns true if profile exists otherwise false.
*/
public async doesProfileExist (profileName: string): Promise<boolean> {
const profile = await this.getAmtProfile(profileName)
public async doesProfileExist (profileName: string, tenantId: string): Promise<boolean> {
const profile = await this.getAmtProfile(profileName, tenantId)
if (profile) {
// this.logger.debug(`found profile ${profileName}`);
return true
Expand Down
20 changes: 11 additions & 9 deletions src/Validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class Validator implements IValidator {
throw new RPSError(`Device ${payload.uuid} activation failed. Missing password.`)
}
// Check for client requested action and profile activation
const profile: AMTConfiguration = await this.configurator.profileManager.getAmtProfile(payload.profile)
const profile: AMTConfiguration = await this.configurator.profileManager.getAmtProfile(payload.profile, msg.tenantId)
if (!profile) {
throw new RPSError(`Device ${payload.uuid} activation failed. ${payload.profile} does not match list of available AMT profiles.`)
}
Expand All @@ -97,7 +97,7 @@ export class Validator implements IValidator {
}
// Validate client message to configure ACM message
if (clientObj.action === ClientAction.ADMINCTLMODE) {
await this.verifyActivationMsgForACM(payload)
await this.verifyActivationMsgForACM(msg)
}
// }
}
Expand Down Expand Up @@ -236,15 +236,17 @@ export class Validator implements IValidator {
return msg.payload
}

async verifyActivationMsgForACM (payload: Payload): Promise<void> {
if (!payload.certHashes) {
throw new RPSError(`Device ${payload.uuid} activation failed. Missing certificate hashes from the device.`)
async verifyActivationMsgForACM (msg: ClientMsg): Promise<void> {
if (!msg.payload.certHashes) {
throw new RPSError(`Device ${msg.payload.uuid} activation failed. Missing certificate hashes from the device.`)
}
if (!payload.fqdn && (payload.currentMode !== 2)) {
throw new RPSError(`Device ${payload.uuid} activation failed. Missing DNS Suffix.`)

if (!msg.payload.fqdn && (msg.payload.currentMode !== 2)) {
throw new RPSError(`Device ${msg.payload.uuid} activation failed. Missing DNS Suffix.`)
}
if (!(await this.configurator.domainCredentialManager.doesDomainExist(payload.fqdn)) && (payload.currentMode !== 2)) {
throw new RPSError(`Device ${payload.uuid} activation failed. Specified AMT domain suffix: ${payload.fqdn} does not match list of available AMT domain suffixes.`)

if (!(await this.configurator.domainCredentialManager.doesDomainExist(msg.payload.fqdn, msg.tenantId)) && (msg.payload.currentMode !== 2)) {
throw new RPSError(`Device ${msg.payload.uuid} activation failed. Specified AMT domain suffix: ${msg.payload.fqdn} does not match list of available AMT domain suffixes.`)
}
}

Expand Down
1 change: 1 addition & 0 deletions src/commandParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function getBasicMessage (): ClientMsg {
protocolVersion: '4.0.0',
status: 'ok',
message: "all's good!",
tenantId: '',
payload: {
ver: '11.8.50',
build: '3425',
Expand Down
4 changes: 2 additions & 2 deletions src/data/postgres/tables/ciraConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class CiraConfigTable implements ICiraConfigTable {
ciraConfig.tenantId
])
if (results.rowCount > 0) {
return await this.getByName(ciraConfig.configName)
return await this.getByName(ciraConfig.configName, ciraConfig.tenantId)
}
return null
} catch (error) {
Expand Down Expand Up @@ -168,7 +168,7 @@ export class CiraConfigTable implements ICiraConfigTable {
ciraConfig.tenantId,
ciraConfig.version
])
latestItem = await this.getByName(ciraConfig.configName)
latestItem = await this.getByName(ciraConfig.configName, ciraConfig.tenantId)
if (results.rowCount > 0) {
return latestItem
}
Expand Down
4 changes: 2 additions & 2 deletions src/data/postgres/tables/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class DomainsTable implements IDomainsTable {
amtDomain.tenantId
])
if (results.rowCount > 0) {
const domain = await this.getByName(amtDomain.profileName)
const domain = await this.getByName(amtDomain.profileName, amtDomain.tenantId)
return domain
}
return null
Expand Down Expand Up @@ -168,7 +168,7 @@ export class DomainsTable implements IDomainsTable {
amtDomain.tenantId,
amtDomain.version
])
latestItem = await this.getByName(amtDomain.profileName)
latestItem = await this.getByName(amtDomain.profileName, amtDomain.tenantId)
if (results.rowCount > 0) {
return latestItem
}
Expand Down
8 changes: 4 additions & 4 deletions src/data/postgres/tables/profiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ describe('profiles tests', () => {
test('should get ciraconfig for profile', async () => {
const ciraConfigSpy = jest.spyOn(db.ciraConfigs, 'getByName')
ciraConfigSpy.mockResolvedValue({} as any)
const result = await profilesTable.getCiraConfigForProfile(profileName)
const result = await profilesTable.getCiraConfigForProfile(profileName, '')
expect(result).toStrictEqual({})
expect(ciraConfigSpy).toHaveBeenCalledWith(profileName)
expect(ciraConfigSpy).toHaveBeenCalledWith(profileName, '')
})
})
describe('Delete', () => {
Expand Down Expand Up @@ -193,7 +193,7 @@ describe('profiles tests', () => {

expect(result).toBe(amtConfig)
expect(profileWirelessConfigsSpy).toHaveBeenCalledWith(amtConfig.wifiConfigs, amtConfig.profileName, amtConfig.tenantId)
expect(getByNameSpy).toHaveBeenCalledWith(amtConfig.profileName)
expect(getByNameSpy).toHaveBeenCalledWith(amtConfig.profileName, amtConfig.tenantId)
expect(querySpy).toBeCalledTimes(1)
expect(querySpy).toBeCalledWith(`
INSERT INTO profiles(
Expand Down Expand Up @@ -233,7 +233,7 @@ describe('profiles tests', () => {

expect(result).toBe(amtConfig)
expect(profileWirelessConfigsSpy).not.toHaveBeenCalledWith(amtConfig.wifiConfigs, amtConfig.profileName, amtConfig.tenantId)
expect(getByNameSpy).toHaveBeenCalledWith(amtConfig.profileName)
expect(getByNameSpy).toHaveBeenCalledWith(amtConfig.profileName, amtConfig.tenantId)
expect(querySpy).toBeCalledTimes(1)
expect(querySpy).toBeCalledWith(`
INSERT INTO profiles(
Expand Down
15 changes: 10 additions & 5 deletions src/data/postgres/tables/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class ProfilesTable implements IProfilesTable {

/**
* @description Get count of all profiles from DB
* @param {string} tenantId
* @returns {number}
*/
async getCount (tenantId: string = ''): Promise<number> {
Expand All @@ -39,6 +40,7 @@ export class ProfilesTable implements IProfilesTable {
* @description Get all AMT Profiles from DB
* @param {number} top
* @param {number} skip
* @param {string} tenantId
* @returns {Pagination} returns an array of AMT profiles from DB
*/
async get (top: number = DEFAULT_TOP, skip: number = DEFAULT_SKIP, tenantId: string = ''): Promise<AMTConfiguration[]> {
Expand Down Expand Up @@ -87,6 +89,7 @@ export class ProfilesTable implements IProfilesTable {
/**
* @description Get AMT Profile from DB by name
* @param {string} profileName
* @param {string} tenantId
* @returns {AMTConfiguration} AMT Profile object
*/
async getByName (profileName: string, tenantId: string = ''): Promise<AMTConfiguration> {
Expand Down Expand Up @@ -134,15 +137,17 @@ export class ProfilesTable implements IProfilesTable {
/**
* @description Get CIRA config from DB by name
* @param {string} configName
* @param {string} tenantId
* @returns {CIRAConfig} CIRA config object
*/
async getCiraConfigForProfile (configName: string): Promise<CIRAConfig> {
return await this.db.ciraConfigs.getByName(configName)
async getCiraConfigForProfile (configName: string, tenantId: string): Promise<CIRAConfig> {
return await this.db.ciraConfigs.getByName(configName, tenantId)
}

/**
* @description Delete AMT Profile from DB by name
* @param {string} profileName
* @param {string} tenantId
* @returns {boolean} Return true on successful deletion
*/
async delete (profileName: string, tenantId: string = ''): Promise<boolean> {
Expand Down Expand Up @@ -203,7 +208,7 @@ export class ProfilesTable implements IProfilesTable {
await this.db.profileWirelessConfigs.createProfileWifiConfigs(amtConfig.wifiConfigs, amtConfig.profileName, amtConfig.tenantId)
}

return await this.getByName(amtConfig.profileName)
return await this.getByName(amtConfig.profileName, amtConfig.tenantId)
} catch (error) {
this.log.error(`Failed to insert AMT profile: ${amtConfig.profileName}`, error)
if (error instanceof RPSError) {
Expand Down Expand Up @@ -255,11 +260,11 @@ export class ProfilesTable implements IProfilesTable {
if (amtConfig.wifiConfigs?.length > 0) {
await this.db.profileWirelessConfigs.createProfileWifiConfigs(amtConfig.wifiConfigs, amtConfig.profileName)
}
latestItem = await this.getByName(amtConfig.profileName)
latestItem = await this.getByName(amtConfig.profileName, amtConfig.tenantId)
return latestItem
}
// if rowcount is 0, we assume update failed and grab the current reflection of the record in the DB to be returned in the Concurrency Error
latestItem = await this.getByName(amtConfig.profileName)
latestItem = await this.getByName(amtConfig.profileName, amtConfig.tenantId)
} catch (error) {
this.log.error(`Failed to update AMT profile: ${amtConfig.profileName}`, error)
if (error.code === '23503') { // Foreign key constraint violation
Expand Down
6 changes: 3 additions & 3 deletions src/data/postgres/tables/wirelessProfiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe('wireless profiles tests', () => {
const result = await wirelessProfilesTable.insert(wirelessConfig)

expect(result).toBe(wirelessConfig)
expect(getByNameSpy).toHaveBeenCalledWith(wirelessConfig.profileName)
expect(getByNameSpy).toHaveBeenCalledWith(wirelessConfig.profileName, wirelessConfig.tenantId)
expect(querySpy).toBeCalledTimes(1)
expect(querySpy).toBeCalledWith(`
INSERT INTO wirelessconfigs
Expand Down Expand Up @@ -191,7 +191,7 @@ describe('wireless profiles tests', () => {
getByNameSpy.mockResolvedValue(wirelessConfig)
const result = await wirelessProfilesTable.update(wirelessConfig)
expect(result).toBe(wirelessConfig)
expect(getByNameSpy).toHaveBeenCalledWith(wirelessConfig.profileName)
expect(getByNameSpy).toHaveBeenCalledWith(wirelessConfig.profileName, wirelessConfig.tenantId)
expect(querySpy).toBeCalledTimes(1)
expect(querySpy).toBeCalledWith(`
UPDATE wirelessconfigs
Expand All @@ -218,7 +218,7 @@ describe('wireless profiles tests', () => {
const getByNameSpy = jest.spyOn(wirelessProfilesTable, 'getByName')
getByNameSpy.mockResolvedValue(wirelessConfig)
await expect(wirelessProfilesTable.update(wirelessConfig)).rejects.toThrow(CONCURRENCY_MESSAGE)
expect(getByNameSpy).toHaveBeenCalledWith(wirelessConfig.profileName)
expect(getByNameSpy).toHaveBeenCalledWith(wirelessConfig.profileName, wirelessConfig.tenantId)
expect(querySpy).toBeCalledTimes(1)
expect(querySpy).toBeCalledWith(`
UPDATE wirelessconfigs
Expand Down
4 changes: 2 additions & 2 deletions src/data/postgres/tables/wirelessProfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class WirelessProfilesTable implements IWirelessProfilesTable {
wirelessConfig.tenantId
])
if (results?.rowCount > 0) {
const profile = await this.getByName(wirelessConfig.profileName)
const profile = await this.getByName(wirelessConfig.profileName, wirelessConfig.tenantId)
return profile
}
return null
Expand Down Expand Up @@ -186,7 +186,7 @@ export class WirelessProfilesTable implements IWirelessProfilesTable {
wirelessConfig.tenantId,
wirelessConfig.version
])
latestItem = await this.getByName(wirelessConfig.profileName)
latestItem = await this.getByName(wirelessConfig.profileName, wirelessConfig.tenantId)
if (results?.rowCount > 0) {
return latestItem
}
Expand Down
Loading