Skip to content

Commit

Permalink
OpenAPI based User management (#745)
Browse files Browse the repository at this point in the history
  • Loading branch information
Didainius authored Jan 30, 2025
1 parent 6d3f570 commit d1690a1
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changes/v3.0.0/745-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Added types `OpenApiUser` and `types.OpenApiUser` for managing Org Users using OpenAPI
`VCDClient.CreateUser`, `VCDClient.GetAllUsers`, `VCDClient.GetUserByName`,
`VCDClient.GetUserById`, `OpenApiUser.Update`, `OpenApiUser.Delete` [GH-745]
1 change: 1 addition & 0 deletions govcd/openapi_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var endpointMinApiVersions = map[string]string{
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles: "31.0",
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles: "31.0",
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles + types.OpenApiEndpointRights: "31.0",
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointUsers: "40.0",
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAuditTrail: "33.0",
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableTier0Routers: "32.0",
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableDvpgs: "36.0",
Expand Down
111 changes: 111 additions & 0 deletions govcd/openapi_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package govcd

import (
"fmt"
"net/url"

"github.com/vmware/go-vcloud-director/v3/types/v56"
)

const labelOpenApiUser = "User"

type OpenApiUser struct {
User *types.OpenApiUser
vcdClient *VCDClient
TenantContext *TenantContext
}

// wrap is a hidden helper that facilitates the usage of a generic CRUD function
//
//lint:ignore U1000 this method is used in generic functions, but annoys staticcheck
func (g OpenApiUser) wrap(inner *types.OpenApiUser) *OpenApiUser {
g.User = inner
return &g
}

// CreateUser creates a new User with a given configuration
func (vcdClient *VCDClient) CreateUser(config *types.OpenApiUser, ctx *TenantContext) (*OpenApiUser, error) {
c := crudConfig{
entityLabel: labelOpenApiUser,
endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointUsers,
additionalHeader: getTenantContextHeader(ctx),
requiresTm: true,
}
outerType := OpenApiUser{vcdClient: vcdClient, TenantContext: ctx}
return createOuterEntity(&vcdClient.Client, outerType, c, config)
}

// GetAllUsers retrieves all Users with an optional filter
func (vcdClient *VCDClient) GetAllUsers(queryParameters url.Values, ctx *TenantContext) ([]*OpenApiUser, error) {
c := crudConfig{
entityLabel: labelOpenApiUser,
endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointUsers,
queryParameters: queryParameters,
additionalHeader: getTenantContextHeader(ctx),
requiresTm: true,
}

outerType := OpenApiUser{vcdClient: vcdClient, TenantContext: ctx}
return getAllOuterEntities(&vcdClient.Client, outerType, c)
}

// GetUserByName retrieves User by Name
func (vcdClient *VCDClient) GetUserByName(username string, ctx *TenantContext) (*OpenApiUser, error) {
if username == "" {
return nil, fmt.Errorf("%s lookup requires username", labelOpenApiUser)
}

queryParams := url.Values{}
queryParams.Add("filter", "username=="+username)

filteredEntities, err := vcdClient.GetAllUsers(queryParams, ctx)
if err != nil {
return nil, err
}

singleEntity, err := oneOrError("username", username, filteredEntities)
if err != nil {
return nil, err
}

return vcdClient.GetUserById(singleEntity.User.ID, ctx)
}

// GetUserById retrieves User by ID
func (vcdClient *VCDClient) GetUserById(id string, ctx *TenantContext) (*OpenApiUser, error) {
c := crudConfig{
entityLabel: labelOpenApiUser,
endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointUsers,
endpointParams: []string{id},
additionalHeader: getTenantContextHeader(ctx),
requiresTm: true,
}

outerType := OpenApiUser{vcdClient: vcdClient, TenantContext: ctx}
return getOuterEntity(&vcdClient.Client, outerType, c)
}

// Update User with a given config
func (o *OpenApiUser) Update(cfg *types.OpenApiUser) (*OpenApiUser, error) {
c := crudConfig{
entityLabel: labelOpenApiUser,
endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointUsers,
endpointParams: []string{o.User.ID},
additionalHeader: getTenantContextHeader(o.TenantContext),
requiresTm: true,
}
outerType := OpenApiUser{vcdClient: o.vcdClient}
return updateOuterEntity(&o.vcdClient.Client, outerType, c, cfg)
}

// Delete User
func (o *OpenApiUser) Delete() error {
c := crudConfig{
entityLabel: labelOpenApiUser,
endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointUsers,
endpointParams: []string{o.User.ID},
additionalHeader: getTenantContextHeader(o.TenantContext),
requiresTm: true,
}
return deleteEntityById(&o.vcdClient.Client, c)
}
93 changes: 93 additions & 0 deletions govcd/openapi_user_tm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//go:build tm || functional || ALL

/*
* Copyright 2025 VMware, Inc. All rights reserved. Licensed under the Apache v2 License.
*/

package govcd

import (
"github.com/vmware/go-vcloud-director/v3/types/v56"
. "gopkg.in/check.v1"
)

func (vcd *TestVCD) Test_TmOpenApiUserLocal(check *C) {
skipNonTm(vcd, check)
sysadminOnly(vcd, check)

org, orgCleanup := createOrg(vcd, check, false)
defer orgCleanup()

// TODO: TM: Change to vcdClient.GetTmOrgById(orgId), requires implementing Role support for that type
adminOrg, err := vcd.client.GetAdminOrgById(org.TmOrg.ID)
check.Assert(err, IsNil)
check.Assert(adminOrg, NotNil)

roleOrgAdmin, err := adminOrg.GetRoleByName("Organization Administrator")
check.Assert(err, IsNil)
check.Assert(roleOrgAdmin, NotNil)

roleOrgUser, err := adminOrg.GetRoleByName("Organization User")
check.Assert(err, IsNil)
check.Assert(roleOrgUser, NotNil)

userConfig := &types.OpenApiUser{
Username: "test-user",
Password: "CHANGE-ME",
RoleEntityRefs: []types.OpenApiReference{{ID: roleOrgAdmin.Role.ID}},
ProviderType: "LOCAL",
OrgEntityRef: &types.OpenApiReference{ID: org.TmOrg.ID, Name: org.TmOrg.Name},
}

tenantContext := &TenantContext{
OrgId: org.TmOrg.ID,
OrgName: org.TmOrg.Name,
}

// Create
createdUser, err := vcd.client.CreateUser(userConfig, tenantContext)
check.Assert(err, IsNil)
check.Assert(createdUser, NotNil)
check.Assert(createdUser.User, NotNil)

// GetAll
allUsers, err := vcd.client.GetAllUsers(nil, tenantContext)
check.Assert(err, IsNil)
check.Assert(len(allUsers), Equals, 1)
check.Assert(allUsers[0].User.ID, Equals, createdUser.User.ID)

// Get by Username
byUsername, err := vcd.client.GetUserByName(userConfig.Username, tenantContext)
check.Assert(err, IsNil)
check.Assert(byUsername.User.ID, Equals, createdUser.User.ID)

// Get by ID
byId, err := vcd.client.GetUserById(createdUser.User.ID, tenantContext)
check.Assert(err, IsNil)
check.Assert(byId.User.ID, Equals, createdUser.User.ID)

// Update
updateConfig := &types.OpenApiUser{
ID: byId.User.ID,
Username: "test-user-updated",
Password: "CHANGE-ME-UPDATED",
RoleEntityRefs: []types.OpenApiReference{{ID: roleOrgUser.Role.ID}},
ProviderType: "LOCAL",
NameInSource: userConfig.Username, // previous username must be provided
OrgEntityRef: &types.OpenApiReference{ID: org.TmOrg.ID, Name: org.TmOrg.Name},
}

updatedUser, err := byId.Update(updateConfig)
check.Assert(err, IsNil)
check.Assert(updatedUser, NotNil)
check.Assert(updatedUser.User.ID, Equals, createdUser.User.ID)

// Delete
err = updatedUser.Delete()
check.Assert(err, IsNil)

// Get by ID and fail
byIdNotFound, err := vcd.client.GetUserById(updatedUser.User.ID, tenantContext)
check.Assert(ContainsNotFound(err), Equals, true)
check.Assert(byIdNotFound, IsNil)
}
1 change: 1 addition & 0 deletions types/v56/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ const (
OpenApiEndpointTmEdgeClustersSync = "edgeClusters/sync"
OpenApiEndpointTmRegionalNetworkingSettings = "regionalNetworkingSettings/"
OpenApiEndpointTmRegionalNetworkingSettingsVpcProfile = "regionalNetworkingSettings/%s/defaultVpcConnectivityProfile"
OpenApiEndpointUsers = "users/"
)

// Header keys to run operations in tenant context
Expand Down
45 changes: 45 additions & 0 deletions types/v56/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -871,3 +871,48 @@ type NsxtManagerOpenApi struct {
// Status of NSX-T Manager
Status string `json:"status,omitempty"`
}

// OpenApiUser defines structure for User management using OpenAPI based endpoint
type OpenApiUser struct {
ID string `json:"id,omitempty"`
// Username for the user
Username string `json:"username"`
// Password for the user. Must be null for external users
Password string `json:"password,omitempty"`
// Enabled state of the user. Defaults to true
Enabled *bool `json:"enabled,omitempty"`
// Description of the user
Description string `json:"description,omitempty"`
// A read-only list of all of a user's roles, both directly assigned and inherited from the user's groups
EffectiveRoleEntityRefs []*OpenApiReference `json:"effectiveRoleEntityRefs,omitempty"`
// A user's email address. Based on org email preferences, notifications can be sent to the user via email
Email string `json:"email,omitempty"`
// Family name of the user (e.g. last name in most Western languages)
FamilyName string `json:"familyName,omitempty"`
// Full name (display name) of the user
FullName string `json:"fullName,omitempty"`
// The directly assigned role(s) of the user
RoleEntityRefs []OpenApiReference `json:"roleEntityRefs,omitempty"`
// Given name of the user (e.g. first name in most Western languages)
GivenName string `json:"givenName,omitempty"`
// Determines if this user can inherit roles from groups. Defaults to false
InheritGroupRoles bool `json:"inheritGroupRoles,omitempty"`
// Determines if this user's role is inherited from a group. Defaults to false
IsGroupRole bool `json:"isGroupRole,omitempty"`
// True if the user account has been locked due to too many invalid login attempts. An
// administrator can unlock a locked user account by setting this flag to false. A user may not
// be explicitly locked. Instead, disable the user, if user's access must be revoked
// temporarily
Locked *bool `json:"locked,omitempty"`
// Name of the user in its source
NameInSource string `json:"nameInSource,omitempty"`
// orgEntityRefOptional
OrgEntityRef *OpenApiReference `json:"orgEntityRef,omitempty"`
// Phone number of the user.
Phone string `json:"phone,omitempty"`
// Provider type of the user. It must be one of: LOCAL, LDAP, SAML, OAUTH
ProviderType string `json:"providerType,omitempty"`
// True if the user account has been stranded, meaning it is unable to be accessed due to its
// original identity source being removed
Stranded bool `json:"stranded,omitempty"`
}

0 comments on commit d1690a1

Please sign in to comment.