264 lines
10 KiB
Go
264 lines
10 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/file"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
)
|
|
|
|
func (svc *Service) GetSoftwareTitleIcon(ctx context.Context, teamID uint, titleID uint) ([]byte, int64, string, error) {
|
|
var err error
|
|
if err = svc.authz.Authorize(ctx, &fleet.SoftwareTitleIcon{TeamID: teamID}, fleet.ActionRead); err != nil {
|
|
return nil, 0, "", err
|
|
}
|
|
|
|
icon, err := svc.ds.GetSoftwareTitleIcon(ctx, teamID, titleID)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return nil, 0, "", ctxerr.Wrap(ctx, err, "getting software title icon")
|
|
}
|
|
if icon == nil {
|
|
vppApp, err := svc.ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &teamID, titleID)
|
|
if vppApp != nil && vppApp.IconURL != nil {
|
|
return nil, 0, "", &fleet.VPPIconAvailable{IconURL: *vppApp.IconURL}
|
|
}
|
|
|
|
return nil, 0, "", ctxerr.Wrap(ctx, err, "getting software title icon")
|
|
}
|
|
|
|
iconData, size, err := svc.softwareTitleIconStore.Get(ctx, icon.StorageID)
|
|
if err != nil {
|
|
return nil, 0, "", ctxerr.Wrap(ctx, err, "getting software title icon data")
|
|
}
|
|
defer iconData.Close()
|
|
imageBytes, err := io.ReadAll(iconData)
|
|
if err != nil {
|
|
return nil, 0, "", ctxerr.Wrap(ctx, err, "reading icon data")
|
|
}
|
|
|
|
return imageBytes, size, icon.Filename, nil
|
|
}
|
|
|
|
func (svc *Service) UploadSoftwareTitleIcon(ctx context.Context, payload *fleet.UploadSoftwareTitleIconPayload) (fleet.SoftwareTitleIcon, error) {
|
|
var err error
|
|
if err = svc.authz.Authorize(ctx, &fleet.SoftwareTitleIcon{TeamID: payload.TeamID}, fleet.ActionWrite); err != nil {
|
|
return fleet.SoftwareTitleIcon{}, err
|
|
}
|
|
var softwareInstaller *fleet.SoftwareInstaller
|
|
var vppApp *fleet.VPPAppStoreApp
|
|
var inHouseApp *fleet.SoftwareInstaller
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return fleet.SoftwareTitleIcon{}, fleet.ErrNoContext
|
|
}
|
|
user := vc.User
|
|
|
|
softwareInstaller, err = svc.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &payload.TeamID, payload.TitleID, false)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "getting software installer")
|
|
}
|
|
if softwareInstaller == nil {
|
|
vppApp, err = svc.ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &payload.TeamID, payload.TitleID)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "getting VPP app")
|
|
}
|
|
if vppApp == nil {
|
|
inHouseApp, err = svc.ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &payload.TeamID, payload.TitleID)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "getting in-house app")
|
|
}
|
|
}
|
|
}
|
|
if softwareInstaller == nil && vppApp == nil && inHouseApp == nil {
|
|
return fleet.SoftwareTitleIcon{}, &fleet.BadRequestError{Message: fmt.Sprintf("Software title has no software installer, VPP app, or in-house app: %d", payload.TitleID)}
|
|
}
|
|
|
|
icon, err := svc.ds.GetSoftwareTitleIcon(ctx, payload.TeamID, payload.TitleID)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "getting software title icon")
|
|
}
|
|
|
|
if payload.IconFile != nil {
|
|
// get sha256 of icon file
|
|
payload.StorageID, err = file.SHA256FromTempFileReader(payload.IconFile)
|
|
if err != nil {
|
|
return fleet.SoftwareTitleIcon{}, err
|
|
}
|
|
}
|
|
|
|
if icon == nil || icon.StorageID != payload.StorageID {
|
|
var exists bool
|
|
|
|
hasAccess, err := userHasAccessToStorageID(ctx, svc, payload.TeamID, payload.StorageID)
|
|
if err != nil {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "checking access to existing software title icon")
|
|
}
|
|
|
|
if hasAccess {
|
|
exists, err = svc.softwareTitleIconStore.Exists(ctx, payload.StorageID)
|
|
if err != nil {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "checking if software title icon exists")
|
|
}
|
|
}
|
|
|
|
if !exists {
|
|
if payload.IconFile != nil {
|
|
if err := svc.softwareTitleIconStore.Put(ctx, payload.StorageID, payload.IconFile); err != nil {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "storing icon")
|
|
}
|
|
} else {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, &fleet.ConflictError{
|
|
Message: fmt.Sprintf("software title icon with hash '%s' does not exist in the icon store", payload.StorageID),
|
|
}, "icon bytes missing for metadata-only update")
|
|
}
|
|
}
|
|
}
|
|
|
|
softwareTitleIcon, err := svc.ds.CreateOrUpdateSoftwareTitleIcon(ctx, payload)
|
|
if err != nil {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "creating or updating software title icon")
|
|
}
|
|
|
|
// if anything on the icon has changed, we need to generate a new activity
|
|
if icon == nil || icon.StorageID != softwareTitleIcon.StorageID || icon.Filename != softwareTitleIcon.Filename {
|
|
iconUrl := softwareTitleIcon.IconUrl()
|
|
activityDetailsForSoftwareTitleIcon, err := svc.ds.ActivityDetailsForSoftwareTitleIcon(ctxdb.RequirePrimary(ctx, true), payload.TeamID, payload.TitleID)
|
|
if err != nil {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "fetching software title icon activity details")
|
|
}
|
|
err = generateEditActivityForSoftwareTitleIcon(ctx, svc, user, iconUrl, activityDetailsForSoftwareTitleIcon)
|
|
if err != nil {
|
|
return fleet.SoftwareTitleIcon{}, ctxerr.Wrap(ctx, err, "generating edit activity for software title icon")
|
|
}
|
|
}
|
|
|
|
return *softwareTitleIcon, nil
|
|
}
|
|
|
|
func (svc *Service) DeleteSoftwareTitleIcon(ctx context.Context, teamID uint, titleID uint) error {
|
|
var err error
|
|
if err = svc.authz.Authorize(ctx, &fleet.SoftwareTitleIcon{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return fleet.ErrNoContext
|
|
}
|
|
user := vc.User
|
|
activityDetailsForSoftwareTitleIcon, err := svc.ds.ActivityDetailsForSoftwareTitleIcon(ctxdb.RequirePrimary(ctx, true), teamID, titleID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "fetching software title icon activity details")
|
|
}
|
|
var iconUrl string
|
|
if activityDetailsForSoftwareTitleIcon.VPPIconUrl != nil {
|
|
iconUrl = *activityDetailsForSoftwareTitleIcon.VPPIconUrl
|
|
}
|
|
|
|
err = svc.ds.DeleteSoftwareTitleIcon(ctx, teamID, titleID)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return ctxerr.Wrap(ctx, err, "deleting software title icon")
|
|
}
|
|
|
|
// since delete is idempotent, we only want to generate an activity if the
|
|
// software title icon was actually deleted. This error will be a not found error,
|
|
// so if it exists, skip the activity generation
|
|
if err == nil {
|
|
err = generateEditActivityForSoftwareTitleIcon(ctx, svc, user, iconUrl, activityDetailsForSoftwareTitleIcon)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "generating edit activity for software title icon")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func userHasAccessToStorageID(ctx context.Context, svc *Service, teamId uint, storageID string) (bool, error) {
|
|
teamIds, err := svc.ds.GetTeamIdsForIconStorageId(ctx, storageID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if slices.Contains(teamIds, teamId) {
|
|
return true, nil
|
|
}
|
|
|
|
for _, tmID := range teamIds {
|
|
if authErr := svc.authz.Authorize(ctx, &fleet.SoftwareTitleIcon{TeamID: tmID}, fleet.ActionWrite); authErr != nil {
|
|
continue
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func generateEditActivityForSoftwareTitleIcon(ctx context.Context, svc *Service, user *fleet.User, iconUrl string, activityDetailsForSoftwareTitleIcon fleet.DetailsForSoftwareIconActivity) error {
|
|
if activityDetailsForSoftwareTitleIcon.AdamID != nil {
|
|
if err := svc.NewActivity(ctx, user, fleet.ActivityEditedAppStoreApp{
|
|
SoftwareTitle: activityDetailsForSoftwareTitleIcon.SoftwareTitle,
|
|
SoftwareTitleID: activityDetailsForSoftwareTitleIcon.SoftwareTitleID,
|
|
AppStoreID: *activityDetailsForSoftwareTitleIcon.AdamID,
|
|
TeamName: activityDetailsForSoftwareTitleIcon.TeamName,
|
|
TeamID: &activityDetailsForSoftwareTitleIcon.TeamID,
|
|
Platform: *activityDetailsForSoftwareTitleIcon.Platform,
|
|
SelfService: activityDetailsForSoftwareTitleIcon.SelfService,
|
|
SoftwareIconURL: &iconUrl,
|
|
LabelsIncludeAny: activityDetailsForSoftwareTitleIcon.LabelsIncludeAny,
|
|
LabelsExcludeAny: activityDetailsForSoftwareTitleIcon.LabelsExcludeAny,
|
|
LabelsIncludeAll: activityDetailsForSoftwareTitleIcon.LabelsIncludeAll,
|
|
}); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "creating activity for software title icon")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if activityDetailsForSoftwareTitleIcon.SoftwareInstallerID != nil {
|
|
if err := svc.NewActivity(ctx, user, fleet.ActivityTypeEditedSoftware{
|
|
SoftwareTitle: activityDetailsForSoftwareTitleIcon.SoftwareTitle,
|
|
SoftwarePackage: activityDetailsForSoftwareTitleIcon.Filename,
|
|
TeamName: activityDetailsForSoftwareTitleIcon.TeamName,
|
|
TeamID: &activityDetailsForSoftwareTitleIcon.TeamID,
|
|
SelfService: activityDetailsForSoftwareTitleIcon.SelfService,
|
|
SoftwareIconURL: &iconUrl,
|
|
LabelsIncludeAny: activityDetailsForSoftwareTitleIcon.LabelsIncludeAny,
|
|
LabelsExcludeAny: activityDetailsForSoftwareTitleIcon.LabelsExcludeAny,
|
|
LabelsIncludeAll: activityDetailsForSoftwareTitleIcon.LabelsIncludeAll,
|
|
SoftwareTitleID: activityDetailsForSoftwareTitleIcon.SoftwareTitleID,
|
|
}); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "creating activity for software title icon")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if activityDetailsForSoftwareTitleIcon.InHouseAppID != nil {
|
|
if err := svc.NewActivity(ctx, user, fleet.ActivityTypeEditedSoftware{
|
|
SoftwareTitle: activityDetailsForSoftwareTitleIcon.SoftwareTitle,
|
|
SoftwarePackage: activityDetailsForSoftwareTitleIcon.Filename,
|
|
TeamName: activityDetailsForSoftwareTitleIcon.TeamName,
|
|
TeamID: &activityDetailsForSoftwareTitleIcon.TeamID,
|
|
SelfService: activityDetailsForSoftwareTitleIcon.SelfService,
|
|
SoftwareIconURL: &iconUrl,
|
|
LabelsIncludeAny: activityDetailsForSoftwareTitleIcon.LabelsIncludeAny,
|
|
LabelsExcludeAny: activityDetailsForSoftwareTitleIcon.LabelsExcludeAny,
|
|
LabelsIncludeAll: activityDetailsForSoftwareTitleIcon.LabelsIncludeAll,
|
|
SoftwareTitleID: activityDetailsForSoftwareTitleIcon.SoftwareTitleID,
|
|
}); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "creating activity for software title icon")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return ctxerr.New(ctx, "no software installer, VPP app, or in-house app found for software title icon")
|
|
}
|