From ed123fe3d7e36d2a1404fa39cf320c347eb1be2c Mon Sep 17 00:00:00 2001 From: Giuseppe Maxia Date: Fri, 5 Jul 2019 16:51:56 +0200 Subject: [PATCH] Add CRUD operations for Org user (#210) * Update AdminOrg structure to include roles and users * Define the new type User * Define auxiliary types RoleReference, RightReference * Add AdminOrg methods GetRole, FetchUserByHref * Add AdminOrg methods FetchUserByName, FetchUserById, FetchUserByNameOrId * Add AdminOrg methods CreateUser, CreateUserSimple * Add OrgUser methods GetRoleName, Update, Enable, Disable * Add OrgUser methods Delete * Add OrgUser methods ChangePassword, Unlock, TakeOwnership * Add tests for org user retrieval, creation, update, deletion * Make sure that System Admin and Org Admin can run these functions Implement recommendations from Issue #211 --- CHANGELOG.md | 1 + govcd/api.go | 80 ++++++- govcd/api_vcd_test.go | 24 +- govcd/monitor.go | 17 ++ govcd/user.go | 510 +++++++++++++++++++++++++++++++++++++++++ govcd/user_test.go | 209 +++++++++++++++++ types/v56/constants.go | 2 + types/v56/types.go | 88 +++++-- 8 files changed, 913 insertions(+), 18 deletions(-) create mode 100644 govcd/user.go create mode 100644 govcd/user_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc9fb17d..ce270caee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Added load balancer server pool [#205](https://github.com/vmware/go-vcloud-director/pull/205) * Added functions for refreshing, getting and update Org VDC [#206](https://github.com/vmware/go-vcloud-director/pull/206) * Added VDC meta data create/get/delete functions [#203](https://github.com/vmware/go-vcloud-director/pull/203) +* Added org user create/delete/update functions [#18](https://github.com/vmware/go-vcloud-director/issues/18) ## 2.2.0 (May 15, 2019) diff --git a/govcd/api.go b/govcd/api.go index ae376927e..f1873428c 100644 --- a/govcd/api.go +++ b/govcd/api.go @@ -9,11 +9,11 @@ import ( "bytes" "encoding/xml" "fmt" - "io" "io/ioutil" "net/http" "net/url" + "os" "reflect" "strings" @@ -36,6 +36,80 @@ type Client struct { MaxRetryTimeout int } +// General purpose error to be used whenever an entity is not found from a "GET" request +// Allows a simpler checking of the call result +// such as +// if err == ErrorEntityNotFound { +// // do what is needed in case of not found +// } +var ErrorEntityNotFound = fmt.Errorf("entity not found") + +// Triggers for debugging functions that show requests and responses +var debugShowRequestEnabled = os.Getenv("GOVCD_SHOW_REQ") != "" +var debugShowResponseEnabled = os.Getenv("GOVCD_SHOW_RESP") != "" + +// Enables the debugging hook to show requests as they are processed. +func enableDebugShowRequest() { + debugShowRequestEnabled = true +} + +// Disables the debugging hook to show requests as they are processed. +func disableDebugShowRequest() { + debugShowRequestEnabled = false + _ = os.Setenv("GOVCD_SHOW_REQ", "") +} + +// Enables the debugging hook to show responses as they are processed. +func enableDebugShowResponse() { + debugShowResponseEnabled = true +} + +// Disables the debugging hook to show responses as they are processed. +func disableDebugShowResponse() { + debugShowResponseEnabled = false + _ = os.Setenv("GOVCD_SHOW_RESP", "") +} + +// On-the-fly debug hook. If either debugShowRequestEnabled or the environment +// variable "GOVCD_SHOW_REQ" are enabled, this function will show the contents +// of the request as it is being processed. +func debugShowRequest(req *http.Request, payload string) { + if debugShowRequestEnabled { + header := "[\n" + for key, value := range req.Header { + header += fmt.Sprintf("\t%s => %s\n", key, value) + } + header += "]\n" + fmt.Printf("method: %s\n", req.Method) + fmt.Printf("host: %s\n", req.Host) + fmt.Printf("length: %d\n", req.ContentLength) + fmt.Printf("URL: %s\n", req.URL.String()) + fmt.Printf("header: %s\n", header) + fmt.Printf("payload: %s\n", payload) + } +} + +// On-the-fly debug hook. If either debugShowResponseEnabled or the environment +// variable "GOVCD_SHOW_RESP" are enabled, this function will show the contents +// of the response as it is being processed. +func debugShowResponse(resp *http.Response, body []byte) { + if debugShowResponseEnabled { + fmt.Printf("status: %d - %s \n", resp.StatusCode, resp.Status) + fmt.Printf("length: %d\n", resp.ContentLength) + fmt.Printf("header: %v\n", resp.Header) + fmt.Printf("body: %s\n", body) + } +} + +// Convenience function, similar to os.IsNotExist that checks whether a given error +// is a "Not found" error, such as +// if isNotFound(err) { +// // do what is needed in case of not found +// } +func IsNotFound(err error) bool { + return err != nil && err == ErrorEntityNotFound +} + // Function allow to pass complex values params which shouldn't be encoded like for queries. e.g. /query?filter=(name=foo) func (cli *Client) NewRequestWitNotEncodedParams(params map[string]string, notEncodedParams map[string]string, method string, reqUrl url.URL, body io.Reader) *http.Request { reqValues := url.Values{} @@ -86,6 +160,8 @@ func (cli *Client) NewRequestWitNotEncodedParams(params map[string]string, notEn } } util.ProcessRequestOutput(util.FuncNameCallStack(), method, reqUrl.String(), payload, req) + + debugShowRequest(req, payload) } return req @@ -119,6 +195,7 @@ func decodeBody(resp *http.Response, out interface{}) error { return err } + debugShowResponse(resp, body) // Unmarshal the XML. if err = xml.Unmarshal(body, &out); err != nil { return err @@ -238,6 +315,7 @@ func (client *Client) ExecuteRequestWithoutResponse(pathURL, requestType, conten // log response explicitly because decodeBody() was not triggered util.ProcessResponseOutput(util.FuncNameCallStack(), resp, fmt.Sprintf("%s", resp.Body)) + debugShowResponse(resp, []byte("SKIPPED RESPONSE")) err = resp.Body.Close() if err != nil { return fmt.Errorf("error closing response body: %s", err) diff --git a/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index 238dadb39..be4333ee9 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -1,4 +1,4 @@ -// +build api functional catalog vapp gateway network org query extnetwork task vm vdc system disk lbServerPool lbServiceMonitor ALL +// +build api functional catalog vapp gateway network org query extnetwork task vm vdc system disk lbServerPool lbServiceMonitor user ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -545,6 +545,28 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) } return + case "user": + if entity.Parent == "" { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] No ORG provided for user '%s'\n", entity.Name) + return + } + org, err := GetAdminOrgByName(vcd.client, entity.Parent) + if org == (AdminOrg{}) || err != nil { + vcd.infoCleanup(notFoundMsg, "org", entity.Parent) + return + } + user, err := org.FetchUserByName(entity.Name, true) + if err != nil { + vcd.infoCleanup(notFoundMsg, "user", entity.Name) + return + } + err = user.Delete(true) + if err == nil { + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + } else { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + } + return case "vdc": if entity.Parent == "" { vcd.infoCleanup("removeLeftoverEntries: [ERROR] No ORG provided for VDC '%s'\n", entity.Name) diff --git a/govcd/monitor.go b/govcd/monitor.go index 995250a9b..8cce99fa1 100644 --- a/govcd/monitor.go +++ b/govcd/monitor.go @@ -53,6 +53,15 @@ func prettyVapp(vapp types.VApp) string { return "" } +// Returns an OrgUser structure as JSON +func prettyUser(user types.User) string { + byteBuf, err := json.MarshalIndent(user, " ", " ") + if err == nil { + return fmt.Sprintf("%s\n", string(byteBuf)) + } + return "" +} + // Returns a VDC structure as JSON func prettyVdc(vdc types.Vdc) string { byteBuf, err := json.MarshalIndent(vdc, " ", " ") @@ -202,6 +211,14 @@ func LogVdc(vdc types.Vdc) { out("log", prettyVdc(vdc)) } +func ShowUser(user types.User) { + out("screen", prettyUser(user)) +} + +func LogUser(user types.User) { + out("log", prettyUser(user)) +} + func ShowDisk(disk types.Disk) { out("screen", prettyDisk(disk)) } diff --git a/govcd/user.go b/govcd/user.go new file mode 100644 index 000000000..88035c361 --- /dev/null +++ b/govcd/user.go @@ -0,0 +1,510 @@ +/* + * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/http" + "net/url" + "time" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" +) + +// Definition of an OrgUser +type OrgUser struct { + User *types.User + client *Client + AdminOrg *AdminOrg // needed to be able to update, as the list of roles is found in the Org +} + +// Simplified structure to insert or modify an organization user +type OrgUserConfiguration struct { + Name string // Mandatory + Password string // Mandatory + RoleName string // Mandatory + ProviderType string // Optional: defaults to "INTEGRATED" + IsEnabled bool // Optional: defaults to false + IsLocked bool // Only used for updates + DeployedVmQuota int // Optional: 0 means "unlimited" + StoredVmQuota int // Optional: 0 means "unlimited" + FullName string // Optional + Description string // Optional + EmailAddress string // Optional + Telephone string // Optional +} + +const ( + // Common role names and provider types are kept here to reduce hard-coded text and prevent mistakes + // Roles that are added to the organization need to be entered as free text + + OrgUserRoleOrganizationAdministrator = "Organization Administrator" + OrgUserRoleCatalogAuthor = "Catalog Author" + OrgUserRoleVappAuthor = "vApp Author" + OrgUserRoleVappUser = "vApp User" + OrgUserRoleConsoleAccessOnly = "Console Access Only" + OrgUserRoleDeferToIdentityProvider = "Defer to Identity Provider" + + // Allowed values for provider types + OrgUserProviderIntegrated = "INTEGRATED" // The user is created locally or imported from LDAP + OrgUserProviderSAML = "SAML" // The user is imported from a SAML identity provider. + OrgUserProviderOAUTH = "OAUTH" // The user is imported from an OAUTH identity provider +) + +// Used to check the validity of provider type on creation +var OrgUserProviderTypes = []string{ + OrgUserProviderIntegrated, + OrgUserProviderSAML, + OrgUserProviderOAUTH, +} + +// NewUser creates an empty user +func NewUser(cli *Client, org *AdminOrg) *OrgUser { + return &OrgUser{ + User: new(types.User), + client: cli, + AdminOrg: org, + } +} + +// FetchUserByHref returns a user by its HREF, without need for +// searching in the adminOrg user list +func (adminOrg *AdminOrg) FetchUserByHref(href string) (*OrgUser, error) { + orgUser := NewUser(adminOrg.client, adminOrg) + + _, err := adminOrg.client.ExecuteRequest(href, http.MethodGet, + types.MimeAdminUser, "error getting user: %s", nil, orgUser.User) + + if err != nil { + return nil, err + } + return orgUser, nil +} + +// FetchUserByName retrieves a user within an admin organization by name +// Returns a valid user if it exists. If it doesn't, returns nil and ErrorEntityNotFound +// If argument refresh is true, the AdminOrg will be refreshed before searching. +// This is usually done after creating, modifying, or deleting users. +// If it is false, it will search within the data already in memory (useful when +// looping through the users and we know that no changes have occurred in the meantime) +func (adminOrg *AdminOrg) FetchUserByName(name string, refresh bool) (*OrgUser, error) { + if refresh { + err := adminOrg.Refresh() + if err != nil { + return nil, err + } + } + + for _, user := range adminOrg.AdminOrg.Users.User { + if user.Name == name { + return adminOrg.FetchUserByHref(user.HREF) + } + } + return nil, ErrorEntityNotFound +} + +// FetchUserById retrieves a user within an admin organization by ID +// Returns a valid user if it exists. If it doesn't, returns nil and ErrorEntityNotFound +// If argument refresh is true, the AdminOrg will be refreshed before searching. +// This is usually done after creating, modifying, or deleting users. +// If it is false, it will search within the data already in memory (useful when +// looping through the users and we know that no changes have occurred in the meantime) +func (adminOrg *AdminOrg) FetchUserById(id string, refresh bool) (*OrgUser, error) { + if refresh { + err := adminOrg.Refresh() + if err != nil { + return nil, err + } + } + + for _, user := range adminOrg.AdminOrg.Users.User { + if user.ID == id { + return adminOrg.FetchUserByHref(user.HREF) + } + } + return nil, ErrorEntityNotFound +} + +// FetchUserByNameOrId retrieves a user within an admin organization +// by either name or ID +// Returns a valid user if it exists. If it doesn't, returns nil and ErrorEntityNotFound +// If argument refresh is true, the AdminOrg will be refreshed before searching. +// This is usually done after creating, modifying, or deleting users. +// If it is false, it will search within the data already in memory (useful when +// looping through the users and we know that no changes have occurred in the meantime) +func (adminOrg *AdminOrg) FetchUserByNameOrId(identifier string, refresh bool) (*OrgUser, error) { + // First look by ID + orgUser, err := adminOrg.FetchUserByName(identifier, true) + // if it fails, look by name + if IsNotFound(err) { + orgUser, err = adminOrg.FetchUserById(identifier, false) + } + return orgUser, err +} + +// GetRole finds a role within the organization +func (adminOrg *AdminOrg) GetRole(roleName string) (*types.Reference, error) { + + // There is no need to refresh the AdminOrg, until we implement CRUD for roles + for _, role := range adminOrg.AdminOrg.RoleReferences.RoleReference { + if role.Name == roleName { + return role, nil + } + } + + return nil, ErrorEntityNotFound +} + +// Retrieves a user within the boundaries of MaxRetryTimeout +func retrieveUserWithTimeout(adminOrg *AdminOrg, userName string) (*OrgUser, error) { + + // Attempting to retrieve the user + delayPerAttempt := 200 * time.Millisecond + maxOperationTimeout := time.Duration(adminOrg.client.MaxRetryTimeout) * time.Second + + // We make sure that the timeout is never less than 2 seconds + if maxOperationTimeout < 2*time.Second { + maxOperationTimeout = 2 * time.Second + } + + // If maxRetryTimeout is set to a higher limit, we lower it to match the + // expectations for this operation. If the user is not created within 10 seconds, + // there is no need to wait for more. Usually, the operation lasts between 200ms and 900ms + if maxOperationTimeout > 10*time.Second { + maxOperationTimeout = 10 * time.Second + } + + startTime := time.Now() + elapsed := time.Since(startTime) + var newUser *OrgUser + var err error + for elapsed < maxOperationTimeout { + newUser, err = adminOrg.FetchUserByName(userName, true) + if err == nil { + break + } + time.Sleep(delayPerAttempt) + elapsed = time.Since(startTime) + } + + elapsed = time.Since(startTime) + + // If the user was not retrieved within the allocated time, we inform the user about the failure + // and the time it occurred to get to this point, so that they may try with a longer time + if err != nil { + return nil, fmt.Errorf("failure to retrieve a new user after %s : %s", elapsed, err) + } + + return newUser, nil +} + +// CreateUser creates an OrgUser from a full configuration structure +// The timeOut variable is the maximum time we wait for the user to be ready +// (This operation does not return a task) +// This function returns as soon as the user has been created, which could be as +// little as 200ms or as much as Client.MaxRetryTimeout +// Mandatory fields are: Name, Role, Password. +// https://code.vmware.com/apis/442/vcloud-director#/doc/doc/operations/POST-CreateUser.html +func (adminOrg *AdminOrg) CreateUser(userConfiguration *types.User) (*OrgUser, error) { + err := validateUserForCreation(userConfiguration) + if err != nil { + return nil, err + } + + userCreateHREF, err := url.ParseRequestURI(adminOrg.AdminOrg.HREF) + if err != nil { + return nil, fmt.Errorf("error parsing admin org url: %s", err) + } + userCreateHREF.Path += "/users" + + user := NewUser(adminOrg.client, adminOrg) + + _, err = adminOrg.client.ExecuteRequest(userCreateHREF.String(), http.MethodPost, + types.MimeAdminUser, "error creating user: %s", userConfiguration, user.User) + if err != nil { + return nil, err + } + + // If there is a valid task, we try to follow through + // A valid task exists if the Task object in the user structure + // is not nil and contains at least a task + if user.User.Tasks != nil && len(user.User.Tasks.Task) > 0 { + task := NewTask(adminOrg.client) + task.Task = user.User.Tasks.Task[0] + err = task.WaitTaskCompletion() + + if err != nil { + return nil, err + } + } + + return retrieveUserWithTimeout(adminOrg, userConfiguration.Name) +} + +// CreateUserSimple creates an org user from a simplified structure +func (adminOrg *AdminOrg) CreateUserSimple(userData OrgUserConfiguration) (*OrgUser, error) { + + if userData.Name == "" { + return nil, fmt.Errorf("name is mandatory to create a user") + } + if userData.Password == "" { + return nil, fmt.Errorf("password is mandatory to create a user") + } + if userData.RoleName == "" { + return nil, fmt.Errorf("role is mandatory to create a user") + } + role, err := adminOrg.GetRole(userData.RoleName) + if err != nil { + return nil, fmt.Errorf("error finding a role named %s", userData.RoleName) + } + + var userConfiguration = types.User{ + Xmlns: types.XMLNamespaceVCloud, + Type: types.MimeAdminUser, + ProviderType: userData.ProviderType, + Name: userData.Name, + IsEnabled: userData.IsEnabled, + Password: userData.Password, + DeployedVmQuota: userData.DeployedVmQuota, + StoredVmQuota: userData.StoredVmQuota, + FullName: userData.FullName, + EmailAddress: userData.EmailAddress, + Description: userData.Description, + Role: &types.Reference{HREF: role.HREF}, + } + + // ShowUser(userConfiguration) + return adminOrg.CreateUser(&userConfiguration) +} + +// GetRoleName retrieves the name of the role currently assigned to the user +func (user *OrgUser) GetRoleName() string { + if user.User.Role == nil { + return "" + } + return user.User.Role.Name +} + +// Delete removes the user, returning an error if the call fails. +// if requested, it will attempt to take ownership before the removal. +// API Documentation: https://code.vmware.com/apis/442/vcloud-director#/doc/doc/operations/DELETE-User.html +// Note: in the GUI we need to disable the user before deleting. +// There is no such constraint with the API. +// +// Expected behaviour: +// with takeOwnership = true, all entities owned by the user being deleted will be transferred to the caller. +// with takeOwnership = false, if the user own catalogs, networks, or running VMs/vApps, the call will fail. +// If the user owns only powered-off VMs/vApps, the call will succeeds and the +// VMs/vApps will be removed. +func (user *OrgUser) Delete(takeOwnership bool) error { + util.Logger.Printf("[TRACE] Deleting user: %#v (take ownership: %v)", user.User.Name, takeOwnership) + + if takeOwnership { + err := user.TakeOwnership() + if err != nil { + return err + } + } + + userHREF, err := url.ParseRequestURI(user.User.Href) + if err != nil { + return fmt.Errorf("error getting HREF for user %s : %v", user.User.Name, err) + } + util.Logger.Printf("[TRACE] Url for deleting user : %#v and name: %s", userHREF, user.User.Name) + + return user.client.ExecuteRequestWithoutResponse(userHREF.String(), http.MethodDelete, + types.MimeAdminUser, "error deleting user : %s", nil) +} + +// UpdateSimple updates the user, using ALL the fields in userData structure +// returning an error if the call fails. +// Careful: DeployedVmQuota and StoredVmQuota use a `0` value to mean "unlimited" +func (user *OrgUser) UpdateSimple(userData OrgUserConfiguration) error { + util.Logger.Printf("[TRACE] Updating user: %#v", user.User.Name) + + if userData.Name != "" { + user.User.Name = userData.Name + } + if userData.ProviderType != "" { + user.User.ProviderType = userData.ProviderType + } + if userData.Description != "" { + user.User.Description = userData.Description + } + if userData.FullName != "" { + user.User.FullName = userData.FullName + } + if userData.EmailAddress != "" { + user.User.EmailAddress = userData.EmailAddress + } + if userData.Telephone != "" { + user.User.Telephone = userData.Telephone + } + if userData.Password != "" { + user.User.Password = userData.Password + } + user.User.StoredVmQuota = userData.StoredVmQuota + user.User.DeployedVmQuota = userData.DeployedVmQuota + user.User.IsEnabled = userData.IsEnabled + user.User.IsLocked = userData.IsLocked + + if userData.RoleName != "" && user.User.Role != nil && user.User.Role.Name != userData.RoleName { + newRole, err := user.AdminOrg.GetRole(userData.RoleName) + if err != nil { + return err + } + user.User.Role = newRole + } + return user.Update() +} + +// Update updates the user, using its own configuration data +// returning an error if the call fails. +// API Documentation: https://code.vmware.com/apis/442/vcloud-director#/doc/doc/operations/PUT-User.html +func (user *OrgUser) Update() error { + util.Logger.Printf("[TRACE] Updating user: %s", user.User.Name) + + // Makes sure that GroupReferences is either properly filled or nil, + // because otherwise vCD will complain that the payload is not well formatted when + // the configuration contains a non-empty password. + if user.User.GroupReferences != nil { + if len(user.User.GroupReferences.GroupReference) == 0 { + user.User.GroupReferences = nil + } + } + + userHREF, err := url.ParseRequestURI(user.User.Href) + if err != nil { + return fmt.Errorf("error getting HREF for user %s : %v", user.User.Name, err) + } + util.Logger.Printf("[TRACE] Url for updating user : %#v and name: %s", userHREF, user.User.Name) + + _, err = user.client.ExecuteRequest(userHREF.String(), http.MethodPut, + types.MimeAdminUser, "error updating user : %s", user.User, nil) + return err +} + +// Disable disables a user, if it is enabled. Fails otherwise. +func (user *OrgUser) Disable() error { + util.Logger.Printf("[TRACE] Disabling user: %s", user.User.Name) + + if !user.User.IsEnabled { + return fmt.Errorf("user %s is already disabled", user.User.Name) + } + user.User.IsEnabled = false + + return user.Update() +} + +// ChangePassword changes user's password +// Constraints: the password must be non-empty, with a minimum of 6 characters +func (user *OrgUser) ChangePassword(newPass string) error { + util.Logger.Printf("[TRACE] Changing user's password user: %s", user.User.Name) + + user.User.Password = newPass + + return user.Update() +} + +// Enable enables a user if it was disabled. Fails otherwise. +func (user *OrgUser) Enable() error { + util.Logger.Printf("[TRACE] Enabling user: %s", user.User.Name) + + if user.User.IsEnabled { + return fmt.Errorf("user %s is already enabled", user.User.Name) + } + user.User.IsEnabled = true + + return user.Update() +} + +// Unlock unlocks a user that was locked out by the system. +// Note that there is no procedure to LOCK a user: it is locked by the system when it exceeds the number of +// unauthorized access attempts +func (user *OrgUser) Unlock() error { + util.Logger.Printf("[TRACE] Unlocking user: %s", user.User.Name) + + if !user.User.IsLocked { + return fmt.Errorf("user %s is not locked", user.User.Name) + } + user.User.IsLocked = false + + return user.Update() +} + +// ChangeRole changes a user's role +// Fails is we try to set the same role as the current one. +// Also fails if the provided role name is not found. +func (user *OrgUser) ChangeRole(roleName string) error { + util.Logger.Printf("[TRACE] Changing user's role: %s", user.User.Name) + + if roleName == "" { + return fmt.Errorf("role name cannot be empty") + } + + if user.User.Role != nil && user.User.Role.Name == roleName { + return fmt.Errorf("new role is the same as current role") + } + + newRole, err := user.AdminOrg.GetRole(roleName) + if err != nil { + return err + } + user.User.Role = newRole + + return user.Update() +} + +// TakeOwnership takes ownership of the user's objects. +// Ownership is transferred to the caller. +// This is a call to make before deleting. Calling user.DeleteTakeOwnership() will +// run TakeOwnership before the actual user removal. +// API Documentation: https://code.vmware.com/apis/442/vcloud-director#/doc/doc/operations/POST-TakeOwnership.html +func (user *OrgUser) TakeOwnership() error { + util.Logger.Printf("[TRACE] Taking ownership from user: %s", user.User.Name) + + userHREF, err := url.ParseRequestURI(user.User.Href + "/action/takeOwnership") + if err != nil { + return fmt.Errorf("error getting HREF for user %s : %v", user.User.Name, err) + } + util.Logger.Printf("[TRACE] Url for taking ownership from user : %#v and name: %s", userHREF, user.User.Name) + + return user.client.ExecuteRequestWithoutResponse(userHREF.String(), http.MethodPost, + types.MimeAdminUser, "error taking ownership from user : %s", nil) +} + +// validateUserForInput makes sure that the minimum data +// needed for creating an org user has been included in the configuration +func validateUserForCreation(user *types.User) error { + var missingField = "missing field %s" + if user.Xmlns == "" { + user.Xmlns = types.XMLNamespaceVCloud + } + if user.Type == "" { + user.Type = types.MimeAdminUser + } + if user.Name == "" { + return fmt.Errorf(missingField, "Name") + } + if user.Password == "" { + return fmt.Errorf(missingField, "Password") + } + if user.ProviderType != "" { + validProviderType := false + for _, pt := range OrgUserProviderTypes { + if user.ProviderType == pt { + validProviderType = true + } + } + if !validProviderType { + return fmt.Errorf("'%s' is not a valid provider type", user.ProviderType) + } + } + if user.Role.HREF == "" { + return fmt.Errorf(missingField, "Role.HREF") + } + return nil +} diff --git a/govcd/user_test.go b/govcd/user_test.go new file mode 100644 index 000000000..cd9553a87 --- /dev/null +++ b/govcd/user_test.go @@ -0,0 +1,209 @@ +// +build user functional ALL + +/* + * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + + . "gopkg.in/check.v1" +) + +/* + TODO: Add test for takeOwnership. + +This is more complicated than it looks, because it requires the following: + +Either: +1a. Separate connection with a newly created user [requires test enhancement] +2a. Creation of entities with new user (vapp/catalog/catalog items) + +OR +1b. create entities with the user that runs the tests +2b. change ownership of such entities to the new user [requires new feature] + +3. Check that the user is the intended one (this is currently doable, because we can + inspect the Owner structure of the entity being created) + +4. Try deleting the user that owns the new entities +5. get an error +6. take ownership from the user +7. delete the user and see the operation succeed +8. Check that the new entities belong to the current user +9. Delete the new entities +*/ + +// Checks that the default roles are available from the organization +func (vcd *TestVCD) Test_GetRole(check *C) { + adminOrg, err := GetAdminOrgByName(vcd.client, vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, Not(Equals), AdminOrg{}) + Roles := []string{ + OrgUserRoleOrganizationAdministrator, + OrgUserRoleVappUser, + OrgUserRoleCatalogAuthor, + OrgUserRoleConsoleAccessOnly, + } + for _, roleName := range Roles { + // fmt.Printf("# retrieving role %s\n", roleName) + roleReference, err := adminOrg.GetRole(roleName) + check.Assert(err, IsNil) + check.Assert(roleReference, NotNil) + check.Assert(roleReference.Name, Equals, roleName) + check.Assert(roleReference.HREF, Not(Equals), "") + } +} + +// Checks that we can retrieve a user by name or ID +func (vcd *TestVCD) Test_GetUserByNameOrId(check *C) { + adminOrg, err := GetAdminOrgByName(vcd.client, vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, Not(Equals), AdminOrg{}) + + // We get the list of users from the organization + var userRefs []types.Reference + for _, userRef := range adminOrg.AdminOrg.Users.User { + userRefs = append(userRefs, *userRef) + } + + // Using the list above, we first try to get each user by name + for _, userRef := range userRefs { + user, err := adminOrg.FetchUserByName(userRef.Name, false) + check.Assert(err, IsNil) + check.Assert(user, NotNil) + check.Assert(user.User.Name, Equals, userRef.Name) + + // Then we try to get the same user by ID + user, err = adminOrg.FetchUserById(userRef.ID, false) + check.Assert(err, IsNil) + check.Assert(user, NotNil) + check.Assert(user.User.Name, Equals, userRef.Name) + + // Then we try to get the same user by Name or ID combined + user, err = adminOrg.FetchUserByNameOrId(userRef.ID, true) + check.Assert(err, IsNil) + check.Assert(user, NotNil) + check.Assert(user.User.Name, Equals, userRef.Name) + + user, err = adminOrg.FetchUserByNameOrId(userRef.Name, false) + check.Assert(err, IsNil) + check.Assert(user, NotNil) + check.Assert(user.User.Name, Equals, userRef.Name) + } +} + +// This test creates 5 users using 5 available roles, +// Then updates each of them with a different role, +// Furthermore, disables, and then enables the users again +// and finally deletes all of them +func (vcd *TestVCD) Test_UserCRUD(check *C) { + adminOrg, err := GetAdminOrgByName(vcd.client, vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, Not(Equals), AdminOrg{}) + + type userTestData struct { + name string // name of the user. Note: only lowercase letters allowed + roleName string // the role this user is created with + secondRole string // The role to which we change using Update() + } + userData := []userTestData{ + { + name: "test_user_admin", + roleName: OrgUserRoleOrganizationAdministrator, + secondRole: OrgUserRoleVappAuthor, + }, + { + name: "test_user_vapp_author", + roleName: OrgUserRoleVappAuthor, + secondRole: OrgUserRoleVappUser, + }, + { + name: "test_user_vapp_user", + roleName: OrgUserRoleVappUser, + secondRole: OrgUserRoleConsoleAccessOnly, + }, + { + name: "test_user_console_access", + roleName: OrgUserRoleConsoleAccessOnly, + secondRole: OrgUserRoleCatalogAuthor, + }, + { + name: "test_user_catalog_author", + roleName: OrgUserRoleCatalogAuthor, + secondRole: OrgUserRoleOrganizationAdministrator, + }, + } + + for _, ud := range userData { + fmt.Printf("# Creating user %s with role %s\n", ud.name, ud.roleName) + // Uncomment the following lines to see creation request and response + // enableDebugShowRequest() + // enableDebugShowResponse() + user, err := adminOrg.CreateUserSimple(OrgUserConfiguration{ + Name: ud.name, + Password: "user_pass", + RoleName: ud.roleName, + ProviderType: OrgUserProviderIntegrated, + DeployedVmQuota: 10, + StoredVmQuota: 10, + FullName: strings.ReplaceAll(ud.name, "_", " "), + Description: "user " + strings.ReplaceAll(ud.name, "_", " "), + IsEnabled: true, + }) + // disableDebugShowRequest() + // disableDebugShowResponse() + check.Assert(err, IsNil) + + AddToCleanupList(ud.name, "user", user.AdminOrg.AdminOrg.Name, check.TestName()) + check.Assert(user.User, NotNil) + check.Assert(user.User.Name, Equals, ud.name) + check.Assert(user.GetRoleName(), Equals, ud.roleName) + check.Assert(user.User.IsEnabled, Equals, true) + + err = user.Disable() + check.Assert(err, IsNil) + check.Assert(user.User.IsEnabled, Equals, false) + + fmt.Printf("# Updating user %s with role %s\n", ud.name, ud.secondRole) + err = user.ChangeRole(ud.secondRole) + check.Assert(err, IsNil) + check.Assert(user.GetRoleName(), Equals, ud.secondRole) + + err = user.Enable() + check.Assert(err, IsNil) + check.Assert(user.User.IsEnabled, Equals, true) + err = user.ChangePassword("new_pass") + check.Assert(err, IsNil) + } + + var enableMap = map[bool]string{ + true: "enabled", + false: "disabled", + } + for _, ud := range userData { + user, err := adminOrg.FetchUserByNameOrId(ud.name, true) + check.Assert(err, IsNil) + + fmt.Printf("# deleting user %s (%s - %s)\n", ud.name, user.GetRoleName(), enableMap[user.User.IsEnabled]) + // uncomment the following two lines to see the deletion request and response + // enableDebugShowRequest() + // enableDebugShowResponse() + err = user.Delete(true) + // disableDebugShowRequest() + // disableDebugShowResponse() + check.Assert(err, IsNil) + user, err = adminOrg.FetchUserByNameOrId(user.User.ID, true) + check.Assert(err, NotNil) + // Tests both the error directly and the function IsNotFound + check.Assert(err, Equals, ErrorEntityNotFound) + check.Assert(IsNotFound(err), Equals, true) + // Expect a null pointer when user is not found + check.Assert(user, IsNil) + } +} diff --git a/types/v56/constants.go b/types/v56/constants.go index 4a46d7f4e..d5c399936 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -97,6 +97,8 @@ const ( MimeExtensionNetwork = "application/vnd.vmware.admin.extension.network+xml" // Mime for an external network MimeExternalNetwork = "application/vnd.vmware.admin.vmwexternalnet+xml" + // Mime of an Org User + MimeAdminUser = "application/vnd.vmware.admin.user+xml" ) const ( diff --git a/types/v56/types.go b/types/v56/types.go index 84f716463..554baccb2 100644 --- a/types/v56/types.go +++ b/types/v56/types.go @@ -596,28 +596,47 @@ type Org struct { Tasks *TasksInProgress `xml:"Tasks,omitempty"` } +// List of the users within the organization +type OrgUserList struct { + User []*Reference `xml:"UserReference,omitempty"` +} + +// List of available roles in the organization +type OrgRoleType struct { + RoleReference []*Reference `xml:"RoleReference,omitempty"` +} + +// List of available rights in the organization +type RightsType struct { + Links LinkList `xml:"Link,omitempty"` + RightReference []*Reference `xml:"RightReference,omitempty"` +} + // AdminOrg represents the admin view of a vCloud Director organization. // Type: AdminOrgType // Namespace: http://www.vmware.com/vcloud/v1.5 // Description: Represents the admin view of a vCloud Director organization. // Since: 0.9 type AdminOrg struct { - XMLName xml.Name `xml:"AdminOrg"` - Xmlns string `xml:"xmlns,attr"` - HREF string `xml:"href,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` - ID string `xml:"id,attr,omitempty"` - OperationKey string `xml:"operationKey,attr,omitempty"` - Name string `xml:"name,attr"` - Description string `xml:"Description,omitempty"` - FullName string `xml:"FullName"` - IsEnabled bool `xml:"IsEnabled,omitempty"` - Link LinkList `xml:"Link,omitempty"` - Tasks *TasksInProgress `xml:"Tasks,omitempty"` - OrgSettings *OrgSettings `xml:"Settings,omitempty"` - Vdcs *VDCList `xml:"Vdcs,omitempty"` - Networks *NetworksList `xml:"Networks,omitempty"` - Catalogs *CatalogsList `xml:"Catalogs,omitempty"` + XMLName xml.Name `xml:"AdminOrg"` + Xmlns string `xml:"xmlns,attr"` + HREF string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + ID string `xml:"id,attr,omitempty"` + OperationKey string `xml:"operationKey,attr,omitempty"` + Name string `xml:"name,attr"` + Description string `xml:"Description,omitempty"` + FullName string `xml:"FullName"` + IsEnabled bool `xml:"IsEnabled,omitempty"` + Link LinkList `xml:"Link,omitempty"` + Tasks *TasksInProgress `xml:"Tasks,omitempty"` + Users *OrgUserList `xml:"Users,omitempty"` + Catalogs *CatalogsList `xml:"Catalogs,omitempty"` + OrgSettings *OrgSettings `xml:"Settings,omitempty"` + Vdcs *VDCList `xml:"Vdcs,omitempty"` + Networks *NetworksList `xml:"Networks,omitempty"` + RightReferences *OrgRoleType `xml:"RightReferences,omitempty"` + RoleReferences *OrgRoleType `xml:"RoleReferences,omitempty"` } // OrgSettingsType represents the settings for a vCloud Director organization. @@ -2506,3 +2525,40 @@ type VMWNetworkPool struct { Description string `xml:"netmask,omitempty"` Tasks *TasksInProgress `xml:"Tasks,omitempty"` } + +type GroupReference struct { + GroupReference []*Reference `xml:"GroupReference,omitempty"` +} + +// Represents an org user +// Reference: vCloud API 27.0 - UserType +// https://code.vmware.com/apis/442/vcloud-director#/doc/doc/types/UserType.html +// Note that the order of fields is important. If this structure needs to change, +// the field order must be preserved. +type User struct { + XMLName xml.Name `xml:"User"` + Xmlns string `xml:"xmlns,attr"` + Href string `xml:"href,attr"` + Type string `xml:"type,attr"` + ID string `xml:"id,attr"` + OperationKey string `xml:"operationKey,attr"` + Name string `xml:"name,attr"` + Links LinkList `xml:"Link,omitempty"` + Description string `xml:"Description,omitempty"` + FullName string `xml:"FullName,omitempty"` + EmailAddress string `xml:"EmailAddress,omitempty"` + Telephone string `xml:"Telephone,omitempty"` + IsEnabled bool `xml:"IsEnabled,omitempty"` + IsLocked bool `xml:"IsLocked,omitempty"` + IM string `xml:"IM,omitempty"` + NameInSource string `xml:"NameInSource,omitempty"` + IsExternal bool `xml:"IsExternal,omitempty"` + ProviderType string `xml:"ProviderType,omitempty"` + IsGroupRole bool `xml:"IsGroupRole,omitempty"` + StoredVmQuota int `xml:"StoredVmQuota,omitempty"` + DeployedVmQuota int `xml:"DeployedVmQuota,omitempty"` + Role *Reference `xml:"Role,omitempty"` + GroupReferences *GroupReference `xml:"GroupReferences,omitempty"` + Password string `xml:"Password,omitempty"` + Tasks *TasksInProgress `xml:"Tasks"` +}