Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion services/httpoverrpc/httpoverrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ package httpoverrpc

// To regenerate the proto headers if the proto changes, just run go generate
// and this encodes the necessary magic:
//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=require_unimplemented_servers=false:. --go-grpc_opt=paths=source_relative --go-grpcproxy_out=. --go-grpcproxy_opt=paths=source_relative httpoverrpc.proto
//go:generate protoc --proto_path=../../ --go_out=../../ --go_opt=paths=source_relative --go-grpc_out=require_unimplemented_servers=false:. --go-grpc_opt=paths=source_relative --go-grpcproxy_out=../../ --go-grpcproxy_opt=paths=source_relative services/httpoverrpc/httpoverrpc.proto
222 changes: 114 additions & 108 deletions services/httpoverrpc/httpoverrpc.pb.go

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion services/httpoverrpc/httpoverrpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
syntax = "proto3";

import "google/protobuf/descriptor.proto";
import "services/mpa/annotations/mpa_annotations.proto";

option go_package = "github.com/Snowflake-Labs/sansshell/httpoverrpc";

Expand All @@ -29,7 +30,7 @@ service HTTPOverRPC {
}

message HostHTTPRequest {
HTTPRequest request = 1;
HTTPRequest request = 1 [(sansshell.annotations.mpa_redacted) = true];
// The port to use for the request on the local host.
int32 port = 2;
// Hostname can be specified as either an ip address or domain name
Expand Down
22 changes: 22 additions & 0 deletions services/mpa/annotations/annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* Copyright (c) 2025 Snowflake Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/

// Package mpa defines the RPC interface for the sansshell MPA actions.
package annotations

// To regenerate the proto headers if the .proto changes, just run go generate
// and this encodes the necessary magic:
//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=require_unimplemented_servers=false:. --go-grpc_opt=paths=source_relative --go-grpcproxy_out=. --go-grpcproxy_opt=paths=source_relative mpa_annotations.proto
94 changes: 94 additions & 0 deletions services/mpa/annotations/mpa_annotations.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions services/mpa/annotations/mpa_annotations.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* Copyright (c) 2019 Snowflake Inc. All rights reserved.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update copyright year


Licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/

syntax = "proto3";

package sansshell.annotations;

import "google/protobuf/descriptor.proto";

option go_package = "github.com/Snowflake-Labs/sansshell/services/mpa/annotations";

extend google.protobuf.FieldOptions {
bool mpa_redacted = 50000; // Using a high number to avoid conflicts
}
117 changes: 117 additions & 0 deletions services/mpa/mpahooks/mpa_redact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* Copyright (c) 2025 Snowflake Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/

package mpahooks

import (
"fmt"

"github.com/Snowflake-Labs/sansshell/services/mpa/annotations"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/known/anypb"
)

// RedactFieldsForMPA processes an Any proto message and redacts (sets to nil/zero)
// any fields that are marked with the mpa_redacted annotation.
// Returns true if the message was modified.
func RedactFieldsForMPA(anyMsg *anypb.Any) (bool, error) {
if anyMsg == nil {
return false, nil
}
// First extract the message from Any
msg, err := anyMsg.UnmarshalNew()
if err != nil {
return false, fmt.Errorf("failed to unmarshal Any message: %v", err)
}

// Process the message to redact marked fields
modified := redactMessageFields(msg)

// If we made changes, update the Any message
if modified {
if err := anyMsg.MarshalFrom(msg); err != nil {
return false, fmt.Errorf("failed to re-marshal message: %v", err)
}
}

return modified, nil
}

// redactMessageFields recursively processes a message and redacts fields
// marked with the mpa_redacted annotation.
// Returns true if any field was redacted.
func redactMessageFields(message proto.Message) bool {
modified := false

// Get the reflective view of the message
m := message.ProtoReflect()

// Iterate through all fields in the message
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
// Check if this field has the mpa_redacted option set
opts := fd.Options().(*descriptorpb.FieldOptions)

if proto.GetExtension(opts, annotations.E_MpaRedacted).(bool) {
m.Clear(fd)
modified = true
return true // Continue iteration
}

// If it's a message field that's not nil, recursively process it
if fd.Kind() == protoreflect.MessageKind && v.IsValid() {
if fd.IsList() {
// Handle repeated message fields
list := v.List()
for i := 0; i < list.Len(); i++ {
item := list.Get(i)
if item.Message().IsValid() {
// Create a new proto.Message from this item
nestedMsg := item.Message().Interface()
if redactMessageFields(nestedMsg) {
modified = true
}
}
}
} else if fd.IsMap() {
// Handle map fields where values are messages
if fd.MapValue().Kind() == protoreflect.MessageKind {
mapVal := v.Map()
mapVal.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
if v.Message().IsValid() {
nestedMsg := v.Message().Interface()
if redactMessageFields(nestedMsg) {
modified = true
}
}
return true
})
}
} else if v.Message().IsValid() {
// Handle regular message fields
nestedMsg := v.Message().Interface()
if redactMessageFields(nestedMsg) {
modified = true
}
}
}

return true // Continue iteration
})

return modified
}
10 changes: 10 additions & 0 deletions services/mpa/mpahooks/mpahooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ func ActionMatchesInput(ctx context.Context, action *mpa.Action, input *rpcauth.
return fmt.Errorf("unable to marshal into anyproto: %v", err)
}

// Redact fields that shouldn't be checked for equality
if _, err := RedactFieldsForMPA(&msg); err != nil {
return fmt.Errorf("error redacting message for MPA: %v", err)
}

// Prefer using a proxied identity if provided
var user string
if p := proxiedidentity.FromContext(ctx); p != nil {
Expand Down Expand Up @@ -120,6 +125,11 @@ func createAndBlockOnSingleTargetMPA(ctx context.Context, method string, req any
return "", fmt.Errorf("unable to marshal into anyproto: %v", err)
}

// Redact fields that shouldn't be stored in the MPA
if _, err := RedactFieldsForMPA(&msg); err != nil {
return "", fmt.Errorf("error redacting message for MPA: %v", err)
}

mpaClient := mpa.NewMpaClient(cc)
result, err := mpaClient.Store(ctx, &mpa.StoreRequest{
Method: method,
Expand Down
8 changes: 8 additions & 0 deletions services/mpa/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,18 @@ func actionId(action *mpa.Action) (string, error) {
// output for the same input. Go provides a deterministic marshalling option,
// but this marshalling isn't guaranteed to be stable over time.
// JSON encoding can be made deterministic by canonicalizing.

msg := action.Message
// Redact fields that shouldn't be checked for equality
if _, err := mpahooks.RedactFieldsForMPA(msg); err != nil {
return "", fmt.Errorf("error redacting message for MPA: %v", err)
}

b, err := protojson.Marshal(action)
if err != nil {
return "", err
}

canonical, err := jcs.Transform(b)
if err != nil {
return "", err
Expand Down
Loading