Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

Heavily based on the information found on the [Devolutions.Server](https://github.com/Devolutions/devolutions-server/tree/main/Powershell%20Module/Devolutions.Server) powershell module.

Users, applications and user groups (`client.Users`, `client.UserGroups`) expose the principal IDs used as assignees in role assignments and as roles in entry permissions.

## Usage
- Run go get `go get github.com/Devolutions/go-dvls`
- Add the import `import "github.com/Devolutions/go-dvls"`
Expand Down
8 changes: 6 additions & 2 deletions authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ type Client struct {

common service

Entries *Entries
Vaults *Vaults
Entries *Entries
Vaults *Vaults
Users *Users
UserGroups *UserGroups
}

type service struct {
Expand Down Expand Up @@ -97,6 +99,8 @@ func (c *Client) initServices() {
Permissions: (*EntryPermissionsService)(&c.common),
}
c.Vaults = (*Vaults)(&c.common)
c.Users = (*Users)(&c.common)
c.UserGroups = (*UserGroups)(&c.common)
}

func (c *Client) login() error {
Expand Down
13 changes: 13 additions & 0 deletions dvlstypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ const (
UserAuthenticationAzureAD
UserAuthenticationApplication
UserAuthenticationOkta
UserAuthenticationPingOne
UserAuthenticationContractor
)

//go:generate stringer -type=UserGroupType -trimprefix UserGroupType
type UserGroupType uint8

const (
UserGroupTypeActiveDirectory UserGroupType = iota
UserGroupTypeCustom
UserGroupTypeOffice365
UserGroupTypeOkta
UserGroupTypePingOne
)

//go:generate stringer -type=ServerLoginResult -trimprefix ServerLogin
Expand Down
6 changes: 6 additions & 0 deletions mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import (

const testVaultID = "test-vault-id"

const (
testRoleID = "11111111-2222-3333-4444-555555555555"
testAssigneeID = "66666666-7777-8888-9999-000000000000"
testUserID = "12121212-3434-5656-7878-909090909090"
)

const testEntryID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"

func newTestClient(t *testing.T, mux *http.ServeMux) *Client {
Expand Down
53 changes: 53 additions & 0 deletions user_groups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package dvls

import (
"context"
"fmt"
)

const userGroupListEndpoint = "/api/security/roles/basic"

var ErrUserGroupNotFound = fmt.Errorf("user group not found")
var ErrMultipleUserGroupsFound = fmt.Errorf("multiple user groups found")

type UserGroups service

// UserGroup represents a DVLS user group.
type UserGroup struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
IsAdministrator bool `json:"isAdministrator"`
Type UserGroupType `json:"roleType"`
}

// List returns all user groups.
func (c *UserGroups) List() ([]UserGroup, error) {
return c.ListWithContext(context.Background())
}

// ListWithContext returns all user groups.
// The provided context can be used to cancel the request.
func (c *UserGroups) ListWithContext(ctx context.Context) ([]UserGroup, error) {
return fetchDataList[UserGroup](ctx, c.client, userGroupListEndpoint, "user groups")
}

// GetByName returns a single user group based on name.
// Returns ErrUserGroupNotFound if no user group is found.
// Returns ErrMultipleUserGroupsFound if more than one user group matches the name.
func (c *UserGroups) GetByName(name string) (UserGroup, error) {
return c.GetByNameWithContext(context.Background(), name)
}

// GetByNameWithContext returns a single user group based on name.
// Returns ErrUserGroupNotFound if no user group is found.
// Returns ErrMultipleUserGroupsFound if more than one user group matches the name.
// The provided context can be used to cancel the request.
func (c *UserGroups) GetByNameWithContext(ctx context.Context, name string) (UserGroup, error) {
groups, err := c.ListWithContext(ctx)
if err != nil {
return UserGroup{}, err
}

return singleByName(groups, name, func(g UserGroup) string { return g.Name }, ErrUserGroupNotFound, ErrMultipleUserGroupsFound)
}
72 changes: 72 additions & 0 deletions user_groups_unit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package dvls

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestUserGroupsList(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/security/roles/basic", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"result":1,"data":[
{"id":"` + testRoleID + `","name":"Ops","description":"Operations","isAdministrator":true,"roleType":1},
{"id":"` + testAssigneeID + `","name":"Domain Admins","description":"","roleType":0}
]}`))
})

client := newTestClient(t, mux)

groups, err := client.UserGroups.List()
require.NoError(t, err)
require.Len(t, groups, 2)
assert.Equal(t, testRoleID, groups[0].Id)
assert.Equal(t, "Ops", groups[0].Name)
assert.True(t, groups[0].IsAdministrator)
assert.Equal(t, UserGroupTypeCustom, groups[0].Type)
assert.Equal(t, UserGroupTypeActiveDirectory, groups[1].Type)
}

func TestUserGroupsList_ResultError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/security/roles/basic", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"result":2,"message":"AccessDenied"}`))
})

client := newTestClient(t, mux)

_, err := client.UserGroups.List()
require.Error(t, err)
assert.Contains(t, err.Error(), "AccessDenied")
}

func TestUserGroupsGetByName(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/security/roles/basic", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"result":1,"data":[
{"id":"` + testRoleID + `","name":"Ops"},
{"id":"` + testAssigneeID + `","name":"Dup"},
{"id":"` + testUserID + `","name":"Dup"}
]}`))
})

client := newTestClient(t, mux)

group, err := client.UserGroups.GetByName("Ops")
require.NoError(t, err)
assert.Equal(t, testRoleID, group.Id)

_, err = client.UserGroups.GetByName("Dup")
assert.ErrorIs(t, err, ErrMultipleUserGroupsFound)

_, err = client.UserGroups.GetByName("nope")
assert.ErrorIs(t, err, ErrUserGroupNotFound)
}
11 changes: 7 additions & 4 deletions userauthenticationtype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions usergrouptype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 94 additions & 0 deletions users.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package dvls

import (
"context"
"fmt"
)

const (
userListEndpoint = "/api/security/users/list"
applicationListEndpoint = "/api/security/application/users/list"
)

var ErrUserNotFound = fmt.Errorf("user not found")
var ErrMultipleUsersFound = fmt.Errorf("multiple users found")

type Users service

// User represents a DVLS user or application account. Application accounts
// have AuthenticationType UserAuthenticationApplication and their Name is the
// application key.
type User struct {
Id string `json:"id"`
Name string `json:"name"`
FullName string `json:"fullName"`
Email string `json:"email"`
AuthenticationType UserAuthenticationType `json:"authenticationType"`
IsAdministrator bool `json:"isAdministrator"`
IsEnabled bool `json:"isEnabled"`
UserGroups []string `json:"userGroups"`
}

func userName(u User) string { return u.Name }

// List returns all users, excluding application accounts.
func (c *Users) List() ([]User, error) {
return c.ListWithContext(context.Background())
}

// ListWithContext returns all users, excluding application accounts.
// The provided context can be used to cancel the request.
func (c *Users) ListWithContext(ctx context.Context) ([]User, error) {
return fetchDataList[User](ctx, c.client, userListEndpoint, "users")
}

// ListApplications returns all application accounts.
func (c *Users) ListApplications() ([]User, error) {
return c.ListApplicationsWithContext(context.Background())
}

// ListApplicationsWithContext returns all application accounts.
// The provided context can be used to cancel the request.
func (c *Users) ListApplicationsWithContext(ctx context.Context) ([]User, error) {
return fetchDataList[User](ctx, c.client, applicationListEndpoint, "applications")
}

// GetByName returns a single user based on its login name.
// Returns ErrUserNotFound if no user is found.
// Returns ErrMultipleUsersFound if more than one user matches the name.
func (c *Users) GetByName(name string) (User, error) {
return c.GetByNameWithContext(context.Background(), name)
}

// GetByNameWithContext returns a single user based on its login name.
// Returns ErrUserNotFound if no user is found.
// Returns ErrMultipleUsersFound if more than one user matches the name.
// The provided context can be used to cancel the request.
func (c *Users) GetByNameWithContext(ctx context.Context, name string) (User, error) {
users, err := c.ListWithContext(ctx)
if err != nil {
return User{}, err
}

return singleByName(users, name, userName, ErrUserNotFound, ErrMultipleUsersFound)
}

// GetApplicationByName returns a single application account based on its application key.
// Returns ErrUserNotFound if no application is found.
// Returns ErrMultipleUsersFound if more than one application matches the name.
func (c *Users) GetApplicationByName(name string) (User, error) {
return c.GetApplicationByNameWithContext(context.Background(), name)
}

// GetApplicationByNameWithContext returns a single application account based on its application key.
// Returns ErrUserNotFound if no application is found.
// Returns ErrMultipleUsersFound if more than one application matches the name.
// The provided context can be used to cancel the request.
func (c *Users) GetApplicationByNameWithContext(ctx context.Context, name string) (User, error) {
apps, err := c.ListApplicationsWithContext(ctx)
if err != nil {
return User{}, err
}

return singleByName(apps, name, userName, ErrUserNotFound, ErrMultipleUsersFound)
}
Loading