From 92412645ecb644f1c7e4b8926b29d49179a1ae3e Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 25 Jul 2023 11:25:34 -0600 Subject: [PATCH] pii handling for restore config (#3896) add pii concealer compliance for restore config structs. --- #### Does this PR need a docs update or release note? - [x] :no_entry: No #### Type of change - [x] :sunflower: Feature --- src/internal/m365/restore.go | 2 +- src/pkg/control/restore.go | 67 ++++++++++++++++++++++++++++-- src/pkg/services/m365/api/sites.go | 8 ++-- 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/internal/m365/restore.go b/src/internal/m365/restore.go index 31e36e2bb..5d58fdb26 100644 --- a/src/internal/m365/restore.go +++ b/src/internal/m365/restore.go @@ -36,7 +36,7 @@ func (ctrl *Controller) ConsumeRestoreCollections( defer end() ctx = graph.BindRateLimiterConfig(ctx, graph.LimiterCfg{Service: sels.PathService()}) - ctx = clues.Add(ctx, "restore_config", restoreCfg) // TODO(rkeepers): needs PII control + ctx = clues.Add(ctx, "restore_config", restoreCfg) if len(dcs) == 0 { return nil, clues.New("no data collections to restore") diff --git a/src/pkg/control/restore.go b/src/pkg/control/restore.go index 2b4129d9f..79d49ae20 100644 --- a/src/pkg/control/restore.go +++ b/src/pkg/control/restore.go @@ -2,13 +2,17 @@ package control import ( "context" + "encoding/json" + "fmt" "strings" + "github.com/alcionai/clues" "golang.org/x/exp/maps" "golang.org/x/exp/slices" "github.com/alcionai/corso/src/internal/common/dttm" "github.com/alcionai/corso/src/pkg/logger" + "github.com/alcionai/corso/src/pkg/path" ) const ( @@ -39,24 +43,24 @@ const RootLocation = "/" type RestoreConfig struct { // Defines the per-item collision handling policy. // Defaults to Skip. - OnCollision CollisionPolicy + OnCollision CollisionPolicy `json:"onCollision"` // ProtectedResource specifies which resource the data will be restored to. // If empty, restores to the same resource that was backed up. // Defaults to empty. - ProtectedResource string + ProtectedResource string `json:"protectedResource"` // Location specifies the container into which the data will be restored. // Only accepts container names, does not accept IDs. // If empty or "/", data will get restored in place, beginning at the root. // Defaults to "Corso_Restore_" - Location string + Location string `json:"location"` // Drive specifies the name of the drive into which the data will be // restored. If empty, data is restored to the same drive that was backed // up. // Defaults to empty. - Drive string + Drive string `json:"drive"` } func DefaultRestoreConfig(timeFormat dttm.TimeFormat) RestoreConfig { @@ -90,3 +94,58 @@ func EnsureRestoreConfigDefaults( return rc } + +// --------------------------------------------------------------------------- +// pii control +// --------------------------------------------------------------------------- + +var ( + // interface compliance required for handling PII + _ clues.Concealer = &RestoreConfig{} + _ fmt.Stringer = &RestoreConfig{} + + // interface compliance for the observe package to display + // values without concealing PII. + _ clues.PlainStringer = &RestoreConfig{} +) + +func (rc RestoreConfig) marshal() string { + bs, err := json.Marshal(rc) + if err != nil { + return "err marshalling" + } + + return string(bs) +} + +func (rc RestoreConfig) concealed() RestoreConfig { + return RestoreConfig{ + OnCollision: rc.OnCollision, + ProtectedResource: clues.Hide(rc.ProtectedResource).Conceal(), + Location: path.LoggableDir(rc.Location), + Drive: clues.Hide(rc.Drive).Conceal(), + } +} + +// Conceal produces a concealed representation of the config, suitable for +// logging, storing in errors, and other output. +func (rc RestoreConfig) Conceal() string { + return rc.concealed().marshal() +} + +// Format produces a concealed representation of the config, even when +// used within a PrintF, suitable for logging, storing in errors, +// and other output. +func (rc RestoreConfig) Format(fs fmt.State, _ rune) { + fmt.Fprint(fs, rc.concealed()) +} + +// String returns a plain text version of the restoreConfig. +func (rc RestoreConfig) String() string { + return rc.PlainString() +} + +// PlainString returns an unescaped, unmodified string of the restore configuration. +func (rc RestoreConfig) PlainString() string { + return rc.marshal() +} diff --git a/src/pkg/services/m365/api/sites.go b/src/pkg/services/m365/api/sites.go index e573cfc07..4e13ebcfb 100644 --- a/src/pkg/services/m365/api/sites.go +++ b/src/pkg/services/m365/api/sites.go @@ -225,13 +225,13 @@ func ValidateSite(item models.Siteable) error { wURL := ptr.Val(item.GetWebUrl()) if len(wURL) == 0 { - return clues.New("missing webURL").With("site_id", id) // TODO: pii + return clues.New("missing webURL").With("site_id", clues.Hide(id)) } // personal (ie: oneDrive) sites have to be filtered out server-side. if strings.Contains(wURL, PersonalSitePath) { return clues.Stack(ErrKnownSkippableCase). - With("site_id", id, "site_web_url", wURL) // TODO: pii + With("site_id", clues.Hide(id), "site_web_url", clues.Hide(wURL)) } name := ptr.Val(item.GetDisplayName()) @@ -239,10 +239,10 @@ func ValidateSite(item models.Siteable) error { // the built-in site at "https://{tenant-domain}/search" never has a name. if strings.HasSuffix(wURL, "/search") { return clues.Stack(ErrKnownSkippableCase). - With("site_id", id, "site_web_url", wURL) // TODO: pii + With("site_id", clues.Hide(id), "site_web_url", clues.Hide(wURL)) } - return clues.New("missing site display name").With("site_id", id) + return clues.New("missing site display name").With("site_id", clues.Hide(id)) } return nil