@@ -82,6 +82,13 @@ func (ds *Datastore) NewUser(ctx context.Context, user *fleet.User) (*fleet.User
8282 if err := saveTeamsForUserDB (ctx , tx , user ); err != nil {
8383 return err
8484 }
85+
86+ if user .APIEndpoints != nil {
87+ if err := replaceUserAPIEndpoints (ctx , tx , user .ID , user .APIEndpoints ); err != nil {
88+ return err
89+ }
90+ }
91+
8592 return nil
8693 })
8794 if err != nil {
@@ -111,6 +118,10 @@ func (ds *Datastore) findUser(ctx context.Context, searchCol string, searchVal i
111118 return nil , ctxerr .Wrap (ctx , err , "load teams" )
112119 }
113120
121+ if err := ds .loadAPIEndpointsForUsers (ctx , []* fleet.User {user }); err != nil {
122+ return nil , ctxerr .Wrap (ctx , err , "load api endpoints" )
123+ }
124+
114125 // When SSO is enabled, we can ignore forced password resets
115126 // However, we want to leave the db untouched, to cover cases where SSO is toggled
116127 if user .SSOEnabled {
@@ -174,6 +185,10 @@ func (ds *Datastore) ListUsers(ctx context.Context, opt fleet.UserListOptions) (
174185 return nil , ctxerr .Wrap (ctx , err , "load teams" )
175186 }
176187
188+ if err := ds .loadAPIEndpointsForUsers (ctx , users ); err != nil {
189+ return nil , ctxerr .Wrap (ctx , err , "load api endpoints" )
190+ }
191+
177192 return users , nil
178193}
179194
@@ -238,8 +253,7 @@ func (ds *Datastore) SaveUser(ctx context.Context, user *fleet.User) error {
238253func (ds * Datastore ) SaveUsers (ctx context.Context , users []* fleet.User ) error {
239254 return ds .withTx (ctx , func (tx sqlx.ExtContext ) error {
240255 for _ , user := range users {
241- err := saveUserDB (ctx , tx , user )
242- if err != nil {
256+ if err := saveUserDB (ctx , tx , user ); err != nil {
243257 return err
244258 }
245259 }
@@ -301,6 +315,57 @@ func saveUserDB(ctx context.Context, tx sqlx.ExtContext, user *fleet.User) error
301315 return err
302316 }
303317
318+ if user .APIEndpoints != nil {
319+ if err := replaceUserAPIEndpoints (ctx , tx , user .ID , user .APIEndpoints ); err != nil {
320+ return err
321+ }
322+ }
323+
324+ return nil
325+ }
326+
327+ // loadAPIEndpointsForUsers loads api_endpoints for any API-only users in the slice.
328+ // Non-API-only users are left untouched.
329+ func (ds * Datastore ) loadAPIEndpointsForUsers (ctx context.Context , users []* fleet.User ) error {
330+ var apiOnlyIDs []uint
331+ for _ , u := range users {
332+ if u .APIOnly {
333+ apiOnlyIDs = append (apiOnlyIDs , u .ID )
334+ }
335+ }
336+ if len (apiOnlyIDs ) == 0 {
337+ return nil
338+ }
339+
340+ query , args , err := sqlx .In (
341+ `SELECT user_id, method, path FROM user_api_endpoints WHERE user_id IN (?) ORDER BY method, path` ,
342+ apiOnlyIDs ,
343+ )
344+ if err != nil {
345+ return ctxerr .Wrap (ctx , err , "build load api endpoints query" )
346+ }
347+
348+ var rows []struct {
349+ UserID uint `db:"user_id"`
350+ Method string `db:"method"`
351+ Path string `db:"path"`
352+ }
353+ if err := sqlx .SelectContext (ctx , ds .reader (ctx ), & rows , query , args ... ); err != nil {
354+ return ctxerr .Wrap (ctx , err , "load api endpoints for users" )
355+ }
356+
357+ byUserID := make (map [uint ][]fleet.APIEndpointRef , len (apiOnlyIDs ))
358+ for _ , row := range rows {
359+ byUserID [row .UserID ] = append (byUserID [row .UserID ], fleet.APIEndpointRef {
360+ Method : row .Method ,
361+ Path : row .Path ,
362+ })
363+ }
364+ for _ , u := range users {
365+ if u .APIOnly {
366+ u .APIEndpoints = byUserID [u .ID ] // nil when no endpoints assigned
367+ }
368+ }
304369 return nil
305370}
306371
@@ -507,3 +572,24 @@ func (ds *Datastore) UserSettings(ctx context.Context, userID uint) (*fleet.User
507572 }
508573 return settings , nil
509574}
575+
576+ // replaceUserAPIEndpoints replaces all API endpoint permissions for the given user.
577+ func replaceUserAPIEndpoints (ctx context.Context , tx sqlx.ExtContext , userID uint , endpoints []fleet.APIEndpointRef ) error {
578+ if _ , err := tx .ExecContext (ctx , `DELETE FROM user_api_endpoints WHERE user_id = ?` , userID ); err != nil {
579+ return ctxerr .Wrap (ctx , err , "delete user api endpoints" )
580+ }
581+ if len (endpoints ) == 0 {
582+ return nil
583+ }
584+ placeholders := strings .Repeat ("(?, ?, ?)," , len (endpoints ))
585+ placeholders = placeholders [:len (placeholders )- 1 ] // trim trailing comma
586+ args := make ([]any , 0 , len (endpoints )* 3 )
587+ for _ , ep := range endpoints {
588+ args = append (args , userID , ep .Path , ep .Method )
589+ }
590+ _ , err := tx .ExecContext (ctx ,
591+ `INSERT INTO user_api_endpoints (user_id, path, method) VALUES ` + placeholders ,
592+ args ... ,
593+ )
594+ return ctxerr .Wrap (ctx , err , "insert user api endpoints" )
595+ }
0 commit comments