Skip to content

Commit

Permalink
User traits editor UI (#42620)
Browse files Browse the repository at this point in the history
* editor mockup

* add empty traits to available traits so user can select from dropdown

* trait name and value editor

* transform editor data to match with onSave func

* add new trait

* remove trait

* add available trait name list

* show traits header only when trait count is more than zero

* add duplicate trait rule, add requireAll wrapper to shared rules

* update editor design

* move TraitsEditor to separate file, add unit test

* fix edit story

* update /users API to handle allTraits

* cleanup

* prettier format

* move validation rule out from TraitsEditor component

* remove requiredAll rule wrapper

* rename trait vars and add api unit test:
- rename traits to traitsPreset
- add unit test for allTraits. Update existing checkAndSetDefault test to use table
- add comment to user types to differentiate traits and allTraits

* fix typo's, add js comments

* use pointer to traitsPreset so empty struct can be compared with nil

* resovle review comments:
- remove double type declaration
- move traitsToTraitsOption to be used as default value in setConfiguredTraits
- label copy update
- udpate test

* remove describe

* use const for array, bring back commented empty string checker block

* add maxWidth to DialogContent inner content to avoid flickering when scrollbar appears

* resolve review comments

* filter empty element only if length equals 1. add unit test

* handlechange: return for empty string, trim whitespace

* explicitely handle null value

* shorten empty string value check block
  • Loading branch information
flyinghermit authored Jun 14, 2024
1 parent e89674d commit 8194c0a
Show file tree
Hide file tree
Showing 10 changed files with 705 additions and 107 deletions.
82 changes: 57 additions & 25 deletions lib/web/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,16 @@ func createUser(r *http.Request, m userAPIGetter, createdBy string) (*ui.User, e

user.SetRoles(req.Roles)

updateUserTraits(req, user)
// checkAndSetDefaults makes sure either TraitsPreset
// or AllTraits field to be populated. Since empty
// AllTraits is also used to delete all user traits,
// we explicitly check if TraitsPreset is empty so
// to prevent traits deletion.
if req.TraitsPreset == nil {
user.SetTraits(req.AllTraits)
} else {
updateUserTraitsPreset(req, user)
}

user.SetCreatedBy(types.CreatedBy{
User: types.UserRef{Name: createdBy},
Expand All @@ -119,30 +128,30 @@ func createUser(r *http.Request, m userAPIGetter, createdBy string) (*ui.User, e
return ui.NewUser(created)
}

// updateUserTraits receives a saveUserRequest and updates the user traits accordingly
// It only updates the traits that have a non-nil value in saveUserRequest
// This allows the partial update of the properties
func updateUserTraits(req *saveUserRequest, user types.User) {
if req.Traits.Logins != nil {
user.SetLogins(*req.Traits.Logins)
// updateUserTraitsPreset receives a saveUserRequest and updates the user traits
// accordingly. It only updates the traits that have a non-nil value in
// saveUserRequest. This allows the partial update of the properties
func updateUserTraitsPreset(req *saveUserRequest, user types.User) {
if req.TraitsPreset.Logins != nil {
user.SetLogins(*req.TraitsPreset.Logins)
}
if req.Traits.DatabaseUsers != nil {
user.SetDatabaseUsers(*req.Traits.DatabaseUsers)
if req.TraitsPreset.DatabaseUsers != nil {
user.SetDatabaseUsers(*req.TraitsPreset.DatabaseUsers)
}
if req.Traits.DatabaseNames != nil {
user.SetDatabaseNames(*req.Traits.DatabaseNames)
if req.TraitsPreset.DatabaseNames != nil {
user.SetDatabaseNames(*req.TraitsPreset.DatabaseNames)
}
if req.Traits.KubeUsers != nil {
user.SetKubeUsers(*req.Traits.KubeUsers)
if req.TraitsPreset.KubeUsers != nil {
user.SetKubeUsers(*req.TraitsPreset.KubeUsers)
}
if req.Traits.KubeGroups != nil {
user.SetKubeGroups(*req.Traits.KubeGroups)
if req.TraitsPreset.KubeGroups != nil {
user.SetKubeGroups(*req.TraitsPreset.KubeGroups)
}
if req.Traits.WindowsLogins != nil {
user.SetWindowsLogins(*req.Traits.WindowsLogins)
if req.TraitsPreset.WindowsLogins != nil {
user.SetWindowsLogins(*req.TraitsPreset.WindowsLogins)
}
if req.Traits.AWSRoleARNs != nil {
user.SetAWSRoleARNs(*req.Traits.AWSRoleARNs)
if req.TraitsPreset.AWSRoleARNs != nil {
user.SetAWSRoleARNs(*req.TraitsPreset.AWSRoleARNs)
}
}

Expand All @@ -169,7 +178,16 @@ func updateUser(r *http.Request, m userAPIGetter) (*ui.User, error) {

user.SetRoles(req.Roles)

updateUserTraits(req, user)
// checkAndSetDefaults makes sure either TraitsPreset
// or AllTraits field to be populated. Since empty
// AllTraits is also used to delete all user traits,
// we explicitly check if TraitsPreset is empty so
// to prevent traits deletion.
if req.TraitsPreset == nil {
user.SetTraits(req.AllTraits)
} else {
updateUserTraitsPreset(req, user)
}

updated, err := m.UpdateUser(r.Context(), user)
if err != nil {
Expand Down Expand Up @@ -287,7 +305,8 @@ type userAPIGetter interface {
DeleteUser(ctx context.Context, user string) error
}

type userTraits struct {
// traitsPreset are user traits that are pre-defined in Teleport
type traitsPreset struct {
Logins *[]string `json:"logins,omitempty"`
DatabaseUsers *[]string `json:"databaseUsers,omitempty"`
DatabaseNames *[]string `json:"databaseNames,omitempty"`
Expand All @@ -303,11 +322,21 @@ type userTraits struct {
// They are optional and respect the following logic:
// - if the value is nil, we ignore it
// - if the value is an empty array we remove every element from the trait
// - otherwise, we replace the list for that trait
// - otherwise, we replace the list for that trait.
// Use TraitsPreset to selectively update traits.
// Use AllTraits to fully replace existing traits.
type saveUserRequest struct {
Name string `json:"name"`
Roles []string `json:"roles"`
Traits userTraits `json:"traits"`
// Name is username.
Name string `json:"name"`
// Roles is slice of user roles assigned to user.
Roles []string `json:"roles"`
// TraitsPreset holds traits that are pre-defined in Teleport.
// Clients may use TraitsPreset to selectively update user traits.
TraitsPreset *traitsPreset `json:"traits"`
// AllTraits may hold all the user traits, including traits key defined
// in TraitsPreset and/or new trait key values defined by Teleport admin.
// AllTraits should be used to fully replace and update user traits.
AllTraits map[string][]string `json:"allTraits"`
}

func (r *saveUserRequest) checkAndSetDefaults() error {
Expand All @@ -317,5 +346,8 @@ func (r *saveUserRequest) checkAndSetDefaults() error {
if len(r.Roles) == 0 {
return trace.BadParameter("missing roles")
}
if len(r.AllTraits) != 0 && r.TraitsPreset != nil {
return trace.BadParameter("either traits or allTraits must be provided")
}
return nil
}
Loading

0 comments on commit 8194c0a

Please sign in to comment.