Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API versioning and localfile parallel read with grep #526

Merged
merged 15 commits into from
Feb 12, 2025
Merged
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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,33 @@ time.

TODO: Document service/.../client expectations.

#### Services API versioning and OPA policy

In most cases, services APIs evolve in a fully-backward compatible model,
where adding new parameters or behaviors do not cause unintentional
side-effects on authz decisions made by OPA policy.

Now consider a localfile read which accepts a path to a file, for example
`/tmp/test.txt`. If we extend this service to allow reading all files
in a particular directory (through read request with `/tmp/*` as argument)
we may end up allowing to read `/tmp/secret` file which could be explicitly
denied in the OPA policy.

To allow extensions of Sansshell services functions in a safe way we introduced
a notion of `API version` which follows https://semver.org/. A MAJOR version will
be changed each time we add a backward-incompatible change to Sansshell services.

Default version supported by Sanasshell server is set to `1.0.0`, in order to use
features of higher API version you should audit your OPA policy to check if there
are no unintentional side-effects of allowing new Sansshell features.

#### List of current API versions

- `1.0.0` -- current snapshot of Sansshell API as of
https://github.com/Snowflake-Labs/sansshell/tree/v1.40.4.
- `2.0.0` -- allow to read a contents of whole directory by specifying a trailing
wildcard, for example `localfile read /tmp/*`.

### The Server class

Most of the logic of instantiating a local SansShell server lives in the
Expand Down
1 change: 0 additions & 1 deletion cmd/sansshell-server/default-policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ allow {

allow {
input.type = "LocalFile.ReadActionRequest"
input.message.file.filename = "/etc/hosts"
sfc-gh-mwalas marked this conversation as resolved.
Show resolved Hide resolved
}

allow {
Expand Down
5 changes: 5 additions & 0 deletions cmd/sansshell-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import (
"github.com/Snowflake-Labs/sansshell/auth/opa/rpcauth"
"github.com/Snowflake-Labs/sansshell/cmd/sansshell-server/server"
"github.com/Snowflake-Labs/sansshell/cmd/util"
"github.com/Snowflake-Labs/sansshell/services"
ssutil "github.com/Snowflake-Labs/sansshell/services/util"
"github.com/Snowflake-Labs/sansshell/telemetry/metrics"

Expand Down Expand Up @@ -84,6 +85,7 @@ var (

policyFlag = flag.String("policy", defaultPolicy, "Local OPA policy governing access. If empty, use builtin policy.")
policyFile = flag.String("policy-file", "", "Path to a file with an OPA policy. If empty, uses --policy.")
apiVersion = flag.String("api-version", "1.0.0", "Version of the Sansshell services API accepted by the server. Policy set in proxy and server must be verified before upgrading against API extensions to avoid unintentional side effects.")
hostport = flag.String("hostport", "localhost:50042", "Where to listen for connections.")
debugport = flag.String("debugport", "localhost:50044", "A separate port for http debug pages. Set to an empty string to disable.")
metricsport = flag.String("metricsport", "localhost:50047", "A separate port for http debug pages. Set to an empty string to disable.")
Expand Down Expand Up @@ -129,6 +131,9 @@ func init() {

func main() {
flag.Parse()
if err := services.SetAPIVersion(*apiVersion); err != nil {
log.Fatalf("Unable to set API version: %v\n", err)
}

if version {
fmt.Printf("Version: %s\n", ssserver.Version)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
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/hashicorp/go-version v1.7.0
github.com/joho/godotenv v1.5.1
github.com/open-policy-agent/opa v0.67.0
github.com/pkg/errors v0.9.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum

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

18 changes: 14 additions & 4 deletions services/localfile/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"context"
"flag"
"fmt"
cli_controllers "github.com/Snowflake-Labs/sansshell/services/localfile/client/cli-controllers"
"io"
"io/fs"
"os"
Expand All @@ -30,6 +29,8 @@ import (
"strings"
"time"

cli_controllers "github.com/Snowflake-Labs/sansshell/services/localfile/client/cli-controllers"

"github.com/google/subcommands"
"github.com/schollz/progressbar/v3"

Expand Down Expand Up @@ -88,21 +89,27 @@ func (p *fileCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfac
}

type readCmd struct {
offset int64
length int64
offset int64
length int64
grep string
ignoreCase bool
invertMatch bool
}

func (*readCmd) Name() string { return "read" }
func (*readCmd) Synopsis() string { return "Read a file." }
func (*readCmd) Usage() string {
return `read <path>:
return `read [--grep=PATTERN] [-i] [-v] [--offset=OFFSET] [--length=LENGTH] <path>:
Read from the remote file named by <path> and write it to the appropriate --output destination.
`
}

func (p *readCmd) SetFlags(f *flag.FlagSet) {
f.Int64Var(&p.offset, "offset", 0, "If positive bytes to skip before reading. If negative apply from the end of the file")
f.Int64Var(&p.length, "length", 0, "If positive the maximum number of bytes to read")
f.StringVar(&p.grep, "grep", "", "regular expression filter")
f.BoolVar(&p.ignoreCase, "i", false, "ignore case")
f.BoolVar(&p.invertMatch, "v", false, "invert match")
}

func (p *readCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
Expand All @@ -121,6 +128,9 @@ func (p *readCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfac
Length: p.length,
},
},
Grep: p.grep,
InvertMatch: p.invertMatch,
IgnoreCase: p.ignoreCase,
}

return readFile(ctx, state, req)
Expand Down
Loading
Loading