add Concealer compliance to paths (#3017)
Adds compliance with clues.Concealer to the paths package. Also introduces a new struct in the same space: Elements, which is a thin wrapper around a slice of strings so that subsections of a path or builder can carry the same pii behavior without additional work on the consumer's end. --- #### Does this PR need a docs update or release note? - [x] 🕐 Yes, but in a later PR #### Type of change - [x] 🌻 Feature - [x] 🤖 Supportability/Tests #### Issue(s) * #2024 #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
9cea2d5333
commit
50e92b65c6
@ -1,6 +1,11 @@
|
|||||||
package pii
|
package pii
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
// MapWithPlurls places the toLower value of each string
|
// MapWithPlurls places the toLower value of each string
|
||||||
// into a map[string]struct{}, along with a copy of the that
|
// into a map[string]struct{}, along with a copy of the that
|
||||||
@ -16,3 +21,23 @@ func MapWithPlurals(ss ...string) map[string]struct{} {
|
|||||||
|
|
||||||
return mss
|
return mss
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConcealElements conceals each element in elems that does not appear in
|
||||||
|
// the safe map. A copy of elems containing the changes is returned.
|
||||||
|
func ConcealElements(elems []string, safe map[string]struct{}) []string {
|
||||||
|
if len(elems) == 0 {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ces := slices.Clone(elems)
|
||||||
|
|
||||||
|
for i := range ces {
|
||||||
|
ce := ces[i]
|
||||||
|
|
||||||
|
if _, ok := safe[strings.ToLower(ce)]; !ok {
|
||||||
|
ces[i] = clues.Conceal(ce)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ces
|
||||||
|
}
|
||||||
|
|||||||
97
src/internal/common/pii/pii_test.go
Normal file
97
src/internal/common/pii/pii_test.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package pii_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/pii"
|
||||||
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PIIUnitSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPIIUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &PIIUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the clues hashing to mask for the span of this suite
|
||||||
|
func (suite *PIIUnitSuite) SetupSuite() {
|
||||||
|
clues.SetHasher(clues.HashCfg{HashAlg: clues.Flatmask})
|
||||||
|
}
|
||||||
|
|
||||||
|
// revert clues hashing to plaintext for all other tests
|
||||||
|
func (suite *PIIUnitSuite) TeardownSuite() {
|
||||||
|
clues.SetHasher(clues.NoHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PIIUnitSuite) TestMapWithPlurals() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
mwp := pii.MapWithPlurals()
|
||||||
|
assert.Equal(t, map[string]struct{}{}, mwp)
|
||||||
|
|
||||||
|
mwp = pii.MapWithPlurals("")
|
||||||
|
assert.Equal(t, map[string]struct{}{"": {}, "s": {}}, mwp)
|
||||||
|
|
||||||
|
mwp = pii.MapWithPlurals(" ")
|
||||||
|
assert.Equal(t, map[string]struct{}{" ": {}, " s": {}}, mwp)
|
||||||
|
|
||||||
|
mwp = pii.MapWithPlurals("foo", "bar")
|
||||||
|
expect := map[string]struct{}{
|
||||||
|
"foo": {},
|
||||||
|
"foos": {},
|
||||||
|
"bar": {},
|
||||||
|
"bars": {},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expect, mwp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PIIUnitSuite) TestConcealElements() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
in []string
|
||||||
|
safe map[string]struct{}
|
||||||
|
expect []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
expect: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no safe words",
|
||||||
|
in: []string{"fnords", "smarfs"},
|
||||||
|
expect: []string{"***", "***"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "safe words",
|
||||||
|
in: []string{"fnords", "smarfs"},
|
||||||
|
safe: map[string]struct{}{"fnords": {}},
|
||||||
|
expect: []string{"fnords", "***"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-matching safe words",
|
||||||
|
in: []string{"fnords", "smarfs"},
|
||||||
|
safe: map[string]struct{}{"beaux": {}},
|
||||||
|
expect: []string{"***", "***"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case insensitivity",
|
||||||
|
in: []string{"FNORDS", "SMARFS"},
|
||||||
|
safe: map[string]struct{}{"fnords": {}},
|
||||||
|
expect: []string{"FNORDS", "***"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
result := pii.ConcealElements(test.in, test.safe)
|
||||||
|
assert.ElementsMatch(t, test.expect, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SafeURL complies with the clues.Concealer and fmt.Stringer
|
// SafeURL complies with the clues.Concealer and fmt.Stringer
|
||||||
@ -42,17 +41,7 @@ func (u SafeURL) Conceal() string {
|
|||||||
return "malformed-URL"
|
return "malformed-URL"
|
||||||
}
|
}
|
||||||
|
|
||||||
elems := slices.Clone(strings.Split(p.EscapedPath(), "/"))
|
elems := ConcealElements(strings.Split(p.EscapedPath(), "/"), u.SafePathElems)
|
||||||
|
|
||||||
// conceal any non-safe path elem
|
|
||||||
for i := range elems {
|
|
||||||
e := elems[i]
|
|
||||||
|
|
||||||
if _, ok := u.SafePathElems[strings.ToLower(e)]; !ok {
|
|
||||||
elems[i] = clues.Conceal(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
qry := maps.Clone(p.Query())
|
qry := maps.Clone(p.Query())
|
||||||
|
|
||||||
// conceal any non-safe query param values
|
// conceal any non-safe query param values
|
||||||
|
|||||||
@ -30,7 +30,7 @@ func (suite *URLUnitSuite) TeardownSuite() {
|
|||||||
clues.SetHasher(clues.NoHash())
|
clues.SetHasher(clues.NoHash())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *URLUnitSuite) TestDoesThings() {
|
func (suite *URLUnitSuite) TestSafeURL() {
|
||||||
stubURL := "https://host.com/foo/bar/baz/qux?fnords=smarfs&fnords=brunhilda&beaux=regard"
|
stubURL := "https://host.com/foo/bar/baz/qux?fnords=smarfs&fnords=brunhilda&beaux=regard"
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
|
|||||||
@ -419,8 +419,7 @@ func (oc *Collection) populateItems(ctx context.Context, errs *fault.Bus) {
|
|||||||
folderProgress, colCloser := observe.ProgressWithCount(
|
folderProgress, colCloser := observe.ProgressWithCount(
|
||||||
ctx,
|
ctx,
|
||||||
observe.ItemQueueMsg,
|
observe.ItemQueueMsg,
|
||||||
// TODO(keepers): conceal compliance in path, drop Hide()
|
path.NewElements(queuedPath),
|
||||||
clues.Hide(queuedPath),
|
|
||||||
int64(len(oc.driveItems)))
|
int64(len(oc.driveItems)))
|
||||||
defer colCloser()
|
defer colCloser()
|
||||||
defer close(folderProgress)
|
defer close(folderProgress)
|
||||||
|
|||||||
@ -77,7 +77,7 @@ func RestoreCollections(
|
|||||||
ctx,
|
ctx,
|
||||||
"resource_owner", clues.Hide(dc.FullPath().ResourceOwner()),
|
"resource_owner", clues.Hide(dc.FullPath().ResourceOwner()),
|
||||||
"category", dc.FullPath().Category(),
|
"category", dc.FullPath().Category(),
|
||||||
"path", dc.FullPath()) // TODO: pii, path needs concealer compliance
|
"path", dc.FullPath())
|
||||||
)
|
)
|
||||||
|
|
||||||
metrics, folderMetas, err = RestoreCollection(
|
metrics, folderMetas, err = RestoreCollection(
|
||||||
|
|||||||
@ -186,8 +186,7 @@ func (sc *Collection) runPopulate(ctx context.Context, errs *fault.Bus) (support
|
|||||||
colProgress, closer := observe.CollectionProgress(
|
colProgress, closer := observe.CollectionProgress(
|
||||||
ctx,
|
ctx,
|
||||||
sc.fullPath.Category().String(),
|
sc.fullPath.Category().String(),
|
||||||
// TODO(keepers): conceal compliance in path, drop Hide()
|
sc.fullPath.Folders())
|
||||||
clues.Hide(sc.fullPath.Folder(false)))
|
|
||||||
go closer()
|
go closer()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
// TODO: Revisit this being a global nd make it a parameter to the progress methods
|
// TODO: Revisit this being a global and make it a parameter to the progress methods
|
||||||
// so that each bar can be initialized with different contexts if needed.
|
// so that each bar can be initialized with different contexts if needed.
|
||||||
contxt context.Context
|
contxt context.Context
|
||||||
writer io.Writer
|
writer io.Writer
|
||||||
|
|||||||
@ -568,7 +568,7 @@ func mergeDetails(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return clues.New("parsing base item info path").
|
return clues.New("parsing base item info path").
|
||||||
WithClues(mctx).
|
WithClues(mctx).
|
||||||
With("repo_ref", entry.RepoRef) // todo: pii, path needs concealer compliance
|
With("repo_ref", path.NewElements(entry.RepoRef))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Although this base has an entry it may not be the most recent. Check
|
// Although this base has an entry it may not be the most recent. Check
|
||||||
|
|||||||
88
src/pkg/path/elements.go
Normal file
88
src/pkg/path/elements.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package path
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/pii"
|
||||||
|
)
|
||||||
|
|
||||||
|
var piiSafePathElems = pii.MapWithPlurals(
|
||||||
|
// services
|
||||||
|
UnknownService.String(),
|
||||||
|
ExchangeService.String(),
|
||||||
|
OneDriveService.String(),
|
||||||
|
SharePointService.String(),
|
||||||
|
ExchangeMetadataService.String(),
|
||||||
|
OneDriveMetadataService.String(),
|
||||||
|
SharePointMetadataService.String(),
|
||||||
|
|
||||||
|
// categories
|
||||||
|
UnknownCategory.String(),
|
||||||
|
EmailCategory.String(),
|
||||||
|
ContactsCategory.String(),
|
||||||
|
EventsCategory.String(),
|
||||||
|
FilesCategory.String(),
|
||||||
|
ListsCategory.String(),
|
||||||
|
LibrariesCategory.String(),
|
||||||
|
PagesCategory.String(),
|
||||||
|
DetailsCategory.String(),
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// interface compliance required for handling PII
|
||||||
|
_ clues.Concealer = &Elements{}
|
||||||
|
_ fmt.Stringer = &Elements{}
|
||||||
|
|
||||||
|
// interface compliance for the observe package to display
|
||||||
|
// values without concealing PII.
|
||||||
|
_ clues.PlainStringer = &Elements{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Elements are a PII Concealer-compliant slice of elements within a path.
|
||||||
|
type Elements []string
|
||||||
|
|
||||||
|
// NewElements creates a new Elements slice by splitting the provided string.
|
||||||
|
func NewElements(p string) Elements {
|
||||||
|
return Split(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conceal produces a concealed representation of the elements, suitable for
|
||||||
|
// logging, storing in errors, and other output.
|
||||||
|
func (el Elements) Conceal() string {
|
||||||
|
escaped := make([]string, 0, len(el))
|
||||||
|
|
||||||
|
for _, e := range el {
|
||||||
|
escaped = append(escaped, escapeElement(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
return join(pii.ConcealElements(escaped, piiSafePathElems))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format produces a concealed representation of the elements, even when
|
||||||
|
// used within a PrintF, suitable for logging, storing in errors,
|
||||||
|
// and other output.
|
||||||
|
func (el Elements) Format(fs fmt.State, _ rune) {
|
||||||
|
fmt.Fprint(fs, el.Conceal())
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string that contains all path elements joined together.
|
||||||
|
// Elements that need escaping are escaped. The result is not concealed, and
|
||||||
|
// is not suitable for logging or structured errors.
|
||||||
|
func (el Elements) String() string {
|
||||||
|
escaped := make([]string, 0, len(el))
|
||||||
|
|
||||||
|
for _, e := range el {
|
||||||
|
escaped = append(escaped, escapeElement(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
return join(escaped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlainString returns an unescaped, unmodified string of the joined elements.
|
||||||
|
// The result is not concealed, and is not suitable for logging or structured
|
||||||
|
// errors.
|
||||||
|
func (el Elements) PlainString() string {
|
||||||
|
return join(el)
|
||||||
|
}
|
||||||
100
src/pkg/path/elements_test.go
Normal file
100
src/pkg/path/elements_test.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package path
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ElementsUnitSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestElementsUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ElementsUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the clues hashing to mask for the span of this suite
|
||||||
|
func (suite *ElementsUnitSuite) SetupSuite() {
|
||||||
|
clues.SetHasher(clues.HashCfg{HashAlg: clues.Flatmask})
|
||||||
|
}
|
||||||
|
|
||||||
|
// revert clues hashing to plaintext for all other tests
|
||||||
|
func (suite *ElementsUnitSuite) TeardownSuite() {
|
||||||
|
clues.SetHasher(clues.NoHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ElementsUnitSuite) TestNewElements() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
result := NewElements("")
|
||||||
|
assert.Equal(t, Elements{""}, result)
|
||||||
|
|
||||||
|
result = NewElements("fnords")
|
||||||
|
assert.Equal(t, Elements{"fnords"}, result)
|
||||||
|
|
||||||
|
result = NewElements("fnords/smarf")
|
||||||
|
assert.Equal(t, Elements{"fnords", "smarf"}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ElementsUnitSuite) TestElements_piiHandling() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
elems Elements
|
||||||
|
expect string
|
||||||
|
expectString string
|
||||||
|
expectPlain string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "all concealed",
|
||||||
|
elems: Elements{"foo", "bar/", "baz"},
|
||||||
|
expect: "***/***/***",
|
||||||
|
expectString: `foo/bar\//baz`,
|
||||||
|
expectPlain: `foo/bar//baz`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all safe",
|
||||||
|
elems: Elements{UnknownService.String(), UnknownCategory.String(), ExchangeMetadataService.String()},
|
||||||
|
expect: "UnknownService/UnknownCategory/exchangeMetadata",
|
||||||
|
expectString: "UnknownService/UnknownCategory/exchangeMetadata",
|
||||||
|
expectPlain: "UnknownService/UnknownCategory/exchangeMetadata",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed",
|
||||||
|
elems: Elements{UnknownService.String(), "smarf", ExchangeMetadataService.String()},
|
||||||
|
expect: "UnknownService/***/exchangeMetadata",
|
||||||
|
expectString: "UnknownService/smarf/exchangeMetadata",
|
||||||
|
expectPlain: "UnknownService/smarf/exchangeMetadata",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty elements",
|
||||||
|
elems: Elements{},
|
||||||
|
expect: "",
|
||||||
|
expectString: "",
|
||||||
|
expectPlain: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
elems: Elements{""},
|
||||||
|
expect: "",
|
||||||
|
expectString: "",
|
||||||
|
expectPlain: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
assert.Equal(t, test.expect, test.elems.Conceal(), "conceal")
|
||||||
|
assert.Equal(t, test.expectString, test.elems.String(), "string")
|
||||||
|
assert.Equal(t, test.expect, fmt.Sprintf("%s", test.elems), "fmt %%s")
|
||||||
|
assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.elems), "fmt %%+v")
|
||||||
|
assert.Equal(t, test.expectPlain, join(test.elems), "plain")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ import "github.com/alcionai/clues"
|
|||||||
// folders[] is []{"Folder1", "Folder2"}
|
// folders[] is []{"Folder1", "Folder2"}
|
||||||
type DrivePath struct {
|
type DrivePath struct {
|
||||||
DriveID string
|
DriveID string
|
||||||
Folders []string
|
Folders Elements
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToOneDrivePath(p Path) (*DrivePath, error) {
|
func ToOneDrivePath(p Path) (*DrivePath, error) {
|
||||||
|
|||||||
@ -86,7 +86,7 @@ type Path interface {
|
|||||||
Tenant() string
|
Tenant() string
|
||||||
ResourceOwner() string
|
ResourceOwner() string
|
||||||
Folder(bool) string
|
Folder(bool) string
|
||||||
Folders() []string
|
Folders() Elements
|
||||||
Item() string
|
Item() string
|
||||||
// UpdateParent updates parent from old to new if the item/folder was
|
// UpdateParent updates parent from old to new if the item/folder was
|
||||||
// parented by old path
|
// parented by old path
|
||||||
@ -102,7 +102,7 @@ type Path interface {
|
|||||||
// Elements returns all the elements in the path. This is a temporary function
|
// Elements returns all the elements in the path. This is a temporary function
|
||||||
// and will likely be updated to handle encoded elements instead of clear-text
|
// and will likely be updated to handle encoded elements instead of clear-text
|
||||||
// elements in the future.
|
// elements in the future.
|
||||||
Elements() []string
|
Elements() Elements
|
||||||
// Append returns a new Path object with the given element added to the end of
|
// Append returns a new Path object with the given element added to the end of
|
||||||
// the old Path if possible. If the old Path is an item Path then Append
|
// the old Path if possible. If the old Path is an item Path then Append
|
||||||
// returns an error.
|
// returns an error.
|
||||||
@ -113,8 +113,23 @@ type Path interface {
|
|||||||
ShortRef() string
|
ShortRef() string
|
||||||
// ToBuilder returns a Builder instance that represents the current Path.
|
// ToBuilder returns a Builder instance that represents the current Path.
|
||||||
ToBuilder() *Builder
|
ToBuilder() *Builder
|
||||||
|
|
||||||
|
// Every path needs to comply with these funcs to ensure that PII
|
||||||
|
// is appropriately hidden from logging, errors, and other outputs.
|
||||||
|
clues.Concealer
|
||||||
|
fmt.Stringer
|
||||||
|
|
||||||
|
// In the rare case that the path needs to get printed as a plain string,
|
||||||
|
// without obscuring values for PII.
|
||||||
|
clues.PlainStringer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interface compliance required for handling PII
|
||||||
|
var (
|
||||||
|
_ clues.Concealer = &Builder{}
|
||||||
|
_ fmt.Stringer = &Builder{}
|
||||||
|
)
|
||||||
|
|
||||||
// Builder is a simple path representation that only tracks path elements. It
|
// Builder is a simple path representation that only tracks path elements. It
|
||||||
// can join, escape, and unescape elements. Higher-level packages are expected
|
// can join, escape, and unescape elements. Higher-level packages are expected
|
||||||
// to wrap this struct to build resource-speicific contexts (e.x. an
|
// to wrap this struct to build resource-speicific contexts (e.x. an
|
||||||
@ -125,7 +140,7 @@ type Path interface {
|
|||||||
// tenant ID, service, user ID, etc).
|
// tenant ID, service, user ID, etc).
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
// Unescaped version of elements.
|
// Unescaped version of elements.
|
||||||
elements []string
|
elements Elements
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnescapeAndAppend creates a copy of this Builder and adds one or more already
|
// UnescapeAndAppend creates a copy of this Builder and adds one or more already
|
||||||
@ -223,18 +238,6 @@ func (pb Builder) LastElem() string {
|
|||||||
return pb.elements[len(pb.elements)-1]
|
return pb.elements[len(pb.elements)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a string that contains all path elements joined together.
|
|
||||||
// Elements of the path that need escaping are escaped.
|
|
||||||
func (pb Builder) String() string {
|
|
||||||
escaped := make([]string, 0, len(pb.elements))
|
|
||||||
|
|
||||||
for _, e := range pb.elements {
|
|
||||||
escaped = append(escaped, escapeElement(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
return join(escaped)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pb Builder) ShortRef() string {
|
func (pb Builder) ShortRef() string {
|
||||||
if len(pb.elements) == 0 {
|
if len(pb.elements) == 0 {
|
||||||
return ""
|
return ""
|
||||||
@ -261,8 +264,8 @@ func (pb Builder) ShortRef() string {
|
|||||||
// Elements returns all the elements in the path. This is a temporary function
|
// Elements returns all the elements in the path. This is a temporary function
|
||||||
// and will likely be updated to handle encoded elements instead of clear-text
|
// and will likely be updated to handle encoded elements instead of clear-text
|
||||||
// elements in the future.
|
// elements in the future.
|
||||||
func (pb Builder) Elements() []string {
|
func (pb Builder) Elements() Elements {
|
||||||
return append([]string{}, pb.elements...)
|
return append(Elements{}, pb.elements...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyInputValues(tenant, resourceOwner string) error {
|
func verifyInputValues(tenant, resourceOwner string) error {
|
||||||
@ -658,3 +661,35 @@ func Split(segment string) []string {
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PII Concealer Compliance
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Conceal produces a concealed representation of the builder, suitable for
|
||||||
|
// logging, storing in errors, and other output.
|
||||||
|
func (pb Builder) Conceal() string {
|
||||||
|
return pb.elements.Conceal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format produces a concealed representation of the builder, even when
|
||||||
|
// used within a PrintF, suitable for logging, storing in errors,
|
||||||
|
// and other output.
|
||||||
|
func (pb Builder) Format(fs fmt.State, _ rune) {
|
||||||
|
fmt.Fprint(fs, pb.Conceal())
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string that contains all path elements joined together.
|
||||||
|
// Elements of the path that need escaping are escaped.
|
||||||
|
// The result is not concealed, and is not suitable for logging or structured
|
||||||
|
// errors.
|
||||||
|
func (pb Builder) String() string {
|
||||||
|
return pb.elements.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlainString returns an unescaped, unmodified string of the builder.
|
||||||
|
// The result is not concealed, and is not suitable for logging or structured
|
||||||
|
// errors.
|
||||||
|
func (pb Builder) PlainString() string {
|
||||||
|
return pb.elements.PlainString()
|
||||||
|
}
|
||||||
|
|||||||
@ -223,6 +223,16 @@ func TestPathUnitSuite(t *testing.T) {
|
|||||||
suite.Run(t, &PathUnitSuite{Suite: tester.NewUnitSuite(t)})
|
suite.Run(t, &PathUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set the clues hashing to mask for the span of this suite
|
||||||
|
func (suite *PathUnitSuite) SetupSuite() {
|
||||||
|
clues.SetHasher(clues.HashCfg{HashAlg: clues.Flatmask})
|
||||||
|
}
|
||||||
|
|
||||||
|
// revert clues hashing to plaintext for all other tests
|
||||||
|
func (suite *PathUnitSuite) TeardownSuite() {
|
||||||
|
clues.SetHasher(clues.NoHash())
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestAppend() {
|
func (suite *PathUnitSuite) TestAppend() {
|
||||||
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
|
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
@ -338,7 +348,7 @@ func (suite *PathUnitSuite) TestElements() {
|
|||||||
p, err := test.pathFunc(test.input)
|
p, err := test.pathFunc(test.input)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
assert.Equal(t, test.output, p.Elements())
|
assert.Equal(t, Elements(test.output), p.Elements())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -709,3 +719,33 @@ func (suite *PathUnitSuite) TestFromString() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *PathUnitSuite) TestPath_piiHandling() {
|
||||||
|
p, err := Build("t", "ro", ExchangeService, EventsCategory, true, "dir", "item")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
p Path
|
||||||
|
expect string
|
||||||
|
expectPlain string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "standard path",
|
||||||
|
p: p,
|
||||||
|
expect: "***/exchange/***/events/***/***",
|
||||||
|
expectPlain: "t/exchange/ro/events/dir/item",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
assert.Equal(t, test.expect, test.p.Conceal(), "conceal")
|
||||||
|
assert.Equal(t, test.expectPlain, test.p.String(), "string")
|
||||||
|
assert.Equal(t, test.expect, fmt.Sprintf("%s", test.p), "fmt %%s")
|
||||||
|
assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.p), "fmt %%+v")
|
||||||
|
assert.Equal(t, test.expectPlain, test.p.PlainString(), "plain")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -218,7 +218,7 @@ func (rp dataLayerResourcePath) Folder(escape bool) string {
|
|||||||
|
|
||||||
// Folders returns the individual folder elements embedded in the
|
// Folders returns the individual folder elements embedded in the
|
||||||
// dataLayerResourcePath.
|
// dataLayerResourcePath.
|
||||||
func (rp dataLayerResourcePath) Folders() []string {
|
func (rp dataLayerResourcePath) Folders() Elements {
|
||||||
endIdx := rp.lastFolderIdx()
|
endIdx := rp.lastFolderIdx()
|
||||||
if endIdx == 4 {
|
if endIdx == 4 {
|
||||||
return nil
|
return nil
|
||||||
@ -239,7 +239,7 @@ func (rp dataLayerResourcePath) Item() string {
|
|||||||
|
|
||||||
func (rp dataLayerResourcePath) Dir() (Path, error) {
|
func (rp dataLayerResourcePath) Dir() (Path, error) {
|
||||||
if len(rp.elements) <= 4 {
|
if len(rp.elements) <= 4 {
|
||||||
return nil, clues.New("unable to shorten path").With("path", fmt.Sprintf("%q", rp))
|
return nil, clues.New("unable to shorten path").With("path", rp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &dataLayerResourcePath{
|
return &dataLayerResourcePath{
|
||||||
|
|||||||
@ -136,6 +136,10 @@ func TestDataLayerResourcePath(t *testing.T) {
|
|||||||
suite.Run(t, &DataLayerResourcePath{Suite: tester.NewUnitSuite(t)})
|
suite.Run(t, &DataLayerResourcePath{Suite: tester.NewUnitSuite(t)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *DataLayerResourcePath) SetupSuite() {
|
||||||
|
clues.SetHasher(clues.NoHash())
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *DataLayerResourcePath) TestMissingInfoErrors() {
|
func (suite *DataLayerResourcePath) TestMissingInfoErrors() {
|
||||||
for _, types := range serviceCategories {
|
for _, types := range serviceCategories {
|
||||||
suite.Run(types.service.String()+types.category.String(), func() {
|
suite.Run(types.service.String()+types.category.String(), func() {
|
||||||
@ -402,7 +406,7 @@ func (suite *DataLayerResourcePath) TestToExchangePathForCategory() {
|
|||||||
assert.Equal(t, test.category, p.Category())
|
assert.Equal(t, test.category, p.Category())
|
||||||
assert.Equal(t, testUser, p.ResourceOwner())
|
assert.Equal(t, testUser, p.ResourceOwner())
|
||||||
assert.Equal(t, strings.Join(m.expectedFolders, "/"), p.Folder(false))
|
assert.Equal(t, strings.Join(m.expectedFolders, "/"), p.Folder(false))
|
||||||
assert.Equal(t, m.expectedFolders, p.Folders())
|
assert.Equal(t, path.Elements(m.expectedFolders), p.Folders())
|
||||||
assert.Equal(t, m.expectedItem, p.Item())
|
assert.Equal(t, m.expectedItem, p.Item())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -496,7 +500,7 @@ func (suite *PopulatedDataLayerResourcePath) TestFolders() {
|
|||||||
suite.Run(m.name, func() {
|
suite.Run(m.name, func() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
|
||||||
assert.Equal(t, m.expectedFolders, suite.paths[m.isItem].Folders())
|
assert.Equal(t, path.Elements(m.expectedFolders), suite.paths[m.isItem].Folders())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user