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

Global flag that outputs minimum policy HCL required for an operation #14899

Merged
merged 42 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
178e45c
WIP: output policy
digivava Feb 24, 2022
43a4552
Merge branch 'main' into digivava/output-policy
digivava Feb 28, 2022
f990174
Outputs example policy HCL for given request
digivava Mar 3, 2022
7b85ee3
Merge branch 'main' into digivava/output-policy
digivava Mar 21, 2022
ca8965b
Simplify conditional
digivava Mar 22, 2022
bd8acc4
Add PATCH capability
digivava Mar 23, 2022
ac41617
Fixed merge conflict
digivava Apr 1, 2022
d98c73b
Use OpenAPI spec and regex patterns to determine if path is sudo
digivava Apr 4, 2022
651dd90
Add test for isSudoPath
digivava Apr 4, 2022
556b690
Add changelog
digivava Apr 4, 2022
75ea6eb
Fix broken CLI tests
digivava Apr 5, 2022
37ff76b
Add output-policy to client cloning code
digivava Apr 5, 2022
584bc78
Smaller fixes from PR comments
digivava Apr 5, 2022
17fc209
Clone client instead of saving and restoring custom values
digivava Apr 6, 2022
96a611b
Fix test
digivava Apr 6, 2022
4ba6890
Address comments
digivava Apr 6, 2022
36c2276
Don't unset output-policy flag on KV requests otherwise the preflight…
digivava Apr 7, 2022
9da6de6
Merge from main
digivava Apr 8, 2022
1d80573
Print errors saved in buffer from preflight KV requests
digivava Apr 18, 2022
e450b9c
Unescape characters in request URL
digivava Apr 18, 2022
79f9bf8
Merge from main
digivava Apr 18, 2022
bfb755f
Rename methods and properties to improve readability
digivava Apr 20, 2022
29dd10b
Put KV-specificness at front of KV-specific error
digivava Apr 20, 2022
93b0373
Simplify logic by doing more direct returns of strings and errors
digivava Apr 20, 2022
c3f9936
Merge branch 'main' into digivava/output-policy
digivava Apr 25, 2022
13d09d8
Use precompiled regexes and move OpenAPI call to tests
digivava Apr 25, 2022
922e550
Merge branch 'main' into digivava/output-policy
digivava Apr 25, 2022
4cdf1ea
Merge branch 'main' into digivava/output-policy-static-list
digivava Apr 25, 2022
987c0b6
Remove commented out code
digivava Apr 25, 2022
e685dbb
Remove legacy MFA paths
digivava Apr 25, 2022
6fc4b6a
Merge branch 'digivava/output-policy' into digivava/output-policy-sta…
digivava Apr 25, 2022
2293944
Remove unnecessary use of client
digivava Apr 25, 2022
cc43942
Move sudo paths map to plugin helper
digivava Apr 25, 2022
42c4025
Remove unused error return
digivava Apr 25, 2022
32fe843
Add explanatory comment
digivava Apr 25, 2022
d44f61f
Remove need to pass in address
digivava Apr 25, 2022
7327e9e
Make {name} regex less greedy
digivava Apr 26, 2022
47f0b05
Use method and path instead of info from retryablerequest
digivava Apr 26, 2022
6107c62
Add test for IsSudoPaths, use more idiomatic naming
digivava Apr 26, 2022
384e2e8
Use precompiled regexes and move OpenAPI call to tests (#15170)
digivava Apr 26, 2022
4b3ad0c
Make stderr writing more obvious, fix nil pointer deref
digivava Apr 27, 2022
d544236
Merge readability and nil pointer fixes
digivava Apr 27, 2022
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
42 changes: 41 additions & 1 deletion api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ type Config struct {
// with the same client. Cloning a client will not clone this value.
OutputCurlString bool

// OutputPolicy causes the actual request to return an error of type
// *OutputPolicyError. Type asserting the error message will display
// an example of the required policy HCL needed for the operation.
//
// Note: It is not thread-safe to set this and make concurrent requests
// with the same client. Cloning a client will not clone this value.
OutputPolicy bool

// curlCACert, curlCAPath, curlClientCert and curlClientKey are used to keep
// track of the name of the TLS certs and keys when OutputCurlString is set.
// Cloning a client will also not clone those values.
Expand Down Expand Up @@ -779,6 +787,24 @@ func (c *Client) SetOutputCurlString(curl bool) {
c.config.OutputCurlString = curl
}

func (c *Client) OutputPolicy() bool {
c.modifyLock.RLock()
defer c.modifyLock.RUnlock()
c.config.modifyLock.RLock()
defer c.config.modifyLock.RUnlock()

return c.config.OutputPolicy
}

func (c *Client) SetOutputPolicy(isSet bool) {
c.modifyLock.RLock()
defer c.modifyLock.RUnlock()
c.config.modifyLock.Lock()
defer c.config.modifyLock.Unlock()

c.config.OutputPolicy = isSet
}

// CurrentWrappingLookupFunc sets a lookup function that returns desired wrap TTLs
// for a given operation and path.
func (c *Client) CurrentWrappingLookupFunc() WrappingLookupFunc {
Expand Down Expand Up @@ -1172,6 +1198,7 @@ func (c *Client) rawRequestWithContext(ctx context.Context, r *Request) (*Respon
httpClient := c.config.HttpClient
ns := c.headers.Get(consts.NamespaceHeaderName)
outputCurlString := c.config.OutputCurlString
outputPolicy := c.config.OutputPolicy
logger := c.config.Logger
c.config.modifyLock.RUnlock()

Expand Down Expand Up @@ -1225,6 +1252,14 @@ START:
return nil, LastOutputStringError
}

if outputPolicy {
LastOutputPolicyError = &OutputPolicyError{
method: req.Method,
path: strings.TrimPrefix(req.URL.Path, "/v1"),
}
return nil, LastOutputPolicyError
}

req.Request = req.Request.WithContext(ctx)

if backoff == nil {
Expand Down Expand Up @@ -1317,6 +1352,8 @@ func (c *Client) httpRequestWithContext(ctx context.Context, r *Request) (*Respo
limiter := c.config.Limiter
httpClient := c.config.HttpClient
outputCurlString := c.config.OutputCurlString
outputPolicy := c.config.OutputPolicy

// add headers
if c.headers != nil {
for header, vals := range c.headers {
Expand All @@ -1333,10 +1370,13 @@ func (c *Client) httpRequestWithContext(ctx context.Context, r *Request) (*Respo
c.config.modifyLock.RUnlock()
c.modifyLock.RUnlock()

// OutputCurlString logic relies on the request type to be retryable.Request as
// OutputCurlString and OutputPolicy logic rely on the request type to be retryable.Request
if outputCurlString {
return nil, fmt.Errorf("output-curl-string is not implemented for this request")
}
if outputPolicy {
return nil, fmt.Errorf("output-policy is not implemented for this request")
}

req.URL.User = r.URL.User
req.URL.Scheme = r.URL.Scheme
Expand Down
1 change: 1 addition & 0 deletions api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ func TestClone(t *testing.T) {
parent.SetLimiter(5.0, 10)
parent.SetMaxRetries(5)
parent.SetOutputCurlString(true)
parent.SetOutputPolicy(true)
parent.SetSRVLookup(true)

if tt.headers != nil {
Expand Down
82 changes: 82 additions & 0 deletions api/output_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package api
digivava marked this conversation as resolved.
Show resolved Hide resolved

import (
"fmt"
"net/http"
"net/url"
"strings"
)

const (
ErrOutputPolicyRequest = "output a policy, please"
)

var LastOutputPolicyError *OutputPolicyError
digivava marked this conversation as resolved.
Show resolved Hide resolved

type OutputPolicyError struct {
method string
path string
finalHCLString string
}

func (d *OutputPolicyError) Error() string {
if d.finalHCLString == "" {
p, err := d.buildSamplePolicy()
if err != nil {
return err.Error()
}
d.finalHCLString = p
}

return ErrOutputPolicyRequest
}

func (d *OutputPolicyError) HCLString() (string, error) {
if d.finalHCLString == "" {
p, err := d.buildSamplePolicy()
if err != nil {
return "", err
}
d.finalHCLString = p
}
return d.finalHCLString, nil
}

// Builds a sample policy document from the request
func (d *OutputPolicyError) buildSamplePolicy() (string, error) {
var capabilities []string
switch d.method {
case http.MethodGet, "":
capabilities = append(capabilities, "read")
case http.MethodPost, http.MethodPut:
capabilities = append(capabilities, "create")
capabilities = append(capabilities, "update")
digivava marked this conversation as resolved.
Show resolved Hide resolved
case http.MethodPatch:
capabilities = append(capabilities, "patch")
case http.MethodDelete:
capabilities = append(capabilities, "delete")
case "LIST":
capabilities = append(capabilities, "list")
}

// sanitize, then trim the Vault address and v1 from the front of the path
path, err := url.PathUnescape(d.path)
if err != nil {
return "", fmt.Errorf("failed to unescape request URL characters: %v", err)
}

// determine whether to add sudo capability
if IsSudoPath(path) {
capabilities = append(capabilities, "sudo")
}

// the OpenAPI response has a / in front of each path,
// but policies need the path without that leading slash
path = strings.TrimLeft(path, "/")

capStr := strings.Join(capabilities, `", "`)
return fmt.Sprintf(
`path "%s" {
capabilities = ["%s"]
}`, path, capStr), nil
}
55 changes: 29 additions & 26 deletions api/output_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,74 +19,77 @@ type OutputStringError struct {
TLSSkipVerify bool
ClientCACert, ClientCAPath string
ClientCert, ClientKey string
parsingError error
parsedCurlString string
finalCurlString string
}

func (d *OutputStringError) Error() string {
if d.parsedCurlString == "" {
d.parseRequest()
if d.parsingError != nil {
return d.parsingError.Error()
if d.finalCurlString == "" {
cs, err := d.buildCurlString()
if err != nil {
return err.Error()
}
d.finalCurlString = cs
}

return ErrOutputStringRequest
}

func (d *OutputStringError) parseRequest() {
func (d *OutputStringError) CurlString() (string, error) {
if d.finalCurlString == "" {
cs, err := d.buildCurlString()
if err != nil {
return "", err
}
d.finalCurlString = cs
}
return d.finalCurlString, nil
}

func (d *OutputStringError) buildCurlString() (string, error) {
body, err := d.Request.BodyBytes()
if err != nil {
d.parsingError = err
return
return "", err
}

// Build cURL string
d.parsedCurlString = "curl "
finalCurlString := "curl "
if d.TLSSkipVerify {
d.parsedCurlString += "--insecure "
finalCurlString += "--insecure "
}
if d.Request.Method != http.MethodGet {
d.parsedCurlString = fmt.Sprintf("%s-X %s ", d.parsedCurlString, d.Request.Method)
finalCurlString = fmt.Sprintf("%s-X %s ", finalCurlString, d.Request.Method)
}
if d.ClientCACert != "" {
clientCACert := strings.Replace(d.ClientCACert, "'", "'\"'\"'", -1)
d.parsedCurlString = fmt.Sprintf("%s--cacert '%s' ", d.parsedCurlString, clientCACert)
finalCurlString = fmt.Sprintf("%s--cacert '%s' ", finalCurlString, clientCACert)
}
if d.ClientCAPath != "" {
clientCAPath := strings.Replace(d.ClientCAPath, "'", "'\"'\"'", -1)
d.parsedCurlString = fmt.Sprintf("%s--capath '%s' ", d.parsedCurlString, clientCAPath)
finalCurlString = fmt.Sprintf("%s--capath '%s' ", finalCurlString, clientCAPath)
}
if d.ClientCert != "" {
clientCert := strings.Replace(d.ClientCert, "'", "'\"'\"'", -1)
d.parsedCurlString = fmt.Sprintf("%s--cert '%s' ", d.parsedCurlString, clientCert)
finalCurlString = fmt.Sprintf("%s--cert '%s' ", finalCurlString, clientCert)
}
if d.ClientKey != "" {
clientKey := strings.Replace(d.ClientKey, "'", "'\"'\"'", -1)
d.parsedCurlString = fmt.Sprintf("%s--key '%s' ", d.parsedCurlString, clientKey)
finalCurlString = fmt.Sprintf("%s--key '%s' ", finalCurlString, clientKey)
}
for k, v := range d.Request.Header {
for _, h := range v {
if strings.ToLower(k) == "x-vault-token" {
h = `$(vault print token)`
}
d.parsedCurlString = fmt.Sprintf("%s-H \"%s: %s\" ", d.parsedCurlString, k, h)
finalCurlString = fmt.Sprintf("%s-H \"%s: %s\" ", finalCurlString, k, h)
}
}

if len(body) > 0 {
// We need to escape single quotes since that's what we're using to
// quote the body
escapedBody := strings.Replace(string(body), "'", "'\"'\"'", -1)
d.parsedCurlString = fmt.Sprintf("%s-d '%s' ", d.parsedCurlString, escapedBody)
finalCurlString = fmt.Sprintf("%s-d '%s' ", finalCurlString, escapedBody)
}

d.parsedCurlString = fmt.Sprintf("%s%s", d.parsedCurlString, d.Request.URL.String())
}

func (d *OutputStringError) CurlString() string {
if d.parsedCurlString == "" {
d.parseRequest()
}
return d.parsedCurlString
return fmt.Sprintf("%s%s", finalCurlString, d.Request.URL.String()), nil
}
61 changes: 61 additions & 0 deletions api/plugin_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"flag"
"net/url"
"os"
"regexp"

squarejwt "gopkg.in/square/go-jose.v2/jwt"

Expand All @@ -23,6 +24,41 @@ var (
// PluginUnwrapTokenEnv is the ENV name used to pass unwrap tokens to the
// plugin.
PluginUnwrapTokenEnv = "VAULT_UNWRAP_TOKEN"

// sudoPaths is a map containing the paths that require a token's policy
// to have the "sudo" capability. The keys are the paths as strings, in
// the same format as they are returned by the OpenAPI spec. The values
// are the regular expressions that can be used to test whether a given
// path matches that path or not (useful specifically for the paths that
// contain templated fields.)
sudoPaths = map[string]*regexp.Regexp{
"/auth/token/accessors/": regexp.MustCompile(`^/auth/token/accessors/$`),
"/pki/root": regexp.MustCompile(`^/pki/root$`),
"/pki/root/sign-self-issued": regexp.MustCompile(`^/pki/root/sign-self-issued$`),
"/sys/audit": regexp.MustCompile(`^/sys/audit$`),
"/sys/audit/{path}": regexp.MustCompile(`^/sys/audit/.+$`),
"/sys/auth/{path}": regexp.MustCompile(`^/sys/auth/.+$`),
"/sys/auth/{path}/tune": regexp.MustCompile(`^/sys/auth/.+/tune$`),
"/sys/config/auditing/request-headers": regexp.MustCompile(`^/sys/config/auditing/request-headers$`),
"/sys/config/auditing/request-headers/{header}": regexp.MustCompile(`^/sys/config/auditing/request-headers/.+$`),
"/sys/config/cors": regexp.MustCompile(`^/sys/config/cors$`),
"/sys/config/ui/headers/": regexp.MustCompile(`^/sys/config/ui/headers/$`),
"/sys/config/ui/headers/{header}": regexp.MustCompile(`^/sys/config/ui/headers/.+$`),
"/sys/leases": regexp.MustCompile(`^/sys/leases$`),
"/sys/leases/lookup/": regexp.MustCompile(`^/sys/leases/lookup/$`),
"/sys/leases/lookup/{prefix}": regexp.MustCompile(`^/sys/leases/lookup/.+$`),
"/sys/leases/revoke-force/{prefix}": regexp.MustCompile(`^/sys/leases/revoke-force/.+$`),
"/sys/leases/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/leases/revoke-prefix/.+$`),
"/sys/plugins/catalog/{name}": regexp.MustCompile(`^/sys/plugins/catalog/[^/]+$`),
"/sys/plugins/catalog/{type}": regexp.MustCompile(`^/sys/plugins/catalog/[\w-]+$`),
"/sys/plugins/catalog/{type}/{name}": regexp.MustCompile(`^/sys/plugins/catalog/[\w-]+/[^/]+$`),
"/sys/raw": regexp.MustCompile(`^/sys/raw$`),
"/sys/raw/{path}": regexp.MustCompile(`^/sys/raw/.+$`),
"/sys/remount": regexp.MustCompile(`^/sys/remount$`),
"/sys/revoke-force/{prefix}": regexp.MustCompile(`^/sys/revoke-force/.+$`),
"/sys/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/revoke-prefix/.+$`),
"/sys/rotate": regexp.MustCompile(`^/sys/rotate$`),
}
)

// PluginAPIClientMeta is a helper that plugins can use to configure TLS connections
Expand Down Expand Up @@ -192,3 +228,28 @@ func VaultPluginTLSProviderContext(ctx context.Context, apiTLSConfig *TLSConfig)
return tlsConfig, nil
}
}

func SudoPaths() map[string]*regexp.Regexp {
return sudoPaths
}

// Determine whether the given path requires the sudo capability
func IsSudoPath(path string) bool {
// Return early if the path is any of the non-templated sudo paths.
if _, ok := sudoPaths[path]; ok {
return true
}

// Some sudo paths have templated fields in them.
// (e.g. /sys/revoke-prefix/{prefix})
// The values in the sudoPaths map are actually regular expressions,
// so we can check if our path matches against them.
for _, sudoPathRegexp := range sudoPaths {
match := sudoPathRegexp.MatchString(path)
if match {
return true
}
}

return false
}
Loading