mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 00:49:03 +00:00
Add user activity logs for MDM bootstrap package endpoints (#11302)
This commit is contained in:
parent
4866bccb3f
commit
8df5f26bea
7 changed files with 274 additions and 0 deletions
|
|
@ -731,6 +731,44 @@ This activity contains the following fields:
|
|||
}
|
||||
```
|
||||
|
||||
### Type `added_bootstrap_package`
|
||||
|
||||
Generated when a user adds a new bootstrap package to a team (or no team).
|
||||
|
||||
This activity contains the following fields:
|
||||
- "package_name": Name of the package.
|
||||
- "team_id": The ID of the team that the package applies to, null if it applies to devices that are not in a team.
|
||||
- "team_name": The name of the team that the package applies to, null if it applies to devices that are not in a team.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"bootstrap_package_name": "bootstrap-package.pkg",
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
}
|
||||
```
|
||||
|
||||
### Type `deleted_bootstrap_package`
|
||||
|
||||
Generated when a user deletes a bootstrap package from a team (or no team).
|
||||
|
||||
This activity contains the following fields:
|
||||
- "package_name": Name of the package.
|
||||
- "team_id": The ID of the team that the package applies to, null if it applies to devices that are not in a team.
|
||||
- "team_name": The name of the team that the package applies to, null if it applies to devices that are not in a team.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"package_name": "bootstrap-package.pkg",
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
<meta name="pageOrderInSection" value="1400">
|
||||
|
|
@ -155,6 +155,17 @@ func (svc *Service) MDMAppleUploadBootstrapPackage(ctx context.Context, name str
|
|||
return err
|
||||
}
|
||||
|
||||
var ptrTeamName *string
|
||||
var ptrTeamId *uint
|
||||
if teamID >= 1 {
|
||||
tm, err := svc.teamByIDOrName(ctx, &teamID, nil)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get team name for upload bootstrap package activity details")
|
||||
}
|
||||
ptrTeamName = &tm.Name
|
||||
ptrTeamId = &teamID
|
||||
}
|
||||
|
||||
hashBuf := bytes.NewBuffer(nil)
|
||||
if err := file.CheckPKGSignature(io.TeeReader(pkg, hashBuf)); err != nil {
|
||||
msg := "invalid package"
|
||||
|
|
@ -185,6 +196,10 @@ func (svc *Service) MDMAppleUploadBootstrapPackage(ctx context.Context, name str
|
|||
return err
|
||||
}
|
||||
|
||||
if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeAddedBootstrapPackage{BootstrapPackageName: name, TeamID: ptrTeamId, TeamName: ptrTeamName}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for upload bootstrap package")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -217,10 +232,30 @@ func (svc *Service) DeleteMDMAppleBootstrapPackage(ctx context.Context, teamID u
|
|||
return err
|
||||
}
|
||||
|
||||
var ptrTeamName *string
|
||||
var ptrTeamId *uint
|
||||
if teamID >= 1 {
|
||||
tm, err := svc.teamByIDOrName(ctx, &teamID, nil)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get team name for delete bootstrap package activity details")
|
||||
}
|
||||
ptrTeamName = &tm.Name
|
||||
ptrTeamId = &teamID
|
||||
}
|
||||
|
||||
meta, err := svc.ds.GetMDMAppleBootstrapPackageMeta(ctx, teamID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "fetching bootstrap package metadata")
|
||||
}
|
||||
|
||||
if err := svc.ds.DeleteMDMAppleBootstrapPackage(ctx, teamID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "deleting bootstrap package")
|
||||
}
|
||||
|
||||
if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedBootstrapPackage{BootstrapPackageName: meta.Name, TeamID: ptrTeamId, TeamName: ptrTeamName}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for delete bootstrap package")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ export enum ActivityType {
|
|||
EditedMacOSProfile = "edited_macos_profile",
|
||||
EnabledMacDiskEncryption = "enabled_macos_disk_encryption",
|
||||
DisabledMacDiskEncryption = "disabled_macos_disk_encryption",
|
||||
AddedBootstrapPackage = "added_bootstrap_package",
|
||||
DeletedBootstrapPackage = "deleted_bootstrap_package",
|
||||
ChangedMacOSSetupAssistant = "changed_macos_setup_assistant",
|
||||
DeletedMacOSSetupAssistant = "deleted_macos_setup_assistant",
|
||||
}
|
||||
|
|
@ -77,5 +79,6 @@ export interface IActivityDetails {
|
|||
deadline?: string;
|
||||
profile_name?: string;
|
||||
profile_identifier?: string;
|
||||
package_name?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -488,4 +488,86 @@ describe("Activity Feed", () => {
|
|||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a 'added_bootstrap_package' type activity for a team", () => {
|
||||
const activity = createMockActivity({
|
||||
type: ActivityType.AddedBootstrapPackage,
|
||||
details: { package_name: "foo.pkg", team_name: "Alphas" },
|
||||
});
|
||||
render(<ActivityItem activity={activity} isPremiumTier />);
|
||||
|
||||
expect(
|
||||
screen.getByText("added a bootstrap package (", { exact: false })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("foo.pkg", { exact: false })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(") for macOS hosts that automatically enroll to the ", {
|
||||
exact: false,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Alphas")).toBeInTheDocument();
|
||||
expect(screen.getByText(" team.", { exact: false })).toBeInTheDocument();
|
||||
const withNoTeams = screen.queryByText("automatically enroll to no team");
|
||||
expect(withNoTeams).toBeNull();
|
||||
});
|
||||
|
||||
it("renders a 'deleted_bootstrap_package' type activity for a team", () => {
|
||||
const activity = createMockActivity({
|
||||
type: ActivityType.DeletedBootstrapPackage,
|
||||
details: { package_name: "foo.pkg", team_name: "Alphas" },
|
||||
});
|
||||
render(<ActivityItem activity={activity} isPremiumTier />);
|
||||
|
||||
expect(
|
||||
screen.getByText("deleted a bootstrap package (", { exact: false })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("foo.pkg", { exact: false })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(") for macOS hosts that automatically enroll to the ", {
|
||||
exact: false,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Alphas")).toBeInTheDocument();
|
||||
expect(screen.getByText(" team.", { exact: false })).toBeInTheDocument();
|
||||
const withNoTeams = screen.queryByText("automatically enroll to no team");
|
||||
expect(withNoTeams).toBeNull();
|
||||
});
|
||||
|
||||
it("renders a 'added_bootstrap_package' type activity for hosts with no team.", () => {
|
||||
const activity = createMockActivity({
|
||||
type: ActivityType.AddedBootstrapPackage,
|
||||
details: { package_name: "foo.pkg" },
|
||||
});
|
||||
render(<ActivityItem activity={activity} isPremiumTier />);
|
||||
|
||||
expect(
|
||||
screen.getByText("added a bootstrap package (", { exact: false })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("foo.pkg", { exact: false })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
") for macOS hosts that automatically enroll to no team.",
|
||||
{ exact: false }
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a 'deleted_bootstrap_package' type activity for hosts with no team.", () => {
|
||||
const activity = createMockActivity({
|
||||
type: ActivityType.DeletedBootstrapPackage,
|
||||
details: { package_name: "foo.pkg" },
|
||||
});
|
||||
render(<ActivityItem activity={activity} isPremiumTier />);
|
||||
|
||||
expect(
|
||||
screen.getByText("deleted a bootstrap package (", { exact: false })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("foo.pkg", { exact: false })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
") for macOS hosts that automatically enroll to no team.",
|
||||
{ exact: false }
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -345,6 +345,56 @@ const TAGGED_TEMPLATES = {
|
|||
</>
|
||||
);
|
||||
},
|
||||
addedMDMBootstrapPackage: (activity: IActivity) => {
|
||||
const packageName = activity.details?.package_name;
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
added a bootstrap package{" "}
|
||||
{packageName ? (
|
||||
<>
|
||||
(<b>{packageName}</b>){" "}
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
for macOS hosts that automatically enroll to{" "}
|
||||
{activity.details?.team_name ? (
|
||||
<>
|
||||
the <b>{activity.details.team_name}</b> team
|
||||
</>
|
||||
) : (
|
||||
"no team"
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
deletedMDMBootstrapPackage: (activity: IActivity) => {
|
||||
const packageName = activity.details?.package_name;
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
deleted a bootstrap package{" "}
|
||||
{packageName ? (
|
||||
<>
|
||||
(<b>{packageName}</b>){" "}
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
for macOS hosts that automatically enroll to{" "}
|
||||
{activity.details?.team_name ? (
|
||||
<>
|
||||
the <b>{activity.details.team_name}</b> team
|
||||
</>
|
||||
) : (
|
||||
"no team"
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const getDetail = (
|
||||
|
|
@ -428,6 +478,12 @@ const getDetail = (
|
|||
case ActivityType.DisabledMacDiskEncryption: {
|
||||
return TAGGED_TEMPLATES.disableMacDiskEncryption(activity);
|
||||
}
|
||||
case ActivityType.AddedBootstrapPackage: {
|
||||
return TAGGED_TEMPLATES.addedMDMBootstrapPackage(activity);
|
||||
}
|
||||
case ActivityType.DeletedBootstrapPackage: {
|
||||
return TAGGED_TEMPLATES.deletedMDMBootstrapPackage(activity);
|
||||
}
|
||||
case ActivityType.ChangedMacOSSetupAssistant: {
|
||||
return TAGGED_TEMPLATES.changedMacOSSetupAssistant(activity);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ var ActivityDetailsList = []ActivityDetails{
|
|||
|
||||
ActivityTypeEnabledMacosDiskEncryption{},
|
||||
ActivityTypeDisabledMacosDiskEncryption{},
|
||||
|
||||
ActivityTypeAddedBootstrapPackage{},
|
||||
ActivityTypeDeletedBootstrapPackage{},
|
||||
}
|
||||
|
||||
type ActivityDetails interface {
|
||||
|
|
@ -892,6 +895,50 @@ func (a ActivityTypeDisabledMacosDiskEncryption) Documentation() (activity, deta
|
|||
}`
|
||||
}
|
||||
|
||||
type ActivityTypeAddedBootstrapPackage struct {
|
||||
BootstrapPackageName string `json:"bootstrap_package_name"`
|
||||
TeamID *uint `json:"team_id"`
|
||||
TeamName *string `json:"team_name"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeAddedBootstrapPackage) ActivityName() string {
|
||||
return "added_bootstrap_package"
|
||||
}
|
||||
|
||||
func (a ActivityTypeAddedBootstrapPackage) Documentation() (activity, details, detailsExample string) {
|
||||
return `Generated when a user adds a new bootstrap package to a team (or no team).`,
|
||||
`This activity contains the following fields:
|
||||
- "package_name": Name of the package.
|
||||
- "team_id": The ID of the team that the package applies to, null if it applies to devices that are not in a team.
|
||||
- "team_name": The name of the team that the package applies to, null if it applies to devices that are not in a team.`, `{
|
||||
"bootstrap_package_name": "bootstrap-package.pkg",
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
}`
|
||||
}
|
||||
|
||||
type ActivityTypeDeletedBootstrapPackage struct {
|
||||
BootstrapPackageName string `json:"bootstrap_package_name"`
|
||||
TeamID *uint `json:"team_id"`
|
||||
TeamName *string `json:"team_name"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeDeletedBootstrapPackage) ActivityName() string {
|
||||
return "deleted_bootstrap_package"
|
||||
}
|
||||
|
||||
func (a ActivityTypeDeletedBootstrapPackage) Documentation() (activity, details, detailsExample string) {
|
||||
return `Generated when a user deletes a bootstrap package from a team (or no team).`,
|
||||
`This activity contains the following fields:
|
||||
- "package_name": Name of the package.
|
||||
- "team_id": The ID of the team that the package applies to, null if it applies to devices that are not in a team.
|
||||
- "team_name": The name of the team that the package applies to, null if it applies to devices that are not in a team.`, `{
|
||||
"package_name": "bootstrap-package.pkg",
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
}`
|
||||
}
|
||||
|
||||
// LogRoleChangeActivities logs activities for each role change, globally and one for each change in teams.
|
||||
func LogRoleChangeActivities(ctx context.Context, ds Datastore, adminUser *User, oldGlobalRole *string, oldTeamRoles []UserTeam, user *User) error {
|
||||
if user.GlobalRole != nil && (oldGlobalRole == nil || *oldGlobalRole != *user.GlobalRole) {
|
||||
|
|
|
|||
|
|
@ -2619,6 +2619,12 @@ func (s *integrationMDMTestSuite) TestBootstrapPackage() {
|
|||
s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: wrongTOCPkg, Name: "pkg.pkg"}, http.StatusBadRequest, "invalid package")
|
||||
// successfully upload a package
|
||||
s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: signedPkg, Name: "pkg.pkg", TeamID: 0}, http.StatusOK, "")
|
||||
// check the activity log
|
||||
s.lastActivityMatches(
|
||||
fleet.ActivityTypeAddedBootstrapPackage{}.ActivityName(),
|
||||
`{"bootstrap_package_name": "pkg.pkg", "team_id": null, "team_name": null}`,
|
||||
0,
|
||||
)
|
||||
|
||||
// get package metadata
|
||||
var metadataResp bootstrapPackageMetadataResponse
|
||||
|
|
@ -2643,6 +2649,13 @@ func (s *integrationMDMTestSuite) TestBootstrapPackage() {
|
|||
// delete package
|
||||
var deleteResp deleteBootstrapPackageResponse
|
||||
s.DoJSON("DELETE", "/api/latest/fleet/mdm/apple/bootstrap/0", nil, http.StatusOK, &deleteResp)
|
||||
// check the activity log
|
||||
s.lastActivityMatches(
|
||||
fleet.ActivityTypeDeletedBootstrapPackage{}.ActivityName(),
|
||||
`{"bootstrap_package_name": "pkg.pkg", "team_id": null, "team_name": null}`,
|
||||
0,
|
||||
)
|
||||
|
||||
metadataResp = bootstrapPackageMetadataResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple/bootstrap/0/metadata", nil, http.StatusNotFound, &metadataResp)
|
||||
// trying to delete again is a bad request
|
||||
|
|
|
|||
Loading…
Reference in a new issue