Skip to content

API + auth + UI changes for team labels - #37208

Merged
iansltx merged 78 commits into
mainfrom
36758-team-labels-api
Dec 30, 2025
Merged

API + auth + UI changes for team labels#37208
iansltx merged 78 commits into
mainfrom
36758-team-labels-api

Conversation

@iansltx

@iansltx iansltx commented Dec 12, 2025

Copy link
Copy Markdown
Contributor

Covers #36760, #36758.

Checklist for submitter

If some of the following don't apply, delete the relevant line.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements)

Testing

@codecov

codecov Bot commented Dec 12, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.72951% with 177 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.87%. Comparing base (9adfe73) to head (f939b1b).
⚠️ Report is 22 commits behind head on main.

Files with missing lines Patch % Lines
server/datastore/mysql/labels.go 52.27% 91 Missing and 14 partials ⚠️
server/service/labels.go 58.51% 27 Missing and 12 partials ⚠️
frontend/services/entities/labels.ts 56.25% 7 Missing ⚠️
...ManageLabelsPage/LabelsTable/LabelsTableConfig.tsx 40.00% 6 Missing ⚠️
...d/fleetctl/fleetctl/testing_utils/testing_utils.go 72.72% 1 Missing and 2 partials ⚠️
server/service/metrics_labels.go 0.00% 3 Missing ⚠️
...ntend/pages/labels/EditLabelPage/EditLabelPage.tsx 66.66% 2 Missing ⚠️
...s/components/DynamicLabelForm/DynamicLabelForm.tsx 33.33% 1 Missing and 1 partial ⚠️
server/fleet/teams.go 50.00% 1 Missing and 1 partial ⚠️
server/service/global_policies.go 88.88% 1 Missing and 1 partial ⚠️
... and 4 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #37208      +/-   ##
==========================================
- Coverage   65.88%   65.87%   -0.02%     
==========================================
  Files        2360     2361       +1     
  Lines      187285   187383      +98     
  Branches     8006     8017      +11     
==========================================
+ Hits       123402   123441      +39     
- Misses      52605    52645      +40     
- Partials    11278    11297      +19     
Flag Coverage Δ
backend 67.69% <63.25%> (-0.06%) ⬇️
frontend 54.40% <67.24%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Zed + Opus 4.5. Prompt below:

Try to run `make test` and fix compile errors where the interface no longer matches the implementation until the compile completes. Don't change the associated interfaces, and use commit 5ad3c18 as a guide for the sort of changes that need to be made.
Courtesy Junie. Prompt:

Using PlatformField.tsx as a pattern, give me a text field component (not an input, not editable) for "Team name". Include this in both ManualLabelForm and DynamicLabelForm, showing the component only if a new teamName prop (nullable string, required) is truthy. Pass this property all the way from LabelForm. There, add team_name to ILabel as an optional nullable string, and pass that to the DynamicLabelForm and ManualLabelForm components.

TODO: A bunch of manual fixes, but figured I'd split those into a commit to show what's mine/what the robot did.
… controls on label edit page and manage labels action menu
Used Zed + Opus 4.5 with the prompt:

I'm getting test failures on frontend tests (`make test-js`) because I'm pretty sure real server requests are leaking through HTTP mocks. Fix the issues.

It made some other changes that weren't relevant for fixing the problem, so I reverted those prior to committing.
@iansltx
iansltx marked this pull request as ready for review December 24, 2025 03:27
@iansltx
iansltx requested review from a team as code owners December 24, 2025 03:27
@iansltx

iansltx commented Dec 24, 2025

Copy link
Copy Markdown
Contributor Author

@lucasmrod Going to stack additional tests in another PR since this one's massive. Added TODOs on where I believe we want more test coverage. Adding that checklist to the parent issue momentarily.

@iansltx iansltx changed the title [WIP] Team labels work API + UI changes for team labels Dec 24, 2025
@iansltx iansltx changed the title API + UI changes for team labels API + auth + UI changes for team labels Dec 24, 2025
@iansltx
iansltx marked this pull request as draft December 26, 2025 21:01

@lucasmrod lucasmrod left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM!

Publishing review while I review test code.

Comment thread cmd/fleet/cron.go
Comment thread server/authz/policy.rego
Comment thread server/authz/policy.rego
action == create
}

# Team admins, maintainers, and gitops can create global labels

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Not something to fix now) At some point in the future we need to revisit this. Team folks should not be able to configure queries that run on ALL hosts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have visibility checks elsewhere to avoid this, maybe?

Comment thread server/service/labels.go
return nil
}

// Get the user from the context.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail early if !ok || user == nil || user.User == nil?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous code let user-less viewers through, so I assumed we needed to keep that behavior. But looking at code paths again, it doesn't look like we ever actually call this somewhere we don't have a ViewerContext injected, so fair enough.

Comment on lines +115 to +117
if hasWriteRoleAnywhere() && label.AuthorID != nil && *label.AuthorID == user.ID {
continue
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why check authorship of a team label?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, I suppose. But I'm fine with nuking this, since label authorship editability is a holdover from shimming team-ish functionality into global labels.

Comment on lines +364 to +379
rows, err := tx.QueryContext(ctx, hostTeamCheckSql, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "execute host team membership check query")
}

rows.Next()
var hostCountOnWrongTeam int
if err := rows.Scan(&hostCountOnWrongTeam); err != nil {
return ctxerr.Wrap(ctx, err, "check host team membership")
}
if err := rows.Err(); err != nil {
return ctxerr.Wrap(ctx, err, "check host team membership")
}
if err := rows.Close(); err != nil { //nolint:sqlclosecheck
return ctxerr.Wrap(ctx, err, "close result set for host team membership")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rows, err := tx.QueryContext(ctx, hostTeamCheckSql, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "execute host team membership check query")
}
rows.Next()
var hostCountOnWrongTeam int
if err := rows.Scan(&hostCountOnWrongTeam); err != nil {
return ctxerr.Wrap(ctx, err, "check host team membership")
}
if err := rows.Err(); err != nil {
return ctxerr.Wrap(ctx, err, "check host team membership")
}
if err := rows.Close(); err != nil { //nolint:sqlclosecheck
return ctxerr.Wrap(ctx, err, "close result set for host team membership")
}
var hostCountOnWrongTeam int
if err := tx.QueryRowxContext(ctx, hostTeamCheckSql, args...).Scan(&hostCountOnWrongTeam); err != nil {
return ctxerr.Wrap(ctx, err, "execute host team membership check query")
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for calling this out. Somehow missed that QueryRowxContext was a thing.

SELECT l.*,
(SELECT COUNT(1)
FROM label_membership lm
JOIN hosts h ON (lm.host_id = h.id) WHERE label_id = l.id AND %s

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
JOIN hosts h ON (lm.host_id = h.id) WHERE label_id = l.id AND %s
JOIN hosts h ON (lm.host_id = h.id) WHERE lm.label_id = l.id AND %s

}

func verifyLabelsToAssociate(ctx context.Context, ds fleet.Datastore, entityTeamID *uint, labelNames []string) error {
func verifyLabelsToAssociate(ctx context.Context, ds fleet.Datastore, entityTeamID *uint, labelNames []string, user *fleet.User) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's return an error early if user == nil.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. Will see if tests break on this, given that there are a fair number of paths that land here and ViewerContext doesn't guarantee a user.

Comment thread server/datastore/mysql/labels.go

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See TODOs (linking for referencing in future PRs)

@RachelElysia RachelElysia self-assigned this Dec 29, 2025
@lucasmrod
lucasmrod marked this pull request as ready for review December 29, 2025 17:22
} catch (error) {
renderFlash(
"error",
(error as { status: number }).status === 409

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this 409 check might now or in the future return false negatives?

I think we're trying to use a better error handling pattern such as:

const duplicateEntryReason = getErrorReason(err, {
   reasonIncludes: "Duplicate entry",
 });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status code should be fully reliable here, and tbh is preferable to a substring match on error message contents. For a generic 400/422, given that we don't hand back error codes, I get why we're doing substring matches, but this kinda goes back to what I mentioned in ~August of last year about having error messages that we don't have to substring to decipher.

(isTeamAdmin(currentUser, label.team_id) ||
isTeamMaintainer(currentUser, label.team_id)))
);
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

<LabelForm
defaultName={defaultName}
defaultDescription={defaultDescription}
teamName={teamName}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming, this feature is not supporting adding teams to labels via the UI, only via gitops, but can view that it's a team label in the UI, correct? @iansltx

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. Editing team labels is in scope, creating them is not. Team is also an immutable property, and will remain immutable when we add the ability to add team labels via the UI, at least for now, since there are so many things that have to be recalculated if moving a label from one team to another (making a team specific label global is less painful).

readOnly={isEditing}
onLoad={onLoad}
wrapperClassName={`${baseClass}__text-editor-wrapper form-field`}
helpText={isEditing ? IMMUTABLE_QUERY_HELP_TEXT : ""}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure this was removed on purpose? This branch has the query immutable, but it looks like you can change it with no help text saying otherwise that it's read only.

Screen.Recording.2025-12-29.at.1.42.12.PM.mov

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nm I see it's up top.

Maybe quick iteration to make this look disabled, also it's missing the copy button. Don't want to block this PR for those improvements cc: design @marko-lisica

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Label query editability bug is preexisting; just checked Dogfood. Definitely missed that when clicking around when testing this though.

Re: help text, the help text is right below the description, which matches this Figma.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#34124 actually covers the missing disabled state on the query editor.

.slice(0, -1)
.join(", ")} and ${immutableFields.pop()} ${SUFFIX}`;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need fix: Somehow this function is missing platforms when I tested this:
Screenshot 2025-12-29 at 1 49 50 PM

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably the .slice

@RachelElysia RachelElysia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving so we won't need to re-review from BE, but will need an immediate FE followup PR to address this copy bug:
https://github.com/fleetdm/fleet/pull/37208/files#r2651557303

@iansltx

iansltx commented Dec 30, 2025

Copy link
Copy Markdown
Contributor Author

Building follow-up PRs (split into BE/FE) to address feedback. Merging this as tests currently pass, and per earlier discussion today.

Once additional feedback is in I'll create a cherry-pick into 4.79RC with everything.

@iansltx
iansltx merged commit 8e4e89f into main Dec 30, 2025
52 checks passed
@iansltx
iansltx deleted the 36758-team-labels-api branch December 30, 2025 03:28
iansltx added a commit that referenced this pull request Dec 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants