Skip to content

Commit bda5def

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

File tree

8 files changed

+335
-5
lines changed

8 files changed

+335
-5
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,6 @@ jobs:
7777
TEST_CERTIFICATE_ENTRY_ID: ${{ secrets.TEST_CERTIFICATE_ENTRY_ID }}
7878
TEST_CERTIFICATE_FILE_PATH: '${{ runner.temp }}/test.p12'
7979
TEST_VAULT_ID: ${{ secrets.TEST_VAULT_ID }}
80+
TEST_WEBSITE_ENTRY_ID: ${{ secrets.TEST_WEBSITE_ENTRY_ID }}
8081
with:
8182
github_token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}

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

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)