diff --git a/api/sys_namespaces.go b/api/sys_namespaces.go new file mode 100644 index 0000000000..5d86a5514f --- /dev/null +++ b/api/sys_namespaces.go @@ -0,0 +1,97 @@ +package api + +import ( + "fmt" + "net/http" +) + +// ListNamespacesResponse is the response from the ListNamespaces call. +type ListNamespacesResponse struct { + // NamespacePaths is the list of child namespace paths + NamespacePaths []string `json:"namespace_paths"` +} + +type GetNamespaceResponse struct { + Path string `json:"path"` +} + +// ListNamespaces lists any existing namespace relative to the namespace +// provided in the client's namespace header. +func (c *Sys) ListNamespaces() (*ListNamespacesResponse, error) { + r := c.c.NewRequest("LIST", "/v1/sys/namespaces") + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result struct { + Data struct { + Keys []string `json:"keys"` + } `json:"data"` + } + err = resp.DecodeJSON(&result) + if err != nil { + return nil, err + } + + return &ListNamespacesResponse{NamespacePaths: result.Data.Keys}, nil +} + +// GetNamespace returns namespace information +func (c *Sys) GetNamespace(path string) (*GetNamespaceResponse, error) { + r := c.c.NewRequest("GET", fmt.Sprintf("/v1/sys/namespaces/%s", path)) + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + return nil, nil + } + + ret := &GetNamespaceResponse{} + result := map[string]interface{}{ + "data": map[string]interface{}{}, + } + if err := resp.DecodeJSON(&result); err != nil { + return nil, err + } + + if data, ok := result["data"]; ok { + if pathOk, ok := data.(map[string]interface{})["path"]; ok { + if pathRaw, ok := pathOk.(string); ok { + ret.Path = pathRaw + } + } + } + + return ret, nil +} + +// CreateNamespace creates a new namespace relative to the namespace provided +// in the client's namespace header. +func (c *Sys) CreateNamespace(path string) error { + r := c.c.NewRequest("POST", fmt.Sprintf("/v1/sys/namespaces/%s", path)) + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// DeleteNamespace delete an existing namespace relative to the namespace +// provided in the client's namespace header. +func (c *Sys) DeleteNamespace(path string) error { + r := c.c.NewRequest("DELETE", fmt.Sprintf("/v1/sys/namespaces/%s", path)) + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/command/base.go b/command/base.go index e0381267d4..99c77b12da 100644 --- a/command/base.go +++ b/command/base.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/command/token" + "github.com/hashicorp/vault/helper/namespace" "github.com/mitchellh/cli" "github.com/pkg/errors" "github.com/posener/complete" @@ -37,6 +38,7 @@ type BaseCommand struct { flagCAPath string flagClientCert string flagClientKey string + flagNamespace string flagTLSServerName string flagTLSSkipVerify bool flagWrapTTL time.Duration @@ -118,6 +120,7 @@ func (c *BaseCommand) Client() (*api.Client, error) { } client.SetMFACreds(c.flagMFA) + client.SetNamespace(namespace.Canonicalize(c.flagNamespace)) c.client = client @@ -236,6 +239,16 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { "matching the client certificate from -client-cert.", }) + f.StringVar(&StringVar{ + Name: "namespace", + Target: &c.flagNamespace, + Default: "", + EnvVar: "VAULT_NAMESPACE", + Completion: complete.PredictAnything, + Usage: "The namespace to use for the command. Setting this is not " + + "necessary but allows using relative paths.", + }) + f.StringVar(&StringVar{ Name: "tls-server-name", Target: &c.flagTLSServerName, diff --git a/helper/namespace/namespace.go b/helper/namespace/namespace.go new file mode 100644 index 0000000000..1c57040a97 --- /dev/null +++ b/helper/namespace/namespace.go @@ -0,0 +1,107 @@ +package namespace + +import ( + "context" + "errors" + "strings" +) + +type nsContext struct { + context.Context + // Note: this is currently not locked because we think all uses will take + // place within a single goroutine. If that isn't the case, this should be + // protected by an atomic.Value. + cachedNS *Namespace +} + +type contextValues struct{} + +const ( + RootNamespaceID = "root" +) + +var ( + contextNamespace contextValues = struct{}{} + ErrNoNamespace error = errors.New("no namespace") +) + +type Namespace struct { + ID string `json:"id"` + Path string `json:"path"` +} + +func New(id, path string) *Namespace { + return &Namespace{ + ID: id, + Path: path, + } +} + +func (n *Namespace) HasParent(possibleParent *Namespace) bool { + switch { + case n.Path == "": + return false + case possibleParent.Path == "": + return true + default: + return strings.HasPrefix(n.Path, possibleParent.Path) + } +} + +func (n *Namespace) TrimmedPath(path string) string { + return strings.TrimPrefix(path, n.Path) +} + +func ContextWithNamespace(ctx context.Context, ns *Namespace) context.Context { + nsCtx := context.WithValue(ctx, contextNamespace, ns) + return &nsContext{ + Context: nsCtx, + cachedNS: ns, + } +} + +func FromContext(ctx context.Context) (*Namespace, error) { + if ctx == nil { + return nil, errors.New("context was nil") + } + + nsCtx, ok := ctx.(*nsContext) + if ok { + if nsCtx.cachedNS != nil { + return nsCtx.cachedNS, nil + } + } + + ns := ctx.Value(contextNamespace) + if ns == nil { + return nil, ErrNoNamespace + } + + if ok { + nsCtx.cachedNS = ns.(*Namespace) + } + + return ns.(*Namespace), nil +} + +func TestContext() context.Context { + return ContextWithNamespace(context.Background(), New(RootNamespaceID, "")) +} + +// Canonicalize trims any prefix '/' and adds a trailing '/' to the +// provided string +func Canonicalize(nsPath string) string { + if nsPath == "" { + return "" + } + + // Canonicalize the path to not have a '/' prefix + nsPath = strings.TrimPrefix(nsPath, "/") + + // Canonicalize the path to always having a '/' suffix + if !strings.HasSuffix(nsPath, "/") { + nsPath += "/" + } + + return nsPath +}