Skip to content

Commit

Permalink
feat: DEVOPS-3353 add entry Website
Browse files Browse the repository at this point in the history
  • Loading branch information
rbstp committed Nov 21, 2024
1 parent f03a362 commit 38145e8
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 5 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,6 @@ jobs:
TEST_CERTIFICATE_ENTRY_ID: ${{ secrets.TEST_CERTIFICATE_ENTRY_ID }}
TEST_CERTIFICATE_FILE_PATH: '${{ runner.temp }}/test.p12'
TEST_VAULT_ID: ${{ secrets.TEST_VAULT_ID }}
TEST_WEBSITE_ENTRY_ID: ${{ secrets.TEST_WEBSITE_ENTRY_ID }}
with:
github_token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Heavily based on the information found on the [Devolutions.Server](https://githu
## Usage
- Run go get `go get github.com/Devolutions/go-dvls`
- Add the import `import "github.com/Devolutions/go-dvls"`
- Setup the client (we recommend using an [Application ID](https://helpserver.devolutions.net/webinterface_applications.html?q=application+id))
- Setup the client (we recommend using an [Application ID](https://docs.devolutions.net/server/web-interface/administration/security-management/applications/))
``` go
package main

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.8.0
0.9.0
1 change: 1 addition & 0 deletions authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func NewClient(username string, password string, baseUri string) (Client, error)
client.Entries = &Entries{
UserCredential: (*EntryUserCredentialService)(&client.common),
Certificate: (*EntryCertificateService)(&client.common),
Website: (*EntryWebsiteService)(&client.common),
}
client.Vaults = (*Vaults)(&client.common)

Expand Down
10 changes: 8 additions & 2 deletions dvlstypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,14 @@ const (
type ServerConnectionSubType string

const (
ServerConnectionSubTypeDefault ServerConnectionSubType = "Default"
ServerConnectionSubTypeCertificate ServerConnectionSubType = "Certificate"
ServerConnectionSubTypeDefault ServerConnectionSubType = "Default"
ServerConnectionSubTypeCertificate ServerConnectionSubType = "Certificate"
ServerConnectionSubTypeMicrosoftEdge ServerConnectionSubType = "Edge"
ServerConnectionSubTypeFirefox ServerConnectionSubType = "FireFox"
ServerConnectionSubTypeGoogleChrome ServerConnectionSubType = "GoogleChrome"
ServerConnectionSubTypeInternetExplorer ServerConnectionSubType = "IE"
ServerConnectionSubTypeOpera ServerConnectionSubType = "Opera"
ServerConnectionSubTypeAppleSafari ServerConnectionSubType = "Safari"
)

type VaultVisibility int
Expand Down
3 changes: 2 additions & 1 deletion entries.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ const (
)

type Entries struct {
UserCredential *EntryUserCredentialService
Certificate *EntryCertificateService
UserCredential *EntryUserCredentialService
Website *EntryWebsiteService
}

func keywordsToSlice(kw string) []string {
Expand Down
246 changes: 246 additions & 0 deletions entry_website.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package dvls

import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)

type EntryWebsiteService service

// EntryWebsite represents a website entry in DVLS
type EntryWebsite struct {
ID string `json:"id,omitempty"`
VaultId string `json:"repositoryId"`
EntryName string `json:"name"`
Description string `json:"description"`
EntryFolderPath string `json:"group"`
ModifiedDate *ServerTime `json:"modifiedDate,omitempty"`
ConnectionType ServerConnectionType `json:"connectionType"`
ConnectionSubType ServerConnectionSubType `json:"connectionSubType"`
Tags []string `json:"keywords,omitempty"`

WebsiteDetails EntryWebsiteAuthDetails `json:"data"`
}

// MarshalJSON implements the json.Marshaler interface.
func (e EntryWebsite) MarshalJSON() ([]byte, error) {
raw := struct {
ID string `json:"id,omitempty"`
RepositoryId string `json:"repositoryId"`
Name string `json:"name"`
Description string `json:"description"`
Events struct {
OpenCommentPrompt bool `json:"openCommentPrompt"`
CredentialViewedPrompt bool `json:"credentialViewedPrompt"`
TicketNumberIsRequiredOnCredentialViewed bool `json:"ticketNumberIsRequiredOnCredentialViewed"`
TicketNumberIsRequiredOnClose bool `json:"ticketNumberIsRequiredOnClose"`
CredentialViewedCommentIsRequired bool `json:"credentialViewedCommentIsRequired"`
TicketNumberIsRequiredOnOpen bool `json:"ticketNumberIsRequiredOnOpen"`
CloseCommentIsRequired bool `json:"closeCommentIsRequired"`
OpenCommentPromptOnBrowserExtensionLink bool `json:"openCommentPromptOnBrowserExtensionLink"`
CloseCommentPrompt bool `json:"closeCommentPrompt"`
OpenCommentIsRequired bool `json:"openCommentIsRequired"`
WarnIfAlreadyOpened bool `json:"warnIfAlreadyOpened"`
} `json:"events"`
Data string `json:"data"`
Expiration string `json:"expiration"`
CheckOutMode int `json:"checkOutMode"`
Group string `json:"group"`
ConnectionType ServerConnectionType `json:"connectionType"`
ConnectionSubType ServerConnectionSubType `json:"connectionSubType"`
Keywords string `json:"keywords"`
}{}

raw.ID = e.ID
raw.Keywords = sliceToKeywords(e.Tags)
raw.Description = e.Description
raw.RepositoryId = e.VaultId
raw.Group = e.EntryFolderPath
raw.ConnectionSubType = e.ConnectionSubType
raw.ConnectionType = e.ConnectionType
raw.Name = e.EntryName
sensitiveJson, err := json.Marshal(e.WebsiteDetails)
if err != nil {
return nil, fmt.Errorf("failed to marshal sensitive data. error: %w", err)
}

raw.Data = string(sensitiveJson)

entryJson, err := json.Marshal(raw)
if err != nil {
return nil, err
}

return entryJson, nil
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (e *EntryWebsite) UnmarshalJSON(d []byte) error {
raw := struct {
ID string `json:"id"`
Description string `json:"description"`
Name string `json:"name"`
Group string `json:"group"`
ModifiedDate *ServerTime `json:"modifiedDate"`
Keywords string `json:"keywords"`
RepositoryId string `json:"repositoryId"`
ConnectionType ServerConnectionType `json:"connectionType"`
ConnectionSubType ServerConnectionSubType `json:"connectionSubType"`
Data json.RawMessage `json:"data"`
}{}

err := json.Unmarshal(d, &raw)
if err != nil {
return err
}

e.ID = raw.ID
e.EntryName = raw.Name
e.ConnectionType = raw.ConnectionType
e.ConnectionSubType = raw.ConnectionSubType
e.ModifiedDate = raw.ModifiedDate
e.Description = raw.Description
e.EntryFolderPath = raw.Group
e.VaultId = raw.RepositoryId
e.Tags = keywordsToSlice(raw.Keywords)

if len(raw.Data) > 0 {
if err := json.Unmarshal(raw.Data, &e.WebsiteDetails); err != nil {
return fmt.Errorf("failed to unmarshal website details: %w", err)
}
}

return nil
}

// EntryWebsiteAuthDetails represents website-specific fields
type EntryWebsiteAuthDetails struct {
Username string
Password *string
URL string
WebBrowserApplication int
}

// MarshalJSON implements the json.Marshaler interface.
func (s EntryWebsiteAuthDetails) MarshalJSON() ([]byte, error) {
raw := struct {
AutoFillLogin bool `json:"AutoFillLogin"`
AutoSubmit bool `json:"AutoSubmit"`
AutomaticRefreshTime int `json:"AutomaticRefreshTime"`
ChromeProxyType int `json:"ChromeProxyType"`
CustomJavaScript string `json:"CustomJavaScript"`
Host string `json:"Host"`
URL string `json:"URL"`
Username string `json:"Username"`
WebBrowserApplication int `json:"WebBrowserApplication"`
PasswordItem struct {
HasSensitiveData bool `json:"HasSensitiveData"`
SensitiveData string `json:"SensitiveData"`
} `json:"PasswordItem"`
VPN struct {
EnableAutoDetectIsOnlineVPN int `json:"EnableAutoDetectIsOnlineVPN"`
} `json:"VPN"`
}{}

if s.Password != nil {
raw.PasswordItem.HasSensitiveData = true
raw.PasswordItem.SensitiveData = *s.Password
} else {
raw.PasswordItem.HasSensitiveData = false
}

raw.Username = s.Username
raw.URL = s.URL
raw.WebBrowserApplication = s.WebBrowserApplication

secretJson, err := json.Marshal(raw)
if err != nil {
return nil, err
}

return secretJson, nil
}

// GetWebsiteDetails returns entry with the entry.WebsiteDetails.Password field.
func (c *EntryWebsiteService) GetWebsiteDetails(entry EntryWebsite) (EntryWebsite, error) {
var respData struct {
Data string `json:"data"`
}

reqUrl, err := url.JoinPath(c.client.baseUri, entryEndpoint, entry.ID, "/sensitive-data")
if err != nil {
return EntryWebsite{}, fmt.Errorf("failed to build entry url. error: %w", err)
}

resp, err := c.client.Request(reqUrl, http.MethodPost, nil)
if err != nil {
return EntryWebsite{}, fmt.Errorf("error while fetching sensitive data. error: %w", err)
} else if err = resp.CheckRespSaveResult(); err != nil {
return EntryWebsite{}, err
}

if err := json.Unmarshal(resp.Response, &respData); err != nil {
return EntryWebsite{}, fmt.Errorf("failed to unmarshal response body. error: %w", err)
}

var sensitiveDataResponse struct {
Data struct {
PasswordItem struct {
HasSensitiveData bool `json:"hasSensitiveData"`
SensitiveData *string `json:"sensitiveData,omitempty"`
} `json:"passwordItem"`
} `json:"data"`
}

if err := json.Unmarshal([]byte(respData.Data), &sensitiveDataResponse); err != nil {
return EntryWebsite{}, fmt.Errorf("failed to unmarshal inner data. error: %w", err)
}

if sensitiveDataResponse.Data.PasswordItem.HasSensitiveData {
entry.WebsiteDetails.Password = sensitiveDataResponse.Data.PasswordItem.SensitiveData
} else {
entry.WebsiteDetails.Password = nil
}

return entry, nil
}

// Get returns a single Entry specified by entryId. Call GetWebsiteDetails with
// the returned Entry to fetch the password.
func (s *EntryWebsiteService) Get(entryId string) (EntryWebsite, error) {
var respData struct {
Data EntryWebsite `json:"data"`
}

reqUrl, err := url.JoinPath(s.client.baseUri, entryEndpoint, entryId)
if err != nil {
return EntryWebsite{}, fmt.Errorf("failed to build entry url: %w", err)
}

resp, err := s.client.Request(reqUrl, http.MethodGet, nil)
if err != nil {
return EntryWebsite{}, fmt.Errorf("error fetching entry: %w", err)
}
if err = resp.CheckRespSaveResult(); err != nil {
return EntryWebsite{}, err
}
if resp.Response == nil {
return EntryWebsite{}, fmt.Errorf("response body is nil for request to %s", reqUrl)
}

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

return respData.Data, nil
}

// NewEntryWebsiteAuthDetails returns an EntryWebsiteAuthDetails with an initialised EntryWebsiteAuthDetails.Password
func (c *EntryWebsiteService) NewWebsiteDetails(username, password string) EntryWebsiteAuthDetails {
return EntryWebsiteAuthDetails{
Username: username,
Password: &password,
}
}
75 changes: 75 additions & 0 deletions entry_website_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package dvls

import (
"os"
"reflect"
"testing"
)

var (
testWebsiteEntryId string
testWebsiteEntry EntryWebsite = EntryWebsite{
Description: "Test website description",
EntryName: "TestWebsite",
ConnectionType: ServerConnectionWebBrowser,
ConnectionSubType: ServerConnectionSubTypeGoogleChrome,
Tags: []string{"Test tag 1", "Test tag 2", "web"},
}
)

const (
testWebsiteUsername string = "testuser"
testWebsiteURL string = "https://test.example.com"
testWebsiteBrowser string = "GoogleChrome"
)

var testWebsitePassword = "testpass123"

func Test_EntryWebsite(t *testing.T) {
testWebsiteEntryId = os.Getenv("TEST_WEBSITE_ENTRY_ID")
testWebsiteEntry.ID = testWebsiteEntryId
testWebsiteEntry.VaultId = testVaultId
testWebsiteEntry.WebsiteDetails = EntryWebsiteAuthDetails{
Username: testWebsiteUsername,
URL: testWebsiteURL,
WebBrowserApplication: 3,
}
testWebsiteEntry.ConnectionSubType = ServerConnectionSubTypeGoogleChrome

t.Run("GetEntry", test_GetWebsiteEntry)
t.Run("GetEntryWebsite", test_GetWebsiteDetails)
}

func test_GetWebsiteEntry(t *testing.T) {
entry, err := testClient.Entries.Website.Get(testWebsiteEntry.ID)
if err != nil {
t.Fatal(err)
}

testWebsiteEntry.ModifiedDate = entry.ModifiedDate
if !reflect.DeepEqual(entry, testWebsiteEntry) {
t.Fatalf("fetched entry did not match test entry. Expected %#v, got %#v", testWebsiteEntry, entry)
}
}

func test_GetWebsiteDetails(t *testing.T) {
entry, err := testClient.Entries.Website.Get(testWebsiteEntry.ID)
if err != nil {
t.Fatal(err)
}

entryWithSensitiveData, err := testClient.Entries.Website.GetWebsiteDetails(entry)
if err != nil {
t.Fatal(err)
}

entry.WebsiteDetails.Password = entryWithSensitiveData.WebsiteDetails.Password

expectedDetails := testWebsiteEntry.WebsiteDetails

expectedDetails.Password = &testWebsitePassword

if !reflect.DeepEqual(expectedDetails, entry.WebsiteDetails) {
t.Fatalf("fetched secret did not match test secret. Expected %#v, got %#v", expectedDetails, entry.WebsiteDetails)
}
}

0 comments on commit 38145e8

Please sign in to comment.