From 6af77b3e33c282b3de3f6ae17e58fc8c937a4ab5 Mon Sep 17 00:00:00 2001 From: Dion Gionet Mallet Date: Wed, 2 Sep 2026 16:23:36 -0400 Subject: [PATCH 1/2] [DEVOPS-4655] feat(entries): add entry and folder permissions management --- authentication.go | 1 + dvlstypes.go | 56 +++++++ entries.go | 1 + entry_permissions.go | 289 +++++++++++++++++++++++++++++++++ entry_permissions_test.go | 41 +++++ entry_permissions_unit_test.go | 212 ++++++++++++++++++++++++ mock_test.go | 2 + securityroleoverride_string.go | 29 ++++ securityroleright_string.go | 61 +++++++ utils.go | 9 + 10 files changed, 701 insertions(+) create mode 100644 entry_permissions.go create mode 100644 entry_permissions_test.go create mode 100644 entry_permissions_unit_test.go create mode 100644 securityroleoverride_string.go create mode 100644 securityroleright_string.go diff --git a/authentication.go b/authentication.go index 1163688..6ef731f 100644 --- a/authentication.go +++ b/authentication.go @@ -94,6 +94,7 @@ func (c *Client) initServices() { Folder: (*EntryFolderService)(&c.common), Host: (*EntryHostService)(&c.common), Website: (*EntryWebsiteService)(&c.common), + Permissions: (*EntryPermissionsService)(&c.common), } c.Vaults = (*Vaults)(&c.common) } diff --git a/dvlstypes.go b/dvlstypes.go index 8dad657..aafaf4c 100644 --- a/dvlstypes.go +++ b/dvlstypes.go @@ -232,3 +232,59 @@ const ( EntryCertificateDataModeURL EntryCertificateDataMode = 3 EntryCertificateDataModeFile EntryCertificateDataMode = 2 ) + +//go:generate stringer -type=SecurityRoleOverride -trimprefix SecurityRoleOverride +type SecurityRoleOverride uint8 + +const ( + SecurityRoleOverrideDefault SecurityRoleOverride = iota + SecurityRoleOverrideCustom + SecurityRoleOverrideInherited + SecurityRoleOverrideEveryone + SecurityRoleOverrideNever + SecurityRoleOverrideCustomInherited +) + +//go:generate stringer -type=SecurityRoleRight -trimprefix SecurityRoleRight +type SecurityRoleRight uint8 + +const ( + SecurityRoleRightView SecurityRoleRight = iota + SecurityRoleRightViewPassword + SecurityRoleRightAdd + SecurityRoleRightDelete + SecurityRoleRightEdit + SecurityRoleRightEditStatus + SecurityRoleRightEditDescription + SecurityRoleRightEditSecurity + SecurityRoleRightPasswordHistory + SecurityRoleRightConnectionHistory + SecurityRoleRightRemoteTools + SecurityRoleRightAttachment + SecurityRoleRightEditAttachment + SecurityRoleRightInventory + SecurityRoleRightViewLogs + SecurityRoleRightHandbook + SecurityRoleRightEditHandbook + SecurityRoleRightWebManagementTools + SecurityRoleRightConsoleManagementTools + SecurityRoleRightMacroScriptTools + SecurityRoleRightMacroScriptToolsEntry + SecurityRoleRightEditPassword + SecurityRoleRightExecute + SecurityRoleRightViewSessionRecording + SecurityRoleRightViewInformation + SecurityRoleRightExport + SecurityRoleRightEditInformation + SecurityRoleRightMove + SecurityRoleRightDeleteHandbook + SecurityRoleRightViewSensitiveInformation + SecurityRoleRightResetPassword + SecurityRoleRightApproveCheckoutRequest + SecurityRoleRightForceCheckin + SecurityRoleRightCheckout + SecurityRoleRightReadLogs + SecurityRoleRightSealed + SecurityRoleRightEditVPNSSHGatewayConfiguration + SecurityRoleRightEditSessionRecordingConfiguration +) diff --git a/entries.go b/entries.go index 29be068..2340cb8 100644 --- a/entries.go +++ b/entries.go @@ -40,6 +40,7 @@ type Entries struct { Credential *EntryCredentialService Website *EntryWebsiteService Folder *EntryFolderService + Permissions *EntryPermissionsService } type Entry struct { diff --git a/entry_permissions.go b/entry_permissions.go new file mode 100644 index 0000000..b0d93ec --- /dev/null +++ b/entry_permissions.go @@ -0,0 +1,289 @@ +package dvls + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +type EntryPermissionsService service + +// EntryPermission represents the principals granted a single right on an +// entry or folder. Roles contains principal ids (users, user groups and +// applications share the same list). +type EntryPermission struct { + Override SecurityRoleOverride `json:"override"` + Right SecurityRoleRight `json:"right"` + Roles []string `json:"roles"` +} + +// EntrySecurity represents the permission block of an entry or folder. +// The View right is not part of Permissions: it is controlled by ViewOverride +// and ViewRoles. When RoleOverride is not Custom or CustomInherited, the +// server forces ViewOverride to RoleOverride and clears ViewRoles, so a Get +// following a Set can return normalized values. +type EntrySecurity struct { + RoleOverride SecurityRoleOverride `json:"roleOverride"` + ViewOverride SecurityRoleOverride `json:"viewOverride"` + ViewRoles []string `json:"viewRoles"` + Permissions []EntryPermission `json:"permissions"` +} + +// Get returns the EntrySecurity of the entry or folder specified by entryId. +// Returns ErrEntryNotFound if no entry is found. +func (c *EntryPermissionsService) Get(entryId string) (EntrySecurity, error) { + return c.GetWithContext(context.Background(), entryId) +} + +// GetWithContext returns the EntrySecurity of the entry or folder specified by entryId. +// Returns ErrEntryNotFound if no entry is found. +// The provided context can be used to cancel the request. +func (c *EntryPermissionsService) GetWithContext(ctx context.Context, entryId string) (EntrySecurity, error) { + connectionJson, err := c.getPartialConnection(ctx, entryId) + if err != nil { + return EntrySecurity{}, err + } + + var connection struct { + Security json.RawMessage `json:"security"` + } + err = json.Unmarshal(connectionJson, &connection) + if err != nil { + return EntrySecurity{}, fmt.Errorf("failed to unmarshal response body: %w", err) + } + + if !jsonValuePresent(connection.Security) { + return EntrySecurity{}, nil + } + + var security EntrySecurity + err = json.Unmarshal(connection.Security, &security) + if err != nil { + return EntrySecurity{}, fmt.Errorf("failed to unmarshal response body: %w", err) + } + + return security, nil +} + +// Set replaces the permission block (RoleOverride, ViewOverride, ViewRoles and +// Permissions) of the entry or folder specified by entryId. Every other entry +// field and security setting is preserved. +// Set fetches the entry and saves it back in two requests; a concurrent +// modification of the entry between the two requests is overwritten. +func (c *EntryPermissionsService) Set(entryId string, security EntrySecurity) error { + return c.SetWithContext(context.Background(), entryId, security) +} + +// SetWithContext replaces the permission block (RoleOverride, ViewOverride, ViewRoles and +// Permissions) of the entry or folder specified by entryId. Every other entry +// field and security setting is preserved. +// SetWithContext fetches the entry and saves it back in two requests; a concurrent +// modification of the entry between the two requests is overwritten. +// The provided context can be used to cancel the request. +func (c *EntryPermissionsService) SetWithContext(ctx context.Context, entryId string, security EntrySecurity) error { + err := validateEntrySecurity(entryId, security) + if err != nil { + return err + } + + connectionJson, err := c.getPartialConnection(ctx, entryId) + if err != nil { + return err + } + + connection := map[string]json.RawMessage{} + err = json.Unmarshal(connectionJson, &connection) + if err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + + securityNode := map[string]json.RawMessage{} + if existing, ok := connection["security"]; ok && jsonValuePresent(existing) { + err = json.Unmarshal(existing, &securityNode) + if err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + } + + err = setSecurityPermissionFields(securityNode, security) + if err != nil { + return err + } + + securityJson, err := json.Marshal(securityNode) + if err != nil { + return fmt.Errorf("failed to marshal body: %w", err) + } + connection["security"] = securityJson + + err = fixupFolderGroup(connection) + if err != nil { + return err + } + + saveJson, err := json.Marshal(connection) + if err != nil { + return fmt.Errorf("failed to marshal body: %w", err) + } + + reqUrl, err := url.JoinPath(c.client.baseUri, entryEndpoint, "save") + if err != nil { + return fmt.Errorf("failed to build entry url: %w", err) + } + + resp, err := c.client.RequestWithContext(ctx, reqUrl, http.MethodPut, bytes.NewBuffer(saveJson)) + if err != nil { + return fmt.Errorf("error while saving entry permissions: %w", err) + } + + return resp.CheckRespSaveResult() +} + +// getPartialConnection fetches the raw partial connection JSON. The save +// endpoint overwrites the whole entry, so every field must be preserved byte +// for byte; never decode the connection into a typed struct before saving. +func (c *EntryPermissionsService) getPartialConnection(ctx context.Context, entryId string) (json.RawMessage, error) { + reqUrl, err := url.JoinPath(c.client.baseUri, entryEndpoint, entryId) + if err != nil { + return nil, fmt.Errorf("failed to build entry url: %w", err) + } + + resp, err := c.client.RequestWithContext(ctx, reqUrl, http.MethodGet, nil) + if err != nil { + if IsNotFound(err) { + return nil, ErrEntryNotFound + } + + return nil, fmt.Errorf("error while fetching entry: %w", err) + } + + if SaveResult(resp.Result) == SaveResultNotFound { + return nil, ErrEntryNotFound + } + if err = resp.CheckRespSaveResult(); err != nil { + return nil, err + } + + var envelope struct { + Data json.RawMessage `json:"data"` + } + err = json.Unmarshal(resp.Response, &envelope) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response body: %w", err) + } + + return envelope.Data, nil +} + +func validateEntrySecurity(entryId string, security EntrySecurity) error { + if entryId == "" { + return fmt.Errorf("entry id is required") + } + + seen := map[SecurityRoleRight]struct{}{} + for _, permission := range security.Permissions { + if permission.Right == SecurityRoleRightView { + return fmt.Errorf("the View right cannot be set through Permissions, use ViewOverride and ViewRoles") + } + + if _, ok := seen[permission.Right]; ok { + return fmt.Errorf("duplicate permission right %s", permission.Right) + } + seen[permission.Right] = struct{}{} + } + + customOverride := security.RoleOverride == SecurityRoleOverrideCustom || security.RoleOverride == SecurityRoleOverrideCustomInherited + if !customOverride && (len(security.Permissions) > 0 || len(security.ViewRoles) > 0) { + return fmt.Errorf("Permissions and ViewRoles require RoleOverride to be Custom or CustomInherited") + } + + return nil +} + +func setSecurityPermissionFields(securityNode map[string]json.RawMessage, security EntrySecurity) error { + normalized := security + if normalized.ViewRoles == nil { + normalized.ViewRoles = []string{} + } + + normalized.Permissions = make([]EntryPermission, len(security.Permissions)) + copy(normalized.Permissions, security.Permissions) + for i := range normalized.Permissions { + if normalized.Permissions[i].Roles == nil { + normalized.Permissions[i].Roles = []string{} + } + } + + normalizedJson, err := json.Marshal(normalized) + if err != nil { + return fmt.Errorf("failed to marshal body: %w", err) + } + + fields := map[string]json.RawMessage{} + err = json.Unmarshal(normalizedJson, &fields) + if err != nil { + return fmt.Errorf("failed to marshal body: %w", err) + } + + for field, value := range fields { + securityNode[field] = value + } + + return nil +} + +// fixupFolderGroup rewrites the group field of folders to the parent path +// before saving. The partial connection endpoint returns a folder's group as +// its own full path; saving it unchanged would move the folder into itself. +func fixupFolderGroup(connection map[string]json.RawMessage) error { + var connectionType int + if raw, ok := connection["connectionType"]; ok { + if err := json.Unmarshal(raw, &connectionType); err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + } + + var connectionSubType string + if raw, ok := connection["connectionSubType"]; ok { + if err := json.Unmarshal(raw, &connectionSubType); err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + } + + isFolder := connectionType == int(ServerConnectionGroup) || + (connectionType == int(ServerConnectionPAM) && strings.EqualFold(connectionSubType, "Group")) + if !isFolder { + return nil + } + + var name, group string + if raw, ok := connection["name"]; ok { + if err := json.Unmarshal(raw, &name); err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + } + if raw, ok := connection["group"]; ok { + if err := json.Unmarshal(raw, &group); err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + } + + if group == name { + group = "" + } else { + group = strings.TrimSuffix(group, "\\"+name) + } + group = strings.TrimRight(group, "\\") + + groupJson, err := json.Marshal(group) + if err != nil { + return fmt.Errorf("failed to marshal body: %w", err) + } + connection["group"] = groupJson + + return nil +} diff --git a/entry_permissions_test.go b/entry_permissions_test.go new file mode 100644 index 0000000..7990a01 --- /dev/null +++ b/entry_permissions_test.go @@ -0,0 +1,41 @@ +//go:build integration + +package dvls + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_EntryPermissions(t *testing.T) { + vault := createTestVault(t, "entry-permissions") + + folderId, err := testClient.Entries.Folder.New(Entry{ + VaultId: vault.Id, + Name: "Permissions Folder", + Type: EntryFolderType, + SubType: EntryFolderSubTypeFolder, + Data: &EntryFolderData{}, + }) + require.NoError(t, err) + + security, err := testClient.Entries.Permissions.Get(folderId) + require.NoError(t, err) + assert.Equal(t, SecurityRoleOverrideDefault, security.RoleOverride) + + err = testClient.Entries.Permissions.Set(folderId, EntrySecurity{ + RoleOverride: SecurityRoleOverrideEveryone, + ViewOverride: SecurityRoleOverrideEveryone, + }) + require.NoError(t, err) + + security, err = testClient.Entries.Permissions.Get(folderId) + require.NoError(t, err) + assert.Equal(t, SecurityRoleOverrideEveryone, security.RoleOverride) + + folder, err := testClient.Entries.Folder.GetById(vault.Id, folderId) + require.NoError(t, err) + assert.Equal(t, "Permissions Folder", folder.Name) +} diff --git a/entry_permissions_unit_test.go b/entry_permissions_unit_test.go new file mode 100644 index 0000000..36b4644 --- /dev/null +++ b/entry_permissions_unit_test.go @@ -0,0 +1,212 @@ +package dvls + +import ( + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEntryPermissionsGet(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/connections/partial/"+testEntryID, 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":"` + testEntryID + `","name":"Entry","connectionType":26, + "security":{"roleOverride":1,"viewOverride":5,"viewRoles":["role-1"],"permissions":[{"override":1,"right":4,"roles":["role-2"]}]}}}`)) + }) + + client := newTestClient(t, mux) + + security, err := client.Entries.Permissions.Get(testEntryID) + require.NoError(t, err) + assert.Equal(t, SecurityRoleOverrideCustom, security.RoleOverride) + assert.Equal(t, SecurityRoleOverrideCustomInherited, security.ViewOverride) + assert.Equal(t, []string{"role-1"}, security.ViewRoles) + require.Len(t, security.Permissions, 1) + assert.Equal(t, SecurityRoleRightEdit, security.Permissions[0].Right) + assert.Equal(t, SecurityRoleOverrideCustom, security.Permissions[0].Override) + assert.Equal(t, []string{"role-2"}, security.Permissions[0].Roles) +} + +func TestEntryPermissionsGet_NoSecurityNode(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/connections/partial/"+testEntryID, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"result":1,"data":{"id":"` + testEntryID + `","name":"Entry","connectionType":26}}`)) + }) + + client := newTestClient(t, mux) + + security, err := client.Entries.Permissions.Get(testEntryID) + require.NoError(t, err) + assert.Equal(t, EntrySecurity{}, security) +} + +func TestEntryPermissionsSet_RoundTripPreservesUnknownFields(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/connections/partial/"+testEntryID, 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":"` + testEntryID + `","name":"Entry","connectionType":26,"repositoryId":"` + testVaultID + `", + "events":{"openCommentPrompt":true},"data":"{\"nested\":true}","futureField":"abc", + "security":{"roleOverride":0,"checkOutMode":2,"allowOffline":1,"passwordComplexityId":"pc-1"}}}`)) + }) + + var savedBody []byte + mux.HandleFunc("/api/connections/partial/save", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + savedBody = body + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"result":1}`)) + }) + + client := newTestClient(t, mux) + + err := client.Entries.Permissions.Set(testEntryID, EntrySecurity{ + RoleOverride: SecurityRoleOverrideCustom, + ViewOverride: SecurityRoleOverrideCustom, + ViewRoles: []string{"role-1"}, + Permissions: []EntryPermission{ + {Override: SecurityRoleOverrideCustom, Right: SecurityRoleRightEdit, Roles: []string{"role-2"}}, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, savedBody) + + var saved map[string]any + require.NoError(t, json.Unmarshal(savedBody, &saved)) + + assert.NotContains(t, saved, "result") + assert.Equal(t, testEntryID, saved["id"]) + assert.Equal(t, map[string]any{"openCommentPrompt": true}, saved["events"]) + assert.Equal(t, `{"nested":true}`, saved["data"]) + assert.Equal(t, "abc", saved["futureField"]) + assert.Equal(t, testVaultID, saved["repositoryId"]) + + security, ok := saved["security"].(map[string]any) + require.True(t, ok) + assert.Equal(t, float64(2), security["checkOutMode"]) + assert.Equal(t, float64(1), security["allowOffline"]) + assert.Equal(t, "pc-1", security["passwordComplexityId"]) + assert.Equal(t, float64(1), security["roleOverride"]) + assert.Equal(t, float64(1), security["viewOverride"]) + assert.Equal(t, []any{"role-1"}, security["viewRoles"]) + permissions, ok := security["permissions"].([]any) + require.True(t, ok) + require.Len(t, permissions, 1) + assert.Equal(t, map[string]any{"override": float64(1), "right": float64(4), "roles": []any{"role-2"}}, permissions[0]) +} + +func TestEntryPermissionsSet_FolderGroupTrimmed(t *testing.T) { + cases := []struct { + name string + entryName string + group string + expectedGroup string + }{ + {name: "NestedFolder", entryName: "Sub", group: `Parent\Sub`, expectedGroup: "Parent"}, + {name: "RootFolder", entryName: "Root", group: "Root", expectedGroup: ""}, + } + + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/connections/partial/"+testEntryID, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + entryJson, err := json.Marshal(map[string]any{ + "id": testEntryID, + "name": testCase.entryName, + "group": testCase.group, + "connectionType": 25, + }) + require.NoError(t, err) + w.Write([]byte(`{"result":1,"data":` + string(entryJson) + `}`)) + }) + + mux.HandleFunc("/api/connections/partial/save", func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + + var saved map[string]any + require.NoError(t, json.Unmarshal(body, &saved)) + assert.Equal(t, testCase.expectedGroup, saved["group"]) + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"result":1}`)) + }) + + client := newTestClient(t, mux) + + err := client.Entries.Permissions.Set(testEntryID, EntrySecurity{RoleOverride: SecurityRoleOverrideEveryone, ViewOverride: SecurityRoleOverrideEveryone}) + require.NoError(t, err) + }) + } +} + +func TestEntryPermissionsSet_ValidationErrors(t *testing.T) { + client := newTestClient(t, http.NewServeMux()) + + err := client.Entries.Permissions.Set("", EntrySecurity{}) + assert.ErrorContains(t, err, "entry id is required") + + err = client.Entries.Permissions.Set(testEntryID, EntrySecurity{ + RoleOverride: SecurityRoleOverrideCustom, + Permissions: []EntryPermission{{Right: SecurityRoleRightView}}, + }) + assert.ErrorContains(t, err, "View right") + + err = client.Entries.Permissions.Set(testEntryID, EntrySecurity{ + RoleOverride: SecurityRoleOverrideCustom, + Permissions: []EntryPermission{ + {Right: SecurityRoleRightEdit}, + {Right: SecurityRoleRightEdit}, + }, + }) + assert.ErrorContains(t, err, "duplicate permission right") + + err = client.Entries.Permissions.Set(testEntryID, EntrySecurity{ + RoleOverride: SecurityRoleOverrideDefault, + Permissions: []EntryPermission{{Right: SecurityRoleRightEdit}}, + }) + assert.ErrorContains(t, err, "RoleOverride") +} + +func TestEntryPermissionsSet_SaveResultError(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/connections/partial/"+testEntryID, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"result":1,"data":{"id":"` + testEntryID + `","name":"Entry","connectionType":26}}`)) + }) + mux.HandleFunc("/api/connections/partial/save", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"result":2,"message":"access denied"}`)) + }) + + client := newTestClient(t, mux) + + err := client.Entries.Permissions.Set(testEntryID, EntrySecurity{RoleOverride: SecurityRoleOverrideEveryone, ViewOverride: SecurityRoleOverrideEveryone}) + require.Error(t, err) + assert.Contains(t, err.Error(), "AccessDenied") +} + +func TestSecurityRoleEnumValues(t *testing.T) { + assert.EqualValues(t, 0, SecurityRoleRightView) + assert.EqualValues(t, 4, SecurityRoleRightEdit) + assert.EqualValues(t, 22, SecurityRoleRightExecute) + assert.EqualValues(t, 37, SecurityRoleRightEditSessionRecordingConfiguration) + assert.EqualValues(t, 5, SecurityRoleOverrideCustomInherited) +} diff --git a/mock_test.go b/mock_test.go index 98dd5f6..14bfcd4 100644 --- a/mock_test.go +++ b/mock_test.go @@ -8,6 +8,8 @@ import ( const testVaultID = "test-vault-id" +const testEntryID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + func newTestClient(t *testing.T, mux *http.ServeMux) *Client { t.Helper() diff --git a/securityroleoverride_string.go b/securityroleoverride_string.go new file mode 100644 index 0000000..b81e4a5 --- /dev/null +++ b/securityroleoverride_string.go @@ -0,0 +1,29 @@ +// Code generated by "stringer -type=SecurityRoleOverride -trimprefix SecurityRoleOverride"; DO NOT EDIT. + +package dvls + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[SecurityRoleOverrideDefault-0] + _ = x[SecurityRoleOverrideCustom-1] + _ = x[SecurityRoleOverrideInherited-2] + _ = x[SecurityRoleOverrideEveryone-3] + _ = x[SecurityRoleOverrideNever-4] + _ = x[SecurityRoleOverrideCustomInherited-5] +} + +const _SecurityRoleOverride_name = "DefaultCustomInheritedEveryoneNeverCustomInherited" + +var _SecurityRoleOverride_index = [...]uint8{0, 7, 13, 22, 30, 35, 50} + +func (i SecurityRoleOverride) String() string { + idx := int(i) - 0 + if i < 0 || idx >= len(_SecurityRoleOverride_index)-1 { + return "SecurityRoleOverride(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _SecurityRoleOverride_name[_SecurityRoleOverride_index[idx]:_SecurityRoleOverride_index[idx+1]] +} diff --git a/securityroleright_string.go b/securityroleright_string.go new file mode 100644 index 0000000..0a9ad25 --- /dev/null +++ b/securityroleright_string.go @@ -0,0 +1,61 @@ +// Code generated by "stringer -type=SecurityRoleRight -trimprefix SecurityRoleRight"; DO NOT EDIT. + +package dvls + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[SecurityRoleRightView-0] + _ = x[SecurityRoleRightViewPassword-1] + _ = x[SecurityRoleRightAdd-2] + _ = x[SecurityRoleRightDelete-3] + _ = x[SecurityRoleRightEdit-4] + _ = x[SecurityRoleRightEditStatus-5] + _ = x[SecurityRoleRightEditDescription-6] + _ = x[SecurityRoleRightEditSecurity-7] + _ = x[SecurityRoleRightPasswordHistory-8] + _ = x[SecurityRoleRightConnectionHistory-9] + _ = x[SecurityRoleRightRemoteTools-10] + _ = x[SecurityRoleRightAttachment-11] + _ = x[SecurityRoleRightEditAttachment-12] + _ = x[SecurityRoleRightInventory-13] + _ = x[SecurityRoleRightViewLogs-14] + _ = x[SecurityRoleRightHandbook-15] + _ = x[SecurityRoleRightEditHandbook-16] + _ = x[SecurityRoleRightWebManagementTools-17] + _ = x[SecurityRoleRightConsoleManagementTools-18] + _ = x[SecurityRoleRightMacroScriptTools-19] + _ = x[SecurityRoleRightMacroScriptToolsEntry-20] + _ = x[SecurityRoleRightEditPassword-21] + _ = x[SecurityRoleRightExecute-22] + _ = x[SecurityRoleRightViewSessionRecording-23] + _ = x[SecurityRoleRightViewInformation-24] + _ = x[SecurityRoleRightExport-25] + _ = x[SecurityRoleRightEditInformation-26] + _ = x[SecurityRoleRightMove-27] + _ = x[SecurityRoleRightDeleteHandbook-28] + _ = x[SecurityRoleRightViewSensitiveInformation-29] + _ = x[SecurityRoleRightResetPassword-30] + _ = x[SecurityRoleRightApproveCheckoutRequest-31] + _ = x[SecurityRoleRightForceCheckin-32] + _ = x[SecurityRoleRightCheckout-33] + _ = x[SecurityRoleRightReadLogs-34] + _ = x[SecurityRoleRightSealed-35] + _ = x[SecurityRoleRightEditVPNSSHGatewayConfiguration-36] + _ = x[SecurityRoleRightEditSessionRecordingConfiguration-37] +} + +const _SecurityRoleRight_name = "ViewViewPasswordAddDeleteEditEditStatusEditDescriptionEditSecurityPasswordHistoryConnectionHistoryRemoteToolsAttachmentEditAttachmentInventoryViewLogsHandbookEditHandbookWebManagementToolsConsoleManagementToolsMacroScriptToolsMacroScriptToolsEntryEditPasswordExecuteViewSessionRecordingViewInformationExportEditInformationMoveDeleteHandbookViewSensitiveInformationResetPasswordApproveCheckoutRequestForceCheckinCheckoutReadLogsSealedEditVPNSSHGatewayConfigurationEditSessionRecordingConfiguration" + +var _SecurityRoleRight_index = [...]uint16{0, 4, 16, 19, 25, 29, 39, 54, 66, 81, 98, 109, 119, 133, 142, 150, 158, 170, 188, 210, 226, 247, 259, 266, 286, 301, 307, 322, 326, 340, 364, 377, 399, 411, 419, 427, 433, 463, 496} + +func (i SecurityRoleRight) String() string { + idx := int(i) - 0 + if i < 0 || idx >= len(_SecurityRoleRight_index)-1 { + return "SecurityRoleRight(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _SecurityRoleRight_name[_SecurityRoleRight_index[idx]:_SecurityRoleRight_index[idx+1]] +} diff --git a/utils.go b/utils.go index 0efc665..b715ca1 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,8 @@ package dvls import ( + "bytes" + "encoding/json" "strconv" "strings" ) @@ -37,3 +39,10 @@ func sliceToKeywords(kw []string) string { return kString } + +// jsonValuePresent reports whether a raw JSON value holds anything other than +// nothing or null. +func jsonValuePresent(raw json.RawMessage) bool { + trimmed := bytes.TrimSpace(raw) + return len(trimmed) > 0 && !bytes.Equal(trimmed, []byte("null")) +} From 3a555f8454e06d847ce0e76b9bd22b9c33baf356 Mon Sep 17 00:00:00 2001 From: Dion Gionet Mallet Date: Thu, 3 Sep 2026 09:54:40 -0400 Subject: [PATCH 2/2] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- entry_permissions.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/entry_permissions.go b/entry_permissions.go index b0d93ec..314c4fd 100644 --- a/entry_permissions.go +++ b/entry_permissions.go @@ -43,6 +43,10 @@ func (c *EntryPermissionsService) Get(entryId string) (EntrySecurity, error) { // Returns ErrEntryNotFound if no entry is found. // The provided context can be used to cancel the request. func (c *EntryPermissionsService) GetWithContext(ctx context.Context, entryId string) (EntrySecurity, error) { + if entryId == "" { + return EntrySecurity{}, fmt.Errorf("entry id is required") + } + connectionJson, err := c.getPartialConnection(ctx, entryId) if err != nil { return EntrySecurity{}, err @@ -100,6 +104,9 @@ func (c *EntryPermissionsService) SetWithContext(ctx context.Context, entryId st if err != nil { return fmt.Errorf("failed to unmarshal response body: %w", err) } + if connection == nil { + return fmt.Errorf("response data is null") + } securityNode := map[string]json.RawMessage{} if existing, ok := connection["security"]; ok && jsonValuePresent(existing) {