Skip to content

Commit

Permalink
Add server and client implementations for MPA.
Browse files Browse the repository at this point in the history
These changes alongside Snowflake-Labs#351 are sufficient for MPA when using a direct connection to the server. Here's a few sample commands to try it out.

```
go run ./cmd/sansshell-server
go run ./cmd/sanssh -mpa -targets localhost healthcheck validate
go run ./cmd/sanssh -targets localhost mpa approve 12345
```

This implements the client and server portion, but not the proxy portion. The proxy needs some additional features in core sansshell code before we can implement it.

- Snowflake-Labs#361 for implementing the proxy equivalent of `ServerMPAAuthzHook()`
- Snowflake-Labs#358 for implementing the proxy equivalents of `mpahooks.UnaryClientIntercepter()` and `mpahooks.StreamClientIntercepter()`
- Snowflake-Labs#359 so that MPA can use the identity of the caller to the proxy instead of the identity of the proxy.

Part of Snowflake-Labs#346
  • Loading branch information
stvnrhodes committed Oct 24, 2023
1 parent f8d2ce3 commit 888a7a5
Show file tree
Hide file tree
Showing 14 changed files with 1,297 additions and 1 deletion.
20 changes: 19 additions & 1 deletion auth/opa/rpcauth/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
)

// RPCAuthInput is used as policy input to validate Sansshell RPCs
// NOTE: RPCAuthInputForLogging must be updated when this changes.
type RPCAuthInput struct {
// The GRPC method name, as '/Package.Service/Method'
Method string `json:"method"`
Expand All @@ -52,6 +51,9 @@ type RPCAuthInput struct {
// Information about the host serving the RPC.
Host *HostAuthInput `json:"host"`

// Information about approvers when using multi-party authentication.
Approvers []*PrincipalAuthInput `json:"approvers"`

// Information about the environment in which the policy evaluation is
// happening.
Environment *EnvironmentInput `json:"environment"`
Expand Down Expand Up @@ -153,9 +155,25 @@ func NewRPCAuthInput(ctx context.Context, method string, req proto.Message) (*RP
return out, nil
}

type peerInfoKey struct{}

func AddPeerToContext(ctx context.Context, p *PeerAuthInput) context.Context {
if p == nil {
return ctx
}
return context.WithValue(ctx, peerInfoKey{}, p)
}

// PeerInputFromContext populates peer information from the supplied
// context, if available.
func PeerInputFromContext(ctx context.Context) *PeerAuthInput {
// If this runs after rpcauth hooks, we can return richer data that includes
// information added by the hooks.
cached, ok := ctx.Value(peerInfoKey{}).(*PeerAuthInput)
if ok {
return cached
}

out := &PeerAuthInput{}
p, ok := peer.FromContext(ctx)
if !ok {
Expand Down
7 changes: 7 additions & 0 deletions cmd/sanssh/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/Snowflake-Labs/sansshell/proxy/proxy"

cmdUtil "github.com/Snowflake-Labs/sansshell/cmd/util"
"github.com/Snowflake-Labs/sansshell/services/mpa/mpahooks"
"github.com/Snowflake-Labs/sansshell/services/util"
)

Expand Down Expand Up @@ -74,6 +75,8 @@ type RunState struct {
// BatchSize if non-zero will do the requested operation to the targets but in
// N calls to the proxy where N is the target list size divided by BatchSize.
BatchSize int
// If true, add an interceptor that performs the multi-party auth flow
EnableMPA bool
}

const (
Expand Down Expand Up @@ -317,6 +320,10 @@ func Run(ctx context.Context, rs RunState) {
streamInterceptors = append(streamInterceptors, clientAuthz.AuthorizeClientStream)
unaryInterceptors = append(unaryInterceptors, clientAuthz.AuthorizeClient)
}
if rs.EnableMPA {
unaryInterceptors = append(unaryInterceptors, mpahooks.UnaryClientIntercepter())
streamInterceptors = append(streamInterceptors, mpahooks.StreamClientIntercepter())
}
// timeout interceptor should be the last item in ops so that it's executed first.
streamInterceptors = append(streamInterceptors, StreamClientTimeoutInterceptor(rs.IdleTimeout))
unaryInterceptors = append(unaryInterceptors, UnaryClientTimeoutInterceptor(rs.IdleTimeout))
Expand Down
3 changes: 3 additions & 0 deletions cmd/sanssh/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ If port is blank the default of %d will be used`, proxyEnv, defaultProxyPort))
verbosity = flag.Int("v", -1, "Verbosity level. > 0 indicates more extensive logging")
prefixHeader = flag.Bool("h", false, "If true prefix each line of output with '<index>-<target>: '")
batchSize = flag.Int("batch-size", 0, "If non-zero will perform the proxy->target work in batches of this size (with any remainder done at the end).")
mpa = flag.Bool("mpa", false, "Request multi-party approval for changes")

// targets will be bound to --targets for sending a single request to N nodes.
targetsFlag util.StringSliceCommaOrWhitespaceFlag
Expand Down Expand Up @@ -118,6 +119,7 @@ func init() {
subcommands.ImportantFlag("justification")
subcommands.ImportantFlag("client-policy")
subcommands.ImportantFlag("client-policy-file")
subcommands.ImportantFlag("mpa")
subcommands.ImportantFlag("v")
}

Expand Down Expand Up @@ -192,6 +194,7 @@ func main() {
ClientPolicy: clientPolicy,
PrefixOutput: *prefixHeader,
BatchSize: *batchSize,
EnableMPA: *mpa,
}
ctx := logr.NewContext(context.Background(), logger)

Expand Down
2 changes: 2 additions & 0 deletions cmd/sansshell-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import (
fdbserver "github.com/Snowflake-Labs/sansshell/services/fdb/server"
_ "github.com/Snowflake-Labs/sansshell/services/healthcheck/server"
_ "github.com/Snowflake-Labs/sansshell/services/localfile/server"
mpa "github.com/Snowflake-Labs/sansshell/services/mpa/server"
_ "github.com/Snowflake-Labs/sansshell/services/power/server"

// Packages needs a real import to bind flags.
Expand Down Expand Up @@ -170,6 +171,7 @@ func main() {
server.WithHostPort(*hostport),
server.WithParsedPolicy(parsed),
server.WithJustification(*justification),
server.WithAuthzHook(mpa.ServerMPAAuthzHook()),
server.WithRawServerOption(func(s *grpc.Server) { reflection.Register(s) }),
server.WithRawServerOption(func(s *grpc.Server) { channelz.RegisterChannelzServiceToServer(s) }),
server.WithDebugPort(*debugport),
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/go-logr/stdr v1.2.2
github.com/google/go-cmp v0.6.0
github.com/google/subcommands v1.2.0
github.com/gowebpki/jcs v1.0.1
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0
github.com/open-policy-agent/opa v0.57.1
github.com/pkg/errors v0.9.1
Expand All @@ -21,6 +22,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.19.0
go.opentelemetry.io/otel/trace v1.19.0
gocloud.dev v0.32.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/sync v0.4.0
golang.org/x/sys v0.13.0
google.golang.org/grpc v1.59.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum

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

Loading

0 comments on commit 888a7a5

Please sign in to comment.