2022-01-10 19:43:39 +00:00
package service
import (
"context"
2022-02-28 12:34:44 +00:00
"database/sql"
2022-01-10 19:43:39 +00:00
"encoding/base64"
2022-01-13 19:57:44 +00:00
"errors"
2022-01-10 19:43:39 +00:00
"html/template"
2022-03-08 16:27:38 +00:00
"net/http"
"time"
2022-01-10 19:43:39 +00:00
2023-06-07 19:06:36 +00:00
"github.com/go-kit/kit/log/level"
2022-01-10 19:43:39 +00:00
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/authz"
2022-04-18 17:27:30 +00:00
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
2022-01-10 19:43:39 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
2023-04-05 18:23:49 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/license"
2022-01-10 19:43:39 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mail"
"github.com/fleetdm/fleet/v4/server/ptr"
)
////////////////////////////////////////////////////////////////////////////////
// Create User
////////////////////////////////////////////////////////////////////////////////
type createUserRequest struct {
fleet . UserPayload
}
type createUserResponse struct {
User * fleet . User ` json:"user,omitempty" `
Err error ` json:"error,omitempty" `
}
func ( r createUserResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func createUserEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-10 19:43:39 +00:00
req := request . ( * createUserRequest )
user , err := svc . CreateUser ( ctx , req . UserPayload )
if err != nil {
return createUserResponse { Err : err } , nil
}
return createUserResponse { User : user } , nil
}
func ( svc * Service ) CreateUser ( ctx context . Context , p fleet . UserPayload ) ( * fleet . User , error ) {
var teams [ ] fleet . UserTeam
if p . Teams != nil {
teams = * p . Teams
}
if err := svc . authz . Authorize ( ctx , & fleet . User { Teams : teams } , fleet . ActionWrite ) ; err != nil {
return nil , err
}
2022-03-08 16:27:38 +00:00
if err := p . VerifyAdminCreate ( ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "verify user payload" )
}
2022-01-10 19:43:39 +00:00
if invite , err := svc . ds . InviteByEmail ( ctx , * p . Email ) ; err == nil && invite != nil {
return nil , ctxerr . Errorf ( ctx , "%s already invited" , * p . Email )
}
if p . AdminForcedPasswordReset == nil {
// By default, force password reset for users created this way.
p . AdminForcedPasswordReset = ptr . Bool ( true )
}
2022-08-15 17:42:33 +00:00
return svc . NewUser ( ctx , p )
2022-01-10 19:43:39 +00:00
}
2022-03-08 16:27:38 +00:00
////////////////////////////////////////////////////////////////////////////////
// Create User From Invite
////////////////////////////////////////////////////////////////////////////////
2022-12-27 14:26:59 +00:00
func createUserFromInviteEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-03-08 16:27:38 +00:00
req := request . ( * createUserRequest )
user , err := svc . CreateUserFromInvite ( ctx , req . UserPayload )
if err != nil {
return createUserResponse { Err : err } , nil
}
return createUserResponse { User : user } , nil
}
func ( svc * Service ) CreateUserFromInvite ( ctx context . Context , p fleet . UserPayload ) ( * fleet . User , error ) {
// skipauth: There is no viewer context at this point. We rely on verifying
// the invite for authNZ.
svc . authz . SkipAuthorization ( ctx )
if err := p . VerifyInviteCreate ( ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "verify user payload" )
}
invite , err := svc . VerifyInvite ( ctx , * p . InviteToken )
if err != nil {
return nil , err
}
// set the payload role property based on an existing invite.
p . GlobalRole = invite . GlobalRole . Ptr ( )
p . Teams = & invite . Teams
2022-08-15 17:42:33 +00:00
user , err := svc . NewUser ( ctx , p )
2022-03-08 16:27:38 +00:00
if err != nil {
return nil , err
}
err = svc . ds . DeleteInvite ( ctx , invite . ID )
if err != nil {
return nil , err
}
return user , nil
}
2022-01-10 19:43:39 +00:00
////////////////////////////////////////////////////////////////////////////////
// List Users
////////////////////////////////////////////////////////////////////////////////
type listUsersRequest struct {
ListOptions fleet . UserListOptions ` url:"user_options" `
}
type listUsersResponse struct {
Users [ ] fleet . User ` json:"users" `
Err error ` json:"error,omitempty" `
}
func ( r listUsersResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func listUsersEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-10 19:43:39 +00:00
req := request . ( * listUsersRequest )
users , err := svc . ListUsers ( ctx , req . ListOptions )
if err != nil {
return listUsersResponse { Err : err } , nil
}
resp := listUsersResponse { Users : [ ] fleet . User { } }
for _ , user := range users {
resp . Users = append ( resp . Users , * user )
}
return resp , nil
}
func ( svc * Service ) ListUsers ( ctx context . Context , opt fleet . UserListOptions ) ( [ ] * fleet . User , error ) {
2022-04-18 17:27:30 +00:00
user := & fleet . User { }
if opt . TeamID != 0 {
user . Teams = [ ] fleet . UserTeam { { Team : fleet . Team { ID : opt . TeamID } } }
}
if err := svc . authz . Authorize ( ctx , user , fleet . ActionRead ) ; err != nil {
2022-01-10 19:43:39 +00:00
return nil , err
}
return svc . ds . ListUsers ( ctx , opt )
}
2022-01-25 14:34:00 +00:00
////////////////////////////////////////////////////////////////////////////////
// Me (get own current user)
////////////////////////////////////////////////////////////////////////////////
2022-12-27 14:26:59 +00:00
func meEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-25 14:34:00 +00:00
user , err := svc . AuthenticatedUser ( ctx )
if err != nil {
return getUserResponse { Err : err } , nil
}
availableTeams , err := svc . ListAvailableTeamsForUser ( ctx , user )
if err != nil {
if errors . Is ( err , fleet . ErrMissingLicense ) {
availableTeams = [ ] * fleet . TeamSummary { }
} else {
return getUserResponse { Err : err } , nil
}
}
return getUserResponse { User : user , AvailableTeams : availableTeams } , nil
}
func ( svc * Service ) AuthenticatedUser ( ctx context . Context ) ( * fleet . User , error ) {
vc , ok := viewer . FromContext ( ctx )
if ! ok {
return nil , fleet . ErrNoContext
}
if err := svc . authz . Authorize ( ctx , & fleet . User { ID : vc . UserID ( ) } , fleet . ActionRead ) ; err != nil {
return nil , err
}
if ! vc . IsLoggedIn ( ) {
return nil , fleet . NewPermissionError ( "not logged in" )
}
return vc . User , nil
}
2022-01-10 19:43:39 +00:00
////////////////////////////////////////////////////////////////////////////////
// Get User
////////////////////////////////////////////////////////////////////////////////
type getUserRequest struct {
ID uint ` url:"id" `
}
type getUserResponse struct {
2022-01-13 19:57:44 +00:00
User * fleet . User ` json:"user,omitempty" `
AvailableTeams [ ] * fleet . TeamSummary ` json:"available_teams" `
Err error ` json:"error,omitempty" `
2022-01-10 19:43:39 +00:00
}
func ( r getUserResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func getUserEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-10 19:43:39 +00:00
req := request . ( * getUserRequest )
user , err := svc . User ( ctx , req . ID )
if err != nil {
return getUserResponse { Err : err } , nil
}
2022-01-13 19:57:44 +00:00
availableTeams , err := svc . ListAvailableTeamsForUser ( ctx , user )
if err != nil {
if errors . Is ( err , fleet . ErrMissingLicense ) {
availableTeams = [ ] * fleet . TeamSummary { }
} else {
return getUserResponse { Err : err } , nil
}
}
return getUserResponse { User : user , AvailableTeams : availableTeams } , nil
2022-01-10 19:43:39 +00:00
}
2022-04-18 17:27:30 +00:00
// setAuthCheckedOnPreAuthErr can be used to set the authentication as checked
// in case of errors that happened before an auth check can be performed.
// Otherwise the endpoints return a "authentication skipped" error instead of
// the actual returned error.
func setAuthCheckedOnPreAuthErr ( ctx context . Context ) {
if az , ok := authz_ctx . FromContext ( ctx ) ; ok {
az . SetChecked ( )
}
}
2022-01-10 19:43:39 +00:00
func ( svc * Service ) User ( ctx context . Context , id uint ) ( * fleet . User , error ) {
2022-04-18 17:27:30 +00:00
user , err := svc . ds . UserByID ( ctx , id )
if err != nil {
setAuthCheckedOnPreAuthErr ( ctx )
return nil , ctxerr . Wrap ( ctx , err )
2022-01-10 19:43:39 +00:00
}
2022-04-18 17:27:30 +00:00
if err := svc . authz . Authorize ( ctx , user , fleet . ActionRead ) ; err != nil {
return nil , err
}
return user , nil
2022-01-10 19:43:39 +00:00
}
////////////////////////////////////////////////////////////////////////////////
// Modify User
////////////////////////////////////////////////////////////////////////////////
type modifyUserRequest struct {
ID uint ` json:"-" url:"id" `
fleet . UserPayload
}
type modifyUserResponse struct {
User * fleet . User ` json:"user,omitempty" `
Err error ` json:"error,omitempty" `
}
func ( r modifyUserResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func modifyUserEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-10 19:43:39 +00:00
req := request . ( * modifyUserRequest )
user , err := svc . ModifyUser ( ctx , req . ID , req . UserPayload )
if err != nil {
return modifyUserResponse { Err : err } , nil
}
return modifyUserResponse { User : user } , nil
}
func ( svc * Service ) ModifyUser ( ctx context . Context , userID uint , p fleet . UserPayload ) ( * fleet . User , error ) {
user , err := svc . User ( ctx , userID )
if err != nil {
2022-04-18 17:27:30 +00:00
setAuthCheckedOnPreAuthErr ( ctx )
2022-01-10 19:43:39 +00:00
return nil , err
}
2022-12-21 17:30:19 +00:00
oldGlobalRole := user . GlobalRole
oldTeams := user . Teams
2022-01-10 19:43:39 +00:00
if err := svc . authz . Authorize ( ctx , user , fleet . ActionWrite ) ; err != nil {
return nil , err
}
2022-03-08 16:27:38 +00:00
vc , ok := viewer . FromContext ( ctx )
if ! ok {
return nil , ctxerr . New ( ctx , "viewer not present" ) // should never happen, authorize would've failed
}
2022-03-15 12:11:53 +00:00
ownUser := vc . UserID ( ) == userID
if err := p . VerifyModify ( ownUser ) ; err != nil {
2022-03-08 16:27:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "verify user payload" )
}
2022-01-10 19:43:39 +00:00
if p . GlobalRole != nil || p . Teams != nil {
if err := svc . authz . Authorize ( ctx , user , fleet . ActionWriteRole ) ; err != nil {
return nil , err
}
2023-04-05 18:23:49 +00:00
license , _ := license . FromContext ( ctx )
if license == nil {
return nil , ctxerr . New ( ctx , "license not found" )
}
2023-04-12 19:11:04 +00:00
if err := fleet . ValidateUserRoles ( false , p , * license ) ; err != nil {
2023-04-05 18:23:49 +00:00
return nil , ctxerr . Wrap ( ctx , err , "validate role" )
}
2022-01-10 19:43:39 +00:00
}
2022-03-15 12:11:53 +00:00
if p . NewPassword != nil {
if err := svc . authz . Authorize ( ctx , user , fleet . ActionChangePassword ) ; err != nil {
return nil , err
}
if err := fleet . ValidatePasswordRequirements ( * p . NewPassword ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "new_password" , err . Error ( ) ) )
}
if ownUser {
// when changing one's own password, user cannot reuse the same password
// and the old password must be provided (validated by p.VerifyModify above)
// and must be valid. If changed by admin, then this is not required.
if err := vc . User . ValidatePassword ( * p . NewPassword ) ; err == nil {
2022-07-25 17:14:05 +00:00
return nil , ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "new_password" , "Cannot reuse old password" ) )
2022-03-15 12:11:53 +00:00
}
if err := vc . User . ValidatePassword ( * p . Password ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , fleet . NewPermissionError ( "incorrect password" ) )
}
}
}
2022-01-10 19:43:39 +00:00
if p . Name != nil {
user . Name = * p . Name
}
if p . Email != nil && * p . Email != user . Email {
err = svc . modifyEmailAddress ( ctx , user , * p . Email , p . Password )
if err != nil {
return nil , err
}
}
if p . Position != nil {
user . Position = * p . Position
}
if p . GravatarURL != nil {
user . GravatarURL = * p . GravatarURL
}
if p . SSOEnabled != nil {
user . SSOEnabled = * p . SSOEnabled
}
currentUser := authz . UserFromContext ( ctx )
if p . GlobalRole != nil && * p . GlobalRole != "" {
if currentUser . GlobalRole == nil {
2022-04-18 17:27:30 +00:00
return nil , authz . ForbiddenWithInternal (
"cannot edit global role as a team member" ,
currentUser , user , fleet . ActionWriteRole ,
)
2022-01-10 19:43:39 +00:00
}
if p . Teams != nil && len ( * p . Teams ) > 0 {
return nil , fleet . NewInvalidArgumentError ( "teams" , "may not be specified with global_role" )
}
user . GlobalRole = p . GlobalRole
user . Teams = [ ] fleet . UserTeam { }
} else if p . Teams != nil {
if ! isAdminOfTheModifiedTeams ( currentUser , user . Teams , * p . Teams ) {
2022-04-18 17:27:30 +00:00
return nil , authz . ForbiddenWithInternal (
"cannot modify teams in that way" ,
currentUser , user , fleet . ActionWriteRole ,
)
2022-01-10 19:43:39 +00:00
}
user . Teams = * p . Teams
user . GlobalRole = nil
}
2022-03-15 12:11:53 +00:00
if p . NewPassword != nil {
// setNewPassword takes care of calling saveUser
err = svc . setNewPassword ( ctx , user , * p . NewPassword )
} else {
err = svc . saveUser ( ctx , user )
}
2022-01-10 19:43:39 +00:00
if err != nil {
return nil , err
}
2022-12-21 17:30:19 +00:00
// load user again to get team-details like names.
user , err = svc . User ( ctx , userID )
if err != nil {
return nil , err
}
adminUser := authz . UserFromContext ( ctx )
2023-03-01 23:18:40 +00:00
if err := fleet . LogRoleChangeActivities ( ctx , svc . ds , adminUser , oldGlobalRole , oldTeams , user ) ; err != nil {
2022-12-21 17:30:19 +00:00
return nil , err
}
2022-01-10 19:43:39 +00:00
return user , nil
}
////////////////////////////////////////////////////////////////////////////////
// Delete User
////////////////////////////////////////////////////////////////////////////////
type deleteUserRequest struct {
ID uint ` url:"id" `
}
type deleteUserResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r deleteUserResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func deleteUserEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-10 19:43:39 +00:00
req := request . ( * deleteUserRequest )
err := svc . DeleteUser ( ctx , req . ID )
if err != nil {
return deleteUserResponse { Err : err } , nil
}
return deleteUserResponse { } , nil
}
func ( svc * Service ) DeleteUser ( ctx context . Context , id uint ) error {
2022-04-18 17:27:30 +00:00
user , err := svc . ds . UserByID ( ctx , id )
if err != nil {
setAuthCheckedOnPreAuthErr ( ctx )
return ctxerr . Wrap ( ctx , err )
}
if err := svc . authz . Authorize ( ctx , user , fleet . ActionWrite ) ; err != nil {
2022-01-10 19:43:39 +00:00
return err
}
2022-12-21 17:30:19 +00:00
if err := svc . ds . DeleteUser ( ctx , id ) ; err != nil {
return err
}
adminUser := authz . UserFromContext ( ctx )
if err := svc . ds . NewActivity (
ctx ,
adminUser ,
2022-12-23 16:05:16 +00:00
fleet . ActivityTypeDeletedUser {
UserID : user . ID ,
UserName : user . Name ,
UserEmail : user . Email ,
} ,
2022-12-21 17:30:19 +00:00
) ; err != nil {
return err
}
return nil
2022-01-10 19:43:39 +00:00
}
////////////////////////////////////////////////////////////////////////////////
// Require Password Reset
////////////////////////////////////////////////////////////////////////////////
type requirePasswordResetRequest struct {
Require bool ` json:"require" `
ID uint ` json:"-" url:"id" `
}
type requirePasswordResetResponse struct {
User * fleet . User ` json:"user,omitempty" `
Err error ` json:"error,omitempty" `
}
func ( r requirePasswordResetResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func requirePasswordResetEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-10 19:43:39 +00:00
req := request . ( * requirePasswordResetRequest )
user , err := svc . RequirePasswordReset ( ctx , req . ID , req . Require )
if err != nil {
return requirePasswordResetResponse { Err : err } , nil
}
return requirePasswordResetResponse { User : user } , nil
}
func ( svc * Service ) RequirePasswordReset ( ctx context . Context , uid uint , require bool ) ( * fleet . User , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . User { ID : uid } , fleet . ActionWrite ) ; err != nil {
return nil , err
}
user , err := svc . ds . UserByID ( ctx , uid )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "loading user by ID" )
}
if user . SSOEnabled {
return nil , ctxerr . New ( ctx , "password reset for single sign on user not allowed" )
}
// Require reset on next login
user . AdminForcedPasswordReset = require
if err := svc . saveUser ( ctx , user ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "saving user" )
}
if require {
// Clear all of the existing sessions
if err := svc . DeleteSessionsForUser ( ctx , user . ID ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "deleting user sessions" )
}
}
return user , nil
}
////////////////////////////////////////////////////////////////////////////////
// Change Password
////////////////////////////////////////////////////////////////////////////////
type changePasswordRequest struct {
OldPassword string ` json:"old_password" `
NewPassword string ` json:"new_password" `
}
type changePasswordResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r changePasswordResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func changePasswordEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-10 19:43:39 +00:00
req := request . ( * changePasswordRequest )
err := svc . ChangePassword ( ctx , req . OldPassword , req . NewPassword )
return changePasswordResponse { Err : err } , nil
}
func ( svc * Service ) ChangePassword ( ctx context . Context , oldPass , newPass string ) error {
vc , ok := viewer . FromContext ( ctx )
if ! ok {
return fleet . ErrNoContext
}
2022-03-15 12:11:53 +00:00
if err := svc . authz . Authorize ( ctx , vc . User , fleet . ActionChangePassword ) ; err != nil {
2022-01-10 19:43:39 +00:00
return err
}
2022-03-08 16:27:38 +00:00
if oldPass == "" {
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "old_password" , "Old password cannot be empty" ) )
}
if newPass == "" {
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "new_password" , "New password cannot be empty" ) )
}
if err := fleet . ValidatePasswordRequirements ( newPass ) ; err != nil {
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "new_password" , err . Error ( ) ) )
}
2022-01-10 19:43:39 +00:00
if vc . User . SSOEnabled {
return ctxerr . New ( ctx , "change password for single sign on user not allowed" )
}
if err := vc . User . ValidatePassword ( newPass ) ; err == nil {
2022-07-25 17:14:05 +00:00
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "new_password" , "Cannot reuse old password" ) )
2022-01-10 19:43:39 +00:00
}
if err := vc . User . ValidatePassword ( oldPass ) ; err != nil {
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "old_password" , "old password does not match" ) )
}
if err := svc . setNewPassword ( ctx , vc . User , newPass ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "setting new password" )
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Get Info About Sessions For User
////////////////////////////////////////////////////////////////////////////////
type getInfoAboutSessionsForUserRequest struct {
ID uint ` url:"id" `
}
type getInfoAboutSessionsForUserResponse struct {
Sessions [ ] getInfoAboutSessionResponse ` json:"sessions" `
Err error ` json:"error,omitempty" `
}
func ( r getInfoAboutSessionsForUserResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func getInfoAboutSessionsForUserEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-10 19:43:39 +00:00
req := request . ( * getInfoAboutSessionsForUserRequest )
sessions , err := svc . GetInfoAboutSessionsForUser ( ctx , req . ID )
if err != nil {
return getInfoAboutSessionsForUserResponse { Err : err } , nil
}
var resp getInfoAboutSessionsForUserResponse
for _ , session := range sessions {
resp . Sessions = append ( resp . Sessions , getInfoAboutSessionResponse {
SessionID : session . ID ,
UserID : session . UserID ,
CreatedAt : session . CreatedAt ,
} )
}
return resp , nil
}
func ( svc * Service ) GetInfoAboutSessionsForUser ( ctx context . Context , id uint ) ( [ ] * fleet . Session , error ) {
2022-01-25 14:34:00 +00:00
if err := svc . authz . Authorize ( ctx , & fleet . Session { UserID : id } , fleet . ActionRead ) ; err != nil {
2022-01-10 19:43:39 +00:00
return nil , err
}
var validatedSessions [ ] * fleet . Session
sessions , err := svc . ds . ListSessionsForUser ( ctx , id )
if err != nil {
return validatedSessions , err
}
for _ , session := range sessions {
if svc . validateSession ( ctx , session ) == nil {
validatedSessions = append ( validatedSessions , session )
}
}
return validatedSessions , nil
}
////////////////////////////////////////////////////////////////////////////////
// Delete Sessions For User
////////////////////////////////////////////////////////////////////////////////
type deleteSessionsForUserRequest struct {
ID uint ` url:"id" `
}
type deleteSessionsForUserResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r deleteSessionsForUserResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func deleteSessionsForUserEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-10 19:43:39 +00:00
req := request . ( * deleteSessionsForUserRequest )
err := svc . DeleteSessionsForUser ( ctx , req . ID )
if err != nil {
return deleteSessionsForUserResponse { Err : err } , nil
}
return deleteSessionsForUserResponse { } , nil
}
func ( svc * Service ) DeleteSessionsForUser ( ctx context . Context , id uint ) error {
if err := svc . authz . Authorize ( ctx , & fleet . Session { UserID : id } , fleet . ActionWrite ) ; err != nil {
return err
}
return svc . ds . DestroyAllSessionsForUser ( ctx , id )
}
2022-02-15 20:22:19 +00:00
////////////////////////////////////////////////////////////////////////////////
// Change user email
////////////////////////////////////////////////////////////////////////////////
type changeEmailRequest struct {
Token string ` url:"token" `
}
type changeEmailResponse struct {
NewEmail string ` json:"new_email" `
Err error ` json:"error,omitempty" `
}
func ( r changeEmailResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func changeEmailEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-02-15 20:22:19 +00:00
req := request . ( * changeEmailRequest )
newEmailAddress , err := svc . ChangeUserEmail ( ctx , req . Token )
if err != nil {
return changeEmailResponse { Err : err } , nil
}
return changeEmailResponse { NewEmail : newEmailAddress } , nil
}
func ( svc * Service ) ChangeUserEmail ( ctx context . Context , token string ) ( string , error ) {
vc , ok := viewer . FromContext ( ctx )
if ! ok {
return "" , fleet . ErrNoContext
}
if err := svc . authz . Authorize ( ctx , & fleet . User { ID : vc . UserID ( ) } , fleet . ActionWrite ) ; err != nil {
return "" , err
}
return svc . ds . ConfirmPendingEmailChange ( ctx , vc . UserID ( ) , token )
}
2022-04-18 17:27:30 +00:00
// isAdminOfTheModifiedTeams checks whether the current user is allowed to modify the user
// roles in the teams.
//
// TODO: End-goal is to move all this logic to policy.rego.
2022-01-10 19:43:39 +00:00
func isAdminOfTheModifiedTeams ( currentUser * fleet . User , originalUserTeams , newUserTeams [ ] fleet . UserTeam ) bool {
2022-04-18 17:27:30 +00:00
// Global admins can modify all user teams roles.
if currentUser . GlobalRole != nil && * currentUser . GlobalRole == fleet . RoleAdmin {
2022-01-10 19:43:39 +00:00
return true
}
2022-04-18 17:27:30 +00:00
// Otherwise, make a map of the original and resulting teams.
newTeams := make ( map [ uint ] string )
2022-01-10 19:43:39 +00:00
for _ , team := range newUserTeams {
2022-04-18 17:27:30 +00:00
newTeams [ team . ID ] = team . Role
}
originalTeams := make ( map [ uint ] struct { } )
for _ , team := range originalUserTeams {
originalTeams [ team . ID ] = struct { } { }
2022-01-10 19:43:39 +00:00
}
2022-04-18 17:27:30 +00:00
// See which ones were removed or changed from the original.
2022-01-10 19:43:39 +00:00
teamsAffected := make ( map [ uint ] struct { } )
for _ , team := range originalUserTeams {
2022-04-18 17:27:30 +00:00
if newTeams [ team . ID ] != team . Role {
teamsAffected [ team . ID ] = struct { } { }
}
}
// See which ones of the new are not in the original.
for _ , team := range newUserTeams {
if _ , ok := originalTeams [ team . ID ] ; ! ok {
2022-01-10 19:43:39 +00:00
teamsAffected [ team . ID ] = struct { } { }
}
}
2022-04-18 17:27:30 +00:00
// Then gather the teams the current user is admin for.
2022-01-10 19:43:39 +00:00
currentUserTeamAdmin := make ( map [ uint ] struct { } )
for _ , team := range currentUser . Teams {
if team . Role == fleet . RoleAdmin {
currentUserTeamAdmin [ team . ID ] = struct { } { }
}
}
2022-04-18 17:27:30 +00:00
// And finally, let's check that the teams that were either removed
// or changed are also teams this user is an admin of.
2022-01-10 19:43:39 +00:00
for teamID := range teamsAffected {
if _ , ok := currentUserTeamAdmin [ teamID ] ; ! ok {
return false
}
}
return true
}
func ( svc * Service ) modifyEmailAddress ( ctx context . Context , user * fleet . User , email string , password * string ) error {
// password requirement handled in validation middleware
if password != nil {
err := user . ValidatePassword ( * password )
if err != nil {
return fleet . NewPermissionError ( "incorrect password" )
}
}
random , err := server . GenerateRandomText ( svc . config . App . TokenKeySize )
if err != nil {
return err
}
token := base64 . URLEncoding . EncodeToString ( [ ] byte ( random ) )
2022-02-28 12:34:44 +00:00
switch _ , err = svc . ds . UserByEmail ( ctx , email ) ; {
case err == nil :
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return ctxerr . Wrap ( ctx , newAlreadyExistsError ( ) )
2022-02-28 12:34:44 +00:00
case errors . Is ( err , sql . ErrNoRows ) :
// OK
default :
return ctxerr . Wrap ( ctx , err )
}
2022-02-28 16:17:10 +00:00
switch _ , err = svc . ds . InviteByEmail ( ctx , email ) ; {
case err == nil :
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return ctxerr . Wrap ( ctx , newAlreadyExistsError ( ) )
2022-02-28 16:17:10 +00:00
case errors . Is ( err , sql . ErrNoRows ) :
// OK
default :
return ctxerr . Wrap ( ctx , err )
}
2022-01-10 19:43:39 +00:00
err = svc . ds . PendingEmailChange ( ctx , user . ID , email , token )
if err != nil {
return err
}
2023-03-28 18:23:15 +00:00
config , err := svc . ds . AppConfig ( ctx )
2022-01-10 19:43:39 +00:00
if err != nil {
return err
}
2023-06-07 19:06:36 +00:00
var smtpSettings fleet . SMTPSettings
if config . SMTPSettings != nil {
smtpSettings = * config . SMTPSettings
}
2022-01-10 19:43:39 +00:00
changeEmail := fleet . Email {
2023-06-07 19:06:36 +00:00
Subject : "Confirm Fleet Email Change" ,
To : [ ] string { email } ,
SMTPSettings : smtpSettings ,
ServerURL : config . ServerSettings . ServerURL ,
2022-01-10 19:43:39 +00:00
Mailer : & mail . ChangeEmailMailer {
Token : token ,
BaseURL : template . URL ( config . ServerSettings . ServerURL + svc . config . Server . URLPrefix ) ,
AssetURL : getAssetURL ( ) ,
} ,
}
return svc . mailService . SendEmail ( changeEmail )
}
// saves user in datastore.
// doesn't need to be exposed to the transport
// the service should expose actions for modifying a user instead
func ( svc * Service ) saveUser ( ctx context . Context , user * fleet . User ) error {
return svc . ds . SaveUser ( ctx , user )
}
2022-03-08 16:27:38 +00:00
////////////////////////////////////////////////////////////////////////////////
// Perform Required Password Reset
////////////////////////////////////////////////////////////////////////////////
type performRequiredPasswordResetRequest struct {
Password string ` json:"new_password" `
ID uint ` json:"id" `
}
type performRequiredPasswordResetResponse struct {
User * fleet . User ` json:"user,omitempty" `
Err error ` json:"error,omitempty" `
}
func ( r performRequiredPasswordResetResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func performRequiredPasswordResetEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-03-08 16:27:38 +00:00
req := request . ( * performRequiredPasswordResetRequest )
user , err := svc . PerformRequiredPasswordReset ( ctx , req . Password )
if err != nil {
return performRequiredPasswordResetResponse { Err : err } , nil
}
return performRequiredPasswordResetResponse { User : user } , nil
}
func ( svc * Service ) PerformRequiredPasswordReset ( ctx context . Context , password string ) ( * fleet . User , error ) {
vc , ok := viewer . FromContext ( ctx )
if ! ok {
2023-11-02 17:32:34 +00:00
// No user in the context -- authentication issue
svc . authz . SkipAuthorization ( ctx )
return nil , authz . ForbiddenWithInternal ( "No user in the context" , nil , nil , nil )
2022-03-08 16:27:38 +00:00
}
if ! vc . CanPerformPasswordReset ( ) {
2023-08-09 19:28:04 +00:00
svc . authz . SkipAuthorization ( ctx )
2022-03-08 16:27:38 +00:00
return nil , fleet . NewPermissionError ( "cannot reset password" )
}
user := vc . User
2022-03-15 12:11:53 +00:00
if err := svc . authz . Authorize ( ctx , user , fleet . ActionChangePassword ) ; err != nil {
2022-03-08 16:27:38 +00:00
return nil , err
}
if user . SSOEnabled {
2023-08-09 19:28:04 +00:00
// should never happen because this would get caught by the
// CanPerformPasswordReset check above
err := fleet . NewPermissionError ( "password reset for single sign on user not allowed" )
return nil , ctxerr . Wrap ( ctx , err )
2022-03-08 16:27:38 +00:00
}
if ! user . IsAdminForcedPasswordReset ( ) {
2023-08-09 19:28:04 +00:00
// should never happen because this would get caught by the
// CanPerformPasswordReset check above
err := fleet . NewPermissionError ( "cannot reset password" )
return nil , ctxerr . Wrap ( ctx , err )
2022-03-08 16:27:38 +00:00
}
// prevent setting the same password
if err := user . ValidatePassword ( password ) ; err == nil {
2022-07-25 17:14:05 +00:00
return nil , fleet . NewInvalidArgumentError ( "new_password" , "Cannot reuse old password" )
2022-03-08 16:27:38 +00:00
}
2022-05-18 17:03:00 +00:00
if err := fleet . ValidatePasswordRequirements ( password ) ; err != nil {
2023-09-28 18:21:25 +00:00
return nil , fleet . NewInvalidArgumentError ( "new_password" , "Password does not meet required criteria: Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)." )
2022-05-18 17:03:00 +00:00
}
2022-03-08 16:27:38 +00:00
user . AdminForcedPasswordReset = false
err := svc . setNewPassword ( ctx , user , password )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "setting new password" )
}
// Sessions should already have been cleared when the reset was
// required
return user , nil
}
// setNewPassword is a helper for changing a user's password. It should be
// called to set the new password after proper authorization has been
// performed.
func ( svc * Service ) setNewPassword ( ctx context . Context , user * fleet . User , password string ) error {
err := user . SetPassword ( password , svc . config . Auth . SaltKeySize , svc . config . Auth . BcryptCost )
if err != nil {
return ctxerr . Wrap ( ctx , err , "setting new password" )
}
if user . SSOEnabled {
return ctxerr . New ( ctx , "set password for single sign on user not allowed" )
}
err = svc . saveUser ( ctx , user )
if err != nil {
return ctxerr . Wrap ( ctx , err , "saving changed password" )
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Reset Password
////////////////////////////////////////////////////////////////////////////////
type resetPasswordRequest struct {
PasswordResetToken string ` json:"password_reset_token" `
NewPassword string ` json:"new_password" `
}
type resetPasswordResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r resetPasswordResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func resetPasswordEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-03-08 16:27:38 +00:00
req := request . ( * resetPasswordRequest )
err := svc . ResetPassword ( ctx , req . PasswordResetToken , req . NewPassword )
return resetPasswordResponse { Err : err } , nil
}
func ( svc * Service ) ResetPassword ( ctx context . Context , token , password string ) error {
// skipauth: No viewer context available. The user is locked out of their
// account and authNZ is performed entirely by providing a valid password
// reset token.
svc . authz . SkipAuthorization ( ctx )
if token == "" {
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "token" , "Token cannot be empty field" ) )
}
if password == "" {
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "new_password" , "New password cannot be empty field" ) )
}
if err := fleet . ValidatePasswordRequirements ( password ) ; err != nil {
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "new_password" , err . Error ( ) ) )
}
reset , err := svc . ds . FindPasswordResetByToken ( ctx , token )
if err != nil {
2022-10-25 14:46:41 +00:00
return ctxerr . Wrap ( ctx , fleet . NewAuthFailedError ( err . Error ( ) ) , "find password reset request by token" )
2022-03-08 16:27:38 +00:00
}
user , err := svc . ds . UserByID ( ctx , reset . UserID )
if err != nil {
2022-10-25 14:46:41 +00:00
return ctxerr . Wrap ( ctx , fleet . NewAuthFailedError ( err . Error ( ) ) , "find user by id" )
2022-03-08 16:27:38 +00:00
}
if user . SSOEnabled {
return ctxerr . New ( ctx , "password reset for single sign on user not allowed" )
}
// prevent setting the same password
if err := user . ValidatePassword ( password ) ; err == nil {
2022-07-25 17:14:05 +00:00
return fleet . NewInvalidArgumentError ( "new_password" , "Cannot reuse old password" )
2022-03-08 16:27:38 +00:00
}
2022-05-18 17:03:00 +00:00
// password requirements are validated as part of `setNewPassword``
2022-03-08 16:27:38 +00:00
err = svc . setNewPassword ( ctx , user , password )
if err != nil {
2022-05-18 17:03:00 +00:00
return fleet . NewInvalidArgumentError ( "new_password" , err . Error ( ) )
2022-03-08 16:27:38 +00:00
}
// delete password reset tokens for user
if err := svc . ds . DeletePasswordResetRequestsForUser ( ctx , user . ID ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "delete password reset requests" )
}
// Clear sessions so that any other browsers will have to log in with
// the new password
if err := svc . ds . DestroyAllSessionsForUser ( ctx , user . ID ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "delete user sessions" )
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Forgot Password
////////////////////////////////////////////////////////////////////////////////
type forgotPasswordRequest struct {
Email string ` json:"email" `
}
type forgotPasswordResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r forgotPasswordResponse ) error ( ) error { return r . Err }
2022-03-15 19:14:42 +00:00
func ( r forgotPasswordResponse ) Status ( ) int { return http . StatusAccepted }
2022-03-08 16:27:38 +00:00
2022-12-27 14:26:59 +00:00
func forgotPasswordEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-03-08 16:27:38 +00:00
req := request . ( * forgotPasswordRequest )
// Any error returned by the service should not be returned to the
// client to prevent information disclosure (it will be logged in the
// server logs).
_ = svc . RequestPasswordReset ( ctx , req . Email )
return forgotPasswordResponse { } , nil
}
func ( svc * Service ) RequestPasswordReset ( ctx context . Context , email string ) error {
// skipauth: No viewer context available. The user is locked out of their
// account and trying to reset their password.
svc . authz . SkipAuthorization ( ctx )
// Regardless of error, sleep until the request has taken at least 1 second.
// This means that any request to this method will take ~1s and frustrate a timing attack.
defer func ( start time . Time ) {
time . Sleep ( time . Until ( start . Add ( 1 * time . Second ) ) )
} ( time . Now ( ) )
user , err := svc . ds . UserByEmail ( ctx , email )
if err != nil {
return err
}
if user . SSOEnabled {
return ctxerr . New ( ctx , "password reset for single sign on user not allowed" )
}
random , err := server . GenerateRandomText ( svc . config . App . TokenKeySize )
if err != nil {
return err
}
token := base64 . URLEncoding . EncodeToString ( [ ] byte ( random ) )
request := & fleet . PasswordResetRequest {
2022-10-25 14:46:41 +00:00
UserID : user . ID ,
Token : token ,
2022-03-08 16:27:38 +00:00
}
_ , err = svc . ds . NewPasswordResetRequest ( ctx , request )
if err != nil {
return err
}
config , err := svc . ds . AppConfig ( ctx )
if err != nil {
return err
}
2023-06-07 19:06:36 +00:00
var smtpSettings fleet . SMTPSettings
if config . SMTPSettings != nil {
smtpSettings = * config . SMTPSettings
}
2022-03-08 16:27:38 +00:00
resetEmail := fleet . Email {
2023-06-07 19:06:36 +00:00
Subject : "Reset Your Fleet Password" ,
To : [ ] string { user . Email } ,
SMTPSettings : smtpSettings ,
ServerURL : config . ServerSettings . ServerURL ,
2022-03-08 16:27:38 +00:00
Mailer : & mail . PasswordResetMailer {
BaseURL : template . URL ( config . ServerSettings . ServerURL + svc . config . Server . URLPrefix ) ,
AssetURL : getAssetURL ( ) ,
Token : token ,
} ,
}
2023-04-06 18:21:07 +00:00
err = svc . mailService . SendEmail ( resetEmail )
if err != nil {
level . Error ( svc . logger ) . Log ( "err" , err , "msg" , "failed to send password reset request email" )
}
return err
2022-03-08 16:27:38 +00:00
}
func ( svc * Service ) ListAvailableTeamsForUser ( ctx context . Context , user * fleet . User ) ( [ ] * fleet . TeamSummary , error ) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return nil , fleet . ErrMissingLicense
}