diff --git a/ee/bulk-operations-dashboard/api/controllers/get-labels.js b/ee/bulk-operations-dashboard/api/controllers/get-labels.js new file mode 100644 index 0000000000..ba8917c91c --- /dev/null +++ b/ee/bulk-operations-dashboard/api/controllers/get-labels.js @@ -0,0 +1,45 @@ +module.exports = { + + + friendlyName: 'Get labels', + + + description: 'Builds and returns an array of labels on the Fleet instance.', + + + exits: { + success: { + outputType: [{}], + } + }, + + + fn: async function () { + + + let labelsOnThisInstance = []; + + let labelsResponseData = await sails.helpers.http.get.with({ + url: '/api/v1/fleet/labels', + baseUrl: sails.config.custom.fleetBaseUrl, + headers: { + Authorization: `Bearer ${sails.config.custom.fleetApiToken}` + } + }) + .timeout(120000) + .retry(['requestFailed', {name: 'TimeoutError'}]); + + for(let label of labelsResponseData.labels) { + labelsOnThisInstance.push({ + name: label.name, + value: label.id + }); + } + labelsOnThisInstance = _.sortBy(labelsOnThisInstance, 'name'); + // All done. + return labelsOnThisInstance; + + } + + +}; diff --git a/ee/bulk-operations-dashboard/api/controllers/get-profiles.js b/ee/bulk-operations-dashboard/api/controllers/get-profiles.js index 7326fea9ae..dfe52aa8f4 100644 --- a/ee/bulk-operations-dashboard/api/controllers/get-profiles.js +++ b/ee/bulk-operations-dashboard/api/controllers/get-profiles.js @@ -13,7 +13,9 @@ module.exports = { }, + fn: async function () { + // Get all teams on the Fleet instance. let teamsResponseData = await sails.helpers.http.get.with({ url: '/api/v1/fleet/teams', @@ -57,7 +59,6 @@ module.exports = { let profilesForThisTeam = configurationProfilesResponseData.profiles; allProfiles = allProfiles.concat(profilesForThisTeam); } - // Add the configurations profiles that are assigned to the "no team" team. let noTeamConfigurationProfilesResponseData = await sails.helpers.http.get.with({ url: '/api/v1/fleet/configuration_profiles', @@ -68,45 +69,53 @@ module.exports = { }) .timeout(120000) .retry(['requestFailed', {name: 'TimeoutError'}]); - let profilesForThisTeam = noTeamConfigurationProfilesResponseData.profiles; - allProfiles = allProfiles.concat(profilesForThisTeam); - - // console.log(allProfiles); - - let profilesOnThisFleetInstance = []; - // Group configuration profiles by their identifier. - let allProfilesByIdentifier = _.groupBy(allProfiles, 'identifier'); - for(let profileIdentifier in allProfilesByIdentifier) { - // Iterate through the arrays of profiles with the same unique identifier. - let teamsForThisProfile = []; - // Add the profile's UUID and information about the team this profile is assigned to the teams array for profiles. - for(let profile of allProfilesByIdentifier[profileIdentifier]){ - let informationAboutThisProfile = { + let profilesForNoTeam = noTeamConfigurationProfilesResponseData.profiles; + allProfiles = allProfiles.concat(profilesForNoTeam); + let profilesInformation = []; + for(let profile of allProfiles) { + let profileInformation = { + name: profile.name, + identifier: profile.identifier, + platform: profile.platform, + createdAt: new Date(profile.created_at).getTime(), + team: { uuid: profile.profile_uuid, fleetApid: profile.team_id, teamName: _.find(teams, {fleetApid: profile.team_id}).teamName, - }; - teamsForThisProfile.push(informationAboutThisProfile); - } - let profile = allProfilesByIdentifier[profileIdentifier][0];// Grab the first profile returned in the api repsonse to build our profile configuration. - let profileInformation = { - name: profile.name, - identifier: profileIdentifier, - platform: profile.platform, - createdAt: new Date(profile.created_at).getTime(), - teams: teamsForThisProfile + }, + profileTarget: 'all', }; - profilesOnThisFleetInstance.push(profileInformation); + if(profile.labels_include_all) { + profileInformation.labels = _.pluck(profile.labels_include_all, 'name'); + profileInformation.profileTarget = 'custom'; + profileInformation.labelTargetBehavior = 'include'; + } else if(profile.labels_exclude_any){ + profileInformation.labels = _.pluck(profile.labels_exclude_any, 'name'); + profileInformation.profileTarget = 'custom'; + profileInformation.labelTargetBehavior = 'exclude'; + } + profilesInformation.push(profileInformation); } + // Group the profiles based on identifier, labels, and labelTargetBehavior + let profilesGroupedbyLabelsAndIdentifier = _.groupBy(profilesInformation, (profile)=>{ + return `${profile.identifier}|${JSON.stringify(profile.labels)}|${profile.labelTargetBehavior}`; + }); + + // map the grouped profiles and merge profiles that have the same labels, target behavior, and identifier. + let allProfilesOnFleetInstance = Object.values(profilesGroupedbyLabelsAndIdentifier).map(profileGroup => { + return { + ...profileGroup[0],// Expand the first item in the profileGroup + teams: profileGroup.map(item => item.team)// Merge the teams arrays + }; + }); // Get the undeployed profiles from the app's database. let undeployedProfiles = await UndeployedProfile.find(); - profilesOnThisFleetInstance = _.union(profilesOnThisFleetInstance, undeployedProfiles); - + allProfilesOnFleetInstance = _.union(allProfilesOnFleetInstance, undeployedProfiles); // Sort profiles by their name. - profilesOnThisFleetInstance = _.sortByOrder(profilesOnThisFleetInstance, 'name', 'asc'); - - return profilesOnThisFleetInstance; + allProfilesOnFleetInstance = _.sortByOrder(allProfilesOnFleetInstance, 'name', 'asc'); + // return the updated list of profiles + return allProfilesOnFleetInstance; } diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js b/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js index bede01b44f..512fd95813 100644 --- a/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js +++ b/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js @@ -22,6 +22,20 @@ module.exports = { type: 'ref', description: 'A file that will be replacing the profile.' }, + profileTarget: { + type: 'string', + description: 'The target for this configuration profile', + defaultsTo: 'all', + isIn: ['all', 'custom'], + }, + labelTargetBehavior: { + type: 'string', + isIn: ['include', 'exclude'], + }, + labels: { + type: ['string'], + description: 'A list of the names of labels that will be included/excluded.' + } }, @@ -34,7 +48,7 @@ module.exports = { - fn: async function ({profile, newTeamIds, newProfile}) { + fn: async function ({profile, newTeamIds, newProfile, profileTarget, labelTargetBehavior, labels}) { if(newProfile.isNoop){ newProfile.noMoreFiles(); newProfile = undefined; @@ -92,6 +106,8 @@ module.exports = { filename = profile.name; extension = profile.profileType; } + + // ╔═╗╔═╗╔═╗╦╔═╗╔╗╔ ╔═╗╦═╗╔═╗╔═╗╦╦ ╔═╗ // ╠═╣╚═╗╚═╗║║ ╦║║║ ╠═╝╠╦╝║ ║╠╣ ║║ ║╣ // ╩ ╩╚═╝╚═╝╩╚═╝╝╚╝ ╩ ╩╚═╚═╝╚ ╩╩═╝╚═╝ @@ -116,21 +132,24 @@ module.exports = { } for(let teamApid of addedTeams){ // console.log(`Adding ${profile.name} to team id ${teamApid}`); + let bodyForThisRequest = { + team_id: teamApid,// eslint-disable-line camelcase + labels_exclude_any: labelTargetBehavior === 'exclude' ? labels : undefined,// eslint-disable-line camelcase + labels_include_all: labelTargetBehavior === 'include' ? labels : undefined,// eslint-disable-line camelcase + profile: { + options: { + filename: filename + extension, + contentType: 'application/octet-stream' + }, + value: profileContents, + } + }; await sails.helpers.http.sendHttpRequest.with({ method: 'POST', baseUrl: sails.config.custom.fleetBaseUrl, url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`, enctype: 'multipart/form-data', - body: { - team_id: teamApid,// eslint-disable-line camelcase - profile: { - options: { - filename: filename + extension, - contentType: 'application/octet-stream' - }, - value: profileContents, - } - }, + body: bodyForThisRequest, headers: { Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, }, @@ -152,22 +171,25 @@ module.exports = { } } for(let teamApid of newTeamIds){ + let bodyForThisRequest = { + team_id: teamApid,// eslint-disable-line camelcase + labels_exclude_any: labelTargetBehavior === 'exclude' ? labels : undefined,// eslint-disable-line camelcase + labels_include_all: labelTargetBehavior === 'include' ? labels : undefined,// eslint-disable-line camelcase + profile: { + options: { + filename: filename + extension, + contentType: 'application/octet-stream' + }, + value: profileContents, + } + }; // console.log(`Adding ${profile.name} to team id ${teamApid}`); await sails.helpers.http.sendHttpRequest.with({ method: 'POST', baseUrl: sails.config.custom.fleetBaseUrl, url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`, enctype: 'multipart/form-data', - body: { - team_id: teamApid,// eslint-disable-line camelcase - profile: { - options: { - filename: filename + extension, - contentType: 'application/octet-stream' - }, - value: profileContents, - } - }, + body: bodyForThisRequest, headers: { Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, }, @@ -187,12 +209,26 @@ module.exports = { platform: extension === '.xml' ? 'windows' : 'darwin', profileContents, profileType: extension, + labels, + labelTargetBehavior, + profileTarget, }); } else if(profile.id && newProfile){ // If there is a new profile that is replacing a database record, update the profileContents in the database. // console.log('Updating existing undeployed profile!'); await UndeployedProfile.updateOne({id: profile.id}).set({ profileContents, + labels, + labelTargetBehavior, + profileTarget, + }); + } else if(profile.id && labels) { + // Update label target behavior for undeployed profiles. + await UndeployedProfile.updateOne({id: profile.id}).set({ + profileContents, + labels, + labelTargetBehavior, + profileTarget, }); } // All done. diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js b/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js index 19c847bf62..ddb28f9d40 100644 --- a/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js +++ b/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js @@ -17,6 +17,20 @@ module.exports = { teams: { type: ['string'], description: 'An array of team IDs that this profile will be added to' + }, + profileTarget: { + type: 'string', + description: 'The target for this configuration profile', + defaultsTo: 'all', + isIn: ['all', 'custom'], + }, + labelTargetBehavior: { + type: 'string', + isIn: ['include', 'exclude'], + }, + labels: { + type: ['string'], + description: 'A list of the names of labels that will be included/excluded.' } }, @@ -40,7 +54,7 @@ module.exports = { }, - fn: async function ({newProfile, teams}) { + fn: async function ({newProfile, teams, profileTarget, labelTargetBehavior, labels}) { let util = require('util'); let profile = await sails.reservoir(newProfile) .intercept('E_EXCEEDS_UPLOAD_LIMIT', 'tooBig') @@ -63,6 +77,9 @@ module.exports = { platform: profilePlatform, profileType: extension, createdAt: Date.now(), + profileTarget, + labels, + labelTargetBehavior, }; if(!teams) { newProfileInfo.profileContents = profileContents; @@ -70,21 +87,24 @@ module.exports = { } else { let newTeams = []; for(let teamApid of teams){ + let bodyForThisRequest = { + team_id: teamApid,// eslint-disable-line camelcase + labels_exclude_any: labelTargetBehavior === 'exclude' ? labels : undefined,// eslint-disable-line camelcase + labels_include_all: labelTargetBehavior === 'include' ? labels : undefined,// eslint-disable-line camelcase + profile: { + options: { + filename: profileFileName, + contentType: 'application/octet-stream' + }, + value: profileContents, + } + }; let newProfileResponse = await sails.helpers.http.sendHttpRequest.with({ method: 'POST', baseUrl: sails.config.custom.fleetBaseUrl, url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`, enctype: 'multipart/form-data', - body: { - team_id: teamApid,// eslint-disable-line camelcase - profile: { - options: { - filename: profileFileName, - contentType: 'application/octet-stream' - }, - value: profileContents, - } - }, + body: bodyForThisRequest, headers: { Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, }, diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js b/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js index 9ebe807932..05dff23e1e 100644 --- a/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js +++ b/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js @@ -71,42 +71,55 @@ module.exports = { }) .timeout(120000) .retry(['requestFailed', {name: 'TimeoutError'}]); - let profilesForThisTeam = noTeamConfigurationProfilesResponseData.profiles; - allProfiles = allProfiles.concat(profilesForThisTeam); + + let profilesForNoTeam = noTeamConfigurationProfilesResponseData.profiles; + allProfiles = allProfiles.concat(profilesForNoTeam); + let profilesInformation = []; - // Group configuration profiles by their identifier. - let allProfilesByIdentifier = _.groupBy(allProfiles, 'identifier'); - for(let profileIdentifier in allProfilesByIdentifier) { - // Iterate through the arrays of profiles with the same unique identifier. - let teamsForThisProfile = []; - // Add the profile's UUID and information about the team this profile is assigned to the teams array for profiles. - for(let profile of allProfilesByIdentifier[profileIdentifier]) { - let informationAboutThisProfile = { + for(let profile of allProfiles) { + let profileInformation = { + name: profile.name, + identifier: profile.identifier, + platform: profile.platform, + createdAt: new Date(profile.created_at).getTime(), + team: { uuid: profile.profile_uuid, fleetApid: profile.team_id, teamName: _.find(teams, {fleetApid: profile.team_id}).teamName, - }; - teamsForThisProfile.push(informationAboutThisProfile); - } - let profile = allProfilesByIdentifier[profileIdentifier][0];// Grab the first profile returned in the api repsonse to build our profile configuration. - let profileInformation = { - name: profile.name, - identifier: profileIdentifier, - platform: profile.platform, - createdAt: new Date(profile.created_at).getTime(), - teams: teamsForThisProfile + }, + profileTarget: 'all', }; + if(profile.labels_include_all) { + profileInformation.labels = _.pluck(profile.labels_include_all, 'name'); + profileInformation.profileTarget = 'custom'; + profileInformation.labelTargetBehavior = 'include'; + } else if(profile.labels_exclude_any){ + profileInformation.labels = _.pluck(profile.labels_exclude_any, 'name'); + profileInformation.profileTarget = 'custom'; + profileInformation.labelTargetBehavior = 'exclude'; + } profilesInformation.push(profileInformation); } + // Group the profiles based on identifier, labels, and labelTargetBehavior + let profilesGroupedbyLabelsAndIdentifier = _.groupBy(profilesInformation, (profile)=>{ + return `${profile.identifier}|${JSON.stringify(profile.labels)}|${profile.labelTargetBehavior}`; + }); + + // map the grouped profiles and merge profiles that have the same labels, target behavior, and identifier. + let allProfilesOnFleetInstance = Object.values(profilesGroupedbyLabelsAndIdentifier).map(profileGroup => { + return { + ...profileGroup[0],// expand the first item in the profileGroup + teams: profileGroup.map(item => item.team)// Merge the teams arrays + }; + }); // Get the undeployed profiles from the app's database. let undeployedProfiles = await UndeployedProfile.find(); - profilesInformation = _.union(profilesInformation, undeployedProfiles); - + allProfilesOnFleetInstance = _.union(allProfilesOnFleetInstance, undeployedProfiles); // Sort profiles by their name. - profilesInformation = _.sortByOrder(profilesInformation, 'name', 'asc'); + allProfilesOnFleetInstance = _.sortByOrder(allProfilesOnFleetInstance, 'name', 'asc'); // Respond with view. - return {profiles: profilesInformation, teams}; + return {profiles: allProfilesOnFleetInstance, teams}; } diff --git a/ee/bulk-operations-dashboard/api/models/UndeployedProfile.js b/ee/bulk-operations-dashboard/api/models/UndeployedProfile.js index 2d13a1993f..e869256c5f 100644 --- a/ee/bulk-operations-dashboard/api/models/UndeployedProfile.js +++ b/ee/bulk-operations-dashboard/api/models/UndeployedProfile.js @@ -46,6 +46,26 @@ module.exports = { }, + labels: { + type: 'json', + example: ['All hosts', 'Linux hosts'], + description: 'A list of the Fleet API IDs of labels this profile is associated with (if any).', + }, + + labelTargetBehavior: { + type: 'string', + description: 'Whether to exclude or include hosts with the labels in the labels attribute when assigning this profile.', + isIn: ['exclude', 'include'], + defaultsTo: 'include', + }, + + profileTarget: { + type: 'string', + description: 'What hosts will be targetted when this profile is deployed. "all" for all hosts, or "custom" if a profile targets hosts by labels', + isIn: ['all', 'custom'], + defaultsTo: 'all', + }, + // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗ // ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝ diff --git a/ee/bulk-operations-dashboard/assets/images/Icon-error-16x16@2x.png b/ee/bulk-operations-dashboard/assets/images/Icon-error-16x16@2x.png new file mode 100644 index 0000000000..0ec36a820b Binary files /dev/null and b/ee/bulk-operations-dashboard/assets/images/Icon-error-16x16@2x.png differ diff --git a/ee/bulk-operations-dashboard/assets/js/cloud.setup.js b/ee/bulk-operations-dashboard/assets/js/cloud.setup.js index 8e0e987240..5e65967a68 100644 --- a/ee/bulk-operations-dashboard/assets/js/cloud.setup.js +++ b/ee/bulk-operations-dashboard/assets/js/cloud.setup.js @@ -13,7 +13,7 @@ Cloud.setup({ /* eslint-disable */ - methods: {"confirmEmail":{"verb":"GET","url":"/email/confirm","args":["token"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"updatePassword":{"verb":"PUT","url":"/api/v1/account/update-password","args":["password"]},"updateProfile":{"verb":"PUT","url":"/api/v1/account/update-profile","args":["fullName","emailAddress"]},"login":{"verb":"PUT","url":"/api/v1/entrance/login","args":["emailAddress","password","rememberMe"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deleteProfile":{"verb":"POST","url":"/api/v1/delete-profile","args":["profile"]},"downloadProfile":{"verb":"GET","url":"/download-profile","args":["id","uuid"]},"uploadProfile":{"verb":"POST","url":"/api/v1/upload-profile","args":["newProfile","teams"]},"editProfile":{"verb":"POST","url":"/api/v1/edit-profile","args":["profile","newTeamIds","newProfile"]},"getProfiles":{"verb":"GET","url":"/api/v1/get-profiles","args":[]},"getScripts":{"verb":"GET","url":"/api/v1/get-scripts","args":[]},"deleteScript":{"verb":"POST","url":"/api/v1/delete-script","args":["script"]},"downloadScript":{"verb":"GET","url":"/download-script","args":["fleetApid","id"]},"uploadScript":{"verb":"POST","url":"/api/v1/upload-script","args":["newScript","teams"]},"editScript":{"verb":"POST","url":"/api/v1/edit-script","args":["script","newTeamIds","newScript"]},"getSoftware":{"verb":"GET","url":"/api/v1/get-software","args":[]},"downloadSoftware":{"verb":"GET","url":"/download-software","args":["id","fleetApid","teamApid"]},"deleteSoftware":{"verb":"POST","url":"/api/v1/software/delete-software","args":["software"]},"editSoftware":{"verb":"POST","url":"/api/v1/software/edit-software","args":["newSoftware","newTeamIds","software","preInstallQuery","installScript","postInstallScript","uninstallScript"]},"uploadSoftware":{"verb":"POST","url":"/api/v1/software/upload-software","args":["newSoftware","teams"]}} + methods: {"confirmEmail":{"verb":"GET","url":"/email/confirm","args":["token"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"updatePassword":{"verb":"PUT","url":"/api/v1/account/update-password","args":["password"]},"updateProfile":{"verb":"PUT","url":"/api/v1/account/update-profile","args":["fullName","emailAddress"]},"login":{"verb":"PUT","url":"/api/v1/entrance/login","args":["emailAddress","password","rememberMe"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deleteProfile":{"verb":"POST","url":"/api/v1/delete-profile","args":["profile"]},"downloadProfile":{"verb":"GET","url":"/download-profile","args":["id","uuid"]},"uploadProfile":{"verb":"POST","url":"/api/v1/upload-profile","args":["newProfile","teams","profileTarget","labelTargetBehavior","labels"]},"editProfile":{"verb":"POST","url":"/api/v1/edit-profile","args":["profile","newTeamIds","newProfile","profileTarget","labelTargetBehavior","labels"]},"getProfiles":{"verb":"GET","url":"/api/v1/get-profiles","args":[]},"getScripts":{"verb":"GET","url":"/api/v1/get-scripts","args":[]},"deleteScript":{"verb":"POST","url":"/api/v1/delete-script","args":["script"]},"downloadScript":{"verb":"GET","url":"/download-script","args":["fleetApid","id"]},"uploadScript":{"verb":"POST","url":"/api/v1/upload-script","args":["newScript","teams"]},"editScript":{"verb":"POST","url":"/api/v1/edit-script","args":["script","newTeamIds","newScript"]},"getSoftware":{"verb":"GET","url":"/api/v1/get-software","args":[]},"downloadSoftware":{"verb":"GET","url":"/download-software","args":["id","fleetApid","teamApid"]},"deleteSoftware":{"verb":"POST","url":"/api/v1/software/delete-software","args":["software"]},"editSoftware":{"verb":"POST","url":"/api/v1/software/edit-software","args":["newSoftware","newTeamIds","software","preInstallQuery","installScript","postInstallScript","uninstallScript"]},"uploadSoftware":{"verb":"POST","url":"/api/v1/software/upload-software","args":["newSoftware","teams"]},"getLabels":{"verb":"GET","url":"/api/v1/get-labels","args":[]}} /* eslint-enable */ }); diff --git a/ee/bulk-operations-dashboard/assets/js/components/cloud-error.component.js b/ee/bulk-operations-dashboard/assets/js/components/cloud-error.component.js index a5ebbf60d0..057ef70fbe 100644 --- a/ee/bulk-operations-dashboard/assets/js/components/cloud-error.component.js +++ b/ee/bulk-operations-dashboard/assets/js/components/cloud-error.component.js @@ -42,8 +42,8 @@ parasails.registerComponent('cloud-error', { // ╠═╣ ║ ║║║║ // ╩ ╩ ╩ ╩ ╩╩═╝ template: ` -
- {{profileToEdit.name}}
-{{platformFriendlyNames[profileToEdit.platform]}}
+Teams
-
+ {{profileToEdit.name}}
+{{platformFriendlyNames[profileToEdit.platform]}}
+Teams
+Target
+Teams
-Teams
+Target
+ + +