UNPKG

@okta/stormpath-migration

Version:

Migration tool to import Stormpath data into an Okta tenant

267 lines (231 loc) 9.16 kB
/*! * Copyright (c) 2017, Okta, Inc. and/or its affiliates. All rights reserved. * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") * * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and limitations under the License. */ const ApiError = require('../util/api-error'); const logger = require('../util/logger'); const rs = require('../util/request-scheduler'); const MAPPINGS_PATH = `/api/internal/v1/mappings`; const APPS_PATH = '/api/v1/apps'; const TYPES_PATH = `${APPS_PATH}/user/types`; const SCHEMAS_PATH = '/api/v1/meta/schemas/user/default'; function getExternalToAppUserMap(schema) { const map = {}; const properties = schema.schema.properties; for (let appUserAttribute of Object.keys(properties)) { map[properties[appUserAttribute].externalName] = appUserAttribute; } return map; } async function getSchemaInfo(idpId) { try { const types = await rs.get(`${APPS_PATH}/${idpId}/user/types/default?expand=schema`); const schemas = {}; const externalToAppUserMap = {}; // Existing base and custom schemas for (let schema of types._embedded.schemas) { schemas[schema.name] = schema; } // Imported schema for the IdP - this gives us any additional attributes // that have not already been mapped by default. if (types.type === 'IMPORTED') { schemas.imported = await rs.get(`${APPS_PATH}/${idpId}/user/imported/schema`); for (let schemaType of Object.keys(schemas)) { externalToAppUserMap[schemaType] = getExternalToAppUserMap(schemas[schemaType]); } } else { schemas.imported = {}; externalToAppUserMap.custom = getExternalToAppUserMap(schemas.custom); externalToAppUserMap.base = externalToAppUserMap.imported = {}; } return { typeId: types.id, type: types.type, schemas, externalToAppUserMap }; } catch (err) { throw new ApiError(`Failed to get schema info for idpId=${idpId}`, err); } } async function addSamlAttributes(idpId, schemaInfo, mappings) { logger.verbose(`Adding attributes to saml idpId=${idpId} externalNames=[${mappings.map(m => m.externalName)}]`); try { const missing = []; const existing = []; const userSchema = await rs.get(SCHEMAS_PATH); const userProperties = Object.keys(userSchema.definitions).reduce((memo, type) => { return Object.assign(memo, userSchema.definitions[type].properties); }, {}); for (let mapping of mappings) { if (schemaInfo.externalToAppUserMap.custom[mapping.externalName]) { existing.push(mapping); } else if (!userProperties[mapping.userAttribute]) { logger.warn(`Mapping found for unsupported user attribute ${mapping.userAttribute}, skipping`); } else { missing.push(mapping); } } if (existing.length > 0) { logger.exists(`IdP attributes: ${existing.map(m => m.externalName)}`); } if (missing.length === 0) { return; } logger.verbose(`Adding missing attributes to idpId=${idpId}: ${missing.map(m => m.externalName)}`); const customSchema = Object.assign({}, schemaInfo.schemas.custom); for (let mapping of missing) { const property = userProperties[mapping.userAttribute]; property.externalName = mapping.externalName; customSchema.schema.properties[mapping.externalName] = property; } await rs.put({ url: `${TYPES_PATH}/${schemaInfo.typeId}/schemas/${schemaInfo.schemas.custom.id}`, body: customSchema }); logger.created(`IdP attributes: ${missing.map(m => m.externalName)}`); } catch (err) { throw new ApiError(`Failed to add attributes for idpId=${idpId}`, err); } } async function addSocialAttributes(idpId, schemaInfo, mappings) { logger.verbose(`Adding attributes to social idpId=${idpId} externalNames=[${mappings.map(m => m.externalName)}]`); const missing = []; const existing = []; const desiredExternal = mappings.map(mapping => mapping.externalName); for (let externalName of desiredExternal) { const inBase = schemaInfo.externalToAppUserMap.base[externalName]; const inCustom = schemaInfo.externalToAppUserMap.custom[externalName]; const inImported = schemaInfo.externalToAppUserMap.imported[externalName]; if (inBase || inCustom) { existing.push(externalName); } else if (!inImported) { logger.warn(`Mapping found for unsupported external attribute ${externalName}, skipping`); } else { missing.push(externalName); } } if (existing.length > 0) { logger.exists(`IdP attributes: ${existing}`); } if (missing.length === 0) { return; } try { logger.verbose(`Adding missing attributes to idpId=${idpId}: ${missing}`); const customSchema = Object.assign({}, schemaInfo.schemas.custom); for (let externalName of missing) { const appUserAttribute = schemaInfo.externalToAppUserMap.imported[externalName]; const property = schemaInfo.schemas.imported.schema.properties[appUserAttribute]; customSchema.schema.properties[appUserAttribute] = property; } await rs.put({ url: `${TYPES_PATH}/${schemaInfo.typeId}/schemas/${schemaInfo.schemas.custom.id}`, body: customSchema }); logger.created(`IdP attributes: ${missing}`); } catch (err) { throw new ApiError(`Failed to add attributes for idpId=${idpId}`, err); } } async function mapIdpAttributes(idpId, mappings) { logger.verbose(`Mapping attributes for idpId=${idpId}`); // Reload schemaInfo because it can change after adding attributes const schemaInfo = await getSchemaInfo(idpId); const userTypes = await rs.get('/api/v1/user/types/default?expand=schema'); const res = await rs.get({ url: MAPPINGS_PATH, qs: { source: schemaInfo.typeId, target: userTypes.id } }); const existingMappings = res[0]; const desiredMappings = mappings.map((mapping) => { // If the target user profile attribute does not exist, warn and skip. This // occurs if the attribute does not exist on the base user profile and has // not been added as a custom user schema property. const validTarget = userTypes._embedded.schemas.find((schema) => { return schema.schema.properties[mapping.userAttribute]; }); if (!validTarget) { logger.warn(`Unknown user attribute for mapping '${mapping.userAttribute}', skipping`); return; } let appUserAttribute = schemaInfo.externalToAppUserMap.base[mapping.externalName]; if (!appUserAttribute) { appUserAttribute = schemaInfo.externalToAppUserMap.custom[mapping.externalName]; } if (!appUserAttribute) { appUserAttribute = schemaInfo.externalToAppUserMap.imported[mapping.externalName]; } if (!appUserAttribute) { // If the external name does not exist on the AppUser, this means that // we were not able to add the attribute earlier. This occurs if, for // example, it hasn't been added to the known list of IdP mappings. logger.warn(`External name '${mapping.externalName}' is not available, skipping`); return; } return { sourceExpression: `appuser.${appUserAttribute}`, targetField: mapping.userAttribute, pushStatus: 'PUSH' }; }); const existing = []; const missing = []; for (let mapping of desiredMappings) { if (!mapping) { continue; } const exists = existingMappings.propertyMappings.find((current) => { const sameTarget = current.targetField === mapping.targetField; const sameSource = current.sourceExpression === mapping.sourceExpression; return sameTarget && sameSource; }); if (exists) { existing.push(mapping); } else { missing.push(mapping); } } if (existing.length > 0) { logger.exists(`IdP attribute mappings`, existing); } if (missing.length === 0) { return; } try { for (let mapping of missing) { existingMappings.propertyMappings.push(mapping); } await rs.put({ url: '/api/internal/v1/mappings', body: existingMappings }); logger.created(`IdP attribute mappings`, missing); } catch (err) { throw new ApiError(`Failed to map attributes for idpId=${idpId}`, err); } } async function addAndMapIdpAttributes(idpId, mappings) { logger.verbose(`Adding and mapping attributes for idpId=${idpId}`); if (mappings.length === 0) { logger.verbose(`No mappings for idpId=${idpId}`); return; } const schemaInfo = await getSchemaInfo(idpId); if (schemaInfo.type === 'IMPORTED') { await addSocialAttributes(idpId, schemaInfo, mappings); } else { await addSamlAttributes(idpId, schemaInfo, mappings); } return mapIdpAttributes(idpId, mappings); } module.exports = addAndMapIdpAttributes;