diff --git a/client/setec/keyring.go b/client/setec/keyring.go new file mode 100644 index 0000000..7fa6511 --- /dev/null +++ b/client/setec/keyring.go @@ -0,0 +1,64 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//lint:file-ignore U1000 This is work in progress. + +package setec + +import ( + "sync" + + "github.com/tailscale/setec/types/api" +) + +/* + TODO: + - Cache needs to be able to hold multiple values. + - Keep track of single- vs keyring-valued secrets. + - Update polling logic to use GetAll for keyrings. + - Add settings. +*/ + +// A Keyring represents a a collection of one or more available versions of a +// named secret. +type Keyring struct { + name string + + mu sync.Mutex + active int // index into values + values []*api.SecretValue // in increasing order by version +} + +// Get returns the key at index i of the keyring. Index 0 always refers to the +// current active version, and further indexes refer to other versions in order +// from newest to oldest. +// +// If i >= r.Len(), Get returns nil. If i < 0, Get panics. +// +// The Keyring retains ownership of the bytes returned, but the store will +// never modify the contents of the secret, so it is safe to share the slice +// without copying as long as the caller does not modify them. +func (r *Keyring) Get(i int) []byte { + r.mu.Lock() + defer r.mu.Unlock() + switch { + case i < 0: + panic("index is negative") + case i < len(r.values): + return r.values[i].Value + default: + return nil + } +} + +// GetString returns a copy of the key at index i of the keyring as a string. +// If i >= r.Len(), GetString returns "". Otherwise, GetString behaves as Get. +func (r *Keyring) GetString(i int) string { return string(r.Get(i)) } + +// Len reports the number of keys stored in r. The length is always positive, +// counting the active version. +func (r *Keyring) Len() int { + r.mu.Lock() + defer r.mu.Unlock() + return len(r.values) +} diff --git a/db/db.go b/db/db.go index fdf9671..00c1135 100644 --- a/db/db.go +++ b/db/db.go @@ -183,6 +183,47 @@ func (db *DB) Get(caller Caller, name string) (*api.SecretValue, error) { return db.kv.get(name) } +// GetAllFiltered returns all versions of a secret and the active version number. +func (db *DB) GetAllFiltered(caller Caller, name string, skip []api.SecretVersion) (*Filtered, error) { + // We will not log an access unless we are returning at least one unfiltered + // secret value. However, we still want a log if authorization fails. + if !caller.Permissions.Allow(acl.ActionGet, name) { + return nil, db.checkAndLog(caller, acl.ActionGet, name, 0) + } + db.mu.Lock() + defer db.mu.Unlock() + active, all, err := db.kv.getAll(name) + if err != nil { + return nil, err + } + out := &Filtered{ + Active: active, + Versions: make([]api.SecretVersion, len(all)), + } + for i, sv := range all { + out.Versions[i] = sv.Version + if !slices.Contains(skip, sv.Version) { + out.Values = append(out.Values, sv) + } + } + + if len(out.Values) != 0 { + // We got unfiltered secret values, log an access. We already know it's + // authorized from the original check. + if err := db.checkAndLog(caller, acl.ActionGet, name, 0); err != nil { + return nil, err + } + } + return out, nil +} + +// Filtered is the result of a successful GetAllFiltered call. +type Filtered struct { + Active api.SecretVersion // the active version of the secret + Versions []api.SecretVersion // all known versions of the secret + Values []*api.SecretValue // all non-filtered values of the secret +} + // GetConditional returns a secret's active value if it is different from oldVersion. // If the active version is the same as oldVersion, it reports api.ErrValueNotChanged. func (db *DB) GetConditional(caller Caller, name string, oldVersion api.SecretVersion) (*api.SecretValue, error) { diff --git a/db/kv.go b/db/kv.go index e30bb51..84903c7 100644 --- a/db/kv.go +++ b/db/kv.go @@ -304,6 +304,22 @@ func (kv *kv) get(name string) (*api.SecretValue, error) { }, nil } +// getAll returns all the versions of a secret, and the active version number. +func (kv *kv) getAll(name string) (api.SecretVersion, []*api.SecretValue, error) { + secret := kv.secrets[name] + if secret == nil { + return 0, nil, ErrNotFound + } + all := make([]*api.SecretValue, 0, len(secret.Versions)) + for v, bs := range secret.Versions { + all = append(all, &api.SecretValue{ + Value: []byte(bs), + Version: v, + }) + } + return secret.ActiveVersion, all, nil +} + // getVersion returns a secret's value at a specific version. func (kv *kv) getVersion(name string, version api.SecretVersion) (*api.SecretValue, error) { secret := kv.secrets[name] diff --git a/server/server.go b/server/server.go index cc2ae05..52dc59c 100644 --- a/server/server.go +++ b/server/server.go @@ -15,6 +15,7 @@ import ( "log" "net/http" "net/netip" + "slices" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" @@ -149,6 +150,7 @@ func New(ctx context.Context, cfg Config) (*Server, error) { cfg.Mux.Handle("/static/", http.FileServer(http.FS(staticFiles))) cfg.Mux.HandleFunc("/api/list", ret.list) cfg.Mux.HandleFunc("/api/get", ret.get) + cfg.Mux.HandleFunc("/api/get-all", ret.getAll) cfg.Mux.HandleFunc("/api/info", ret.info) cfg.Mux.HandleFunc("/api/put", ret.put) cfg.Mux.HandleFunc("/api/activate", ret.activate) @@ -239,6 +241,21 @@ func (s *Server) get(w http.ResponseWriter, r *http.Request) { }) } +func (s *Server) getAll(w http.ResponseWriter, r *http.Request) { + serveJSON(s, w, r, func(req api.GetAllRequest, id db.Caller) (*api.GetAllResponse, error) { + filtered, err := s.db.GetAllFiltered(id, req.Name, req.SkipVersions) + if err != nil { + return nil, err + } + slices.Sort(filtered.Versions) + return &api.GetAllResponse{ + Active: filtered.Active, + Versions: filtered.Versions, + Values: filtered.Values, + }, nil + }) +} + func (s *Server) info(w http.ResponseWriter, r *http.Request) { serveJSON(s, w, r, func(req api.InfoRequest, id db.Caller) (*api.SecretInfo, error) { return s.db.Info(id, req.Name) diff --git a/types/api/api.go b/types/api/api.go index b43d0a9..e1b04ab 100644 --- a/types/api/api.go +++ b/types/api/api.go @@ -82,6 +82,31 @@ type GetRequest struct { UpdateIfChanged bool } +// GetAllRequest is a request to get all the available versions of a secret. +type GetAllRequest struct { + // Name is the name of the secret to fetch. + Name string + + // SkipVersions, if non-empty, lists versions the caller already has and + // does not need to have returned. The server will not return any of the + // versions listed here. + SkipVersions []SecretVersion +} + +// GetAllResponse is a response from a successful GetAllRequest. +type GetAllResponse struct { + // Active is the current active version of the secret. It is set even if no + // secret values are returned. + Active SecretVersion + + // Versions contains all the known versions of the secret. + Versions []SecretVersion + + // Values contains all the known values of the secret, but omitting any + // versions that were listed in the SkipVersions field of the request. + Values []*SecretValue +} + // InfoRequest is a request for secret metadata. type InfoRequest struct { // Name is the name of the secret whose metadata to return.