From 96a7fbe38153e5eea8a51c958285ff960aec37e7 Mon Sep 17 00:00:00 2001 From: Daniel Lorych Date: Wed, 26 Apr 2023 22:32:38 +0200 Subject: [PATCH 1/2] Add basic CRUD operations for User V2 API --- access/services/v2/users.go | 145 ++++++++++++++++++++++++++++++++++++ tests/accessusers_test.go | 83 +++++++++++++++++++++ tests/utils_test.go | 2 + 3 files changed, 230 insertions(+) create mode 100644 access/services/v2/users.go create mode 100644 tests/accessusers_test.go diff --git a/access/services/v2/users.go b/access/services/v2/users.go new file mode 100644 index 000000000..b6a552149 --- /dev/null +++ b/access/services/v2/users.go @@ -0,0 +1,145 @@ +package v2 + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/jfrog/jfrog-client-go/auth" + "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/httputils" +) + +const usersApi = "api/v2/users" + +type UserParams struct { + CommonUserParams +} + +type UserResponse struct { + CommonUserParams + Status string `json:"status,omitempty"` +} + +func NewUserParams() UserParams { + return UserParams{} +} + +type CommonUserParams struct { + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + Password string `json:"password,omitempty"` + Admin *bool `json:"admin,omitempty"` + ProfileUpdatable *bool `json:"profile_updatable,omitempty"` + DisableUIAccess *bool `json:"disable_ui_access,omitempty"` + InternalPasswordDisabled *bool `json:"internal_password_disabled,omitempty"` + Realm string `json:"realm,omitempty"` + Groups *[]string `json:"groups,omitempty"` +} + +type UserService struct { + client *jfroghttpclient.JfrogHttpClient + ServiceDetails auth.ServiceDetails +} + +func NewUserService(client *jfroghttpclient.JfrogHttpClient) *UserService { + return &UserService{client: client} +} + +func (us *UserService) getBaseUrl() string { + return fmt.Sprintf("%s%s", us.ServiceDetails.GetUrl(), usersApi) +} + +func (us *UserService) GetAll() ([]UserResponse, error) { + httpDetails := us.ServiceDetails.CreateHttpClientDetails() + url := us.getBaseUrl() + resp, body, _, err := us.client.SendGet(url, true, &httpDetails) + if err != nil { + return nil, err + } + + if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil { + return nil, err + } + var users []UserResponse + err = json.Unmarshal(body, &users) + return users, errorutils.CheckError(err) +} + +func (us *UserService) Get(username string) (u *UserResponse, err error) { + httpDetails := us.ServiceDetails.CreateHttpClientDetails() + url := fmt.Sprintf("%s/%s", us.getBaseUrl(), username) + resp, body, _, err := us.client.SendGet(url, true, &httpDetails) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusNotFound { + return nil, nil + } + if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil { + return nil, err + } + var user UserResponse + err = json.Unmarshal(body, &user) + return &user, errorutils.CheckError(err) +} + +func (us *UserService) Create(params UserParams) error { + user, err := us.Get(params.Username) + if err != nil { + return err + } + if user != nil { + return errorutils.CheckErrorf("user '%s' already exists", user.Username) + } + content, httpDetails, err := us.createOrUpdateRequest(params) + if err != nil { + return err + } + resp, body, err := us.client.SendPost(us.getBaseUrl(), content, &httpDetails) + if err != nil { + return err + } + return errorutils.CheckResponseStatusWithBody(resp, body, http.StatusCreated) +} + +func (us *UserService) Update(params UserParams) error { + content, httpDetails, err := us.createOrUpdateRequest(params) + if err != nil { + return err + } + url := fmt.Sprintf("%s/%s", us.getBaseUrl(), params.Username) + resp, body, err := us.client.SendPatch(url, content, &httpDetails) + if err != nil { + return err + } + return errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK) +} + +func (us *UserService) createOrUpdateRequest(user UserParams) (requestContent []byte, httpDetails httputils.HttpClientDetails, err error) { + httpDetails = us.ServiceDetails.CreateHttpClientDetails() + requestContent, err = json.Marshal(user) + if errorutils.CheckError(err) != nil { + return + } + httpDetails.Headers = map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + } + return +} + +func (us *UserService) Delete(username string) error { + httpDetails := us.ServiceDetails.CreateHttpClientDetails() + url := fmt.Sprintf("%s/%s", us.getBaseUrl(), username) + resp, body, err := us.client.SendDelete(url, nil, &httpDetails) + if err != nil { + return err + } + if resp == nil { + return errorutils.CheckErrorf("no response provided (including status code)") + } + return errorutils.CheckResponseStatusWithBody(resp, body, http.StatusNoContent) +} diff --git a/tests/accessusers_test.go b/tests/accessusers_test.go new file mode 100644 index 000000000..7d8793ef0 --- /dev/null +++ b/tests/accessusers_test.go @@ -0,0 +1,83 @@ +package tests + +import ( + "fmt" + "reflect" + "testing" + + services "github.com/jfrog/jfrog-client-go/access/services/v2" + "github.com/stretchr/testify/assert" +) + +func TestAccessUsers(t *testing.T) { + initAccessTest(t) + t.Run("create-update-delete", testAccessUserCreateUpdateDelete) +} + +func getTestAccessUserParams(nameSuffix string) services.UserParams { + userDetails := services.CommonUserParams{ + Username: fmt.Sprintf("test%s%s", nameSuffix, timestampStr), + Email: "john.doe@example.com", + Password: "Password1*", + Admin: &trueValue, + Realm: "internal", + ProfileUpdatable: &trueValue, + DisableUIAccess: &falseValue, + InternalPasswordDisabled: &falseValue, + } + return services.UserParams{ + CommonUserParams: userDetails, + } +} + +func testAccessUserCreateUpdateDelete(t *testing.T) { + userParams := getTestAccessUserParams("testProject") + assert.NoError(t, testAccessUserService.Create(userParams)) + defer deleteAccessUserAndAssert(t, userParams.Username) + userParams.Email = "joe.blow@example.org" + userParams.Admin = &trueValue + userParams.ProfileUpdatable = &falseValue + userParams.DisableUIAccess = &trueValue + userParams.InternalPasswordDisabled = &trueValue + + assert.NoError(t, testAccessUserService.Update(userParams)) + + updatedUser, err := testAccessUserService.Get(userParams.Username) + + assert.NoError(t, err) + assert.NotNil(t, updatedUser, "Expected user %s but got nil", userParams.Username) + assert.Equal(t, "active", updatedUser.Status, "Expected user in status 'active' but got status '%s'", updatedUser.Status) + if !reflect.DeepEqual(userParams, updatedUser.CommonUserParams) { + t.Error("Unexpected user details built. Expected: `", userParams, "` Got `", updatedUser.CommonUserParams, "`") + } +} + +func deleteAccessUserAndAssert(t *testing.T, username string) { + assert.NoError(t, testAccessUserService.Delete(username)) +} + +func TestGetAllAccessUsers(t *testing.T) { + initAccessTest(t) + preUsers, err := testAccessUserService.GetAll() + assert.NoError(t, err) + + noOfUsersBeforeTest := len(preUsers) + user1 := getTestAccessUserParams("u1") + user2 := getTestAccessUserParams("u2") + user3 := getTestAccessUserParams("u3") + user4 := getTestAccessUserParams("u4") + + assert.NoError(t, testAccessUserService.Create(user1)) + assert.NoError(t, testAccessUserService.Create(user2)) + assert.NoError(t, testAccessUserService.Create(user3)) + assert.NoError(t, testAccessUserService.Create(user4)) + + projects, err := testAccessUserService.GetAll() + assert.NoError(t, err, "Failed to Unmarshal") + assert.Equal(t, noOfUsersBeforeTest+4, len(projects)) + + deleteUserAndAssert(t, user1.Username) + deleteUserAndAssert(t, user2.Username) + deleteUserAndAssert(t, user3.Username) + deleteUserAndAssert(t, user4.Username) +} diff --git a/tests/utils_test.go b/tests/utils_test.go index ce87597e7..fbdc767af 100644 --- a/tests/utils_test.go +++ b/tests/utils_test.go @@ -22,6 +22,7 @@ import ( accessAuth "github.com/jfrog/jfrog-client-go/access/auth" accessServices "github.com/jfrog/jfrog-client-go/access/services" + accessServicesV2 "github.com/jfrog/jfrog-client-go/access/services/v2" pipelinesAuth "github.com/jfrog/jfrog-client-go/pipelines/auth" pipelinesServices "github.com/jfrog/jfrog-client-go/pipelines/services" @@ -123,6 +124,7 @@ var ( testsAccessProjectService *accessServices.ProjectService testsAccessInviteService *accessServices.InviteService testsAccessTokensService *accessServices.TokenService + testAccessUserService *accessServicesV2.UserService timestamp = time.Now().Unix() timestampStr = strconv.FormatInt(timestamp, 10) From b6c344c2f5b9c7b5fb483bb534ff30e79ec2cf65 Mon Sep 17 00:00:00 2001 From: Daniel Lorych Date: Wed, 26 Apr 2023 22:34:25 +0200 Subject: [PATCH 2/2] Add basic CRUD operations for Group V2 API --- access/services/v2/groups.go | 146 +++++++++++++++++++++++++++++++++++ tests/accessgroups_test.go | 92 ++++++++++++++++++++++ tests/utils_test.go | 1 + 3 files changed, 239 insertions(+) create mode 100644 access/services/v2/groups.go create mode 100644 tests/accessgroups_test.go diff --git a/access/services/v2/groups.go b/access/services/v2/groups.go new file mode 100644 index 000000000..bfb88965c --- /dev/null +++ b/access/services/v2/groups.go @@ -0,0 +1,146 @@ +package v2 + +import ( + "encoding/json" + "fmt" + "github.com/jfrog/jfrog-client-go/auth" + "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/httputils" + "net/http" +) + +const groupsApi = "api/v2/groups" + +type GroupParams struct { + GroupDetails +} + +type GroupDetails struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + AutoJoin *bool `json:"auto_join,omitempty"` + AdminPrivileges *bool `json:"admin_privileges,omitempty"` + Realm string `json:"realm,omitempty"` + RealmAttributes string `json:"realm_attributes,omitempty"` + ExternalId string `json:"external_id,omitempty"` + Members []string `json:"members,omitempty"` +} + +type GroupListItem struct { + GroupName string `json:"group_name"` + Uri string `json:"uri"` +} +type GroupList struct { + Cursor string `json:"cursor,omitempty"` + Groups []GroupListItem `json:"groups"` +} + +func NewGroupParams() GroupParams { + return GroupParams{} +} + +type GroupService struct { + client *jfroghttpclient.JfrogHttpClient + ServiceDetails auth.ServiceDetails +} + +func NewGroupService(client *jfroghttpclient.JfrogHttpClient) *GroupService { + return &GroupService{client: client} +} + +func (gs *GroupService) getBaseUrl() string { + return fmt.Sprintf("%s%s", gs.ServiceDetails.GetUrl(), groupsApi) +} +func (gs *GroupService) GetAll() ([]GroupListItem, error) { + httpDetails := gs.ServiceDetails.CreateHttpClientDetails() + url := gs.getBaseUrl() + resp, body, _, err := gs.client.SendGet(url, true, &httpDetails) + if err != nil { + return nil, err + } + + if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil { + return nil, err + } + var groupList GroupList + err = json.Unmarshal(body, &groupList) + return groupList.Groups, errorutils.CheckError(err) +} + +func (gs *GroupService) Get(name string) (u *GroupDetails, err error) { + httpDetails := gs.ServiceDetails.CreateHttpClientDetails() + url := fmt.Sprintf("%s/%s", gs.getBaseUrl(), name) + resp, body, _, err := gs.client.SendGet(url, true, &httpDetails) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusNotFound { + return nil, nil + } + if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil { + return nil, err + } + var group GroupDetails + err = json.Unmarshal(body, &group) + return &group, errorutils.CheckError(err) +} + +func (gs *GroupService) Create(params GroupParams) error { + group, err := gs.Get(params.Name) + if err != nil { + return err + } + if group != nil { + return errorutils.CheckErrorf("group '%s' already exists", group.Name) + } + content, httpDetails, err := gs.createOrUpdateRequest(params) + if err != nil { + return err + } + resp, body, err := gs.client.SendPost(gs.getBaseUrl(), content, &httpDetails) + if err != nil { + return err + } + return errorutils.CheckResponseStatusWithBody(resp, body, http.StatusCreated) +} + +func (gs *GroupService) Update(params GroupParams) error { + content, httpDetails, err := gs.createOrUpdateRequest(params) + if err != nil { + return err + } + url := fmt.Sprintf("%s/%s", gs.getBaseUrl(), params.Name) + resp, body, err := gs.client.SendPatch(url, content, &httpDetails) + if err != nil { + return err + } + return errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK) +} + +func (gs *GroupService) createOrUpdateRequest(group GroupParams) (requestContent []byte, httpDetails httputils.HttpClientDetails, err error) { + httpDetails = gs.ServiceDetails.CreateHttpClientDetails() + requestContent, err = json.Marshal(group) + if errorutils.CheckError(err) != nil { + return + } + httpDetails.Headers = map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + } + return +} + +func (gs *GroupService) Delete(name string) error { + httpDetails := gs.ServiceDetails.CreateHttpClientDetails() + url := fmt.Sprintf("%s/%s", gs.getBaseUrl(), name) + resp, body, err := gs.client.SendDelete(url, nil, &httpDetails) + if err != nil { + return err + } + if resp == nil { + return errorutils.CheckErrorf("no response provided (including status code)") + } + return errorutils.CheckResponseStatusWithBody(resp, body, http.StatusNoContent) +} diff --git a/tests/accessgroups_test.go b/tests/accessgroups_test.go new file mode 100644 index 000000000..ac79c97fb --- /dev/null +++ b/tests/accessgroups_test.go @@ -0,0 +1,92 @@ +package tests + +import ( + "fmt" + "testing" + + services "github.com/jfrog/jfrog-client-go/access/services/v2" + "github.com/stretchr/testify/assert" +) + +func TestAccessGroups(t *testing.T) { + initAccessTest(t) + t.Run("create", testCreateAccessGroup) + t.Run("update", testUpdateAccessGroup) + t.Run("delete", testDeleteAccessGroup) +} + +func getTestAccessGroupParams() services.GroupParams { + group := services.GroupDetails{ + Name: fmt.Sprintf("test-%s", getRunId()), + Description: "hello", + AutoJoin: &falseValue, + AdminPrivileges: &trueValue, + Realm: "internal", + RealmAttributes: "", + ExternalId: "", + } + return services.GroupParams{GroupDetails: group} +} + +func testCreateAccessGroup(t *testing.T) { + groupParams := getTestAccessGroupParams() + err := testAccessGroupService.Create(groupParams) + defer deleteAccessGroupAndAssert(t, groupParams.GroupDetails.Name) + assert.NoError(t, err) + + createdGroup, err := testAccessGroupService.Get(groupParams.Name) + assert.NoError(t, err) + assert.NotNil(t, createdGroup) + assert.Equal(t, groupParams.GroupDetails, *createdGroup) + + allGroups, err := testAccessGroupService.GetAll() + assert.NoError(t, err) + assert.NotNil(t, allGroups) + + var groupNames []string + for _, v := range allGroups { + groupNames = append(groupNames, v.GroupName) + } + assert.Contains(t, groupNames, groupParams.GroupDetails.Name) + +} + +func testUpdateAccessGroup(t *testing.T) { + groupParams := getTestAccessGroupParams() + err := testAccessGroupService.Create(groupParams) + defer deleteAccessGroupAndAssert(t, groupParams.Name) + assert.NoError(t, err) + groupParams.Description = "Changed description" + groupParams.AutoJoin = &trueValue + groupParams.AdminPrivileges = &falseValue + err = testAccessGroupService.Update(groupParams) + assert.NoError(t, err) + group, err := testAccessGroupService.Get(groupParams.Name) + assert.NoError(t, err) + assert.Equal(t, groupParams.GroupDetails, *group) +} + +func testDeleteAccessGroup(t *testing.T) { + groupParams := getTestAccessGroupParams() + assert.NoError(t, testAccessGroupService.Create(groupParams)) + + deleteAccessGroupAndAssert(t, groupParams.Name) + + group, err := testAccessGroupService.Get(groupParams.Name) + assert.NoError(t, err) + assert.Nil(t, group) + + allGroups, err := testAccessGroupService.GetAll() + assert.NoError(t, err) + assert.NotNil(t, allGroups) + + var allGroupNames []string + for _, v := range allGroups { + allGroupNames = append(allGroupNames, v.GroupName) + } + assert.NotContains(t, allGroupNames, groupParams.Name) +} + +func deleteAccessGroupAndAssert(t *testing.T, groupName string) { + assert.NoError(t, testAccessGroupService.Delete(groupName)) +} diff --git a/tests/utils_test.go b/tests/utils_test.go index fbdc767af..b00c7a2a0 100644 --- a/tests/utils_test.go +++ b/tests/utils_test.go @@ -125,6 +125,7 @@ var ( testsAccessInviteService *accessServices.InviteService testsAccessTokensService *accessServices.TokenService testAccessUserService *accessServicesV2.UserService + testAccessGroupService *accessServicesV2.GroupService timestamp = time.Now().Unix() timestampStr = strconv.FormatInt(timestamp, 10)