Skip to content
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
1 change: 1 addition & 0 deletions .rpsrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"delay_timer": 12,
"mqtt_address": "",
"disable_cira_domain_name": "",
"enterprise_assistant_url": "http://localhost:8000",
"consul_enabled": false,
"consul_host": "localhost",
"consul_port": "8500",
Expand Down
3 changes: 1 addition & 2 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"express-ws": "^5.0.2",
"got": "^14.4.7",
"http-z": "^7.0.0",
"js-yaml": "^4.1.0",
"minimist": "^1.2.8",
"mqtt": "^5.13.1",
"node-forge": "^1.3.1",
Expand Down
81 changes: 81 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export interface RPSConfig {
delay_tls_put_data_sync: number
mqtt_address?: string
disable_cira_domain_name?: string
enterprise_assistant_url?: string
jwt_token_header: string
jwt_tenant_property: string
}
Expand Down Expand Up @@ -274,3 +275,83 @@ export interface OpenAMTEvent {
guid?: string
timestamp: number
}

// Export-specific interfaces for profile export functionality
export interface ExportWifiConfig {
profileName: string
ssid: string
priority: number
authenticationMethod: number
encryptionMethod: number
pskPassphrase: string
ieee8021xProfileName: string
}

export interface ExportConfiguration {
id: number
name: string
configuration: {
generalSettings: {
sharedFQDN: boolean
networkInterfaceEnabled: number
pingResponseEnabled: boolean
}
network: {
wired: {
dhcpEnabled: boolean
ipSyncEnabled: boolean
sharedStaticIP: boolean
ipAddress: string
subnetMask: string
defaultGateway: string
primaryDNS: string
secondaryDNS: string
authentication: string
ieee8021x: Ieee8021xConfig | null
}
wireless: {
wifiSyncEnabled: boolean
profiles: ExportWifiConfig[]
}
}
tls: {
mutualAuthentication: boolean
enabled: boolean
trustedCN: string[]
allowNonTLS: boolean
}
redirection: {
enabled: boolean
services: {
kvm: boolean
sol: boolean
ider: boolean
}
userConsent: string
}
userAccounts: {
userAccounts: any[]
}
enterpriseAssistant: {
url: string
username: string
password: string
}
amtSpecific: {
controlMode: string
adminPassword: string
provisioningCert: string
provisioningCertPwd: string
mebxPassword: string
}
bmcSpecific: {
adminPassword: string
}
dashSpecific: {
adminPassword: string
}
redfishSpecific: {
adminPassword: string
}
}
}
214 changes: 214 additions & 0 deletions src/routes/admin/profiles/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { WirelessConfig, Ieee8021xConfig, ProfileWifiConfigs } from 'models/RCS.Config.js'
import { type ExportWifiConfig, type ExportConfiguration } from '../../../models/index.js'
import Logger from '../../../Logger.js'
import { NOT_FOUND_EXCEPTION, NOT_FOUND_MESSAGE } from '../../../utils/constants.js'
import handleError from '../../../utils/handleError.js'
import { RPSError } from '../../../utils/RPSError.js'
import { type Request, type Response } from 'express'
import yaml from 'js-yaml'
import { encryptWithRandomKey } from '../../../utils/encryptWithRandomKey.js'
import { type CertCredentials } from '../../../interfaces/ISecretManagerService.js'
import { Environment } from '../../../utils/Environment.js'

// Constants for export configuration
const DEFAULT_USER_CONSENT = 'None'

export async function exportProfile(req: Request, res: Response): Promise<void> {
const log = new Logger('exportProfile')
const { profileName } = req.params
const { domainName } = req.query as { domainName: string }
try {
const result = await req.db.profiles.getByName(profileName, req.tenantId)
if (result == null) {
throw new RPSError(NOT_FOUND_MESSAGE('AMT', profileName), NOT_FOUND_EXCEPTION)
}
if (result.ciraConfigName && result.ciraConfigName !== '') {
result.ciraConfigObject = await req.db.ciraConfigs.getByName(result.ciraConfigName, req.tenantId)
if (result.ciraConfigObject == null) {
throw new RPSError(NOT_FOUND_MESSAGE('CIRA', result.ciraConfigName), NOT_FOUND_EXCEPTION)
}
}

// Helper function to get WiFi configuration
const getWifiConfiguration = async (wifiConfig: ProfileWifiConfigs): Promise<ExportWifiConfig> => {
const wifiConfigObject = await req.db.wirelessProfiles.getByName(wifiConfig.profileName, req.tenantId)
if (wifiConfigObject == null) {
throw new RPSError(NOT_FOUND_MESSAGE('Wireless', wifiConfig.profileName), NOT_FOUND_EXCEPTION)
}
return {
profileName: wifiConfigObject.profileName,
ssid: wifiConfigObject.ssid,
priority: wifiConfig.priority,
authenticationMethod: wifiConfigObject.authenticationMethod,
encryptionMethod: wifiConfigObject.encryptionMethod,
pskPassphrase: wifiConfigObject.pskPassphrase,
ieee8021xProfileName: wifiConfigObject.ieee8021xProfileName
}
}

// Get WiFi configurations
const wifiConfigs: ExportWifiConfig[] =
result.wifiConfigs && result.wifiConfigs.length > 0
? await Promise.all(result.wifiConfigs.map(getWifiConfiguration))
: []
let ieee8021xConfigs: Ieee8021xConfig[] = []
if (result.ieee8021xProfileName && result.ieee8021xProfileName !== '') {
const ieee8021xProfile = await req.db.ieee8021xProfiles.getByName(result.ieee8021xProfileName, req.tenantId)
if (ieee8021xProfile == null) {
throw new RPSError(NOT_FOUND_MESSAGE('802.1x', result.ieee8021xProfileName), NOT_FOUND_EXCEPTION)
}
ieee8021xConfigs = [
{
profileName: ieee8021xProfile.profileName,
username: ieee8021xProfile.username,
password: ieee8021xProfile.password,
authenticationProtocol: ieee8021xProfile.authenticationProtocol,
pxeTimeout: ieee8021xProfile.pxeTimeout,
wiredInterface: ieee8021xProfile.wiredInterface,
tenantId: ieee8021xProfile.tenantId
}
]
}

// Map wireless profiles to the new format (this mapping is now redundant)
const wirelessProfiles: ExportWifiConfig[] = wifiConfigs

// Get domain object for ACM activation with secrets
let domainObject: any = null
const passwords = {
amt: '',
mebx: '',
mps: ''
}

if (result.activation === 'acmactivate' && domainName) {
domainObject = await req.db.domains.getByName(domainName, req.tenantId)

// Get provisioning cert and password from secret manager if available
if (domainObject && req.secretsManager) {
try {
const certCredentials = (await req.secretsManager.getSecretAtPath(
`certs/${domainObject.profileName}`
)) as CertCredentials
if (certCredentials?.CERT) {
domainObject.provisioningCert = certCredentials.CERT || domainObject.provisioningCert || ''
domainObject.provisioningCertPassword =
certCredentials.CERT_PASSWORD || domainObject.provisioningCertPassword || ''
}
} catch (error) {
log.error(`Failed to get cert credentials from secret manager: ${error}`)
}
}
}

// Helper function to get password from secret manager or fallback
const getPassword = async (secretKey: string, fallbackValue: string | null | undefined): Promise<string> => {
if (req.secretsManager) {
try {
return (
(await req.secretsManager.getSecretFromKey(`profiles/${profileName}`, secretKey)) || fallbackValue || ''
)
} catch (error) {
log.error(`Failed to get ${secretKey}: ${error}`)
return fallbackValue || ''
}
}
return fallbackValue || ''
}

// Get passwords from secret manager or profile
passwords.amt = await getPassword('AMT_PASSWORD', result.amtPassword)
passwords.mebx = await getPassword('MEBX_PASSWORD', result.mebxPassword)

// Get MPS password - this is typically generated randomly
if (result.ciraConfigObject && req.secretsManager) {
try {
passwords.mps = (await req.secretsManager.getSecretFromKey(`profiles/${profileName}`, 'MPS_PASSWORD')) || ''
} catch (error) {
log.error(`Failed to get MPS password: ${error}`)
}
}

// Create the new structured output
const output: ExportConfiguration = {
id: 0,
name: profileName,
configuration: {
generalSettings: {
sharedFQDN: false,
networkInterfaceEnabled: 0,
pingResponseEnabled: false
},
network: {
wired: {
dhcpEnabled: result.dhcpEnabled ?? true,
ipSyncEnabled: result.ipSyncEnabled ?? false,
sharedStaticIP: false,
ipAddress: '',
subnetMask: '',
defaultGateway: '',
primaryDNS: '',
secondaryDNS: '',
authentication: '',
ieee8021x: ieee8021xConfigs.length > 0 ? ieee8021xConfigs[0] : null
},
wireless: {
wifiSyncEnabled: result.localWifiSyncEnabled ?? false,
profiles: wirelessProfiles
}
},
tls: {
mutualAuthentication: false,
enabled: result.tlsMode ? true : false,
trustedCN: [],
allowNonTLS: false
},
redirection: {
enabled: false,
services: {
kvm: true,
sol: true,
ider: true
},
userConsent: DEFAULT_USER_CONSENT
},
userAccounts: {
userAccounts: []
},
enterpriseAssistant: {
url: Environment.Config.enterprise_assistant_url ?? 'http://localhost:8000',
username: '',
password: ''
},
amtSpecific: {
controlMode: result.activation ?? '',
adminPassword: passwords.amt,
provisioningCert: domainObject?.provisioningCert ?? '',
provisioningCertPwd: domainObject?.provisioningCertPassword ?? '',
mebxPassword: passwords.mebx
},
bmcSpecific: {
adminPassword: ''
},
dashSpecific: {
adminPassword: ''
},
redfishSpecific: {
adminPassword: ''
}
}
}

const yamlStr = yaml.dump(output)
// Encrypt the YAML string and provide the key
const { cipherText, key } = encryptWithRandomKey(yamlStr)

res.status(200).json({
filename: `${profileName}.yaml`,
content: cipherText,
key
})
} catch (error) {
handleError(log, profileName, req, res, error)
}
}
4 changes: 3 additions & 1 deletion src/routes/admin/profiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
**********************************************************************/

import { deleteProfile } from './delete.js'

import { Router } from 'express'
import { allProfiles } from './all.js'
import { getProfile } from './get.js'
Expand All @@ -14,12 +13,15 @@ import { amtProfileValidator, profileUpdateValidator } from './amtProfileValidat
import { odataValidator } from '../odataValidator.js'
import validateMiddleware from '../../../middleware/validate.js'
import ifMatchMiddleware from '../../../middleware/if-match.js'
import { exportProfile } from './export.js'

const profileRouter: Router = Router()

profileRouter.get('/', odataValidator(), validateMiddleware, allProfiles)
profileRouter.get('/:profileName', getProfile)
profileRouter.post('/', amtProfileValidator(), validateMiddleware, createProfile)
profileRouter.patch('/', profileUpdateValidator(), validateMiddleware, ifMatchMiddleware, editProfile)
profileRouter.delete('/:profileName', deleteProfile)
profileRouter.get('/export/:profileName', exportProfile)

export default profileRouter
Loading