Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
.vscode
*.test
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

:warning: **This client is a work in progress, expect breaking changes between releases** :warning:

## Compatibility

| go-dvls version | DVLS version |
|-----------------|----------------|
| 0.16.0+ | 2026.x |
| 0.15.0 | 2024.x, 2025.x |

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

## Usage
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.15.0
0.16.0
5 changes: 3 additions & 2 deletions authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ func NewClient(appKey string, appSecret string, baseUri string) (Client, error)
client.common.client = &client

client.Entries = &Entries{
Credential: (*EntryCredentialService)(&client.common),
Certificate: (*EntryCertificateService)(&client.common),
Website: (*EntryWebsiteService)(&client.common),
Credential: (*EntryCredentialService)(&client.common),
Folder: (*EntryFolderService)(&client.common),
Host: (*EntryHostService)(&client.common),
Website: (*EntryWebsiteService)(&client.common),
}
client.Vaults = (*Vaults)(&client.common)

Expand Down
4 changes: 2 additions & 2 deletions dvls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (

var (
testClient Client
testVaultId string
testVaultId string // Used by legacy tests (certificate, host, website)
)

func TestMain(m *testing.M) {
testVaultId = os.Getenv("TEST_VAULT_ID")
testVaultId = os.Getenv("TEST_VAULT_ID") // Optional, only for legacy tests

err := setupTestClient()
if err != nil {
Expand Down
15 changes: 0 additions & 15 deletions dvlstypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,21 +226,6 @@ const (
ServerConnectionSubTypeAppleSafari ServerConnectionSubType = "Safari"
)

type VaultVisibility int

const (
VaultVisibilityDefault VaultVisibility = 0
VaultVisibilityPublic VaultVisibility = 2
VaultVisibilityPrivate VaultVisibility = 3
)

type VaultSecurityLevel int

const (
VaultSecurityLevelStandard VaultSecurityLevel = 0
VaultSecurityLevelHigh VaultSecurityLevel = 1
)

type EntryCertificateDataMode int

const (
Expand Down
103 changes: 102 additions & 1 deletion entries.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package dvls

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
)

Expand All @@ -13,11 +17,28 @@ const (
entryPublicEndpoint string = "/api/v1/vault/{vaultId}/entry/{id}"
)

// ErrUnsupportedEntryType is returned when an entry type/subtype is not supported by this client.
type ErrUnsupportedEntryType struct {
Type string
SubType string
}

func (e ErrUnsupportedEntryType) Error() string {
return fmt.Sprintf("unsupported entry type/subtype: %s/%s", e.Type, e.SubType)
}

// IsUnsupportedEntryType returns true if the error is an ErrUnsupportedEntryType.
func IsUnsupportedEntryType(err error) bool {
var unsupportedErr ErrUnsupportedEntryType
return errors.As(err, &unsupportedErr)
}

type Entries struct {
Certificate *EntryCertificateService
Host *EntryHostService
Credential *EntryCredentialService
Website *EntryWebsiteService
Folder *EntryFolderService
}

type Entry struct {
Expand Down Expand Up @@ -55,6 +76,22 @@ var entryFactories = map[string]func() EntryData{
"Credential/ConnectionString": func() EntryData { return &EntryCredentialConnectionStringData{} },
"Credential/Default": func() EntryData { return &EntryCredentialDefaultData{} },
"Credential/PrivateKey": func() EntryData { return &EntryCredentialPrivateKeyData{} },
"Folder/Company": func() EntryData { return &EntryFolderData{} },
"Folder/Credentials": func() EntryData { return &EntryFolderData{} },
"Folder/Customer": func() EntryData { return &EntryFolderData{} },
"Folder/Database": func() EntryData { return &EntryFolderData{} },
"Folder/Device": func() EntryData { return &EntryFolderData{} },
"Folder/Domain": func() EntryData { return &EntryFolderData{} },
"Folder/Folder": func() EntryData { return &EntryFolderData{} },
"Folder/Identity": func() EntryData { return &EntryFolderData{} },
"Folder/MacroScriptTools": func() EntryData { return &EntryFolderData{} },
"Folder/Printer": func() EntryData { return &EntryFolderData{} },
"Folder/Server": func() EntryData { return &EntryFolderData{} },
"Folder/Site": func() EntryData { return &EntryFolderData{} },
"Folder/SmartFolder": func() EntryData { return &EntryFolderData{} },
"Folder/Software": func() EntryData { return &EntryFolderData{} },
"Folder/Team": func() EntryData { return &EntryFolderData{} },
"Folder/Workstation": func() EntryData { return &EntryFolderData{} },
}

func (e *Entry) UnmarshalJSON(data []byte) error {
Expand All @@ -73,7 +110,7 @@ func (e *Entry) UnmarshalJSON(data []byte) error {
key := fmt.Sprintf("%s/%s", raw.Type, raw.SubType)
factory, ok := entryFactories[key]
if !ok {
return fmt.Errorf("unsupported entry type/subtype: %s", key)
return ErrUnsupportedEntryType{Type: raw.Type, SubType: raw.SubType}
}

dataStruct := factory()
Expand Down Expand Up @@ -112,3 +149,67 @@ func entryPublicBaseEndpointReplacer(vaultId string) string {
replacer := strings.NewReplacer("{vaultId}", vaultId)
return replacer.Replace(entryBasePublicEndpoint)
}

// entryListRawResponse represents the raw paginated response from the entry list endpoint.
type entryListRawResponse struct {
Data []json.RawMessage `json:"data"`
}

// getEntriesOptions contains optional filters for listing entries.
type getEntriesOptions struct {
Name string
Path string
}

// getEntries returns a list of entries from a vault with optional filters.
// Entries with unsupported types are skipped.
func (c *Client) getEntries(ctx context.Context, vaultId string, opts getEntriesOptions) ([]Entry, error) {
if vaultId == "" {
return nil, fmt.Errorf("vaultId is required")
}

baseEndpoint := entryPublicBaseEndpointReplacer(vaultId)
reqUrl, err := url.JoinPath(c.baseUri, baseEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to build entry url: %w", err)
}

parsedUrl, err := url.Parse(reqUrl)
if err != nil {
return nil, fmt.Errorf("failed to parse entry url: %w", err)
}

q := parsedUrl.Query()
if opts.Name != "" {
q.Set("name", opts.Name)
}
if opts.Path != "" {
q.Set("path", opts.Path)
}
parsedUrl.RawQuery = q.Encode()

resp, err := c.RequestWithContext(ctx, parsedUrl.String(), http.MethodGet, nil)
if err != nil {
return nil, fmt.Errorf("error while fetching entries: %w", err)
}

var rawResp entryListRawResponse
if err := json.Unmarshal(resp.Response, &rawResp); err != nil {
return nil, fmt.Errorf("failed to unmarshal entry list response: %w", err)
}

var entries []Entry
for _, raw := range rawResp.Data {
var entry Entry
if err := json.Unmarshal(raw, &entry); err != nil {
if IsUnsupportedEntryType(err) {
continue
}
return nil, fmt.Errorf("failed to unmarshal entry: %w", err)
}
entry.VaultId = vaultId
entries = append(entries, entry)
}

return entries, nil
}
4 changes: 4 additions & 0 deletions entry_certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ var (
)

func Test_EntryCertificate(t *testing.T) {
if testVaultId == "" {
t.Skip("Skipping legacy API test: TEST_VAULT_ID not set")
}

testCertificateFilePath = os.Getenv("TEST_CERTIFICATE_FILE_PATH")
testCertificateEntryId = os.Getenv("TEST_CERTIFICATE_ENTRY_ID")
testCertificateEntry.Id = testCertificateEntryId
Expand Down
27 changes: 27 additions & 0 deletions entry_credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,30 @@ func (c *EntryCredentialService) DeleteByIdWithContext(ctx context.Context, vaul

return nil
}

// GetEntries returns a list of credential entries from a vault with optional name and path filters.
func (c *EntryCredentialService) GetEntries(vaultId, name, path string) ([]Entry, error) {
return c.GetEntriesWithContext(context.Background(), vaultId, name, path)
}

// GetEntriesWithContext returns a list of credential entries from a vault with optional name and path filters.
// The provided context can be used to cancel the request.
func (c *EntryCredentialService) GetEntriesWithContext(ctx context.Context, vaultId, name, path string) ([]Entry, error) {
entries, err := c.client.getEntries(ctx, vaultId, getEntriesOptions{
Name: name,
Path: path,
})
if err != nil {
return nil, err
}

// Filter only Credential type entries
var credentials []Entry
for _, entry := range entries {
if entry.GetType() == EntryCredentialType {
credentials = append(credentials, entry)
}
}

return credentials, nil
}
Loading
Loading