Add user activity logs for MDM bootstrap package endpoints (#11302)

This commit is contained in:
gillespi314 2023-04-26 13:40:14 -05:00 committed by GitHub
parent 4866bccb3f
commit 8df5f26bea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 274 additions and 0 deletions

View file

@ -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">

View file

@ -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
}

View file

@ -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;
}

View file

@ -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();
});
});

View file

@ -345,6 +345,56 @@ const TAGGED_TEMPLATES = {
</>
);
},
addedMDMBootstrapPackage: (activity: IActivity) => {
const packageName = activity.details?.package_name;
return (
<>
{" "}
added a bootstrap package{" "}
{packageName ? (
<>
&#40;<b>{packageName}</b>&#41;{" "}
</>
) : (
""
)}
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 ? (
<>
&#40;<b>{packageName}</b>&#41;{" "}
</>
) : (
""
)}
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);
}

View file

@ -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) {

View file

@ -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