Skip to content

Commit 0d1201d

Browse files
committed
feat: DEVOPS-3353 add entry Website
1 parent f03a362 commit 0d1201d

File tree

8 files changed

+334
-6
lines changed

8 files changed

+334
-6
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
- name: Setup Go environment
4040
uses: actions/setup-go@v5
4141
with:
42-
go-version: '1.20'
42+
go-version: '1.23'
4343
check-latest: true
4444

4545
- name: Download CA certificate

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Heavily based on the information found on the [Devolutions.Server](https://githu
1111
## Usage
1212
- Run go get `go get github.com/Devolutions/go-dvls`
1313
- Add the import `import "github.com/Devolutions/go-dvls"`
14-
- Setup the client (we recommend using an [Application ID](https://helpserver.devolutions.net/webinterface_applications.html?q=application+id))
14+
- Setup the client (we recommend using an [Application ID](https://docs.devolutions.net/server/web-interface/administration/security-management/applications/))
1515
``` go
1616
package main
1717

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.8.0
1+
0.9.0

authentication.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func NewClient(username string, password string, baseUri string) (Client, error)
115115
client.Entries = &Entries{
116116
UserCredential: (*EntryUserCredentialService)(&client.common),
117117
Certificate: (*EntryCertificateService)(&client.common),
118+
Website: (*EntryWebsiteService)(&client.common),
118119
}
119120
client.Vaults = (*Vaults)(&client.common)
120121

dvlstypes.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,14 @@ const (
216216
type ServerConnectionSubType string
217217

218218
const (
219-
ServerConnectionSubTypeDefault ServerConnectionSubType = "Default"
220-
ServerConnectionSubTypeCertificate ServerConnectionSubType = "Certificate"
219+
ServerConnectionSubTypeDefault ServerConnectionSubType = "Default"
220+
ServerConnectionSubTypeCertificate ServerConnectionSubType = "Certificate"
221+
ServerConnectionSubTypeMicrosoftEdge ServerConnectionSubType = "Edge"
222+
ServerConnectionSubTypeFirefox ServerConnectionSubType = "FireFox"
223+
ServerConnectionSubTypeGoogleChrome ServerConnectionSubType = "GoogleChrome"
224+
ServerConnectionSubTypeInternetExplorer ServerConnectionSubType = "IE"
225+
ServerConnectionSubTypeOpera ServerConnectionSubType = "Opera"
226+
ServerConnectionSubTypeAppleSafari ServerConnectionSubType = "Safari"
221227
)
222228

223229
type VaultVisibility int

entries.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ const (
1111
)
1212

1313
type Entries struct {
14-
UserCredential *EntryUserCredentialService
1514
Certificate *EntryCertificateService
15+
UserCredential *EntryUserCredentialService
16+
Website *EntryWebsiteService
1617
}
1718

1819
func keywordsToSlice(kw string) []string {

entry_website.go

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package dvls
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
)
9+
10+
type EntryWebsiteService service
11+
12+
// EntryWebsite represents a website entry in DVLS
13+
type EntryWebsite struct {
14+
ID string `json:"id,omitempty"`
15+
VaultId string `json:"repositoryId"`
16+
EntryName string `json:"name"`
17+
Description string `json:"description"`
18+
EntryFolderPath string `json:"group"`
19+
ModifiedDate *ServerTime `json:"modifiedDate,omitempty"`
20+
ConnectionType ServerConnectionType `json:"connectionType"`
21+
ConnectionSubType ServerConnectionSubType `json:"connectionSubType"`
22+
Tags []string `json:"keywords,omitempty"`
23+
24+
WebsiteDetails EntryWebsiteAuthDetails `json:"data"`
25+
}
26+
27+
func (e EntryWebsite) MarshalJSON() ([]byte, error) {
28+
raw := struct {
29+
ID string `json:"id,omitempty"`
30+
RepositoryId string `json:"repositoryId"`
31+
Name string `json:"name"`
32+
Description string `json:"description"`
33+
Events struct {
34+
OpenCommentPrompt bool `json:"openCommentPrompt"`
35+
CredentialViewedPrompt bool `json:"credentialViewedPrompt"`
36+
TicketNumberIsRequiredOnCredentialViewed bool `json:"ticketNumberIsRequiredOnCredentialViewed"`
37+
TicketNumberIsRequiredOnClose bool `json:"ticketNumberIsRequiredOnClose"`
38+
CredentialViewedCommentIsRequired bool `json:"credentialViewedCommentIsRequired"`
39+
TicketNumberIsRequiredOnOpen bool `json:"ticketNumberIsRequiredOnOpen"`
40+
CloseCommentIsRequired bool `json:"closeCommentIsRequired"`
41+
OpenCommentPromptOnBrowserExtensionLink bool `json:"openCommentPromptOnBrowserExtensionLink"`
42+
CloseCommentPrompt bool `json:"closeCommentPrompt"`
43+
OpenCommentIsRequired bool `json:"openCommentIsRequired"`
44+
WarnIfAlreadyOpened bool `json:"warnIfAlreadyOpened"`
45+
} `json:"events"`
46+
Data string `json:"data"`
47+
Expiration string `json:"expiration"`
48+
CheckOutMode int `json:"checkOutMode"`
49+
Group string `json:"group"`
50+
ConnectionType ServerConnectionType `json:"connectionType"`
51+
ConnectionSubType ServerConnectionSubType `json:"connectionSubType"`
52+
Keywords string `json:"keywords"`
53+
}{}
54+
55+
raw.ID = e.ID
56+
raw.Keywords = sliceToKeywords(e.Tags)
57+
raw.Description = e.Description
58+
raw.RepositoryId = e.VaultId
59+
raw.Group = e.EntryFolderPath
60+
raw.ConnectionSubType = e.ConnectionSubType
61+
raw.ConnectionType = e.ConnectionType
62+
raw.Name = e.EntryName
63+
sensitiveJson, err := json.Marshal(e.WebsiteDetails)
64+
if err != nil {
65+
return nil, fmt.Errorf("failed to marshal sensitive data. error: %w", err)
66+
}
67+
68+
raw.Data = string(sensitiveJson)
69+
70+
entryJson, err := json.Marshal(raw)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
return entryJson, nil
76+
}
77+
78+
// UnmarshalJSON implements the json.Unmarshaler interface.
79+
func (e *EntryWebsite) UnmarshalJSON(d []byte) error {
80+
raw := struct {
81+
ID string `json:"id"`
82+
Description string `json:"description"`
83+
Name string `json:"name"`
84+
Group string `json:"group"`
85+
ModifiedDate *ServerTime `json:"modifiedDate"`
86+
Keywords string `json:"keywords"`
87+
RepositoryId string `json:"repositoryId"`
88+
ConnectionType ServerConnectionType `json:"connectionType"`
89+
ConnectionSubType ServerConnectionSubType `json:"connectionSubType"`
90+
Data json.RawMessage `json:"data"`
91+
}{}
92+
93+
err := json.Unmarshal(d, &raw)
94+
if err != nil {
95+
return err
96+
}
97+
98+
e.ID = raw.ID
99+
e.EntryName = raw.Name
100+
e.ConnectionType = raw.ConnectionType
101+
e.ConnectionSubType = raw.ConnectionSubType
102+
e.ModifiedDate = raw.ModifiedDate
103+
e.Description = raw.Description
104+
e.EntryFolderPath = raw.Group
105+
e.VaultId = raw.RepositoryId
106+
e.Tags = keywordsToSlice(raw.Keywords)
107+
108+
if len(raw.Data) > 0 {
109+
if err := json.Unmarshal(raw.Data, &e.WebsiteDetails); err != nil {
110+
return fmt.Errorf("failed to unmarshal website details: %w", err)
111+
}
112+
}
113+
114+
return nil
115+
}
116+
117+
// EntryWebsiteAuthDetails represents website-specific fields
118+
type EntryWebsiteAuthDetails struct {
119+
Username string
120+
Password *string
121+
URL string
122+
WebBrowserApplication int
123+
}
124+
125+
// MarshalJSON implements the json.Marshaler interface.
126+
func (s EntryWebsiteAuthDetails) MarshalJSON() ([]byte, error) {
127+
raw := struct {
128+
AutoFillLogin bool `json:"AutoFillLogin"`
129+
AutoSubmit bool `json:"AutoSubmit"`
130+
AutomaticRefreshTime int `json:"AutomaticRefreshTime"`
131+
ChromeProxyType int `json:"ChromeProxyType"`
132+
CustomJavaScript string `json:"CustomJavaScript"`
133+
Host string `json:"Host"`
134+
URL string `json:"URL"`
135+
Username string `json:"Username"`
136+
WebBrowserApplication int `json:"WebBrowserApplication"`
137+
PasswordItem struct {
138+
HasSensitiveData bool `json:"HasSensitiveData"`
139+
SensitiveData string `json:"SensitiveData"`
140+
} `json:"PasswordItem"`
141+
VPN struct {
142+
EnableAutoDetectIsOnlineVPN int `json:"EnableAutoDetectIsOnlineVPN"`
143+
} `json:"VPN"`
144+
}{}
145+
146+
if s.Password != nil {
147+
raw.PasswordItem.HasSensitiveData = true
148+
raw.PasswordItem.SensitiveData = *s.Password
149+
} else {
150+
raw.PasswordItem.HasSensitiveData = false
151+
}
152+
153+
raw.Username = s.Username
154+
raw.URL = s.URL
155+
raw.WebBrowserApplication = s.WebBrowserApplication
156+
157+
secretJson, err := json.Marshal(raw)
158+
if err != nil {
159+
return nil, err
160+
}
161+
162+
return secretJson, nil
163+
}
164+
165+
// GetWebsiteDetails returns entry with the entry.Credentials.Password field.
166+
func (c *EntryWebsiteService) GetWebsiteDetails(entry EntryWebsite) (EntryWebsite, error) {
167+
var respData struct {
168+
Data string `json:"data"`
169+
}
170+
171+
reqUrl, err := url.JoinPath(c.client.baseUri, entryEndpoint, entry.ID, "/sensitive-data")
172+
if err != nil {
173+
return EntryWebsite{}, fmt.Errorf("failed to build entry url. error: %w", err)
174+
}
175+
176+
resp, err := c.client.Request(reqUrl, http.MethodPost, nil)
177+
if err != nil {
178+
return EntryWebsite{}, fmt.Errorf("error while fetching sensitive data. error: %w", err)
179+
} else if err = resp.CheckRespSaveResult(); err != nil {
180+
return EntryWebsite{}, err
181+
}
182+
183+
if err := json.Unmarshal(resp.Response, &respData); err != nil {
184+
return EntryWebsite{}, fmt.Errorf("failed to unmarshal response body. error: %w", err)
185+
}
186+
187+
var sensitiveDataResponse struct {
188+
Data struct {
189+
PasswordItem struct {
190+
HasSensitiveData bool `json:"hasSensitiveData"`
191+
SensitiveData *string `json:"sensitiveData,omitempty"`
192+
} `json:"passwordItem"`
193+
} `json:"data"`
194+
}
195+
196+
if err := json.Unmarshal([]byte(respData.Data), &sensitiveDataResponse); err != nil {
197+
return EntryWebsite{}, fmt.Errorf("failed to unmarshal inner data. error: %w", err)
198+
}
199+
200+
if sensitiveDataResponse.Data.PasswordItem.HasSensitiveData {
201+
entry.WebsiteDetails.Password = sensitiveDataResponse.Data.PasswordItem.SensitiveData
202+
} else {
203+
entry.WebsiteDetails.Password = nil
204+
}
205+
206+
return entry, nil
207+
}
208+
209+
// Get returns a single Entry specified by entryId. Call GetEntryCredentialsPassword with
210+
// the returned Entry to fetch the password.
211+
func (s *EntryWebsiteService) Get(entryId string) (EntryWebsite, error) {
212+
var respData struct {
213+
Data EntryWebsite `json:"data"`
214+
}
215+
216+
reqUrl, err := url.JoinPath(s.client.baseUri, entryEndpoint, entryId)
217+
if err != nil {
218+
return EntryWebsite{}, fmt.Errorf("failed to build entry url: %w", err)
219+
}
220+
221+
resp, err := s.client.Request(reqUrl, http.MethodGet, nil)
222+
if err != nil {
223+
return EntryWebsite{}, fmt.Errorf("error fetching entry: %w", err)
224+
}
225+
if err = resp.CheckRespSaveResult(); err != nil {
226+
return EntryWebsite{}, err
227+
}
228+
if resp.Response == nil {
229+
return EntryWebsite{}, fmt.Errorf("response body is nil for request to %s", reqUrl)
230+
}
231+
232+
if err := json.Unmarshal(resp.Response, &respData); err != nil {
233+
return EntryWebsite{}, fmt.Errorf("failed to unmarshal response: %w", err)
234+
}
235+
236+
return respData.Data, nil
237+
}
238+
239+
// NewEntryWebsiteAuthDetails returns an EntryWebsiteAuthDetails with an initialised EntryWebsiteAuthDetails.Password
240+
func (c *EntryWebsiteService) NewWebsiteDetails(username, password string) EntryWebsiteAuthDetails {
241+
return EntryWebsiteAuthDetails{
242+
Username: username,
243+
Password: &password,
244+
}
245+
}

entry_website_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package dvls
2+
3+
import (
4+
"os"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
var (
10+
testWebsiteEntryId string
11+
testWebsiteEntry EntryWebsite = EntryWebsite{
12+
Description: "Test website description",
13+
EntryName: "TestWebsite",
14+
ConnectionType: ServerConnectionWebBrowser,
15+
ConnectionSubType: ServerConnectionSubTypeGoogleChrome,
16+
Tags: []string{"Test tag 1", "Test tag 2", "web"},
17+
}
18+
)
19+
20+
const (
21+
testWebsiteUsername string = "testuser"
22+
testWebsiteURL string = "https://test.example.com"
23+
testWebsiteBrowser string = "GoogleChrome"
24+
)
25+
26+
var testWebsitePassword = "testpass123"
27+
28+
func Test_EntryWebsite(t *testing.T) {
29+
testWebsiteEntryId = os.Getenv("TEST_WEBSITE_ENTRY_ID")
30+
testWebsiteEntry.ID = testWebsiteEntryId
31+
testWebsiteEntry.VaultId = testVaultId
32+
testWebsiteEntry.WebsiteDetails = EntryWebsiteAuthDetails{
33+
Username: testWebsiteUsername,
34+
URL: testWebsiteURL,
35+
WebBrowserApplication: 3,
36+
}
37+
testWebsiteEntry.ConnectionSubType = ServerConnectionSubTypeGoogleChrome
38+
39+
t.Run("GetEntry", test_GetWebsiteEntry)
40+
t.Run("GetEntryWebsite", test_GetWebsiteDetails)
41+
}
42+
43+
func test_GetWebsiteEntry(t *testing.T) {
44+
entry, err := testClient.Entries.Website.Get(testWebsiteEntry.ID)
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
49+
testWebsiteEntry.ModifiedDate = entry.ModifiedDate
50+
if !reflect.DeepEqual(entry, testWebsiteEntry) {
51+
t.Fatalf("fetched entry did not match test entry. Expected %#v, got %#v", testWebsiteEntry, entry)
52+
}
53+
}
54+
55+
func test_GetWebsiteDetails(t *testing.T) {
56+
entry, err := testClient.Entries.Website.Get(testWebsiteEntry.ID)
57+
if err != nil {
58+
t.Fatal(err)
59+
}
60+
61+
entryWithSensitiveData, err := testClient.Entries.Website.GetWebsiteDetails(entry)
62+
if err != nil {
63+
t.Fatal(err)
64+
}
65+
66+
entry.WebsiteDetails.Password = entryWithSensitiveData.WebsiteDetails.Password
67+
68+
expectedDetails := testWebsiteEntry.WebsiteDetails
69+
70+
expectedDetails.Password = &testWebsitePassword
71+
72+
if !reflect.DeepEqual(expectedDetails, entry.WebsiteDetails) {
73+
t.Fatalf("fetched secret did not match test secret. Expected %#v, got %#v", expectedDetails, entry.WebsiteDetails)
74+
}
75+
}

0 commit comments

Comments
 (0)