Add GET /api/teams?name= (TEAM-LOOKUP.md)
Resolves the gap TEAM-LOOKUP.md raised: an instance-scoped service account had no way to recover a team's id after a 409 on POST /api/teams, unlike the already-solved equivalent for service accounts themselves (GET /api/service-accounts?name=). Same route, extended the same way handleListServiceAccounts already branches on ?name=: unset behaves exactly as before (the caller's own teams via team_members); set looks up one team by exact name, open to any authenticated caller -- not gated by isInstanceServiceAccount or AdminOnly, since what it discloses (a name is taken, nothing about who's in it) is the same low sensitivity that lookup already accepts for service-account names. Tests cover the exact motivating scenario (create, 409 on a retry, recover the id via ?name=), the empty-array-not-an-error case, that no role/source is reported for a non-member match, and that a caller who isn't a member of the matched team still gets it.
This commit is contained in:
+36
-4
@@ -13,12 +13,19 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// handleListTeams lists the caller's own teams, each with their role in it. An
|
||||
// administrator listing every team goes through the admin endpoint instead:
|
||||
// this one answers "what am I part of", which is what the UI's team filter and
|
||||
// the combined queue are built from.
|
||||
// handleListTeams lists the caller's own teams, each with their role in it,
|
||||
// or — with ?name= — looks up one team by exact name regardless of caller
|
||||
// identity (TEAM-LOOKUP.md). An administrator listing every team goes
|
||||
// through the admin endpoint instead: the no-name case here answers "what am
|
||||
// I part of", which is what the UI's team filter and the combined queue are
|
||||
// built from.
|
||||
func handleListTeams(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if name := strings.TrimSpace(r.URL.Query().Get("name")); name != "" {
|
||||
handleListTeamsByName(db, w, r, name)
|
||||
return
|
||||
}
|
||||
|
||||
caller, _ := userFromContext(r.Context())
|
||||
rows, err := db.QueryContext(r.Context(), `
|
||||
SELECT t.id, t.name, t.created_at, m.role, m.source
|
||||
@@ -51,6 +58,31 @@ func handleListTeams(db *sql.DB) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// handleListTeamsByName answers "is there a team named exactly this", open to
|
||||
// any authenticated caller including a service account (TEAM-LOOKUP.md) —
|
||||
// mirrors handleListServiceAccounts' own ?name= lookup: a one-or-zero-length
|
||||
// array, never an error on no match, and no caller-identity filtering at
|
||||
// all, since what it discloses (a name is taken, nothing about who's in it
|
||||
// or any of its data) is the same low sensitivity that lookup already
|
||||
// accepts for service-account names.
|
||||
func handleListTeamsByName(db *sql.DB, w http.ResponseWriter, r *http.Request, name string) {
|
||||
var t models.Team
|
||||
var created int64
|
||||
err := db.QueryRowContext(r.Context(),
|
||||
"SELECT id, name, created_at FROM teams WHERE name = $1", name,
|
||||
).Scan(&t.ID, &t.Name, &created)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
respond(w, http.StatusOK, []models.Team{})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
respond(w, http.StatusInternalServerError, errResp("internal error"))
|
||||
return
|
||||
}
|
||||
t.CreatedAt = time.Unix(created, 0).UTC()
|
||||
respond(w, http.StatusOK, []models.Team{t})
|
||||
}
|
||||
|
||||
// handleUserTeams lists one user's teams, for the admin page's per-user view:
|
||||
// "what is this person in", which /api/teams cannot answer because it is always
|
||||
// about the caller.
|
||||
|
||||
Reference in New Issue
Block a user