diff --git a/.travis.yml b/.travis.yml index f46f04caeb3a..aa214be8bc50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,5 +20,5 @@ branches: script: - make bootstrap - - travis_wait 45 make test - - travis_wait 45 make testrace + - travis_wait 75 make test + - travis_wait 75 make testrace diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df7b7657824..351a8b3ec7f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ FEATURES: * **SSH CA Login with `vault ssh`**: `vault ssh` now supports the SSH CA backend for authenticating to machines. It also supports remote host key verification through the SSH CA backend, if enabled. +* **Signing of Self-Issued Certs in PKI**: The `pki` backend now supports + signing self-issued CA certs. This is useful when switching root CAs. IMPROVEMENTS: @@ -21,8 +23,20 @@ IMPROVEMENTS: case-preserving [GH-3240] * cli: Add subcommand autocompletion that can be enabled with `vault -autocomplete-install` [GH-3223] + * cli: Add ability to handle wrapped responses when using `vault auth`. What + is output depends on the other given flags; see the help output for that + command for more information. [GH-3263] * core: TLS cipher suites used for cluster behavior can now be set via `cluster_cipher_suites` in configuration [GH-3228] + * core: The `plugin_name` can now either be specified directly as part of the + parameter or within the `config` object when mounting a secret or auth backend + via `sys/mounts/:path` or `sys/auth/:path` respectively [GH-3202] + * secret/databases/mongo: If an EOF is encountered, attempt reconnecting and + retrying the operation [GH-3269] + * secret/pki: TTLs can now be specified as a string or an integer number of + seconds [GH-3270] + * secret/pki: Self-issued certs can now be signed via + `pki/root/sign-self-issued` [GH-3274] * storage/gcp: Use application default credentials if they exist [GH-3248] BUG FIXES: @@ -34,6 +48,8 @@ BUG FIXES: * core: Fix PROXY when underlying connection is TLS [GH-3195] * core: Policy-related commands would sometimes fail to act case-insensitively [GH-3210] + * storage/consul: Fix parsing TLS configuration when using a bare IPv6 address + [GH-3268] ## 0.8.1 (August 16th, 2017) @@ -323,9 +339,9 @@ FEATURES: Lambda instances, and more. Signed client identity information retrieved using the AWS API `sts:GetCallerIdentity` is validated against the AWS STS service before issuing a Vault token. This backend is unified with the - `aws-ec2` authentication backend, and allows additional EC2-related - restrictions to be applied during the IAM authentication; the previous EC2 - behavior is also still available. [GH-2441] + `aws-ec2` authentication backend under the name `aws`, and allows additional + EC2-related restrictions to be applied during the IAM authentication; the + previous EC2 behavior is also still available. [GH-2441] * **MSSQL Physical Backend**: You can now use Microsoft SQL Server as your Vault physical data store [GH-2546] * **Lease Listing and Lookup**: You can now introspect a lease to get its diff --git a/api/sys_auth.go b/api/sys_auth.go index fd55e429e620..32f4bbddc058 100644 --- a/api/sys_auth.go +++ b/api/sys_auth.go @@ -82,10 +82,15 @@ func (c *Sys) DisableAuth(path string) error { // documentation. Please refer to that documentation for more details. type EnableAuthOptions struct { - Type string `json:"type" structs:"type"` - Description string `json:"description" structs:"description"` - Local bool `json:"local" structs:"local"` - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + Type string `json:"type" structs:"type"` + Description string `json:"description" structs:"description"` + Config AuthConfigInput `json:"config" structs:"config"` + Local bool `json:"local" structs:"local"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty"` +} + +type AuthConfigInput struct { + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` } type AuthMount struct { diff --git a/api/sys_mounts.go b/api/sys_mounts.go index e0bb9ff0d538..091a8f655df9 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -124,6 +124,7 @@ type MountInput struct { Description string `json:"description" structs:"description"` Config MountConfigInput `json:"config" structs:"config"` Local bool `json:"local" structs:"local"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name"` } type MountConfigInput struct { diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go index 4a53f7f3389b..2842c24dbf12 100644 --- a/builtin/credential/aws/cli.go +++ b/builtin/credential/aws/cli.go @@ -69,7 +69,7 @@ func GenerateLoginData(accessKey, secretKey, sessionToken, headerValue string) ( return loginData, nil } -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { mount, ok := m["mount"] if !ok { mount = "aws" @@ -87,23 +87,23 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { loginData, err := GenerateLoginData(m["aws_access_key_id"], m["aws_secret_access_key"], m["aws_security_token"], headerValue) if err != nil { - return "", err + return nil, err } if loginData == nil { - return "", fmt.Errorf("got nil response from GenerateLoginData") + return nil, fmt.Errorf("got nil response from GenerateLoginData") } loginData["role"] = role path := fmt.Sprintf("auth/%s/login", mount) secret, err := c.Logical().Write(path, loginData) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } func (h *CLIHandler) Help() string { diff --git a/builtin/credential/cert/cli.go b/builtin/credential/cert/cli.go index 66809c2e3a8f..a1071fcd388e 100644 --- a/builtin/credential/cert/cli.go +++ b/builtin/credential/cert/cli.go @@ -10,13 +10,13 @@ import ( type CLIHandler struct{} -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { var data struct { Mount string `mapstructure:"mount"` Name string `mapstructure:"name"` } if err := mapstructure.WeakDecode(m, &data); err != nil { - return "", err + return nil, err } if data.Mount == "" { @@ -29,13 +29,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { path := fmt.Sprintf("auth/%s/login", data.Mount) secret, err := c.Logical().Write(path, options) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } func (h *CLIHandler) Help() string { diff --git a/builtin/credential/github/cli.go b/builtin/credential/github/cli.go index dda1dac44dcd..557939b209e0 100644 --- a/builtin/credential/github/cli.go +++ b/builtin/credential/github/cli.go @@ -10,7 +10,7 @@ import ( type CLIHandler struct{} -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { mount, ok := m["mount"] if !ok { mount = "github" @@ -19,7 +19,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { token, ok := m["token"] if !ok { if token = os.Getenv("VAULT_AUTH_GITHUB_TOKEN"); token == "" { - return "", fmt.Errorf("GitHub token should be provided either as 'value' for 'token' key,\nor via an env var VAULT_AUTH_GITHUB_TOKEN") + return nil, fmt.Errorf("GitHub token should be provided either as 'value' for 'token' key,\nor via an env var VAULT_AUTH_GITHUB_TOKEN") } } @@ -28,13 +28,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { "token": token, }) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } func (h *CLIHandler) Help() string { diff --git a/builtin/credential/ldap/cli.go b/builtin/credential/ldap/cli.go index e4d151faf144..262bc998e1b5 100644 --- a/builtin/credential/ldap/cli.go +++ b/builtin/credential/ldap/cli.go @@ -11,7 +11,7 @@ import ( type CLIHandler struct{} -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { mount, ok := m["mount"] if !ok { mount = "ldap" @@ -21,7 +21,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { if !ok { username = usernameFromEnv() if username == "" { - return "", fmt.Errorf("'username' not supplied and neither 'LOGNAME' nor 'USER' env vars set") + return nil, fmt.Errorf("'username' not supplied and neither 'LOGNAME' nor 'USER' env vars set") } } password, ok := m["password"] @@ -31,7 +31,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { password, err = pwd.Read(os.Stdin) fmt.Println() if err != nil { - return "", err + return nil, err } } @@ -51,13 +51,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { path := fmt.Sprintf("auth/%s/login/%s", mount, username) secret, err := c.Logical().Write(path, data) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } func (h *CLIHandler) Help() string { diff --git a/builtin/credential/okta/backend.go b/builtin/credential/okta/backend.go index 0977b2c23ff6..969fd4272194 100644 --- a/builtin/credential/okta/backend.go +++ b/builtin/credential/okta/backend.go @@ -3,6 +3,7 @@ package okta import ( "fmt" + "github.com/chrismalek/oktasdk-go/okta" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -56,18 +57,44 @@ func (b *backend) Login(req *logical.Request, username string, password string) } client := cfg.OktaClient() - auth, err := client.Authenticate(username, password) + + type embeddedResult struct { + User okta.User `json:"user"` + } + + type authResult struct { + Embedded embeddedResult `json:"_embedded"` + } + + authReq, err := client.NewRequest("POST", "authn", map[string]interface{}{ + "username": username, + "password": password, + }) + if err != nil { + return nil, nil, err + } + + var result authResult + rsp, err := client.Do(authReq, &result) if err != nil { return nil, logical.ErrorResponse(fmt.Sprintf("Okta auth failed: %v", err)), nil } - if auth == nil { + if rsp == nil { return nil, logical.ErrorResponse("okta auth backend unexpected failure"), nil } - oktaGroups, err := b.getOktaGroups(cfg, auth.Embedded.User.ID) + oktaUser := &result.Embedded.User + rsp, err = client.Users.PopulateGroups(oktaUser) if err != nil { return nil, logical.ErrorResponse(err.Error()), nil } + if rsp == nil { + return nil, logical.ErrorResponse("okta auth backend unexpected failure"), nil + } + oktaGroups := make([]string, 0, len(oktaUser.Groups)) + for _, group := range oktaUser.Groups { + oktaGroups = append(oktaGroups, group.Profile.Name) + } if b.Logger().IsDebug() { b.Logger().Debug("auth/okta: Groups fetched from Okta", "num_groups", len(oktaGroups), "groups", oktaGroups) } @@ -130,23 +157,6 @@ func (b *backend) Login(req *logical.Request, username string, password string) return policies, oktaResponse, nil } -func (b *backend) getOktaGroups(cfg *ConfigEntry, userID string) ([]string, error) { - if cfg.Token != "" { - client := cfg.OktaClient() - groups, err := client.Groups(userID) - if err != nil { - return nil, err - } - - oktaGroups := make([]string, 0, len(*groups)) - for _, group := range *groups { - oktaGroups = append(oktaGroups, group.Profile.Name) - } - return oktaGroups, err - } - return nil, nil -} - const backendHelp = ` The Okta credential provider allows authentication querying, checking username and password, and associating policies. If an api token is configure diff --git a/builtin/credential/okta/cli.go b/builtin/credential/okta/cli.go index 355e8cb9ba9c..f5f850209e0b 100644 --- a/builtin/credential/okta/cli.go +++ b/builtin/credential/okta/cli.go @@ -13,7 +13,7 @@ import ( type CLIHandler struct{} // Auth cli method -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { mount, ok := m["mount"] if !ok { mount = "okta" @@ -21,7 +21,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { username, ok := m["username"] if !ok { - return "", fmt.Errorf("'username' var must be set") + return nil, fmt.Errorf("'username' var must be set") } password, ok := m["password"] if !ok { @@ -30,7 +30,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { password, err = pwd.Read(os.Stdin) fmt.Println() if err != nil { - return "", err + return nil, err } } @@ -41,13 +41,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { path := fmt.Sprintf("auth/%s/login/%s", mount, username) secret, err := c.Logical().Write(path, data) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } // Help method for okta cli diff --git a/builtin/credential/okta/path_config.go b/builtin/credential/okta/path_config.go index b39d95265390..19014eddf7c1 100644 --- a/builtin/credential/okta/path_config.go +++ b/builtin/credential/okta/path_config.go @@ -3,12 +3,14 @@ package okta import ( "fmt" "net/url" + "strings" "time" + "github.com/chrismalek/oktasdk-go/okta" + "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" - "github.com/sstarcher/go-okta" ) func pathConfig(b *backend) *framework.Path { @@ -17,16 +19,29 @@ func pathConfig(b *backend) *framework.Path { Fields: map[string]*framework.FieldSchema{ "organization": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Okta organization to authenticate against", + Description: "Okta organization to authenticate against (DEPRECATED)", + }, + "org_name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the organization to be used in the Okta API.", }, "token": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Okta admin API token", + Description: "Okta admin API token (DEPRECATED)", + }, + "api_token": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Okta API key.", }, "base_url": &framework.FieldSchema{ Type: framework.TypeString, Description: `The API endpoint to use. Useful if you -are using Okta development accounts.`, +are using Okta development accounts. (DEPRECATED)`, + }, + "production": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: true, + Description: `If set, production API URL prefix will be used to communicate with Okta and if not set, a preview production API URL prefix will be used. Defaults to true.`, }, "ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -84,11 +99,15 @@ func (b *backend) pathConfigRead( resp := &logical.Response{ Data: map[string]interface{}{ "organization": cfg.Org, - "base_url": cfg.BaseURL, + "org_name": cfg.Org, + "production": *cfg.Production, "ttl": cfg.TTL, "max_ttl": cfg.MaxTTL, }, } + if cfg.BaseURL != "" { + resp.Data["base_url"] = cfg.BaseURL + } return resp, nil } @@ -106,18 +125,32 @@ func (b *backend) pathConfigWrite( cfg = &ConfigEntry{} } - org, ok := d.GetOk("organization") + org, ok := d.GetOk("org_name") if ok { cfg.Org = org.(string) - } else if req.Operation == logical.CreateOperation { - cfg.Org = d.Get("organization").(string) + } + if cfg.Org == "" { + org, ok = d.GetOk("organization") + if ok { + cfg.Org = org.(string) + } + } + if cfg.Org == "" && req.Operation == logical.CreateOperation { + return logical.ErrorResponse("org_name is missing"), nil } - token, ok := d.GetOk("token") + token, ok := d.GetOk("api_token") if ok { cfg.Token = token.(string) - } else if req.Operation == logical.CreateOperation { - cfg.Token = d.Get("token").(string) + } + if cfg.Token == "" { + token, ok = d.GetOk("token") + if ok { + cfg.Token = token.(string) + } + } + if cfg.Token == "" && req.Operation == logical.CreateOperation { + return logical.ErrorResponse("api_token is missing"), nil } baseURL, ok := d.GetOk("base_url") @@ -134,6 +167,9 @@ func (b *backend) pathConfigWrite( cfg.BaseURL = d.Get("base_url").(string) } + productionRaw := d.Get("production").(bool) + cfg.Production = &productionRaw + ttl, ok := d.GetOk("ttl") if ok { cfg.TTL = time.Duration(ttl.(int)) * time.Second @@ -171,25 +207,26 @@ func (b *backend) pathConfigExistenceCheck( // OktaClient creates a basic okta client connection func (c *ConfigEntry) OktaClient() *okta.Client { - client := okta.NewClient(c.Org) - if c.BaseURL != "" { - client.Url = c.BaseURL + production := true + if c.Production != nil { + production = *c.Production } - - if c.Token != "" { - client.ApiToken = c.Token + if c.BaseURL != "" { + if strings.Contains(c.BaseURL, "oktapreview.com") { + production = false + } } - - return client + return okta.NewClient(cleanhttp.DefaultClient(), c.Org, c.Token, production) } // ConfigEntry for Okta type ConfigEntry struct { - Org string `json:"organization"` - Token string `json:"token"` - BaseURL string `json:"base_url"` - TTL time.Duration `json:"ttl"` - MaxTTL time.Duration `json:"max_ttl"` + Org string `json:"organization"` + Token string `json:"token"` + BaseURL string `json:"base_url"` + Production *bool `json:"is_production,omitempty"` + TTL time.Duration `json:"ttl"` + MaxTTL time.Duration `json:"max_ttl"` } const pathConfigHelp = ` diff --git a/builtin/credential/userpass/cli.go b/builtin/credential/userpass/cli.go index 80b52e3391a8..4433c0e70616 100644 --- a/builtin/credential/userpass/cli.go +++ b/builtin/credential/userpass/cli.go @@ -14,7 +14,7 @@ type CLIHandler struct { DefaultMount string } -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { var data struct { Username string `mapstructure:"username"` Password string `mapstructure:"password"` @@ -23,18 +23,18 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { Passcode string `mapstructure:"passcode"` } if err := mapstructure.WeakDecode(m, &data); err != nil { - return "", err + return nil, err } if data.Username == "" { - return "", fmt.Errorf("'username' must be specified") + return nil, fmt.Errorf("'username' must be specified") } if data.Password == "" { fmt.Printf("Password (will be hidden): ") password, err := pwd.Read(os.Stdin) fmt.Println() if err != nil { - return "", err + return nil, err } data.Password = password } @@ -55,13 +55,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { path := fmt.Sprintf("auth/%s/login/%s", data.Mount, data.Username) secret, err := c.Logical().Write(path, options) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } func (h *CLIHandler) Help() string { diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go index 9d06b5129897..bf5168d44e28 100644 --- a/builtin/logical/pki/backend.go +++ b/builtin/logical/pki/backend.go @@ -42,6 +42,7 @@ func Backend() *backend { Root: []string{ "root", + "root/sign-self-issued", }, }, @@ -50,6 +51,7 @@ func Backend() *backend { pathRoles(&b), pathGenerateRoot(&b), pathSignIntermediate(&b), + pathSignSelfIssued(&b), pathDeleteRoot(&b), pathGenerateIntermediate(&b), pathSetSignedIntermediate(&b), diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index e1795ae64f7f..9aef6a5a3994 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -1,6 +1,7 @@ package pki import ( + "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" @@ -12,6 +13,7 @@ import ( "encoding/pem" "fmt" "math" + "math/big" mathrand "math/rand" "net" "os" @@ -402,7 +404,7 @@ func checkCertsAndPrivateKey(keyType string, key crypto.Signer, usage x509.KeyUs } if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 10 { - return nil, fmt.Errorf("Validity period of %d too large vs max of 10", cert.NotAfter.Unix()) + return nil, fmt.Errorf("Certificate validity end: %s; expected within 10 seconds of %s", cert.NotAfter.Format(time.RFC3339), time.Now().Add(validity).Format(time.RFC3339)) } return parsedCertBundle, nil @@ -1845,6 +1847,8 @@ func generateRoleSteps(t *testing.T, useCSRs bool) []logicaltest.TestStep { addTests(nil) roleTestStep.ErrorOk = false + roleVals.TTL = "" + roleVals.MaxTTL = "12h" } // Listing test @@ -2126,12 +2130,31 @@ func TestBackend_SignVerbatim(t *testing.T) { "ttl": "12h", }, }) - if resp != nil && !resp.IsError() { - t.Fatalf("sign-verbatim signed too-large-ttl'd CSR: %#v", *resp) + if err != nil { + t.Fatal(err) + } + if resp != nil && resp.IsError() { + t.Fatalf(resp.Error().Error()) } + if resp.Data == nil || resp.Data["certificate"] == nil { + t.Fatal("did not get expected data") + } + certString := resp.Data["certificate"].(string) + block, _ := pem.Decode([]byte(certString)) + if block == nil { + t.Fatal("nil pem block") + } + certs, err := x509.ParseCertificates(block.Bytes) if err != nil { t.Fatal(err) } + if len(certs) != 1 { + t.Fatalf("expected a single cert, got %d", len(certs)) + } + cert := certs[0] + if math.Abs(float64(time.Now().Add(12*time.Hour).Unix()-cert.NotAfter.Unix())) < 10 { + t.Fatalf("sign-verbatim did not properly cap validiaty period on signed CSR") + } // now check that if we set generate-lease it takes it from the role and the TTLs match roleData = map[string]interface{}{ @@ -2326,6 +2349,7 @@ func TestBackend_Permitted_DNS_Domains(t *testing.T) { // Direct issuing from root _, err = client.Logical().Write("root/root/generate/internal", map[string]interface{}{ + "ttl": "40h", "common_name": "myvault.com", "permitted_dns_domains": []string{"foobar.com", ".zipzap.com"}, }) @@ -2437,6 +2461,160 @@ func TestBackend_Permitted_DNS_Domains(t *testing.T) { checkIssue(true, "common_name", "host.xyz.com") } +func TestBackend_SignSelfIssued(t *testing.T) { + // create the backend + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b := Backend() + err := b.Setup(config) + if err != nil { + t.Fatal(err) + } + + // generate root + rootData := map[string]interface{}{ + "common_name": "test.com", + "ttl": "172800", + } + + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "root/generate/internal", + Storage: storage, + Data: rootData, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to generate root, %#v", *resp) + } + if err != nil { + t.Fatal(err) + } + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + + getSelfSigned := func(subject, issuer *x509.Certificate) (string, *x509.Certificate) { + selfSigned, err := x509.CreateCertificate(rand.Reader, subject, issuer, key.Public(), key) + if err != nil { + t.Fatal(err) + } + cert, err := x509.ParseCertificate(selfSigned) + if err != nil { + t.Fatal(err) + } + pemSS := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: selfSigned, + }) + return string(pemSS), cert + } + + template := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "foo.bar.com", + }, + SerialNumber: big.NewInt(1234), + IsCA: false, + BasicConstraintsValid: true, + } + + ss, _ := getSelfSigned(template, template) + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "root/sign-self-issued", + Storage: storage, + Data: map[string]interface{}{ + "certificate": ss, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response") + } + if !resp.IsError() { + t.Fatalf("expected error due to non-CA; got: %#v", *resp) + } + + // Set CA to true, but leave issuer alone + template.IsCA = true + + issuer := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "bar.foo.com", + }, + SerialNumber: big.NewInt(2345), + IsCA: true, + BasicConstraintsValid: true, + } + ss, ssCert := getSelfSigned(template, issuer) + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "root/sign-self-issued", + Storage: storage, + Data: map[string]interface{}{ + "certificate": ss, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response") + } + if !resp.IsError() { + t.Fatalf("expected error due to different issuer; cert info is\nIssuer\n%#v\nSubject\n%#v\n", ssCert.Issuer, ssCert.Subject) + } + + ss, ssCert = getSelfSigned(template, template) + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "root/sign-self-issued", + Storage: storage, + Data: map[string]interface{}{ + "certificate": ss, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response") + } + if resp.IsError() { + t.Fatalf("error in response: %s", resp.Error().Error()) + } + + newCertString := resp.Data["certificate"].(string) + block, _ := pem.Decode([]byte(newCertString)) + newCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatal(err) + } + + signingBundle, err := fetchCAInfo(&logical.Request{Storage: storage}) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(newCert.Subject, newCert.Issuer) { + t.Fatal("expected same subject/issuer") + } + if bytes.Equal(newCert.AuthorityKeyId, newCert.SubjectKeyId) { + t.Fatal("expected different authority/subject") + } + if !bytes.Equal(newCert.AuthorityKeyId, signingBundle.Certificate.SubjectKeyId) { + t.Fatal("expected authority on new cert to be same as signing subject") + } + if newCert.Subject.CommonName != "foo.bar.com" { + t.Fatalf("unexpected common name on new cert: %s", newCert.Subject.CommonName) + } +} + const ( rsaCAKey string = `-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAmPQlK7xD5p+E8iLQ8XlVmll5uU2NKMxKY3UF5tbh+0vkc+Fy diff --git a/builtin/logical/pki/ca_util.go b/builtin/logical/pki/ca_util.go index cab579793d96..7a6deda23621 100644 --- a/builtin/logical/pki/ca_util.go +++ b/builtin/logical/pki/ca_util.go @@ -1,6 +1,8 @@ package pki import ( + "time" + "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -27,7 +29,7 @@ func (b *backend) getGenerationParams( } role = &roleEntry{ - TTL: data.Get("ttl").(string), + TTL: (time.Duration(data.Get("ttl").(int)) * time.Second).String(), KeyType: data.Get("key_type").(string), KeyBits: data.Get("key_bits").(int), AllowLocalhost: true, diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index 663fce7cfc43..29b60e618850 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -728,48 +728,38 @@ func generateCreationBundle(b *backend, } // Get the TTL and very it against the max allowed - var ttlField string var ttl time.Duration var maxTTL time.Duration var notAfter time.Time - var ttlFieldInt interface{} { - ttlFieldInt, ok = data.GetOk("ttl") - if !ok { - ttlField = role.TTL - } else { - ttlField = ttlFieldInt.(string) - } + ttl = time.Duration(data.Get("ttl").(int)) * time.Second - if len(ttlField) == 0 { - ttl = b.System().DefaultLeaseTTL() - } else { - ttl, err = parseutil.ParseDurationSecond(ttlField) - if err != nil { - return nil, errutil.UserError{Err: fmt.Sprintf( - "invalid requested ttl: %s", err)} + if ttl == 0 { + if role.TTL != "" { + ttl, err = parseutil.ParseDurationSecond(role.TTL) + if err != nil { + return nil, errutil.UserError{Err: fmt.Sprintf( + "invalid role ttl: %s", err)} + } } } - if len(role.MaxTTL) == 0 { - maxTTL = b.System().MaxLeaseTTL() - } else { + if role.MaxTTL != "" { maxTTL, err = parseutil.ParseDurationSecond(role.MaxTTL) if err != nil { return nil, errutil.UserError{Err: fmt.Sprintf( - "invalid ttl: %s", err)} + "invalid role max_ttl: %s", err)} } } + if ttl == 0 { + ttl = b.System().DefaultLeaseTTL() + } + if maxTTL == 0 { + maxTTL = b.System().MaxLeaseTTL() + } if ttl > maxTTL { - // Don't error if they were using system defaults, only error if - // they specifically chose a bad TTL - if len(ttlField) == 0 { - ttl = maxTTL - } else { - return nil, errutil.UserError{Err: fmt.Sprintf( - "ttl is larger than maximum allowed (%d)", maxTTL/time.Second)} - } + ttl = maxTTL } notAfter = time.Now().Add(ttl) @@ -939,6 +929,7 @@ func createCertificate(creationInfo *creationBundle) (*certutil.ParsedCertBundle } caCert := creationInfo.SigningBundle.Certificate + certTemplate.AuthorityKeyId = caCert.SubjectKeyId err = checkPermittedDNSDomains(certTemplate, caCert) if err != nil { @@ -962,6 +953,7 @@ func createCertificate(creationInfo *creationBundle) (*certutil.ParsedCertBundle certTemplate.SignatureAlgorithm = x509.ECDSAWithSHA256 } + certTemplate.AuthorityKeyId = subjKeyID certTemplate.BasicConstraintsValid = true certBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, result.PrivateKey.Public(), result.PrivateKey) } @@ -1069,6 +1061,8 @@ func signCertificate(creationInfo *creationBundle, } subjKeyID := sha1.Sum(marshaledKey) + caCert := creationInfo.SigningBundle.Certificate + subject := pkix.Name{ CommonName: creationInfo.CommonName, OrganizationalUnit: creationInfo.OU, @@ -1076,11 +1070,12 @@ func signCertificate(creationInfo *creationBundle, } certTemplate := &x509.Certificate{ - SerialNumber: serialNumber, - Subject: subject, - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: creationInfo.NotAfter, - SubjectKeyId: subjKeyID[:], + SerialNumber: serialNumber, + Subject: subject, + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: creationInfo.NotAfter, + SubjectKeyId: subjKeyID[:], + AuthorityKeyId: caCert.SubjectKeyId, } switch creationInfo.SigningBundle.PrivateKeyType { @@ -1107,7 +1102,6 @@ func signCertificate(creationInfo *creationBundle, addKeyUsages(creationInfo, certTemplate) var certBytes []byte - caCert := creationInfo.SigningBundle.Certificate certTemplate.IssuingCertificateURL = creationInfo.URLs.IssuingCertificates certTemplate.CRLDistributionPoints = creationInfo.URLs.CRLDistributionPoints diff --git a/builtin/logical/pki/fields.go b/builtin/logical/pki/fields.go index 8ecc9a68957e..52adf10eb79e 100644 --- a/builtin/logical/pki/fields.go +++ b/builtin/logical/pki/fields.go @@ -59,7 +59,7 @@ email addresses.`, } fields["ttl"] = &framework.FieldSchema{ - Type: framework.TypeString, + Type: framework.TypeDurationSecond, Description: `The requested Time To Live for the certificate; sets the expiration date. If not specified the role default, backend default, or system @@ -92,7 +92,7 @@ must still be specified in alt_names or ip_sans.`, } fields["ttl"] = &framework.FieldSchema{ - Type: framework.TypeString, + Type: framework.TypeDurationSecond, Description: `The requested Time To Live for the certificate; sets the expiration date. If not specified the role default, backend default, or system diff --git a/builtin/logical/pki/path_roles.go b/builtin/logical/pki/path_roles.go index 4d9e11567d98..8800aea895a9 100644 --- a/builtin/logical/pki/path_roles.go +++ b/builtin/logical/pki/path_roles.go @@ -35,7 +35,7 @@ func pathRoles(b *backend) *framework.Path { }, "ttl": &framework.FieldSchema{ - Type: framework.TypeString, + Type: framework.TypeDurationSecond, Default: "", Description: `The lease duration if no specific lease duration is requested. The lease duration controls the expiration @@ -383,7 +383,7 @@ func (b *backend) pathRoleCreate( entry := &roleEntry{ MaxTTL: data.Get("max_ttl").(string), - TTL: data.Get("ttl").(string), + TTL: (time.Duration(data.Get("ttl").(int)) * time.Second).String(), AllowLocalhost: data.Get("allow_localhost").(bool), AllowedDomains: data.Get("allowed_domains").(string), AllowBareDomains: data.Get("allow_bare_domains").(bool), diff --git a/builtin/logical/pki/path_root.go b/builtin/logical/pki/path_root.go index 5ed49ebf4f75..46f0dc1b11a5 100644 --- a/builtin/logical/pki/path_root.go +++ b/builtin/logical/pki/path_root.go @@ -1,9 +1,15 @@ package pki import ( + "crypto/rand" + "crypto/x509" "encoding/base64" + "encoding/pem" "fmt" + "reflect" + "time" + "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/helper/errutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -81,6 +87,28 @@ the non-repudiation flag.`, return ret } +func pathSignSelfIssued(b *backend) *framework.Path { + ret := &framework.Path{ + Pattern: "root/sign-self-issued", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathCASignSelfIssued, + }, + + Fields: map[string]*framework.FieldSchema{ + "certificate": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `PEM-format self-issued certificate to be signed.`, + }, + }, + + HelpSynopsis: pathSignSelfIssuedHelpSyn, + HelpDescription: pathSignSelfIssuedHelpDesc, + } + + return ret +} + func (b *backend) pathCADeleteRoot( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { return nil, req.Storage.Delete("config/ca_bundle") @@ -214,7 +242,7 @@ func (b *backend) pathCASignIntermediate( } role := &roleEntry{ - TTL: data.Get("ttl").(string), + TTL: (time.Duration(data.Get("ttl").(int)) * time.Second).String(), AllowLocalhost: true, AllowAnyName: true, AllowIPSANs: true, @@ -319,6 +347,76 @@ func (b *backend) pathCASignIntermediate( return resp, nil } +func (b *backend) pathCASignSelfIssued( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var err error + + certPem := data.Get("certificate").(string) + block, _ := pem.Decode([]byte(certPem)) + if block == nil || len(block.Bytes) == 0 { + return logical.ErrorResponse("certificate could not be PEM-decoded"), nil + } + certs, err := x509.ParseCertificates(block.Bytes) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("error parsing certificate: %s", err)), nil + } + if len(certs) != 1 { + return logical.ErrorResponse(fmt.Sprintf("%d certificates found in PEM file, expected 1", len(certs))), nil + } + + cert := certs[0] + if !cert.IsCA { + return logical.ErrorResponse("given certificate is not a CA certificate"), nil + } + if !reflect.DeepEqual(cert.Issuer, cert.Subject) { + return logical.ErrorResponse("given certificate is not self-issued"), nil + } + + var caErr error + signingBundle, caErr := fetchCAInfo(req) + switch caErr.(type) { + case errutil.UserError: + return nil, errutil.UserError{Err: fmt.Sprintf( + "could not fetch the CA certificate (was one set?): %s", caErr)} + case errutil.InternalError: + return nil, errutil.InternalError{Err: fmt.Sprintf( + "error fetching CA certificate: %s", caErr)} + } + + signingCB, err := signingBundle.ToCertBundle() + if err != nil { + return nil, fmt.Errorf("Error converting raw signing bundle to cert bundle: %s", err) + } + + cert.AuthorityKeyId = signingBundle.Certificate.SubjectKeyId + urls := &urlEntries{} + if signingBundle.URLs != nil { + urls = signingBundle.URLs + } + cert.IssuingCertificateURL = urls.IssuingCertificates + cert.CRLDistributionPoints = urls.CRLDistributionPoints + cert.OCSPServer = urls.OCSPServers + + newCert, err := x509.CreateCertificate(rand.Reader, cert, cert, signingBundle.PrivateKey.Public(), signingBundle.PrivateKey) + if err != nil { + return nil, errwrap.Wrapf("error signing self-issued certificate: {{err}}", err) + } + if len(newCert) == 0 { + return nil, fmt.Errorf("nil cert was created when signing self-issued certificate") + } + pemCert := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: newCert, + }) + + return &logical.Response{ + Data: map[string]interface{}{ + "certificate": string(pemCert), + "issuing_ca": signingCB.Certificate, + }, + }, nil +} + const pathGenerateRootHelpSyn = ` Generate a new CA certificate and private key used for signing. ` @@ -340,5 +438,15 @@ Issue an intermediate CA certificate based on the provided CSR. ` const pathSignIntermediateHelpDesc = ` -See the API documentation for more information. +see the API documentation for more information. +` + +const pathSignSelfIssuedHelpSyn = ` +Signs another CA's self-issued certificate. +` + +const pathSignSelfIssuedHelpDesc = ` +Signs another CA's self-issued certificate. This is most often used for rolling roots; unless you know you need this you probably want to use sign-intermediate instead. + +Note that this is a very privileged operation and should be extremely restricted in terms of who is allowed to use it. All values will be taken directly from the incoming certificate and no verification of host names, path lengths, or any other values will be performed. ` diff --git a/builtin/plugin/backend.go b/builtin/plugin/backend.go index 3945f78d2d23..a1c781f57b00 100644 --- a/builtin/plugin/backend.go +++ b/builtin/plugin/backend.go @@ -3,13 +3,20 @@ package plugin import ( "fmt" "net/rpc" + "reflect" "sync" uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" bplugin "github.com/hashicorp/vault/logical/plugin" ) +var ( + ErrMismatchType = fmt.Errorf("mismatch on mounted backend and plugin backend type") + ErrMismatchPaths = fmt.Errorf("mismatch on mounted backend and plugin backend special paths") +) + // Factory returns a configured plugin logical.Backend. func Factory(conf *logical.BackendConfig) (logical.Backend, error) { _, ok := conf.Config["plugin_name"] @@ -31,14 +38,33 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) { // or as a concrete implementation if builtin, casted as logical.Backend. func Backend(conf *logical.BackendConfig) (logical.Backend, error) { var b backend + name := conf.Config["plugin_name"] sys := conf.System - raw, err := bplugin.NewBackend(name, sys, conf.Logger) + // NewBackend with isMetadataMode set to true + raw, err := bplugin.NewBackend(name, sys, conf.Logger, true) + if err != nil { + return nil, err + } + err = raw.Setup(conf) if err != nil { return nil, err } - b.Backend = raw + // Get SpecialPaths and BackendType + paths := raw.SpecialPaths() + btype := raw.Type() + + // Cleanup meta plugin backend + raw.Cleanup() + + // Initialize b.Backend with dummy backend since plugin + // backends will need to be lazy loaded. + b.Backend = &framework.Backend{ + PathsSpecial: paths, + BackendType: btype, + } + b.config = conf return &b, nil @@ -53,16 +79,24 @@ type backend struct { // Used to detect if we already reloaded canary string + + // Used to detect if plugin is set + loaded bool } func (b *backend) reloadBackend() error { + b.Logger().Trace("plugin: reloading plugin backend", "plugin", b.config.Config["plugin_name"]) + return b.startBackend() +} + +// startBackend starts a plugin backend +func (b *backend) startBackend() error { pluginName := b.config.Config["plugin_name"] - b.Logger().Trace("plugin: reloading plugin backend", "plugin", pluginName) // Ensure proper cleanup of the backend (i.e. call client.Kill()) b.Backend.Cleanup() - nb, err := bplugin.NewBackend(pluginName, b.config.System, b.config.Logger) + nb, err := bplugin.NewBackend(pluginName, b.config.System, b.config.Logger, false) if err != nil { return err } @@ -70,7 +104,29 @@ func (b *backend) reloadBackend() error { if err != nil { return err } + + // If the backend has not been loaded (i.e. still in metadata mode), + // check if type and special paths still matches + if !b.loaded { + if b.Backend.Type() != nb.Type() { + nb.Cleanup() + b.Logger().Warn("plugin: failed to start plugin process", "plugin", b.config.Config["plugin_name"], "error", ErrMismatchType) + return ErrMismatchType + } + if !reflect.DeepEqual(b.Backend.SpecialPaths(), nb.SpecialPaths()) { + nb.Cleanup() + b.Logger().Warn("plugin: failed to start plugin process", "plugin", b.config.Config["plugin_name"], "error", ErrMismatchPaths) + return ErrMismatchPaths + } + } + b.Backend = nb + b.loaded = true + + // Call initialize + if err := b.Backend.Initialize(); err != nil { + return err + } return nil } @@ -79,6 +135,23 @@ func (b *backend) reloadBackend() error { func (b *backend) HandleRequest(req *logical.Request) (*logical.Response, error) { b.RLock() canary := b.canary + + // Lazy-load backend + if !b.loaded { + // Upgrade lock + b.RUnlock() + b.Lock() + // Check once more after lock swap + if !b.loaded { + err := b.startBackend() + if err != nil { + b.Unlock() + return nil, err + } + } + b.Unlock() + b.RLock() + } resp, err := b.Backend.HandleRequest(req) b.RUnlock() // Need to compare string value for case were err comes from plugin RPC @@ -112,6 +185,24 @@ func (b *backend) HandleRequest(req *logical.Request) (*logical.Response, error) func (b *backend) HandleExistenceCheck(req *logical.Request) (bool, bool, error) { b.RLock() canary := b.canary + + // Lazy-load backend + if !b.loaded { + // Upgrade lock + b.RUnlock() + b.Lock() + // Check once more after lock swap + if !b.loaded { + err := b.startBackend() + if err != nil { + b.Unlock() + return false, false, err + } + } + b.Unlock() + b.RLock() + } + checkFound, exists, err := b.Backend.HandleExistenceCheck(req) b.RUnlock() if err != nil && err.Error() == rpc.ErrShutdown.Error() { diff --git a/builtin/plugin/backend_test.go b/builtin/plugin/backend_test.go index 0a37691d63fd..5b07197099d3 100644 --- a/builtin/plugin/backend_test.go +++ b/builtin/plugin/backend_test.go @@ -1,6 +1,7 @@ package plugin import ( + "fmt" "os" "testing" @@ -39,7 +40,8 @@ func TestBackend_Factory(t *testing.T) { } func TestBackend_PluginMain(t *testing.T) { - if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" { + args := []string{} + if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" && os.Getenv(pluginutil.PluginMetadaModeEnv) != "true" { return } @@ -48,7 +50,7 @@ func TestBackend_PluginMain(t *testing.T) { t.Fatal("CA cert not passed in") } - args := []string{"--ca-cert=" + caPEM} + args = append(args, fmt.Sprintf("--ca-cert=%s", caPEM)) apiClientMeta := &pluginutil.APIClientMeta{} flags := apiClientMeta.FlagSet() diff --git a/command/auth.go b/command/auth.go index 42754aad3d94..00b21ce41461 100644 --- a/command/auth.go +++ b/command/auth.go @@ -22,7 +22,7 @@ import ( // AuthHandler is the interface that any auth handlers must implement // to enable auth via the CLI. type AuthHandler interface { - Auth(*api.Client, map[string]string) (string, error) + Auth(*api.Client, map[string]string) (*api.Secret, error) Help() string } @@ -167,11 +167,52 @@ func (c *AuthCommand) Run(args []string) int { } // Authenticate - token, err := handler.Auth(client, vars) + secret, err := handler.Auth(client, vars) if err != nil { c.Ui.Error(err.Error()) return 1 } + if secret == nil { + c.Ui.Error("Empty response from auth helper") + return 1 + } + + // If we had requested a wrapped token, we want to unset that request + // before performing further functions + client.SetWrappingLookupFunc(func(string, string) string { + return "" + }) + +CHECK_TOKEN: + var token string + switch { + case secret == nil: + c.Ui.Error("Empty response from auth helper") + return 1 + + case secret.Auth != nil: + token = secret.Auth.ClientToken + + case secret.WrapInfo != nil: + if secret.WrapInfo.WrappedAccessor == "" { + c.Ui.Error("Got a wrapped response from Vault but wrapped reply does not seem to contain a token") + return 1 + } + if tokenOnly { + c.Ui.Output(secret.WrapInfo.Token) + return 0 + } + if noStore { + return OutputSecret(c.Ui, "table", secret) + } + client.SetToken(secret.WrapInfo.Token) + secret, err = client.Logical().Unwrap("") + goto CHECK_TOKEN + + default: + c.Ui.Error("No auth or wrapping info in auth helper response") + return 1 + } // Cache the previous token so that it can be restored if authentication fails var previousToken string @@ -230,6 +271,9 @@ func (c *AuthCommand) Run(args []string) int { } return 1 } + client.SetWrappingLookupFunc(func(string, string) string { + return "" + }) // If in no-store mode it won't have read the token from a token-helper (or // will read an old one) so set it explicitly @@ -238,7 +282,7 @@ func (c *AuthCommand) Run(args []string) int { } // Verify the token - secret, err := client.Auth().Token().LookupSelf() + secret, err = client.Auth().Token().LookupSelf() if err != nil { c.Ui.Error(fmt.Sprintf( "Error validating token: %s", err)) @@ -274,8 +318,8 @@ func (c *AuthCommand) Run(args []string) int { // Get the policies we have policiesRaw, ok := secret.Data["policies"] - if !ok { - policiesRaw = []string{"unknown"} + if !ok || policiesRaw == nil { + policiesRaw = []interface{}{"unknown"} } var policies []string for _, v := range policiesRaw.([]interface{}) { @@ -307,6 +351,9 @@ func (c *AuthCommand) getMethods() (map[string]*api.AuthMount, error) { if err != nil { return nil, err } + client.SetWrappingLookupFunc(func(string, string) string { + return "" + }) auth, err := client.Sys().ListAuth() if err != nil { @@ -387,15 +434,21 @@ Usage: vault auth [options] [auth-information] The value of the "-path" flag is supplied to auth providers as the "mount" option in the payload to specify the mount point. + If response wrapping is used (via -wrap-ttl), the returned token will be + automatically unwrapped unless: + * -token-only is used, in which case the wrapping token will be output + * -no-store is used, in which case the details of the wrapping token + will be printed + General Options: ` + meta.GeneralOptionsUsage() + ` Auth Options: - -method=name Outputs help for the authentication method with the given - name for the remote server. If this authentication method - is not available, exit with code 1. + -method=name Use the method given here, which is a type of backend, not + the path. If this authentication method is not available, + exit with code 1. -method-help If set, the help for the selected method will be shown. @@ -422,7 +475,7 @@ type tokenAuthHandler struct { Token string } -func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (string, error) { +func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (*api.Secret, error) { token := h.Token if token == "" { var err error @@ -432,7 +485,7 @@ func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (string, error) token, err = password.Read(os.Stdin) fmt.Printf("\n") if err != nil { - return "", fmt.Errorf( + return nil, fmt.Errorf( "Error attempting to ask for token. The raw error message\n"+ "is shown below, but the most common reason for this error is\n"+ "that you attempted to pipe a value into auth. If you want to\n"+ @@ -442,12 +495,16 @@ func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (string, error) } if token == "" { - return "", fmt.Errorf( + return nil, fmt.Errorf( "A token must be passed to auth. Please view the help\n" + "for more information.") } - return token, nil + return &api.Secret{ + Auth: &api.SecretAuth{ + ClientToken: token, + }, + }, nil } func (h *tokenAuthHandler) Help() string { diff --git a/command/auth_enable.go b/command/auth_enable.go index 3b18745c889e..e6b7f20f24f1 100644 --- a/command/auth_enable.go +++ b/command/auth_enable.go @@ -57,8 +57,10 @@ func (c *AuthEnableCommand) Run(args []string) int { if err := client.Sys().EnableAuthWithOptions(path, &api.EnableAuthOptions{ Type: authType, Description: description, - PluginName: pluginName, - Local: local, + Config: api.AuthConfigInput{ + PluginName: pluginName, + }, + Local: local, }); err != nil { c.Ui.Error(fmt.Sprintf( "Error: %s", err)) diff --git a/command/auth_test.go b/command/auth_test.go index 6071ea470b18..824312908320 100644 --- a/command/auth_test.go +++ b/command/auth_test.go @@ -9,6 +9,9 @@ import ( "strings" "testing" + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/meta" @@ -84,6 +87,155 @@ func TestAuth_token(t *testing.T) { } } +func TestAuth_wrapping(t *testing.T) { + baseConfig := &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": credUserpass.Factory, + }, + } + cluster := vault.NewTestCluster(t, baseConfig, &vault.TestClusterOptions{ + HandlerFunc: http.Handler, + BaseListenAddress: "127.0.0.1:8200", + }) + cluster.Start() + defer cluster.Cleanup() + + testAuthInit(t) + + client := cluster.Cores[0].Client + err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatal(err) + } + _, err = client.Logical().Write("auth/userpass/users/foo", map[string]interface{}{ + "password": "bar", + "policies": "zip,zap", + }) + if err != nil { + t.Fatal(err) + } + + ui := new(cli.MockUi) + c := &AuthCommand{ + Meta: meta.Meta{ + Ui: ui, + TokenHelper: DefaultTokenHelper, + }, + Handlers: map[string]AuthHandler{ + "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, + }, + } + + args := []string{ + "-address", + "https://127.0.0.1:8200", + "-tls-skip-verify", + "-method", + "userpass", + "username=foo", + "password=bar", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test again with wrapping + ui = new(cli.MockUi) + c = &AuthCommand{ + Meta: meta.Meta{ + Ui: ui, + TokenHelper: DefaultTokenHelper, + }, + Handlers: map[string]AuthHandler{ + "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, + }, + } + + args = []string{ + "-address", + "https://127.0.0.1:8200", + "-tls-skip-verify", + "-wrap-ttl", + "5m", + "-method", + "userpass", + "username=foo", + "password=bar", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test again with no-store + ui = new(cli.MockUi) + c = &AuthCommand{ + Meta: meta.Meta{ + Ui: ui, + TokenHelper: DefaultTokenHelper, + }, + Handlers: map[string]AuthHandler{ + "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, + }, + } + + args = []string{ + "-address", + "https://127.0.0.1:8200", + "-tls-skip-verify", + "-wrap-ttl", + "5m", + "-no-store", + "-method", + "userpass", + "username=foo", + "password=bar", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test again with wrapping and token-only + ui = new(cli.MockUi) + c = &AuthCommand{ + Meta: meta.Meta{ + Ui: ui, + TokenHelper: DefaultTokenHelper, + }, + Handlers: map[string]AuthHandler{ + "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, + }, + } + + args = []string{ + "-address", + "https://127.0.0.1:8200", + "-tls-skip-verify", + "-wrap-ttl", + "5m", + "-token-only", + "-method", + "userpass", + "username=foo", + "password=bar", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + token := strings.TrimSpace(ui.OutputWriter.String()) + if token == "" { + t.Fatal("expected to find token in output") + } + secret, err := client.Logical().Unwrap(token) + if err != nil { + t.Fatal(err) + } + if secret.Auth.ClientToken == "" { + t.Fatal("no client token found") + } +} + func TestAuth_token_nostore(t *testing.T) { core, _, token := vault.TestCoreUnsealed(t) ln, addr := http.TestServer(t, core) @@ -237,8 +389,12 @@ func testAuthInit(t *testing.T) { type testAuthHandler struct{} -func (h *testAuthHandler) Auth(c *api.Client, m map[string]string) (string, error) { - return m["foo"], nil +func (h *testAuthHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { + return &api.Secret{ + Auth: &api.SecretAuth{ + ClientToken: m["foo"], + }, + }, nil } func (h *testAuthHandler) Help() string { return "" } diff --git a/helper/parseutil/parseutil.go b/helper/parseutil/parseutil.go index 9ba2bf78f4d3..957d5332e10a 100644 --- a/helper/parseutil/parseutil.go +++ b/helper/parseutil/parseutil.go @@ -19,6 +19,9 @@ func ParseDurationSecond(in interface{}) (time.Duration, error) { switch in.(type) { case string: inp := in.(string) + if inp == "" { + return time.Duration(0), nil + } var err error // Look for a suffix otherwise its a plain second value if strings.HasSuffix(inp, "s") || strings.HasSuffix(inp, "m") || strings.HasSuffix(inp, "h") { diff --git a/helper/pluginutil/mlock.go b/helper/pluginutil/mlock.go index dd9115a89a29..1660ca8e077b 100644 --- a/helper/pluginutil/mlock.go +++ b/helper/pluginutil/mlock.go @@ -7,7 +7,7 @@ import ( ) var ( - // PluginUnwrapTokenEnv is the ENV name used to pass the configuration for + // PluginMlockEnabled is the ENV name used to pass the configuration for // enabling mlock PluginMlockEnabled = "VAULT_PLUGIN_MLOCK_ENABLED" ) diff --git a/helper/pluginutil/runner.go b/helper/pluginutil/runner.go index e34f070c2b25..2047651ed2af 100644 --- a/helper/pluginutil/runner.go +++ b/helper/pluginutil/runner.go @@ -2,6 +2,7 @@ package pluginutil import ( "crypto/sha256" + "crypto/tls" "flag" "fmt" "os/exec" @@ -22,6 +23,7 @@ type Looker interface { // Wrapper interface defines the functions needed by the runner to wrap the // metadata needed to run a plugin process. This includes looking up Mlock // configuration and wrapping data in a respose wrapped token. +// logical.SystemView implementataions satisfy this interface. type RunnerUtil interface { ResponseWrapData(data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error) MlockEnabled() bool @@ -44,56 +46,82 @@ type PluginRunner struct { BuiltinFactory func() (interface{}, error) `json:"-" structs:"-"` } -// Run takes a wrapper instance, and the go-plugin paramaters and executes a -// plugin. +// Run takes a wrapper RunnerUtil instance along with the go-plugin paramaters and +// returns a configured plugin.Client with TLS Configured and a wrapping token set +// on PluginUnwrapTokenEnv for plugin process consumption. func (r *PluginRunner) Run(wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string, logger log.Logger) (*plugin.Client, error) { - // Get a CA TLS Certificate - certBytes, key, err := generateCert() - if err != nil { - return nil, err - } + return r.runCommon(wrapper, pluginMap, hs, env, logger, false) +} - // Use CA to sign a client cert and return a configured TLS config - clientTLSConfig, err := createClientTLSConfig(certBytes, key) - if err != nil { - return nil, err - } +// RunMetadataMode returns a configured plugin.Client that will dispense a plugin +// in metadata mode. The PluginMetadaModeEnv is passed in as part of the Cmd to +// plugin.Client, and consumed by the plugin process on pluginutil.VaultPluginTLSProvider. +func (r *PluginRunner) RunMetadataMode(wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string, logger log.Logger) (*plugin.Client, error) { + return r.runCommon(wrapper, pluginMap, hs, env, logger, true) - // Use CA to sign a server cert and wrap the values in a response wrapped - // token. - wrapToken, err := wrapServerConfig(wrapper, certBytes, key) - if err != nil { - return nil, err - } +} +func (r *PluginRunner) runCommon(wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string, logger log.Logger, isMetadataMode bool) (*plugin.Client, error) { cmd := exec.Command(r.Command, r.Args...) cmd.Env = append(cmd.Env, env...) - // Add the response wrap token to the ENV of the plugin - cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", PluginUnwrapTokenEnv, wrapToken)) + // Add the mlock setting to the ENV of the plugin if wrapper.MlockEnabled() { cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", PluginMlockEnabled, "true")) } - secureConfig := &plugin.SecureConfig{ - Checksum: r.Sha256, - Hash: sha256.New(), - } - // Create logger for the plugin client clogger := &hclogFaker{ logger: logger, } namedLogger := clogger.ResetNamed("plugin") - client := plugin.NewClient(&plugin.ClientConfig{ + var clientTLSConfig *tls.Config + if !isMetadataMode { + // Add the metadata mode ENV and set it to false + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", PluginMetadaModeEnv, "false")) + + // Get a CA TLS Certificate + certBytes, key, err := generateCert() + if err != nil { + return nil, err + } + + // Use CA to sign a client cert and return a configured TLS config + clientTLSConfig, err = createClientTLSConfig(certBytes, key) + if err != nil { + return nil, err + } + + // Use CA to sign a server cert and wrap the values in a response wrapped + // token. + wrapToken, err := wrapServerConfig(wrapper, certBytes, key) + if err != nil { + return nil, err + } + + // Add the response wrap token to the ENV of the plugin + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", PluginUnwrapTokenEnv, wrapToken)) + } else { + namedLogger = clogger.ResetNamed("plugin.metadata") + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", PluginMetadaModeEnv, "true")) + } + + secureConfig := &plugin.SecureConfig{ + Checksum: r.Sha256, + Hash: sha256.New(), + } + + clientConfig := &plugin.ClientConfig{ HandshakeConfig: hs, Plugins: pluginMap, Cmd: cmd, - TLSConfig: clientTLSConfig, SecureConfig: secureConfig, + TLSConfig: clientTLSConfig, Logger: namedLogger, - }) + } + + client := plugin.NewClient(clientConfig) return client, nil } @@ -108,7 +136,7 @@ type APIClientMeta struct { } func (f *APIClientMeta) FlagSet() *flag.FlagSet { - fs := flag.NewFlagSet("tls settings", flag.ContinueOnError) + fs := flag.NewFlagSet("vault plugin settings", flag.ContinueOnError) fs.StringVar(&f.flagCACert, "ca-cert", "", "") fs.StringVar(&f.flagCAPath, "ca-path", "", "") diff --git a/helper/pluginutil/tls.go b/helper/pluginutil/tls.go index d31344e3c568..112d33cf0508 100644 --- a/helper/pluginutil/tls.go +++ b/helper/pluginutil/tls.go @@ -29,6 +29,10 @@ var ( // PluginCACertPEMEnv is an ENV name used for holding a CA PEM-encoded // string. Used for testing. PluginCACertPEMEnv = "VAULT_TESTING_PLUGIN_CA_PEM" + + // PluginMetadaModeEnv is an ENV name used to disable TLS communication + // to bootstrap mounting plugins. + PluginMetadaModeEnv = "VAULT_PLUGIN_METADATA_MODE" ) // generateCert is used internally to create certificates for the plugin @@ -124,6 +128,10 @@ func wrapServerConfig(sys RunnerUtil, certBytes []byte, key *ecdsa.PrivateKey) ( // VaultPluginTLSProvider is run inside a plugin and retrives the response // wrapped TLS certificate from vault. It returns a configured TLS Config. func VaultPluginTLSProvider(apiTLSConfig *api.TLSConfig) func() (*tls.Config, error) { + if os.Getenv(PluginMetadaModeEnv) == "true" { + return nil + } + return func() (*tls.Config, error) { unwrapToken := os.Getenv(PluginUnwrapTokenEnv) @@ -157,7 +165,10 @@ func VaultPluginTLSProvider(apiTLSConfig *api.TLSConfig) func() (*tls.Config, er clientConf := api.DefaultConfig() clientConf.Address = vaultAddr if apiTLSConfig != nil { - clientConf.ConfigureTLS(apiTLSConfig) + err := clientConf.ConfigureTLS(apiTLSConfig) + if err != nil { + return nil, errwrap.Wrapf("error configuring api client {{err}}", err) + } } client, err := api.NewClient(clientConf) if err != nil { diff --git a/logical/plugin/backend.go b/logical/plugin/backend.go index 8a80be0298bb..081922c9bd3e 100644 --- a/logical/plugin/backend.go +++ b/logical/plugin/backend.go @@ -9,7 +9,8 @@ import ( // BackendPlugin is the plugin.Plugin implementation type BackendPlugin struct { - Factory func(*logical.BackendConfig) (logical.Backend, error) + Factory func(*logical.BackendConfig) (logical.Backend, error) + metadataMode bool } // Server gets called when on plugin.Serve() @@ -19,5 +20,5 @@ func (b *BackendPlugin) Server(broker *plugin.MuxBroker) (interface{}, error) { // Client gets called on plugin.NewClient() func (b BackendPlugin) Client(broker *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { - return &backendPluginClient{client: c, broker: broker}, nil + return &backendPluginClient{client: c, broker: broker, metadataMode: b.metadataMode}, nil } diff --git a/logical/plugin/backend_client.go b/logical/plugin/backend_client.go index f18564b134a7..cc2d83bcfb5e 100644 --- a/logical/plugin/backend_client.go +++ b/logical/plugin/backend_client.go @@ -1,6 +1,7 @@ package plugin import ( + "errors" "net/rpc" "github.com/hashicorp/go-plugin" @@ -8,11 +9,16 @@ import ( log "github.com/mgutz/logxi/v1" ) +var ( + ErrClientInMetadataMode = errors.New("plugin client can not perform action while in metadata mode") +) + // backendPluginClient implements logical.Backend and is the // go-plugin client. type backendPluginClient struct { - broker *plugin.MuxBroker - client *rpc.Client + broker *plugin.MuxBroker + client *rpc.Client + metadataMode bool system logical.SystemView logger log.Logger @@ -83,6 +89,10 @@ type RegisterLicenseReply struct { } func (b *backendPluginClient) HandleRequest(req *logical.Request) (*logical.Response, error) { + if b.metadataMode { + return nil, ErrClientInMetadataMode + } + // Do not send the storage, since go-plugin cannot serialize // interfaces. The server will pick up the storage from the shim. req.Storage = nil @@ -136,6 +146,10 @@ func (b *backendPluginClient) Logger() log.Logger { } func (b *backendPluginClient) HandleExistenceCheck(req *logical.Request) (bool, bool, error) { + if b.metadataMode { + return false, false, ErrClientInMetadataMode + } + // Do not send the storage, since go-plugin cannot serialize // interfaces. The server will pick up the storage from the shim. req.Storage = nil @@ -172,31 +186,49 @@ func (b *backendPluginClient) Cleanup() { } func (b *backendPluginClient) Initialize() error { + if b.metadataMode { + return ErrClientInMetadataMode + } err := b.client.Call("Plugin.Initialize", new(interface{}), &struct{}{}) return err } func (b *backendPluginClient) InvalidateKey(key string) { + if b.metadataMode { + return + } b.client.Call("Plugin.InvalidateKey", key, &struct{}{}) } func (b *backendPluginClient) Setup(config *logical.BackendConfig) error { // Shim logical.Storage + storageImpl := config.StorageView + if b.metadataMode { + storageImpl = &NOOPStorage{} + } storageID := b.broker.NextId() go b.broker.AcceptAndServe(storageID, &StorageServer{ - impl: config.StorageView, + impl: storageImpl, }) // Shim log.Logger + loggerImpl := config.Logger + if b.metadataMode { + loggerImpl = log.NullLog + } loggerID := b.broker.NextId() go b.broker.AcceptAndServe(loggerID, &LoggerServer{ - logger: config.Logger, + logger: loggerImpl, }) // Shim logical.SystemView + sysViewImpl := config.System + if b.metadataMode { + sysViewImpl = &logical.StaticSystemView{} + } sysViewID := b.broker.NextId() go b.broker.AcceptAndServe(sysViewID, &SystemViewServer{ - impl: config.System, + impl: sysViewImpl, }) args := &SetupArgs{ @@ -233,6 +265,10 @@ func (b *backendPluginClient) Type() logical.BackendType { } func (b *backendPluginClient) RegisterLicense(license interface{}) error { + if b.metadataMode { + return ErrClientInMetadataMode + } + var reply RegisterLicenseReply args := RegisterLicenseArgs{ License: license, diff --git a/logical/plugin/backend_server.go b/logical/plugin/backend_server.go index 335bfa5f738e..47045b1c13f1 100644 --- a/logical/plugin/backend_server.go +++ b/logical/plugin/backend_server.go @@ -1,12 +1,19 @@ package plugin import ( + "errors" "net/rpc" + "os" "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/helper/pluginutil" "github.com/hashicorp/vault/logical" ) +var ( + ErrServerInMetadataMode = errors.New("plugin server can not perform action while in metadata mode") +) + // backendPluginServer is the RPC server that backendPluginClient talks to, // it methods conforming to requirements by net/rpc type backendPluginServer struct { @@ -19,7 +26,15 @@ type backendPluginServer struct { storageClient *rpc.Client } +func inMetadataMode() bool { + return os.Getenv(pluginutil.PluginMetadaModeEnv) == "true" +} + func (b *backendPluginServer) HandleRequest(args *HandleRequestArgs, reply *HandleRequestReply) error { + if inMetadataMode() { + return ErrServerInMetadataMode + } + storage := &StorageClient{client: b.storageClient} args.Request.Storage = storage @@ -40,6 +55,10 @@ func (b *backendPluginServer) SpecialPaths(_ interface{}, reply *SpecialPathsRep } func (b *backendPluginServer) HandleExistenceCheck(args *HandleExistenceCheckArgs, reply *HandleExistenceCheckReply) error { + if inMetadataMode() { + return ErrServerInMetadataMode + } + storage := &StorageClient{client: b.storageClient} args.Request.Storage = storage @@ -64,11 +83,19 @@ func (b *backendPluginServer) Cleanup(_ interface{}, _ *struct{}) error { } func (b *backendPluginServer) Initialize(_ interface{}, _ *struct{}) error { + if inMetadataMode() { + return ErrServerInMetadataMode + } + err := b.backend.Initialize() return err } func (b *backendPluginServer) InvalidateKey(args string, _ *struct{}) error { + if inMetadataMode() { + return ErrServerInMetadataMode + } + b.backend.InvalidateKey(args) return nil } @@ -145,6 +172,10 @@ func (b *backendPluginServer) Type(_ interface{}, reply *TypeReply) error { } func (b *backendPluginServer) RegisterLicense(args *RegisterLicenseArgs, reply *RegisterLicenseReply) error { + if inMetadataMode() { + return ErrServerInMetadataMode + } + err := b.backend.RegisterLicense(args.License) if err != nil { *reply = RegisterLicenseReply{ diff --git a/logical/plugin/mock/backend.go b/logical/plugin/mock/backend.go index 5f4c97749618..ac8c0ba88fbf 100644 --- a/logical/plugin/mock/backend.go +++ b/logical/plugin/mock/backend.go @@ -43,6 +43,7 @@ func Backend() *backend { kvPaths(&b), []*framework.Path{ pathInternal(&b), + pathSpecial(&b), }, ), PathsSpecial: &logical.Paths{ diff --git a/logical/plugin/mock/mock-plugin/main.go b/logical/plugin/mock/mock-plugin/main.go index 1cb47bfcfdde..b1b7fbd7137d 100644 --- a/logical/plugin/mock/mock-plugin/main.go +++ b/logical/plugin/mock/mock-plugin/main.go @@ -13,7 +13,7 @@ import ( func main() { apiClientMeta := &pluginutil.APIClientMeta{} flags := apiClientMeta.FlagSet() - flags.Parse(os.Args) + flags.Parse(os.Args[1:]) // Ignore command, strictly parse flags tlsConfig := apiClientMeta.GetTLSConfig() tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig) diff --git a/logical/plugin/mock/path_special.go b/logical/plugin/mock/path_special.go new file mode 100644 index 000000000000..f695e209ef8e --- /dev/null +++ b/logical/plugin/mock/path_special.go @@ -0,0 +1,27 @@ +package mock + +import ( + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +// pathSpecial is used to test special paths. +func pathSpecial(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "special", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathSpecialRead, + }, + } +} + +func (b *backend) pathSpecialRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + // Return the secret + return &logical.Response{ + Data: map[string]interface{}{ + "data": "foo", + }, + }, nil + +} diff --git a/logical/plugin/plugin.go b/logical/plugin/plugin.go index eeb6e073c3e9..ede06229bc17 100644 --- a/logical/plugin/plugin.go +++ b/logical/plugin/plugin.go @@ -5,6 +5,7 @@ import ( "crypto/rsa" "encoding/gob" "fmt" + "time" "sync" @@ -19,6 +20,7 @@ import ( func init() { gob.Register(rsa.PublicKey{}) gob.Register(ecdsa.PublicKey{}) + gob.Register(time.Duration(0)) } // BackendPluginClient is a wrapper around backendPluginClient @@ -40,8 +42,9 @@ func (b *BackendPluginClient) Cleanup() { // NewBackend will return an instance of an RPC-based client implementation of the backend for // external plugins, or a concrete implementation of the backend if it is a builtin backend. -// The backend is returned as a logical.Backend interface. -func NewBackend(pluginName string, sys pluginutil.LookRunnerUtil, logger log.Logger) (logical.Backend, error) { +// The backend is returned as a logical.Backend interface. The isMetadataMode param determines whether +// the plugin should run in metadata mode. +func NewBackend(pluginName string, sys pluginutil.LookRunnerUtil, logger log.Logger, isMetadataMode bool) (logical.Backend, error) { // Look for plugin in the plugin catalog pluginRunner, err := sys.LookupPlugin(pluginName) if err != nil { @@ -65,7 +68,7 @@ func NewBackend(pluginName string, sys pluginutil.LookRunnerUtil, logger log.Log } else { // create a backendPluginClient instance - backend, err = newPluginClient(sys, pluginRunner, logger) + backend, err = newPluginClient(sys, pluginRunner, logger, isMetadataMode) if err != nil { return nil, err } @@ -74,12 +77,21 @@ func NewBackend(pluginName string, sys pluginutil.LookRunnerUtil, logger log.Log return backend, nil } -func newPluginClient(sys pluginutil.RunnerUtil, pluginRunner *pluginutil.PluginRunner, logger log.Logger) (logical.Backend, error) { +func newPluginClient(sys pluginutil.RunnerUtil, pluginRunner *pluginutil.PluginRunner, logger log.Logger, isMetadataMode bool) (logical.Backend, error) { // pluginMap is the map of plugins we can dispense. pluginMap := map[string]plugin.Plugin{ - "backend": &BackendPlugin{}, + "backend": &BackendPlugin{ + metadataMode: isMetadataMode, + }, + } + + var client *plugin.Client + var err error + if isMetadataMode { + client, err = pluginRunner.RunMetadataMode(sys, pluginMap, handshakeConfig, []string{}, logger) + } else { + client, err = pluginRunner.Run(sys, pluginMap, handshakeConfig, []string{}, logger) } - client, err := pluginRunner.Run(sys, pluginMap, handshakeConfig, []string{}, logger) if err != nil { return nil, err } diff --git a/logical/plugin/serve.go b/logical/plugin/serve.go index 4eb69a8c5422..1d70b3a177c7 100644 --- a/logical/plugin/serve.go +++ b/logical/plugin/serve.go @@ -20,7 +20,8 @@ type ServeOpts struct { TLSProviderFunc TLSProdiverFunc } -// Serve is used to serve a backend plugin +// Serve is a helper function used to serve a backend plugin. This +// should be ran on the plugin's main process. func Serve(opts *ServeOpts) error { // pluginMap is the map of plugins we can dispense. var pluginMap = map[string]plugin.Plugin{ @@ -34,6 +35,7 @@ func Serve(opts *ServeOpts) error { return err } + // If FetchMetadata is true, run without TLSProvider plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: handshakeConfig, Plugins: pluginMap, @@ -48,7 +50,7 @@ func Serve(opts *ServeOpts) error { // This prevents users from executing bad plugins or executing a plugin // directory. It is a UX feature, not a security feature. var handshakeConfig = plugin.HandshakeConfig{ - ProtocolVersion: 1, + ProtocolVersion: 2, MagicCookieKey: "VAULT_BACKEND_PLUGIN", MagicCookieValue: "6669da05-b1c8-4f49-97d9-c8e5bed98e20", } diff --git a/logical/plugin/storage.go b/logical/plugin/storage.go index 55cea8449bf8..99c21f6461d0 100644 --- a/logical/plugin/storage.go +++ b/logical/plugin/storage.go @@ -117,3 +117,23 @@ type StoragePutReply struct { type StorageDeleteReply struct { Error *plugin.BasicError } + +// NOOPStorage is used to deny access to the storage interface while running a +// backend plugin in metadata mode. +type NOOPStorage struct{} + +func (s *NOOPStorage) List(prefix string) ([]string, error) { + return []string{}, nil +} + +func (s *NOOPStorage) Get(key string) (*logical.StorageEntry, error) { + return nil, nil +} + +func (s *NOOPStorage) Put(entry *logical.StorageEntry) error { + return nil +} + +func (s *NOOPStorage) Delete(key string) error { + return nil +} diff --git a/physical/consul/consul.go b/physical/consul/consul.go index 8256808b1a13..6c3146683e72 100644 --- a/physical/consul/consul.go +++ b/physical/consul/consul.go @@ -241,7 +241,14 @@ func NewConsulBackend(conf map[string]string, logger log.Logger) (physical.Backe } func setupTLSConfig(conf map[string]string) (*tls.Config, error) { - serverName := strings.Split(conf["address"], ":") + serverName, _, err := net.SplitHostPort(conf["address"]) + switch { + case err == nil: + case strings.Contains(err.Error(), "missing port"): + serverName = conf["address"] + default: + return nil, err + } insecureSkipVerify := false if _, ok := conf["tls_skip_verify"]; ok { @@ -262,7 +269,7 @@ func setupTLSConfig(conf map[string]string) (*tls.Config, error) { tlsClientConfig := &tls.Config{ MinVersion: tlsMinVersion, InsecureSkipVerify: insecureSkipVerify, - ServerName: serverName[0], + ServerName: serverName, } _, okCert := conf["tls_cert_file"] diff --git a/plugins/database/mongodb/mongodb.go b/plugins/database/mongodb/mongodb.go index 31fdd85cc82a..52671dae2f5c 100644 --- a/plugins/database/mongodb/mongodb.go +++ b/plugins/database/mongodb/mongodb.go @@ -1,12 +1,15 @@ package mongodb import ( + "io" + "strings" "time" "encoding/json" "fmt" + "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/builtin/logical/database/dbplugin" "github.com/hashicorp/vault/plugins" @@ -124,7 +127,21 @@ func (m *MongoDB) CreateUser(statements dbplugin.Statements, usernameConfig dbpl } err = session.DB(mongoCS.DB).Run(createUserCmd, nil) - if err != nil { + switch { + case err == nil: + case err == io.EOF, strings.Contains(err.Error(), "EOF"): + if err := m.ConnectionProducer.Close(); err != nil { + return "", "", errwrap.Wrapf("error closing EOF'd mongo connection: {{err}}", err) + } + session, err := m.getConnection() + if err != nil { + return "", "", err + } + err = session.DB(mongoCS.DB).Run(createUserCmd, nil) + if err != nil { + return "", "", err + } + default: return "", "", err } @@ -165,7 +182,21 @@ func (m *MongoDB) RevokeUser(statements dbplugin.Statements, username string) er } err = session.DB(db).RemoveUser(username) - if err != nil && err != mgo.ErrNotFound { + switch { + case err == nil, err == mgo.ErrNotFound: + case err == io.EOF, strings.Contains(err.Error(), "EOF"): + if err := m.ConnectionProducer.Close(); err != nil { + return errwrap.Wrapf("error closing EOF'd mongo connection: {{err}}", err) + } + session, err := m.getConnection() + if err != nil { + return err + } + err = session.DB(db).RemoveUser(username) + if err != nil { + return err + } + default: return err } diff --git a/vault/auth.go b/vault/auth.go index 608a217a54b3..3cd93be2a230 100644 --- a/vault/auth.go +++ b/vault/auth.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/errwrap" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/logical" @@ -397,7 +398,6 @@ func (c *Core) persistAuth(table *MountTable, localOnly bool) error { // setupCredentials is invoked after we've loaded the auth table to // initialize the credential backends and setup the router func (c *Core) setupCredentials() error { - var backend logical.Backend var view *BarrierView var err error var persistNeeded bool @@ -406,6 +406,7 @@ func (c *Core) setupCredentials() error { defer c.authLock.Unlock() for _, entry := range c.auth.Entries { + var backend logical.Backend // Work around some problematic code that existed in master for a while if strings.HasPrefix(entry.Path, credentialRoutePrefix) { entry.Path = strings.TrimPrefix(entry.Path, credentialRoutePrefix) @@ -425,6 +426,9 @@ func (c *Core) setupCredentials() error { backend, err = c.newCredentialBackend(entry.Type, sysView, view, conf) if err != nil { c.logger.Error("core: failed to create credential entry", "path", entry.Path, "error", err) + if errwrap.Contains(err, ErrPluginNotFound.Error()) && entry.Type == "plugin" { + goto ROUTER_MOUNT + } return errLoadAuthFailed } if backend == nil { @@ -432,15 +436,14 @@ func (c *Core) setupCredentials() error { } // Check for the correct backend type - backendType := backend.Type() - if entry.Type == "plugin" && backendType != logical.TypeCredential { - return fmt.Errorf("cannot mount '%s' of type '%s' as an auth backend", entry.Config.PluginName, backendType) + if entry.Type == "plugin" && backend.Type() != logical.TypeCredential { + return fmt.Errorf("cannot mount '%s' of type '%s' as an auth backend", entry.Config.PluginName, backend.Type()) } if err := backend.Initialize(); err != nil { return err } - + ROUTER_MOUNT: // Mount the backend path := credentialRoutePrefix + entry.Path err = c.router.Mount(backend, path, entry, view) diff --git a/vault/core.go b/vault/core.go index e63e29aaf5ef..b79f3fbde81f 100644 --- a/vault/core.go +++ b/vault/core.go @@ -1369,9 +1369,6 @@ func (c *Core) postUnseal() (retErr error) { if err := c.setupMounts(); err != nil { return err } - if err := c.startRollback(); err != nil { - return err - } if err := c.setupPolicyStore(); err != nil { return err } @@ -1384,6 +1381,9 @@ func (c *Core) postUnseal() (retErr error) { if err := c.setupCredentials(); err != nil { return err } + if err := c.startRollback(); err != nil { + return err + } if err := c.setupExpiration(); err != nil { return err } diff --git a/vault/dynamic_system_view.go b/vault/dynamic_system_view.go index af6cb7fbe3ef..3a645636472e 100644 --- a/vault/dynamic_system_view.go +++ b/vault/dynamic_system_view.go @@ -4,6 +4,8 @@ import ( "fmt" "time" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/pluginutil" "github.com/hashicorp/vault/helper/wrapping" @@ -132,7 +134,7 @@ func (d dynamicSystemView) LookupPlugin(name string) (*pluginutil.PluginRunner, return nil, err } if r == nil { - return nil, fmt.Errorf("no plugin found with name: %s", name) + return nil, errwrap.Wrapf(fmt.Sprintf("{{err}}: %s", name), ErrPluginNotFound) } return r, nil diff --git a/vault/logical_system.go b/vault/logical_system.go index 0fab16f12145..0dccc7f917be 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -291,6 +291,10 @@ func NewSystemBackend(core *Core) *SystemBackend { Default: false, Description: strings.TrimSpace(sysHelp["mount_local"][0]), }, + "plugin_name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["mount_plugin_name"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -493,15 +497,19 @@ func NewSystemBackend(core *Core) *SystemBackend { Type: framework.TypeString, Description: strings.TrimSpace(sysHelp["auth_desc"][0]), }, - "plugin_name": &framework.FieldSchema{ - Type: framework.TypeString, - Description: strings.TrimSpace(sysHelp["auth_plugin"][0]), + "config": &framework.FieldSchema{ + Type: framework.TypeMap, + Description: strings.TrimSpace(sysHelp["auth_config"][0]), }, "local": &framework.FieldSchema{ Type: framework.TypeBool, Default: false, Description: strings.TrimSpace(sysHelp["mount_local"][0]), }, + "plugin_name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["auth_plugin"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -1256,6 +1264,7 @@ func (b *SystemBackend) handleMount( path := data.Get("path").(string) logicalType := data.Get("type").(string) description := data.Get("description").(string) + pluginName := data.Get("plugin_name").(string) path = sanitizeMountPath(path) @@ -1310,9 +1319,19 @@ func (b *SystemBackend) handleMount( logical.ErrInvalidRequest } - // Only set plugin-name if mount is of type plugin - if logicalType == "plugin" && apiConfig.PluginName != "" { - config.PluginName = apiConfig.PluginName + // Only set plugin-name if mount is of type plugin, with apiConfig.PluginName + // option taking precedence. + if logicalType == "plugin" { + switch { + case apiConfig.PluginName != "": + config.PluginName = apiConfig.PluginName + case pluginName != "": + config.PluginName = pluginName + default: + return logical.ErrorResponse( + "plugin_name must be provided for plugin backend"), + logical.ErrInvalidRequest + } } // Copy over the force no cache if set @@ -1754,10 +1773,31 @@ func (b *SystemBackend) handleEnableAuth( pluginName := data.Get("plugin_name").(string) var config MountConfig + var apiConfig APIMountConfig - // Only set plugin name if mount is of type plugin - if logicalType == "plugin" && pluginName != "" { - config.PluginName = pluginName + configMap := data.Get("config").(map[string]interface{}) + if configMap != nil && len(configMap) != 0 { + err := mapstructure.Decode(configMap, &apiConfig) + if err != nil { + return logical.ErrorResponse( + "unable to convert given auth config information"), + logical.ErrInvalidRequest + } + } + + // Only set plugin name if mount is of type plugin, with apiConfig.PluginName + // option taking precedence. + if logicalType == "plugin" { + switch { + case apiConfig.PluginName != "": + config.PluginName = apiConfig.PluginName + case pluginName != "": + config.PluginName = pluginName + default: + return logical.ErrorResponse( + "plugin_name must be provided for plugin backend"), + logical.ErrInvalidRequest + } } if logicalType == "" { @@ -2541,6 +2581,11 @@ and max_lease_ttl.`, and is unaffected by replication.`, }, + "mount_plugin_name": { + `Name of the plugin to mount based from the name registered +in the plugin catalog.`, + }, + "tune_default_lease_ttl": { `The default lease TTL for this mount.`, }, @@ -2677,6 +2722,10 @@ Example: you might have an OAuth backend for GitHub, and one for Google Apps. "", }, + "auth_config": { + `Configuration for this mount, such as plugin_name.`, + }, + "auth_plugin": { `Name of the auth plugin to use based from the name in the plugin catalog.`, "", diff --git a/vault/logical_system_integ_test.go b/vault/logical_system_integ_test.go index bc1e0547ebf5..60eab6b69a23 100644 --- a/vault/logical_system_integ_test.go +++ b/vault/logical_system_integ_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "testing" + "time" "github.com/hashicorp/vault/builtin/plugin" "github.com/hashicorp/vault/helper/pluginutil" @@ -15,17 +16,196 @@ import ( ) func TestSystemBackend_Plugin_secret(t *testing.T) { - cluster := testSystemBackendMock(t, 1, logical.TypeLogical) + cluster := testSystemBackendMock(t, 1, 1, logical.TypeLogical) defer cluster.Cleanup() + + core := cluster.Cores[0] + + // Make a request to lazy load the plugin + req := logical.TestRequest(t, logical.ReadOperation, "mock-0/internal") + req.ClientToken = core.Client.Token() + resp, err := core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil { + t.Fatalf("bad: response should not be nil") + } + + // Seal the cluster + cluster.EnsureCoresSealed(t) + + // Unseal the cluster + barrierKeys := cluster.BarrierKeys + for _, core := range cluster.Cores { + for _, key := range barrierKeys { + _, err := core.Unseal(vault.TestKeyCopy(key)) + if err != nil { + t.Fatal(err) + } + } + sealed, err := core.Sealed() + if err != nil { + t.Fatalf("err checking seal status: %s", err) + } + if sealed { + t.Fatal("should not be sealed") + } + // Wait for active so post-unseal takes place + // If it fails, it means unseal process failed + vault.TestWaitActive(t, core.Core) + } } func TestSystemBackend_Plugin_auth(t *testing.T) { - cluster := testSystemBackendMock(t, 1, logical.TypeCredential) + cluster := testSystemBackendMock(t, 1, 1, logical.TypeCredential) + defer cluster.Cleanup() + + core := cluster.Cores[0] + + // Make a request to lazy load the plugin + req := logical.TestRequest(t, logical.ReadOperation, "auth/mock-0/internal") + req.ClientToken = core.Client.Token() + resp, err := core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil { + t.Fatalf("bad: response should not be nil") + } + + // Seal the cluster + cluster.EnsureCoresSealed(t) + + // Unseal the cluster + barrierKeys := cluster.BarrierKeys + for _, core := range cluster.Cores { + for _, key := range barrierKeys { + _, err := core.Unseal(vault.TestKeyCopy(key)) + if err != nil { + t.Fatal(err) + } + } + sealed, err := core.Sealed() + if err != nil { + t.Fatalf("err checking seal status: %s", err) + } + if sealed { + t.Fatal("should not be sealed") + } + // Wait for active so post-unseal takes place + // If it fails, it means unseal process failed + vault.TestWaitActive(t, core.Core) + } +} + +func TestSystemBackend_Plugin_MismatchType(t *testing.T) { + cluster := testSystemBackendMock(t, 1, 1, logical.TypeLogical) + defer cluster.Cleanup() + + core := cluster.Cores[0] + + // Replace the plugin with a credential backend + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainCredentials") + + // Make a request to lazy load the now-credential plugin + // and expect an error + req := logical.TestRequest(t, logical.ReadOperation, "mock-0/internal") + req.ClientToken = core.Client.Token() + _, err := core.HandleRequest(req) + if err == nil { + t.Fatalf("expected error due to mismatch on error type: %s", err) + } + + // Sleep a bit before cleanup is called + time.Sleep(1 * time.Second) +} + +func TestSystemBackend_Plugin_CatalogRemoved(t *testing.T) { + t.Run("secret", func(t *testing.T) { + testPlugin_CatalogRemoved(t, logical.TypeLogical, false) + }) + + t.Run("auth", func(t *testing.T) { + testPlugin_CatalogRemoved(t, logical.TypeCredential, false) + }) + + t.Run("secret-mount-existing", func(t *testing.T) { + testPlugin_CatalogRemoved(t, logical.TypeLogical, true) + }) + + t.Run("auth-mount-existing", func(t *testing.T) { + testPlugin_CatalogRemoved(t, logical.TypeCredential, true) + }) +} + +func testPlugin_CatalogRemoved(t *testing.T, btype logical.BackendType, testMount bool) { + cluster := testSystemBackendMock(t, 1, 1, btype) defer cluster.Cleanup() + + core := cluster.Cores[0] + + // Remove the plugin from the catalog + req := logical.TestRequest(t, logical.DeleteOperation, "sys/plugins/catalog/mock-plugin") + req.ClientToken = core.Client.Token() + resp, err := core.HandleRequest(req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + // Seal the cluster + cluster.EnsureCoresSealed(t) + + // Unseal the cluster + barrierKeys := cluster.BarrierKeys + for _, core := range cluster.Cores { + for _, key := range barrierKeys { + _, err := core.Unseal(vault.TestKeyCopy(key)) + if err != nil { + t.Fatal(err) + } + } + sealed, err := core.Sealed() + if err != nil { + t.Fatalf("err checking seal status: %s", err) + } + if sealed { + t.Fatal("should not be sealed") + } + // Wait for active so post-unseal takes place + // If it fails, it means unseal process failed + vault.TestWaitActive(t, core.Core) + } + + if testMount { + // Add plugin back to the catalog + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainLogical") + + // Mount the plugin at the same path after plugin is re-added to the catalog + // and expect an error due to existing path. + var err error + switch btype { + case logical.TypeLogical: + _, err = core.Client.Logical().Write("sys/mounts/mock-0", map[string]interface{}{ + "type": "plugin", + "config": map[string]interface{}{ + "plugin_name": "mock-plugin", + }, + }) + case logical.TypeCredential: + _, err = core.Client.Logical().Write("sys/auth/mock-0", map[string]interface{}{ + "type": "plugin", + "plugin_name": "mock-plugin", + }) + } + if err == nil { + t.Fatal("expected error when mounting on existing path") + } + } } func TestSystemBackend_Plugin_autoReload(t *testing.T) { - cluster := testSystemBackendMock(t, 1, logical.TypeLogical) + cluster := testSystemBackendMock(t, 1, 1, logical.TypeLogical) defer cluster.Cleanup() core := cluster.Cores[0] @@ -65,6 +245,35 @@ func TestSystemBackend_Plugin_autoReload(t *testing.T) { } } +func TestSystemBackend_Plugin_SealUnseal(t *testing.T) { + cluster := testSystemBackendMock(t, 1, 1, logical.TypeLogical) + defer cluster.Cleanup() + + // Seal the cluster + cluster.EnsureCoresSealed(t) + + // Unseal the cluster + barrierKeys := cluster.BarrierKeys + for _, core := range cluster.Cores { + for _, key := range barrierKeys { + _, err := core.Unseal(vault.TestKeyCopy(key)) + if err != nil { + t.Fatal(err) + } + } + sealed, err := core.Sealed() + if err != nil { + t.Fatalf("err checking seal status: %s", err) + } + if sealed { + t.Fatal("should not be sealed") + } + // Wait for active so post-unseal takes place + // If it fails, it means unseal process failed + vault.TestWaitActive(t, core.Core) + } +} + func TestSystemBackend_Plugin_reload(t *testing.T) { data := map[string]interface{}{ "plugin": "mock-plugin", @@ -77,8 +286,9 @@ func TestSystemBackend_Plugin_reload(t *testing.T) { t.Run("mounts", func(t *testing.T) { testSystemBackend_PluginReload(t, data) }) } +// Helper func to test different reload methods on plugin reload endpoint func testSystemBackend_PluginReload(t *testing.T, reqData map[string]interface{}) { - cluster := testSystemBackendMock(t, 2, logical.TypeLogical) + cluster := testSystemBackendMock(t, 1, 2, logical.TypeLogical) defer cluster.Cleanup() core := cluster.Cores[0] @@ -123,7 +333,7 @@ func testSystemBackend_PluginReload(t *testing.T, reqData map[string]interface{} // testSystemBackendMock returns a systemBackend with the desired number // of mounted mock plugin backends -func testSystemBackendMock(t *testing.T, numMounts int, backendType logical.BackendType) *vault.TestCluster { +func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType logical.BackendType) *vault.TestCluster { coreConfig := &vault.CoreConfig{ LogicalBackends: map[string]logical.Factory{ "plugin": plugin.Factory, @@ -134,7 +344,9 @@ func testSystemBackendMock(t *testing.T, numMounts int, backendType logical.Back } cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, + HandlerFunc: vaulthttp.Handler, + KeepStandbysSealed: true, + NumCores: numCores, }) cluster.Start() @@ -148,12 +360,18 @@ func testSystemBackendMock(t *testing.T, numMounts int, backendType logical.Back case logical.TypeLogical: vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainLogical") for i := 0; i < numMounts; i++ { - resp, err := client.Logical().Write(fmt.Sprintf("sys/mounts/mock-%d", i), map[string]interface{}{ + // Alternate input styles for plugin_name on every other mount + options := map[string]interface{}{ "type": "plugin", - "config": map[string]interface{}{ + } + if (i+1)%2 == 0 { + options["config"] = map[string]interface{}{ "plugin_name": "mock-plugin", - }, - }) + } + } else { + options["plugin_name"] = "mock-plugin" + } + resp, err := client.Logical().Write(fmt.Sprintf("sys/mounts/mock-%d", i), options) if err != nil { t.Fatalf("err: %v", err) } @@ -164,10 +382,18 @@ func testSystemBackendMock(t *testing.T, numMounts int, backendType logical.Back case logical.TypeCredential: vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainCredentials") for i := 0; i < numMounts; i++ { - resp, err := client.Logical().Write(fmt.Sprintf("sys/auth/mock-%d", i), map[string]interface{}{ - "type": "plugin", - "plugin_name": "mock-plugin", - }) + // Alternate input styles for plugin_name on every other mount + options := map[string]interface{}{ + "type": "plugin", + } + if (i+1)%2 == 0 { + options["config"] = map[string]interface{}{ + "plugin_name": "mock-plugin", + } + } else { + options["plugin_name"] = "mock-plugin" + } + resp, err := client.Logical().Write(fmt.Sprintf("sys/auth/mock-%d", i), options) if err != nil { t.Fatalf("err: %v", err) } @@ -183,7 +409,8 @@ func testSystemBackendMock(t *testing.T, numMounts int, backendType logical.Back } func TestBackend_PluginMainLogical(t *testing.T) { - if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" { + args := []string{} + if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" && os.Getenv(pluginutil.PluginMetadaModeEnv) != "true" { return } @@ -191,16 +418,16 @@ func TestBackend_PluginMainLogical(t *testing.T) { if caPEM == "" { t.Fatal("CA cert not passed in") } - - factoryFunc := mock.FactoryType(logical.TypeLogical) - - args := []string{"--ca-cert=" + caPEM} + args = append(args, fmt.Sprintf("--ca-cert=%s", caPEM)) apiClientMeta := &pluginutil.APIClientMeta{} flags := apiClientMeta.FlagSet() flags.Parse(args) tlsConfig := apiClientMeta.GetTLSConfig() tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig) + + factoryFunc := mock.FactoryType(logical.TypeLogical) + err := lplugin.Serve(&lplugin.ServeOpts{ BackendFactoryFunc: factoryFunc, TLSProviderFunc: tlsProviderFunc, @@ -211,7 +438,8 @@ func TestBackend_PluginMainLogical(t *testing.T) { } func TestBackend_PluginMainCredentials(t *testing.T) { - if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" { + args := []string{} + if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" && os.Getenv(pluginutil.PluginMetadaModeEnv) != "true" { return } @@ -219,16 +447,16 @@ func TestBackend_PluginMainCredentials(t *testing.T) { if caPEM == "" { t.Fatal("CA cert not passed in") } - - factoryFunc := mock.FactoryType(logical.TypeCredential) - - args := []string{"--ca-cert=" + caPEM} + args = append(args, fmt.Sprintf("--ca-cert=%s", caPEM)) apiClientMeta := &pluginutil.APIClientMeta{} flags := apiClientMeta.FlagSet() flags.Parse(args) tlsConfig := apiClientMeta.GetTLSConfig() tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig) + + factoryFunc := mock.FactoryType(logical.TypeCredential) + err := lplugin.Serve(&lplugin.ServeOpts{ BackendFactoryFunc: factoryFunc, TLSProviderFunc: tlsProviderFunc, diff --git a/vault/mount.go b/vault/mount.go index 16b048a69fe5..1fd5f748c226 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/hashicorp/errwrap" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/jsonutil" @@ -663,11 +664,12 @@ func (c *Core) setupMounts() error { c.mountsLock.Lock() defer c.mountsLock.Unlock() - var backend logical.Backend var view *BarrierView var err error for _, entry := range c.mounts.Entries { + var backend logical.Backend + // Initialize the backend, special casing for system barrierPath := backendBarrierPrefix + entry.UUID + "/" if entry.Type == "system" { @@ -686,6 +688,9 @@ func (c *Core) setupMounts() error { backend, err = c.newLogicalBackend(entry.Type, sysView, view, conf) if err != nil { c.logger.Error("core: failed to create mount entry", "path", entry.Path, "error", err) + if errwrap.Contains(err, ErrPluginNotFound.Error()) && entry.Type == "plugin" { + goto ROUTER_MOUNT + } return errLoadMountsFailed } if backend == nil { @@ -693,9 +698,8 @@ func (c *Core) setupMounts() error { } // Check for the correct backend type - backendType := backend.Type() - if entry.Type == "plugin" && backendType != logical.TypeLogical { - return fmt.Errorf("cannot mount '%s' of type '%s' as a logical backend", entry.Config.PluginName, backendType) + if entry.Type == "plugin" && backend.Type() != logical.TypeLogical { + return fmt.Errorf("cannot mount '%s' of type '%s' as a logical backend", entry.Config.PluginName, backend.Type()) } if err := backend.Initialize(); err != nil { @@ -710,7 +714,7 @@ func (c *Core) setupMounts() error { ch.saltUUID = entry.UUID ch.storageView = view } - + ROUTER_MOUNT: // Mount the backend err = c.router.Mount(backend, entry.Path, entry, view) if err != nil { diff --git a/vault/plugin_catalog.go b/vault/plugin_catalog.go index 09f612cc87ab..3e2466ff6420 100644 --- a/vault/plugin_catalog.go +++ b/vault/plugin_catalog.go @@ -19,6 +19,7 @@ import ( var ( pluginCatalogPath = "core/plugin-catalog/" ErrDirectoryNotConfigured = errors.New("could not set plugin, plugin directory is not configured") + ErrPluginNotFound = errors.New("plugin not found in the catalog") ) // PluginCatalog keeps a record of plugins known to vault. External plugins need @@ -37,6 +38,10 @@ func (c *Core) setupPluginCatalog() error { directory: c.pluginDirectory, } + if c.logger.IsInfo() { + c.logger.Info("core: successfully setup plugin catalog", "plugin-directory", c.pluginDirectory) + } + return nil } diff --git a/vault/router.go b/vault/router.go index 931a1b5dceb8..6e516be6a65d 100644 --- a/vault/router.go +++ b/vault/router.go @@ -64,9 +64,12 @@ func (r *Router) Mount(backend logical.Backend, prefix string, mountEntry *Mount } // Build the paths - paths := backend.SpecialPaths() - if paths == nil { - paths = new(logical.Paths) + paths := new(logical.Paths) + if backend != nil { + specialPaths := backend.SpecialPaths() + if specialPaths != nil { + paths = specialPaths + } } // Create a mount entry @@ -288,17 +291,19 @@ func (r *Router) RouteExistenceCheck(req *logical.Request) (bool, bool, error) { func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logical.Response, bool, bool, error) { // Find the mount point r.l.RLock() - mount, raw, ok := r.root.LongestPrefix(req.Path) - if !ok { + adjustedPath := req.Path + mount, raw, ok := r.root.LongestPrefix(adjustedPath) + if !ok && !strings.HasSuffix(adjustedPath, "/") { // Re-check for a backend by appending a slash. This lets "foo" mean // "foo/" at the root level which is almost always what we want. - req.Path += "/" - mount, raw, ok = r.root.LongestPrefix(req.Path) + adjustedPath += "/" + mount, raw, ok = r.root.LongestPrefix(adjustedPath) } r.l.RUnlock() if !ok { return logical.ErrorResponse(fmt.Sprintf("no handler for route '%s'", req.Path)), false, false, logical.ErrUnsupportedPath } + req.Path = adjustedPath defer metrics.MeasureSince([]string{"route", string(req.Operation), strings.Replace(mount, "/", "-", -1)}, time.Now()) re := raw.(*routeEntry) diff --git a/vault/testing.go b/vault/testing.go index 3c6a2713c0cd..f4d2cfc5d30a 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -335,11 +335,17 @@ func TestAddTestPlugin(t testing.T, c *Core, name, testFunc string) { } sum := hash.Sum(nil) - c.pluginCatalog.directory, err = filepath.EvalSymlinks(os.Args[0]) + + // Determine plugin directory path + fullPath, err := filepath.EvalSymlinks(os.Args[0]) if err != nil { t.Fatal(err) } - c.pluginCatalog.directory = filepath.Dir(c.pluginCatalog.directory) + directoryPath := filepath.Dir(fullPath) + + // Set core's plugin directory and plugin catalog directory + c.pluginDirectory = directoryPath + c.pluginCatalog.directory = directoryPath command := fmt.Sprintf("%s --test.run=%s", filepath.Base(os.Args[0]), testFunc) err = c.pluginCatalog.Set(name, command, sum) @@ -585,6 +591,7 @@ func GenerateRandBytes(length int) ([]byte, error) { } func TestWaitActive(t testing.T, core *Core) { + t.Helper() start := time.Now() var standby bool var err error @@ -627,6 +634,13 @@ func (c *TestCluster) Start() { } } +func (c *TestCluster) EnsureCoresSealed(t testing.T) { + t.Helper() + if err := c.ensureCoresSealed(); err != nil { + t.Fatal(err) + } +} + func (c *TestCluster) Cleanup() { // Close listeners for _, core := range c.Cores { @@ -638,18 +652,30 @@ func (c *TestCluster) Cleanup() { } // Seal the cores + c.ensureCoresSealed() + + // Remove any temp dir that exists + if c.TempDir != "" { + os.RemoveAll(c.TempDir) + } + + // Give time to actually shut down/clean up before the next test + time.Sleep(time.Second) +} + +func (c *TestCluster) ensureCoresSealed() error { for _, core := range c.Cores { if err := core.Shutdown(); err != nil { - continue + return err } timeout := time.Now().Add(60 * time.Second) for { if time.Now().After(timeout) { - continue + return fmt.Errorf("timeout waiting for core to seal") } sealed, err := core.Sealed() if err != nil { - continue + return err } if sealed { break @@ -657,14 +683,7 @@ func (c *TestCluster) Cleanup() { time.Sleep(250 * time.Millisecond) } } - - // Remove any temp dir that exists - if c.TempDir != "" { - os.RemoveAll(c.TempDir) - } - - // Give time to actually shut down/clean up before the next test - time.Sleep(time.Second) + return nil } type TestListener struct { @@ -692,9 +711,29 @@ type TestClusterOptions struct { KeepStandbysSealed bool HandlerFunc func(*Core) http.Handler BaseListenAddress string + NumCores int } +var DefaultNumCores = 3 + +type certInfo struct { + cert *x509.Certificate + certPEM []byte + certBytes []byte + key *ecdsa.PrivateKey + keyPEM []byte +} + +// NewTestCluster creates a new test cluster based on the provided core config +// and test cluster options. func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *TestCluster { + var numCores int + if opts == nil || opts.NumCores == 0 { + numCores = DefaultNumCores + } else { + numCores = opts.NumCores + } + certIPs := []net.IP{ net.IPv6loopback, net.ParseIP("127.0.0.1"), @@ -770,270 +809,131 @@ func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *Te t.Fatal(err) } - s1Key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - s1CertTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - DNSNames: []string{"localhost"}, - IPAddresses: certIPs, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - s1CertBytes, err := x509.CreateCertificate(rand.Reader, s1CertTemplate, caCert, s1Key.Public(), caKey) - if err != nil { - t.Fatal(err) - } - s1Cert, err := x509.ParseCertificate(s1CertBytes) - if err != nil { - t.Fatal(err) - } - s1CertPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: s1CertBytes, - } - s1CertPEM := pem.EncodeToMemory(s1CertPEMBlock) - s1MarshaledKey, err := x509.MarshalECPrivateKey(s1Key) - if err != nil { - t.Fatal(err) - } - s1KeyPEMBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: s1MarshaledKey, - } - s1KeyPEM := pem.EncodeToMemory(s1KeyPEMBlock) + var certInfoSlice []*certInfo - s2Key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - s2CertTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - DNSNames: []string{"localhost"}, - IPAddresses: certIPs, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - s2CertBytes, err := x509.CreateCertificate(rand.Reader, s2CertTemplate, caCert, s2Key.Public(), caKey) - if err != nil { - t.Fatal(err) - } - s2Cert, err := x509.ParseCertificate(s2CertBytes) - if err != nil { - t.Fatal(err) - } - s2CertPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: s2CertBytes, - } - s2CertPEM := pem.EncodeToMemory(s2CertPEMBlock) - s2MarshaledKey, err := x509.MarshalECPrivateKey(s2Key) - if err != nil { - t.Fatal(err) - } - s2KeyPEMBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: s2MarshaledKey, - } - s2KeyPEM := pem.EncodeToMemory(s2KeyPEMBlock) + // + // Certs generation + // + for i := 0; i < numCores; i++ { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + certTemplate := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "localhost", + }, + DNSNames: []string{"localhost"}, + IPAddresses: certIPs, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, + SerialNumber: big.NewInt(mathrand.Int63()), + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: time.Now().Add(262980 * time.Hour), + } + certBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, caCert, key.Public(), caKey) + if err != nil { + t.Fatal(err) + } + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + t.Fatal(err) + } + certPEMBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + } + certPEM := pem.EncodeToMemory(certPEMBlock) + marshaledKey, err := x509.MarshalECPrivateKey(key) + if err != nil { + t.Fatal(err) + } + keyPEMBlock := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: marshaledKey, + } + keyPEM := pem.EncodeToMemory(keyPEMBlock) - s3Key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - s3CertTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - DNSNames: []string{"localhost"}, - IPAddresses: certIPs, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - s3CertBytes, err := x509.CreateCertificate(rand.Reader, s3CertTemplate, caCert, s3Key.Public(), caKey) - if err != nil { - t.Fatal(err) - } - s3Cert, err := x509.ParseCertificate(s3CertBytes) - if err != nil { - t.Fatal(err) - } - s3CertPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: s3CertBytes, - } - s3CertPEM := pem.EncodeToMemory(s3CertPEMBlock) - s3MarshaledKey, err := x509.MarshalECPrivateKey(s3Key) - if err != nil { - t.Fatal(err) + certInfoSlice = append(certInfoSlice, &certInfo{ + cert: cert, + certPEM: certPEM, + certBytes: certBytes, + key: key, + keyPEM: keyPEM, + }) } - s3KeyPEMBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: s3MarshaledKey, - } - s3KeyPEM := pem.EncodeToMemory(s3KeyPEMBlock) - - logger := logformat.NewVaultLogger(log.LevelTrace) // // Listener setup // - ports := []int{0, 0, 0} + logger := logformat.NewVaultLogger(log.LevelTrace) + ports := make([]int, numCores) if baseAddr != nil { - ports = []int{baseAddr.Port, baseAddr.Port + 1, baseAddr.Port + 2} + for i := 0; i < numCores; i++ { + ports[i] = baseAddr.Port + i + } } else { baseAddr = &net.TCPAddr{ IP: net.ParseIP("127.0.0.1"), Port: 0, } } - baseAddr.Port = ports[0] - ln, err := net.ListenTCP("tcp", baseAddr) - if err != nil { - t.Fatal(err) - } - s1CertFile := filepath.Join(testCluster.TempDir, fmt.Sprintf("node1_port_%d_cert.pem", ln.Addr().(*net.TCPAddr).Port)) - s1KeyFile := filepath.Join(testCluster.TempDir, fmt.Sprintf("node1_port_%d_key.pem", ln.Addr().(*net.TCPAddr).Port)) - err = ioutil.WriteFile(s1CertFile, s1CertPEM, 0755) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(s1KeyFile, s1KeyPEM, 0755) - if err != nil { - t.Fatal(err) - } - s1TLSCert, err := tls.X509KeyPair(s1CertPEM, s1KeyPEM) - if err != nil { - t.Fatal(err) - } - s1CertGetter := reload.NewCertificateGetter(s1CertFile, s1KeyFile) - s1TLSConfig := &tls.Config{ - Certificates: []tls.Certificate{s1TLSCert}, - RootCAs: testCluster.RootCAs, - ClientCAs: testCluster.RootCAs, - ClientAuth: tls.VerifyClientCertIfGiven, - NextProtos: []string{"h2", "http/1.1"}, - GetCertificate: s1CertGetter.GetCertificate, - } - s1TLSConfig.BuildNameToCertificate() - c1lns := []*TestListener{&TestListener{ - Listener: tls.NewListener(ln, s1TLSConfig), - Address: ln.Addr().(*net.TCPAddr), - }, - } - var handler1 http.Handler = http.NewServeMux() - server1 := &http.Server{ - Handler: handler1, - } - if err := http2.ConfigureServer(server1, nil); err != nil { - t.Fatal(err) - } - baseAddr.Port = ports[1] - ln, err = net.ListenTCP("tcp", baseAddr) - if err != nil { - t.Fatal(err) - } - s2CertFile := filepath.Join(testCluster.TempDir, fmt.Sprintf("node2_port_%d_cert.pem", ln.Addr().(*net.TCPAddr).Port)) - s2KeyFile := filepath.Join(testCluster.TempDir, fmt.Sprintf("node2_port_%d_key.pem", ln.Addr().(*net.TCPAddr).Port)) - err = ioutil.WriteFile(s2CertFile, s2CertPEM, 0755) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(s2KeyFile, s2KeyPEM, 0755) - if err != nil { - t.Fatal(err) - } - s2TLSCert, err := tls.X509KeyPair(s2CertPEM, s2KeyPEM) - if err != nil { - t.Fatal(err) - } - s2CertGetter := reload.NewCertificateGetter(s2CertFile, s2KeyFile) - s2TLSConfig := &tls.Config{ - Certificates: []tls.Certificate{s2TLSCert}, - RootCAs: testCluster.RootCAs, - ClientCAs: testCluster.RootCAs, - ClientAuth: tls.VerifyClientCertIfGiven, - NextProtos: []string{"h2", "http/1.1"}, - GetCertificate: s2CertGetter.GetCertificate, - } - s2TLSConfig.BuildNameToCertificate() - c2lns := []*TestListener{&TestListener{ - Listener: tls.NewListener(ln, s2TLSConfig), - Address: ln.Addr().(*net.TCPAddr), - }, - } - var handler2 http.Handler = http.NewServeMux() - server2 := &http.Server{ - Handler: handler2, - } - if err := http2.ConfigureServer(server2, nil); err != nil { - t.Fatal(err) - } - - baseAddr.Port = ports[2] - ln, err = net.ListenTCP("tcp", baseAddr) - if err != nil { - t.Fatal(err) - } - s3CertFile := filepath.Join(testCluster.TempDir, fmt.Sprintf("node3_port_%d_cert.pem", ln.Addr().(*net.TCPAddr).Port)) - s3KeyFile := filepath.Join(testCluster.TempDir, fmt.Sprintf("node3_port_%d_key.pem", ln.Addr().(*net.TCPAddr).Port)) - err = ioutil.WriteFile(s3CertFile, s3CertPEM, 0755) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(s3KeyFile, s3KeyPEM, 0755) - if err != nil { - t.Fatal(err) - } - s3TLSCert, err := tls.X509KeyPair(s3CertPEM, s3KeyPEM) - if err != nil { - t.Fatal(err) - } - s3CertGetter := reload.NewCertificateGetter(s3CertFile, s3KeyFile) - s3TLSConfig := &tls.Config{ - Certificates: []tls.Certificate{s3TLSCert}, - RootCAs: testCluster.RootCAs, - ClientCAs: testCluster.RootCAs, - ClientAuth: tls.VerifyClientCertIfGiven, - NextProtos: []string{"h2", "http/1.1"}, - GetCertificate: s3CertGetter.GetCertificate, - } - s3TLSConfig.BuildNameToCertificate() - c3lns := []*TestListener{&TestListener{ - Listener: tls.NewListener(ln, s3TLSConfig), - Address: ln.Addr().(*net.TCPAddr), - }, - } - var handler3 http.Handler = http.NewServeMux() - server3 := &http.Server{ - Handler: handler3, - } - if err := http2.ConfigureServer(server3, nil); err != nil { - t.Fatal(err) + listeners := [][]*TestListener{} + servers := []*http.Server{} + handlers := []http.Handler{} + tlsConfigs := []*tls.Config{} + certGetters := []*reload.CertificateGetter{} + for i := 0; i < numCores; i++ { + baseAddr.Port = ports[i] + ln, err := net.ListenTCP("tcp", baseAddr) + if err != nil { + t.Fatal(err) + } + certFile := filepath.Join(testCluster.TempDir, fmt.Sprintf("node%d_port_%d_cert.pem", i+1, ln.Addr().(*net.TCPAddr).Port)) + keyFile := filepath.Join(testCluster.TempDir, fmt.Sprintf("node%d_port_%d_key.pem", i+1, ln.Addr().(*net.TCPAddr).Port)) + err = ioutil.WriteFile(certFile, certInfoSlice[i].certPEM, 0755) + if err != nil { + t.Fatal(err) + } + err = ioutil.WriteFile(keyFile, certInfoSlice[i].keyPEM, 0755) + if err != nil { + t.Fatal(err) + } + tlsCert, err := tls.X509KeyPair(certInfoSlice[i].certPEM, certInfoSlice[i].keyPEM) + if err != nil { + t.Fatal(err) + } + certGetter := reload.NewCertificateGetter(certFile, keyFile) + certGetters = append(certGetters, certGetter) + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + RootCAs: testCluster.RootCAs, + ClientCAs: testCluster.RootCAs, + ClientAuth: tls.VerifyClientCertIfGiven, + NextProtos: []string{"h2", "http/1.1"}, + GetCertificate: certGetter.GetCertificate, + } + tlsConfig.BuildNameToCertificate() + tlsConfigs = append(tlsConfigs, tlsConfig) + lns := []*TestListener{&TestListener{ + Listener: tls.NewListener(ln, tlsConfig), + Address: ln.Addr().(*net.TCPAddr), + }, + } + listeners = append(listeners, lns) + var handler http.Handler = http.NewServeMux() + handlers = append(handlers, handler) + server := &http.Server{ + Handler: handler, + } + servers = append(servers, server) + if err := http2.ConfigureServer(server, nil); err != nil { + t.Fatal(err) + } } // Create three cores with the same physical and different redirect/cluster @@ -1049,8 +949,8 @@ func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *Te LogicalBackends: make(map[string]logical.Factory), CredentialBackends: make(map[string]logical.Factory), AuditBackends: make(map[string]audit.Factory), - RedirectAddr: fmt.Sprintf("https://127.0.0.1:%d", c1lns[0].Address.Port), - ClusterAddr: fmt.Sprintf("https://127.0.0.1:%d", c1lns[0].Address.Port+105), + RedirectAddr: fmt.Sprintf("https://127.0.0.1:%d", listeners[0][0].Address.Port), + ClusterAddr: fmt.Sprintf("https://127.0.0.1:%d", listeners[0][0].Address.Port+105), DisableMlock: true, EnableUI: true, } @@ -1126,39 +1026,21 @@ func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *Te coreConfig.HAPhysical = haPhys.(physical.HABackend) } - c1, err := NewCore(coreConfig) - if err != nil { - t.Fatalf("err: %v", err) - } - if opts != nil && opts.HandlerFunc != nil { - handler1 = opts.HandlerFunc(c1) - server1.Handler = handler1 - } - - coreConfig.RedirectAddr = fmt.Sprintf("https://127.0.0.1:%d", c2lns[0].Address.Port) - if coreConfig.ClusterAddr != "" { - coreConfig.ClusterAddr = fmt.Sprintf("https://127.0.0.1:%d", c2lns[0].Address.Port+105) - } - c2, err := NewCore(coreConfig) - if err != nil { - t.Fatalf("err: %v", err) - } - if opts != nil && opts.HandlerFunc != nil { - handler2 = opts.HandlerFunc(c2) - server2.Handler = handler2 - } - - coreConfig.RedirectAddr = fmt.Sprintf("https://127.0.0.1:%d", c3lns[0].Address.Port) - if coreConfig.ClusterAddr != "" { - coreConfig.ClusterAddr = fmt.Sprintf("https://127.0.0.1:%d", c3lns[0].Address.Port+105) - } - c3, err := NewCore(coreConfig) - if err != nil { - t.Fatalf("err: %v", err) - } - if opts != nil && opts.HandlerFunc != nil { - handler3 = opts.HandlerFunc(c3) - server3.Handler = handler3 + cores := []*Core{} + for i := 0; i < numCores; i++ { + coreConfig.RedirectAddr = fmt.Sprintf("https://127.0.0.1:%d", listeners[i][0].Address.Port) + if coreConfig.ClusterAddr != "" { + coreConfig.ClusterAddr = fmt.Sprintf("https://127.0.0.1:%d", listeners[i][0].Address.Port+105) + } + c, err := NewCore(coreConfig) + if err != nil { + t.Fatalf("err: %v", err) + } + cores = append(cores, c) + if opts != nil && opts.HandlerFunc != nil { + handlers[i] = opts.HandlerFunc(c) + servers[i].Handler = handlers[i] + } } // @@ -1175,16 +1057,19 @@ func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *Te return ret } - c2.SetClusterListenerAddrs(clusterAddrGen(c2lns)) - c2.SetClusterHandler(handler2) - c3.SetClusterListenerAddrs(clusterAddrGen(c3lns)) - c3.SetClusterHandler(handler3) + if numCores > 1 { + for i := 1; i < numCores; i++ { + cores[i].SetClusterListenerAddrs(clusterAddrGen(listeners[i])) + cores[i].SetClusterHandler(handlers[i]) + } + } - keys, root := TestCoreInitClusterWrapperSetup(t, c1, clusterAddrGen(c1lns), handler1) + keys, root := TestCoreInitClusterWrapperSetup(t, cores[0], clusterAddrGen(listeners[0]), handlers[0]) barrierKeys, _ := copystructure.Copy(keys) testCluster.BarrierKeys = barrierKeys.([][]byte) testCluster.RootToken = root + // Write root token and barrier keys err = ioutil.WriteFile(filepath.Join(testCluster.TempDir, "root_token"), []byte(root), 0755) if err != nil { t.Fatal(err) @@ -1201,14 +1086,15 @@ func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *Te t.Fatal(err) } + // Unseal first core for _, key := range keys { - if _, err := c1.Unseal(TestKeyCopy(key)); err != nil { + if _, err := cores[0].Unseal(TestKeyCopy(key)); err != nil { t.Fatalf("unseal err: %s", err) } } // Verify unsealed - sealed, err := c1.Sealed() + sealed, err := cores[0].Sealed() if err != nil { t.Fatalf("err checking seal status: %s", err) } @@ -1216,41 +1102,38 @@ func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *Te t.Fatal("should not be sealed") } - TestWaitActive(t, c1) + TestWaitActive(t, cores[0]) - if opts == nil || !opts.KeepStandbysSealed { - for _, key := range keys { - if _, err := c2.Unseal(TestKeyCopy(key)); err != nil { - t.Fatalf("unseal err: %s", err) - } - } - for _, key := range keys { - if _, err := c3.Unseal(TestKeyCopy(key)); err != nil { - t.Fatalf("unseal err: %s", err) + // Unseal other cores unless otherwise specified + if (opts == nil || !opts.KeepStandbysSealed) && numCores > 1 { + for i := 1; i < numCores; i++ { + for _, key := range keys { + if _, err := cores[i].Unseal(TestKeyCopy(key)); err != nil { + t.Fatalf("unseal err: %s", err) + } } } // Let them come fully up to standby time.Sleep(2 * time.Second) - // Ensure cluster connection info is populated - isLeader, _, _, err := c2.Leader() - if err != nil { - t.Fatal(err) - } - if isLeader { - t.Fatal("c2 should not be leader") - } - isLeader, _, _, err = c3.Leader() - if err != nil { - t.Fatal(err) - } - if isLeader { - t.Fatal("c3 should not be leader") + // Ensure cluster connection info is populated. + // Other cores should not come up as leaders. + for i := 1; i < numCores; i++ { + isLeader, _, _, err := cores[i].Leader() + if err != nil { + t.Fatal(err) + } + if isLeader { + t.Fatalf("core[%d] should not be leader", i) + } } } - cluster, err := c1.Cluster() + // + // Set test cluster core(s) and test cluster + // + cluster, err := cores[0].Cluster() if err != nil { t.Fatal(err) } @@ -1278,65 +1161,27 @@ func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *Te } var ret []*TestClusterCore - t1 := &TestClusterCore{ - Core: c1, - ServerKey: s1Key, - ServerKeyPEM: s1KeyPEM, - ServerCert: s1Cert, - ServerCertBytes: s1CertBytes, - ServerCertPEM: s1CertPEM, - Listeners: c1lns, - Handler: handler1, - Server: server1, - TLSConfig: s1TLSConfig, - Client: getAPIClient(c1lns[0].Address.Port, s1TLSConfig), - } - t1.ReloadFuncs = &c1.reloadFuncs - t1.ReloadFuncsLock = &c1.reloadFuncsLock - t1.ReloadFuncsLock.Lock() - (*t1.ReloadFuncs)["listener|tcp"] = []reload.ReloadFunc{s1CertGetter.Reload} - t1.ReloadFuncsLock.Unlock() - ret = append(ret, t1) - - t2 := &TestClusterCore{ - Core: c2, - ServerKey: s2Key, - ServerKeyPEM: s2KeyPEM, - ServerCert: s2Cert, - ServerCertBytes: s2CertBytes, - ServerCertPEM: s2CertPEM, - Listeners: c2lns, - Handler: handler2, - Server: server2, - TLSConfig: s2TLSConfig, - Client: getAPIClient(c2lns[0].Address.Port, s2TLSConfig), - } - t2.ReloadFuncs = &c2.reloadFuncs - t2.ReloadFuncsLock = &c2.reloadFuncsLock - t2.ReloadFuncsLock.Lock() - (*t2.ReloadFuncs)["listener|tcp"] = []reload.ReloadFunc{s2CertGetter.Reload} - t2.ReloadFuncsLock.Unlock() - ret = append(ret, t2) - - t3 := &TestClusterCore{ - Core: c3, - ServerKey: s3Key, - ServerKeyPEM: s3KeyPEM, - ServerCert: s3Cert, - ServerCertBytes: s3CertBytes, - ServerCertPEM: s3CertPEM, - Listeners: c3lns, - Handler: handler3, - Server: server3, - TLSConfig: s3TLSConfig, - Client: getAPIClient(c3lns[0].Address.Port, s3TLSConfig), - } - t3.ReloadFuncs = &c3.reloadFuncs - t3.ReloadFuncsLock = &c3.reloadFuncsLock - t3.ReloadFuncsLock.Lock() - (*t3.ReloadFuncs)["listener|tcp"] = []reload.ReloadFunc{s3CertGetter.Reload} - t3.ReloadFuncsLock.Unlock() - ret = append(ret, t3) + for i := 0; i < numCores; i++ { + tcc := &TestClusterCore{ + Core: cores[i], + ServerKey: certInfoSlice[i].key, + ServerKeyPEM: certInfoSlice[i].keyPEM, + ServerCert: certInfoSlice[i].cert, + ServerCertBytes: certInfoSlice[i].certBytes, + ServerCertPEM: certInfoSlice[i].certPEM, + Listeners: listeners[i], + Handler: handlers[i], + Server: servers[i], + TLSConfig: tlsConfigs[i], + Client: getAPIClient(listeners[i][0].Address.Port, tlsConfigs[i]), + } + tcc.ReloadFuncs = &cores[i].reloadFuncs + tcc.ReloadFuncsLock = &cores[i].reloadFuncsLock + tcc.ReloadFuncsLock.Lock() + (*tcc.ReloadFuncs)["listener|tcp"] = []reload.ReloadFunc{certGetters[i].Reload} + tcc.ReloadFuncsLock.Unlock() + ret = append(ret, tcc) + } testCluster.Cores = ret return &testCluster diff --git a/vendor/github.com/chrismalek/oktasdk-go/LICENSE.txt b/vendor/github.com/chrismalek/oktasdk-go/LICENSE.txt new file mode 100644 index 000000000000..63b4b681cb65 --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/apps.go b/vendor/github.com/chrismalek/oktasdk-go/okta/apps.go new file mode 100644 index 000000000000..d198606758d6 --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/apps.go @@ -0,0 +1,242 @@ +package okta + +import ( + "fmt" + "net/url" + "time" +) + +type AppsService service + +// AppFilterOptions is used to generate a "Filter" to search for different Apps +// The values here coorelate to API Search paramgters on the group API +type AppFilterOptions struct { + NextURL *url.URL `url:"-"` + GetAllPages bool `url:"-"` + NumberOfPages int `url:"-"` + Limit int `url:"limit,omitempty"` +} + +type App struct { + ID string `json:"id"` + Name string `json:"name"` + Label string `json:"label"` + Status string `json:"status"` + LastUpdated time.Time `json:"lastUpdated"` + Created time.Time `json:"created"` + Accessibility struct { + SelfService bool `json:"selfService"` + ErrorRedirectURL interface{} `json:"errorRedirectUrl"` + LoginRedirectURL interface{} `json:"loginRedirectUrl"` + } `json:"accessibility"` + Visibility struct { + AutoSubmitToolbar bool `json:"autoSubmitToolbar"` + Hide struct { + IOS bool `json:"iOS"` + Web bool `json:"web"` + } `json:"hide"` + AppLinks struct { + TestorgoneCustomsaml20App1Link bool `json:"testorgone_customsaml20app_1_link"` + } `json:"appLinks"` + } `json:"visibility"` + Features []interface{} `json:"features"` + SignOnMode string `json:"signOnMode"` + Credentials struct { + UserNameTemplate struct { + Template string `json:"template"` + Type string `json:"type"` + } `json:"userNameTemplate"` + Signing struct { + } `json:"signing"` + } `json:"credentials"` + Settings struct { + App struct { + } `json:"app"` + Notifications struct { + Vpn struct { + Network struct { + Connection string `json:"connection"` + } `json:"network"` + Message interface{} `json:"message"` + HelpURL interface{} `json:"helpUrl"` + } `json:"vpn"` + } `json:"notifications"` + SignOn struct { + DefaultRelayState string `json:"defaultRelayState"` + SsoAcsURL string `json:"ssoAcsUrl"` + IdpIssuer string `json:"idpIssuer"` + Audience string `json:"audience"` + Recipient string `json:"recipient"` + Destination string `json:"destination"` + SubjectNameIDTemplate string `json:"subjectNameIdTemplate"` + SubjectNameIDFormat string `json:"subjectNameIdFormat"` + ResponseSigned bool `json:"responseSigned"` + AssertionSigned bool `json:"assertionSigned"` + SignatureAlgorithm string `json:"signatureAlgorithm"` + DigestAlgorithm string `json:"digestAlgorithm"` + HonorForceAuthn bool `json:"honorForceAuthn"` + AuthnContextClassRef string `json:"authnContextClassRef"` + SpIssuer interface{} `json:"spIssuer"` + RequestCompressed bool `json:"requestCompressed"` + AttributeStatements []interface{} `json:"attributeStatements"` + } `json:"signOn"` + } `json:"settings"` + Links struct { + Logo []struct { + Name string `json:"name"` + Href string `json:"href"` + Type string `json:"type"` + } `json:"logo"` + AppLinks []struct { + Name string `json:"name"` + Href string `json:"href"` + Type string `json:"type"` + } `json:"appLinks"` + Help struct { + Href string `json:"href"` + Type string `json:"type"` + } `json:"help"` + Users struct { + Href string `json:"href"` + } `json:"users"` + Deactivate struct { + Href string `json:"href"` + } `json:"deactivate"` + Groups struct { + Href string `json:"href"` + } `json:"groups"` + Metadata struct { + Href string `json:"href"` + Type string `json:"type"` + } `json:"metadata"` + } `json:"_links"` +} + +func (a App) String() string { + // return Stringify(g) + return fmt.Sprintf("App:(ID: {%v} - Name: {%v})\n", a.ID, a.Name) +} + +// GetByID gets a group from OKTA by the Gropu ID. An error is returned if the group is not found +func (a *AppsService) GetByID(appID string) (*App, *Response, error) { + + u := fmt.Sprintf("apps/%v", appID) + req, err := a.client.NewRequest("GET", u, nil) + + if err != nil { + return nil, nil, err + } + + app := new(App) + + resp, err := a.client.Do(req, app) + + if err != nil { + return nil, resp, err + } + + return app, resp, err +} + +type AppUser struct { + ID string `json:"id"` + ExternalID string `json:"externalId"` + Created time.Time `json:"created"` + LastUpdated time.Time `json:"lastUpdated"` + Scope string `json:"scope"` + Status string `json:"status"` + StatusChanged *time.Time `json:"statusChanged"` + PasswordChanged *time.Time `json:"passwordChanged"` + SyncState string `json:"syncState"` + LastSync *time.Time `json:"lastSync"` + Credentials struct { + UserName string `json:"userName"` + Password struct { + } `json:"password"` + } `json:"credentials"` + Profile struct { + SecondEmail interface{} `json:"secondEmail"` + LastName string `json:"lastName"` + MobilePhone interface{} `json:"mobilePhone"` + Email string `json:"email"` + SalesforceGroups []string `json:"salesforceGroups"` + Role string `json:"role"` + FirstName string `json:"firstName"` + Profile string `json:"profile"` + } `json:"profile"` + Links struct { + App struct { + Href string `json:"href"` + } `json:"app"` + User struct { + Href string `json:"href"` + } `json:"user"` + } `json:"_links"` +} + +// GetUsers returns the members in an App +// Pass in an optional AppFilterOptions struct to filter the results +// The Users in the app are returned +func (a *AppsService) GetUsers(appID string, opt *AppFilterOptions) (appUsers []AppUser, resp *Response, err error) { + + pagesRetreived := 0 + var u string + if opt.NextURL != nil { + u = opt.NextURL.String() + } else { + u = fmt.Sprintf("apps/%v/users", appID) + + if opt.Limit == 0 { + opt.Limit = defaultLimit + } + + u, _ = addOptions(u, opt) + } + + req, err := a.client.NewRequest("GET", u, nil) + + if err != nil { + fmt.Printf("____ERROR HERE\n") + return nil, nil, err + } + resp, err = a.client.Do(req, &appUsers) + + if err != nil { + fmt.Printf("____ERROR HERE 2\n") + return nil, resp, err + } + + pagesRetreived++ + + if (opt.NumberOfPages > 0 && pagesRetreived < opt.NumberOfPages) || opt.GetAllPages { + + for { + + if pagesRetreived == opt.NumberOfPages { + break + } + if resp.NextURL != nil { + + var userPage []AppUser + pageOpts := new(AppFilterOptions) + pageOpts.NextURL = resp.NextURL + pageOpts.Limit = opt.Limit + pageOpts.NumberOfPages = 1 + + userPage, resp, err = a.GetUsers(appID, pageOpts) + + if err != nil { + return appUsers, resp, err + } else { + appUsers = append(appUsers, userPage...) + pagesRetreived++ + } + } else { + break + } + + } + } + + return appUsers, resp, err +} diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/factors.go b/vendor/github.com/chrismalek/oktasdk-go/okta/factors.go new file mode 100644 index 000000000000..fc926f91aa38 --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/factors.go @@ -0,0 +1,8 @@ +package okta + +const ( + // MFAStatusActive is a constant to represent OKTA User State returned by the API + MFAStatusActive = "ACTIVE" + // MFAStatusPending is a user MFA Status of NOT Active + MFAStatusPending = "PENDING_ACTIVATION" +) diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/groups.go b/vendor/github.com/chrismalek/oktasdk-go/okta/groups.go new file mode 100644 index 000000000000..5c9b58086238 --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/groups.go @@ -0,0 +1,306 @@ +package okta + +import ( + "errors" + "fmt" + "net/url" + "time" +) + +const ( + // GroupTypeOKTA - group type constant for an OKTA Mastered Group + GroupTypeOKTA = "OKTA_GROUP" + // GroupTypeBuiltIn - group type constant for a Built in OKTA groups + GroupTypeBuiltIn = "BUILT_IN" + // GroupTypeApp -- group type constant for app mastered group + GroupTypeApp = "APP_GROUP" + + groupTypeFilter = "type" + groupNameFilter = "q" + groupLastMembershipUpdatedFilter = "lastMembershipUpdated" + groupLastUpdatedFilter = "lastUpdated" +) + +// GroupsService handles communication with the Groups data related +// methods of the OKTA API. +type GroupsService service + +// Group represents the Group Object from the OKTA API +type Group struct { + ID string `json:"id"` + Created time.Time `json:"created"` + LastUpdated time.Time `json:"lastUpdated"` + LastMembershipUpdated time.Time `json:"lastMembershipUpdated"` + ObjectClass []string `json:"objectClass"` + Type string `json:"type"` + Profile struct { + Name string `json:"name"` + Description string `json:"description"` + SamAccountName string `json:"samAccountName"` + Dn string `json:"dn"` + WindowsDomainQualifiedName string `json:"windowsDomainQualifiedName"` + ExternalID string `json:"externalId"` + } `json:"profile"` + Links struct { + Logo []struct { + Name string `json:"name"` + Href string `json:"href"` + Type string `json:"type"` + } `json:"logo"` + Users struct { + Href string `json:"href"` + } `json:"users"` + Apps struct { + Href string `json:"href"` + } `json:"apps"` + } `json:"_links"` +} + +// GroupFilterOptions is used to generate a "Filter" to search for different groups +// The values here coorelate to API Search paramgters on the group API +type GroupFilterOptions struct { + // This will be built by internal - may not need to export + FilterString string `url:"filter,omitempty"` + NextURL *url.URL `url:"-"` + GetAllPages bool `url:"-"` + NumberOfPages int `url:"-"` + Limit int `url:"limit,omitempty"` + + NameStartsWith string `url:"q,omitempty"` + GroupTypeEqual string `url:"-"` + + LastUpdated dateFilter `url:"-"` + LastMembershipUpdated dateFilter `url:"-"` +} + +func (g Group) String() string { + // return Stringify(g) + return fmt.Sprintf("Group:(ID: {%v} - Type: {%v} - Group Name: {%v})\n", g.ID, g.Type, g.Profile.Name) +} + +// ListWithFilter - Method to list groups with different filter options. +// Pass in a GroupFilterOptions to specify filters. Values in that struct will turn into Query parameters +func (g *GroupsService) ListWithFilter(opt *GroupFilterOptions) ([]Group, *Response, error) { + + var u string + var err error + + pagesRetreived := 0 + if opt.NextURL != nil { + u = opt.NextURL.String() + } else { + if opt.GroupTypeEqual != "" { + opt.FilterString = appendToFilterString(opt.FilterString, groupTypeFilter, FilterEqualOperator, opt.GroupTypeEqual) + } + + // if opt.NameStartsWith != "" { + // opt.FilterString = appendToFilterString(opt.FilterString, groupNameFilter, filterEqualOperator, opt.NameStartsWith) + // } + if (!opt.LastMembershipUpdated.Value.IsZero()) && (opt.LastMembershipUpdated.Operator != "") { + opt.FilterString = appendToFilterString(opt.FilterString, groupLastMembershipUpdatedFilter, opt.LastMembershipUpdated.Operator, opt.LastMembershipUpdated.Value.UTC().Format(oktaFilterTimeFormat)) + } + + if (!opt.LastUpdated.Value.IsZero()) && (opt.LastUpdated.Operator != "") { + opt.FilterString = appendToFilterString(opt.FilterString, groupLastUpdatedFilter, opt.LastUpdated.Operator, opt.LastUpdated.Value.UTC().Format(oktaFilterTimeFormat)) + } + + if opt.Limit == 0 { + opt.Limit = defaultLimit + } + u, err = addOptions("groups", opt) + if err != nil { + return nil, nil, err + } + } + + req, err := g.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + groups := make([]Group, 1) + resp, err := g.client.Do(req, &groups) + if err != nil { + return nil, resp, err + } + pagesRetreived++ + + if (opt.NumberOfPages > 0 && pagesRetreived < opt.NumberOfPages) || opt.GetAllPages { + + for { + + if pagesRetreived == opt.NumberOfPages { + break + } + if resp.NextURL != nil { + var groupPage []Group + pageOption := new(GroupFilterOptions) + pageOption.NextURL = resp.NextURL + pageOption.NumberOfPages = 1 + pageOption.Limit = opt.Limit + + groupPage, resp, err = g.ListWithFilter(pageOption) + if err != nil { + return groups, resp, err + } else { + groups = append(groups, groupPage...) + pagesRetreived++ + } + } else { + break + } + } + } + return groups, resp, err +} + +// GetByID gets a group from OKTA by the Gropu ID. An error is returned if the group is not found +func (g *GroupsService) GetByID(groupID string) (*Group, *Response, error) { + + u := fmt.Sprintf("groups/%v", groupID) + req, err := g.client.NewRequest("GET", u, nil) + + if err != nil { + return nil, nil, err + } + + group := new(Group) + + resp, err := g.client.Do(req, group) + + if err != nil { + return nil, resp, err + } + + return group, resp, err +} + +// GetUsers returns the members in a group +// Pass in an optional GroupFilterOptions struct to filter the results +// The Users in the group are returned +func (g *GroupsService) GetUsers(groupID string, opt *GroupUserFilterOptions) (users []User, resp *Response, err error) { + pagesRetreived := 0 + var u string + if opt.NextURL != nil { + u = opt.NextURL.String() + } else { + u = fmt.Sprintf("groups/%v/users", groupID) + + if opt.Limit == 0 { + opt.Limit = defaultLimit + } + + u, _ = addOptions(u, opt) + } + + req, err := g.client.NewRequest("GET", u, nil) + + if err != nil { + return nil, nil, err + } + resp, err = g.client.Do(req, &users) + + if err != nil { + return nil, resp, err + } + + pagesRetreived++ + if (opt.NumberOfPages > 0 && pagesRetreived < opt.NumberOfPages) || opt.GetAllPages { + + for { + + if pagesRetreived == opt.NumberOfPages { + break + } + if resp.NextURL != nil { + + var userPage []User + pageOpts := new(GroupUserFilterOptions) + pageOpts.NextURL = resp.NextURL + pageOpts.Limit = opt.Limit + pageOpts.NumberOfPages = 1 + + userPage, resp, err = g.GetUsers(groupID, pageOpts) + if err != nil { + return users, resp, err + } else { + users = append(users, userPage...) + pagesRetreived++ + } + } else { + break + } + + } + } + + return users, resp, err +} + +// Add - Adds an OKTA Mastered Group with name and description. GroupName is required. +func (g *GroupsService) Add(groupName string, groupDescription string) (*Group, *Response, error) { + + if groupName == "" { + return nil, nil, errors.New("groupName parameter is required for ADD") + } + + newGroup := newGroup{} + newGroup.Profile.Name = groupName + newGroup.Profile.Description = groupDescription + + u := fmt.Sprintf("groups") + + req, err := g.client.NewRequest("POST", u, newGroup) + + if err != nil { + return nil, nil, err + } + + group := new(Group) + + resp, err := g.client.Do(req, group) + + if err != nil { + return nil, resp, err + } + + return group, resp, err +} + +// Delete - Delets an OKTA Mastered Group with ID +func (g *GroupsService) Delete(groupID string) (*Response, error) { + + if groupID == "" { + return nil, errors.New("groupID parameter is required for Delete") + } + u := fmt.Sprintf("groups/%v", groupID) + + req, err := g.client.NewRequest("DELETE", u, nil) + + if err != nil { + return nil, err + } + + resp, err := g.client.Do(req, nil) + + if err != nil { + return resp, err + } + + return resp, err +} + +// GroupUserFilterOptions is a struct that you populate which will limit or control group fetches and searches +// The values here will coorelate to the search filtering allowed in the OKTA API. These values are turned into Query Parameters +type GroupUserFilterOptions struct { + Limit int `url:"limit,omitempty"` + NextURL *url.URL `url:"-"` + GetAllPages bool `url:"-"` + NumberOfPages int `url:"-"` +} + +type newGroup struct { + Profile struct { + Name string `json:"name"` + Description string `json:"description"` + } `json:"profile"` +} diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go b/vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go new file mode 100644 index 000000000000..322b4410d4c4 --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go @@ -0,0 +1,503 @@ +package okta + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "regexp" + "strconv" + "sync" + "time" + + "github.com/google/go-querystring/query" + + "reflect" +) + +const ( + libraryVersion = "1" + userAgent = "oktasdk-go/" + libraryVersion + productionURLFormat = "https://%s.okta.com/api/v1/" + previewProductionURLFormat = "https://%s.oktapreview.com/api/v1/" + headerRateLimit = "X-Rate-Limit-Limit" + headerRateRemaining = "X-Rate-Limit-Remaining" + headerRateReset = "X-Rate-Limit-Reset" + headerOKTARequestID = "X-Okta-Request-Id" + headerAuthorization = "Authorization" + headerAuthorizationFormat = "SSWS %v" + mediaTypeJSON = "application/json" + defaultLimit = 50 + // FilterEqualOperator Filter Operatorid for "equal" + FilterEqualOperator = "eq" + // FilterStartsWithOperator - filter operator for "starts with" + FilterStartsWithOperator = "sw" + // FilterGreaterThanOperator - filter operator for "greater than" + FilterGreaterThanOperator = "gt" + // FilterLessThanOperator - filter operator for "less than" + FilterLessThanOperator = "lt" + + // If the API returns a "X-Rate-Limit-Remaining" header less than this the SDK will either pause + // Or throw RateLimitError depending on the client.PauseOnRateLimit value + defaultRateRemainingFloor = 100 +) + +// A Client manages communication with the API. +type Client struct { + clientMu sync.Mutex // clientMu protects the client during calls that modify the CheckRedirect func. + client *http.Client // HTTP client used to communicate with the API. + + // Base URL for API requests. + // This will be built automatically based on inputs to NewClient + // If needed you can override this if needed (your URL is not *.okta.com or *.oktapreview.com) + BaseURL *url.URL + + // User agent used when communicating with the GitHub API. + UserAgent string + + apiKey string + authorizationHeaderValue string + PauseOnRateLimit bool + + // RateRemainingFloor - If the API returns a "X-Rate-Limit-Remaining" header less than this the SDK will either pause + // Or throw RateLimitError depending on the client.PauseOnRateLimit value. It defaults to 30 + // One client doing too much work can lock out all API Access for every other client + // We are trying to be a "good API User Citizen" + RateRemainingFloor int + + rateMu sync.Mutex + mostRecentRate Rate + + Limit int + // mostRecent rateLimitCategory + + common service // Reuse a single struct instead of allocating one for each service on the heap. + + // Services used for talking to different parts of the API. + // Service for Working with Users + Users *UsersService + + // Service for Working with Groups + Groups *GroupsService + + // Service for Working with Apps + Apps *AppsService +} + +type service struct { + client *Client +} + +// NewClient returns a new OKTA API client. If a nil httpClient is +// provided, http.DefaultClient will be used. +func NewClient(httpClient *http.Client, orgName string, apiToken string, isProduction bool) *Client { + if httpClient == nil { + httpClient = http.DefaultClient + } + + var baseURL *url.URL + if isProduction { + baseURL, _ = url.Parse(fmt.Sprintf(productionURLFormat, orgName)) + } else { + baseURL, _ = url.Parse(fmt.Sprintf(previewProductionURLFormat, orgName)) + + } + + c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent} + c.PauseOnRateLimit = true // If rate limit found it will block until that time. If false then Error will be returned + c.authorizationHeaderValue = fmt.Sprintf(headerAuthorizationFormat, apiToken) + c.apiKey = apiToken + c.Limit = defaultLimit + c.RateRemainingFloor = defaultRateRemainingFloor + c.common.client = c + + c.Users = (*UsersService)(&c.common) + c.Groups = (*GroupsService)(&c.common) + c.Apps = (*AppsService)(&c.common) + return c +} + +// Rate represents the rate limit for the current client. +type Rate struct { + // The number of requests per minute the client is currently limited to. + RatePerMinuteLimit int + + // The number of remaining requests the client can make this minute + Remaining int + + // The time at which the current rate limit will reset. + ResetTime time.Time +} + +// Response is a OKTA API response. This wraps the standard http.Response +// returned from OKTA and provides convenient access to things like +// pagination links. +type Response struct { + *http.Response + + // These fields provide the page values for paginating through a set of + // results. + + NextURL *url.URL + // PrevURL *url.URL + SelfURL *url.URL + OKTARequestID string + Rate +} + +// newResponse creates a new Response for the provided http.Response. +func newResponse(r *http.Response) *Response { + response := &Response{Response: r} + + response.OKTARequestID = r.Header.Get(headerOKTARequestID) + + response.populatePaginationURLS() + response.Rate = parseRate(r) + return response +} + +// populatePageValues parses the HTTP Link response headers and populates the +// various pagination link values in the Response. + +// OKTA LINK Header takes this form: +// Link: ; rel="next", +// ; rel="self" + +func (r *Response) populatePaginationURLS() { + + for k, v := range r.Header { + + if k == "Link" { + nextRegex := regexp.MustCompile(`<(.*?)>; rel="next"`) + // prevRegex := regexp.MustCompile(`<(.*?)>; rel="prev"`) + selfRegex := regexp.MustCompile(`<(.*?)>; rel="self"`) + + for _, linkValue := range v { + nextLinkMatch := nextRegex.FindStringSubmatch(linkValue) + if len(nextLinkMatch) != 0 { + r.NextURL, _ = url.Parse(nextLinkMatch[1]) + } + selfLinkMatch := selfRegex.FindStringSubmatch(linkValue) + if len(selfLinkMatch) != 0 { + r.SelfURL, _ = url.Parse(selfLinkMatch[1]) + } + // prevLinkMatch := prevRegex.FindStringSubmatch(linkValue) + // if len(prevLinkMatch) != 0 { + // r.PrevURL, _ = url.Parse(prevLinkMatch[1]) + // } + } + } + } + +} + +// parseRate parses the rate related headers. +func parseRate(r *http.Response) Rate { + var rate Rate + + if limit := r.Header.Get(headerRateLimit); limit != "" { + rate.RatePerMinuteLimit, _ = strconv.Atoi(limit) + } + if remaining := r.Header.Get(headerRateRemaining); remaining != "" { + rate.Remaining, _ = strconv.Atoi(remaining) + } + if reset := r.Header.Get(headerRateReset); reset != "" { + if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { + rate.ResetTime = time.Unix(v, 0) + } + } + return rate +} + +// Do sends an API request and returns the API response. The API response is +// JSON decoded and stored in the value pointed to by v, or returned as an +// error if an API error has occurred. If v implements the io.Writer +// interface, the raw response body will be written to v, without attempting to +// first decode it. If rate limit is exceeded and reset time is in the future, +// Do returns rate immediately without making a network API call. +func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { + + // If we've hit rate limit, don't make further requests before Reset time. + if err := c.checkRateLimitBeforeDo(req); err != nil { + return nil, err + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + defer func() { + // Drain up to 512 bytes and close the body to let the Transport reuse the connection + io.CopyN(ioutil.Discard, resp.Body, 512) + resp.Body.Close() + }() + + response := newResponse(resp) + + c.rateMu.Lock() + c.mostRecentRate.RatePerMinuteLimit = response.Rate.RatePerMinuteLimit + c.mostRecentRate.Remaining = response.Rate.Remaining + c.mostRecentRate.ResetTime = response.Rate.ResetTime + c.rateMu.Unlock() + + err = CheckResponse(resp) + if err != nil { + // even though there was an error, we still return the response + // in case the caller wants to inspect it further + // fmt.Printf("Error after sdk.Do return\n") + + return response, err + } + + if v != nil { + if w, ok := v.(io.Writer); ok { + io.Copy(w, resp.Body) + } else { + err = json.NewDecoder(resp.Body).Decode(v) + if err == io.EOF { + err = nil // ignore EOF errors caused by empty response body + } + } + } + + return response, err +} + +// checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from +// current client state in order to quickly check if *RateLimitError can be immediately returned +// from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. +// Otherwise it returns nil, and Client.Do should proceed normally. +// http://developer.okta.com/docs/api/getting_started/design_principles.html#rate-limiting +func (c *Client) checkRateLimitBeforeDo(req *http.Request) error { + + c.rateMu.Lock() + mostRecentRate := c.mostRecentRate + c.rateMu.Unlock() + // fmt.Printf("checkRateLimitBeforeDo: \t Remaining = %d, \t ResetTime = %s\n", mostRecentRate.Remaining, mostRecentRate.ResetTime.String()) + if !mostRecentRate.ResetTime.IsZero() && mostRecentRate.Remaining < c.RateRemainingFloor && time.Now().Before(mostRecentRate.ResetTime) { + + if c.PauseOnRateLimit { + // If rate limit is hitting threshold then pause until the rate limit resets + // This behavior is controlled by the client PauseOnRateLimit value + // fmt.Printf("checkRateLimitBeforeDo: \t ***pause**** \t Time Now = %s \tPause After = %s\n", time.Now().String(), mostRecentRate.ResetTime.Sub(time.Now().Add(2*time.Second)).String()) + <-time.After(mostRecentRate.ResetTime.Sub(time.Now().Add(2 * time.Second))) + } else { + // fmt.Printf("checkRateLimitBeforeDo: \t ***error****\n") + + return &RateLimitError{ + Rate: mostRecentRate, + } + } + + } + + return nil +} + +// CheckResponse checks the API response for errors, and returns them if +// present. A response is considered an error if it has a status code outside +// the 200 range. API error responses are expected to have either no response +// body, or a JSON response body that maps to ErrorResponse. Any other +// response body will be silently ignored. +// +// The error type will be *RateLimitError for rate limit exceeded errors, +// and *TwoFactorAuthError for two-factor authentication errors. +// TODO - check un-authorized +func CheckResponse(r *http.Response) error { + if c := r.StatusCode; 200 <= c && c <= 299 { + return nil + } + + errorResp := &errorResponse{Response: r} + data, err := ioutil.ReadAll(r.Body) + if err == nil && data != nil { + json.Unmarshal(data, &errorResp.ErrorDetail) + } + switch { + case r.StatusCode == http.StatusTooManyRequests: + + return &RateLimitError{ + Rate: parseRate(r), + Response: r, + ErrorDetail: errorResp.ErrorDetail} + + default: + return errorResp + } + +} + +type apiError struct { + ErrorCode string `json:"errorCode"` + ErrorSummary string `json:"errorSummary"` + ErrorLink string `json:"errorLink"` + ErrorID string `json:"errorId"` + ErrorCauses []struct { + ErrorSummary string `json:"errorSummary"` + } `json:"errorCauses"` +} + +type errorResponse struct { + Response *http.Response // + ErrorDetail apiError +} + +func (r *errorResponse) Error() string { + return fmt.Sprintf("HTTP Method: %v - URL: %v: - HTTP Status Code: %d, OKTA Error Code: %v, OKTA Error Summary: %v, OKTA Error Causes: %v", + r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.ErrorDetail.ErrorCode, r.ErrorDetail.ErrorSummary, r.ErrorDetail.ErrorCauses) +} + +// RateLimitError occurs when OKTA returns 429 "Too Many Requests" response with a rate limit +// remaining value of 0, and error message starts with "API rate limit exceeded for ". +type RateLimitError struct { + Rate Rate // Rate specifies last known rate limit for the client + ErrorDetail apiError + Response *http.Response // +} + +func (r *RateLimitError) Error() string { + + return fmt.Sprintf("rate reset in %v", r.Rate.ResetTime.Sub(time.Now())) + +} + +// Code stolen from Github api libary +// Stringify attempts to create a reasonable string representation of types in +// the library. It does things like resolve pointers to their values +// and omits struct fields with nil values. +func stringify(message interface{}) string { + var buf bytes.Buffer + v := reflect.ValueOf(message) + stringifyValue(&buf, v) + return buf.String() +} + +// stringifyValue was heavily inspired by the goprotobuf library. + +func stringifyValue(w io.Writer, val reflect.Value) { + if val.Kind() == reflect.Ptr && val.IsNil() { + w.Write([]byte("")) + return + } + + v := reflect.Indirect(val) + + switch v.Kind() { + case reflect.String: + fmt.Fprintf(w, `"%s"`, v) + case reflect.Slice: + w.Write([]byte{'['}) + for i := 0; i < v.Len(); i++ { + if i > 0 { + w.Write([]byte{' '}) + } + + stringifyValue(w, v.Index(i)) + } + + w.Write([]byte{']'}) + return + case reflect.Struct: + if v.Type().Name() != "" { + w.Write([]byte(v.Type().String())) + } + w.Write([]byte{'{'}) + + var sep bool + for i := 0; i < v.NumField(); i++ { + fv := v.Field(i) + if fv.Kind() == reflect.Ptr && fv.IsNil() { + continue + } + if fv.Kind() == reflect.Slice && fv.IsNil() { + continue + } + + if sep { + w.Write([]byte(", ")) + } else { + sep = true + } + + w.Write([]byte(v.Type().Field(i).Name)) + w.Write([]byte{':'}) + stringifyValue(w, fv) + } + + w.Write([]byte{'}'}) + default: + if v.CanInterface() { + fmt.Fprint(w, v.Interface()) + } + } +} + +// NewRequest creates an API request. A relative URL can be provided in urlStr, +// in which case it is resolved relative to the BaseURL of the Client. +// Relative URLs should always be specified without a preceding slash. If +// specified, the value pointed to by body is JSON encoded and included as the +// request body. +func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { + rel, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + u := c.BaseURL.ResolveReference(rel) + + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, err + } + + req.Header.Set(headerAuthorization, fmt.Sprintf(headerAuthorizationFormat, c.apiKey)) + + if body != nil { + req.Header.Set("Content-Type", mediaTypeJSON) + } + + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } + return req, nil +} + +// addOptions adds the parameters in opt as URL query parameters to s. opt +// must be a struct whose fields may contain "url" tags. +func addOptions(s string, opt interface{}) (string, error) { + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + return s, nil + } + + u, err := url.Parse(s) + if err != nil { + return s, err + } + + qs, err := query.Values(opt) + if err != nil { + return s, err + } + + u.RawQuery = qs.Encode() + return u.String(), nil +} + +type dateFilter struct { + Value time.Time + Operator string +} diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/users.go b/vendor/github.com/chrismalek/oktasdk-go/okta/users.go new file mode 100644 index 000000000000..73448f50326d --- /dev/null +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/users.go @@ -0,0 +1,600 @@ +package okta + +import ( + "errors" + "fmt" + "net/url" + "time" +) + +const ( + profileEmailFilter = "profile.email" + profileLoginFilter = "profile.login" + profileStatusFilter = "status" + profileIDFilter = "id" + profileFirstNameFilter = "profile.firstName" + profileLastNameFilter = "profile.lastName" + profileLastUpdatedFilter = "lastUpdated" + // UserStatusActive is a constant to represent OKTA User State returned by the API + UserStatusActive = "ACTIVE" + // UserStatusStaged is a constant to represent OKTA User State returned by the API + UserStatusStaged = "STAGED" + // UserStatusProvisioned is a constant to represent OKTA User State returned by the API + UserStatusProvisioned = "PROVISIONED" + // UserStatusRecovery is a constant to represent OKTA User State returned by the API + UserStatusRecovery = "RECOVERY" + // UserStatusLockedOut is a constant to represent OKTA User State returned by the API + UserStatusLockedOut = "LOCKED_OUT" + // UserStatusPasswordExpired is a constant to represent OKTA User State returned by the API + UserStatusPasswordExpired = "PASSWORD_EXPIRED" + // UserStatusSuspended is a constant to represent OKTA User State returned by the API + UserStatusSuspended = "SUSPENDED" + // UserStatusDeprovisioned is a constant to represent OKTA User State returned by the API + UserStatusDeprovisioned = "DEPROVISIONED" + + oktaFilterTimeFormat = "2006-01-02T15:05:05.000Z" +) + +// UsersService handles communication with the User data related +// methods of the OKTA API. +type UsersService service + +// ActivationResponse - Response coming back from a user activation +type activationResponse struct { + ActivationURL string `json:"activationUrl"` +} + +type provider struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` +} + +type recoveryQuestion struct { + Question string `json:"question,omitempty"` + Answer string `json:"answer,omitempty"` +} + +type passwordValue struct { + Value string `json:"value,omitempty"` +} +type credentials struct { + Password *passwordValue `json:"password,omitempty"` + Provider *provider `json:"provider,omitempty"` + RecoveryQuestion *recoveryQuestion `json:"recovery_question,omitempty"` +} + +type userProfile struct { + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Login string `json:"login"` + MobilePhone string `json:"mobilePhone,omitempty"` + SecondEmail string `json:"secondEmail,omitempty"` + PsEmplid string `json:"psEmplid,omitempty"` + NickName string `json:"nickname,omitempty"` + DisplayName string `json:"displayName,omitempty"` + + ProfileURL string `json:"profileUrl,omitempty"` + PreferredLanguage string `json:"preferredLanguage,omitempty"` + UserType string `json:"userType,omitempty"` + Organization string `json:"organization,omitempty"` + Title string `json:"title,omitempty"` + Division string `json:"division,omitempty"` + Department string `json:"department,omitempty"` + CostCenter string `json:"costCenter,omitempty"` + EmployeeNumber string `json:"employeeNumber,omitempty"` + PrimaryPhone string `json:"primaryPhone,omitempty"` + StreetAddress string `json:"streetAddress,omitempty"` + City string `json:"city,omitempty"` + State string `json:"state,omitempty"` + ZipCode string `json:"zipCode,omitempty"` + CountryCode string `json:"countryCode,omitempty"` +} + +type userLinks struct { + ChangePassword struct { + Href string `json:"href"` + } `json:"changePassword"` + ChangeRecoveryQuestion struct { + Href string `json:"href"` + } `json:"changeRecoveryQuestion"` + Deactivate struct { + Href string `json:"href"` + } `json:"deactivate"` + ExpirePassword struct { + Href string `json:"href"` + } `json:"expirePassword"` + ForgotPassword struct { + Href string `json:"href"` + } `json:"forgotPassword"` + ResetFactors struct { + Href string `json:"href"` + } `json:"resetFactors"` + ResetPassword struct { + Href string `json:"href"` + } `json:"resetPassword"` +} + +// User is a struct that represents a user object from OKTA. +type User struct { + Activated string `json:"activated,omitempty"` + Created string `json:"created,omitempty"` + Credentials credentials `json:"credentials,omitempty"` + ID string `json:"id,omitempty"` + LastLogin string `json:"lastLogin,omitempty"` + LastUpdated string `json:"lastUpdated,omitempty"` + PasswordChanged string `json:"passwordChanged,omitempty"` + Profile userProfile `json:"profile"` + Status string `json:"status,omitempty"` + StatusChanged string `json:"statusChanged,omitempty"` + Links userLinks `json:"_links,omitempty"` + MFAFactors []userMFAFactor `json:"-,omitempty"` + Groups []Group `json:"-,omitempty"` +} + +type userMFAFactor struct { + ID string `json:"id,omitempty"` + FactorType string `json:"factorType,omitempty"` + Provider string `json:"provider,omitempty"` + VendorName string `json:"vendorName,omitempty"` + Status string `json:"status,omitempty"` + Created time.Time `json:"created,omitempty"` + LastUpdated time.Time `json:"lastUpdated,omitempty"` + Profile struct { + CredentialID string `json:"credentialId,omitempty"` + } `json:"profile,omitempty"` +} + +// NewUser object to create user objects in OKTA +type NewUser struct { + Profile userProfile `json:"profile"` + Credentials *credentials `json:"credentials,omitempty"` +} + +type newPasswordSet struct { + Credentials credentials `json:"credentials"` +} + +type resetPasswordResponse struct { + ResetPasswordURL string `json:"resetPasswordUrl"` +} + +// NewUser - Returns a new user object. This is used to create users in OKTA. It only has the properties that +// OKTA will take as input. The "User" object has more feilds that are OKTA returned like the ID, etc +func (s *UsersService) NewUser() NewUser { + return NewUser{} +} + +// SetPassword Adds a specified password to the new User +func (u *NewUser) SetPassword(passwordIn string) { + + if passwordIn != "" { + + pass := new(passwordValue) + pass.Value = passwordIn + + var cred *credentials + if u.Credentials == nil { + cred = new(credentials) + } else { + cred = u.Credentials + } + + cred.Password = pass + u.Credentials = cred + + } +} + +// SetRecoveryQuestion - Sets a custom security question and answer on a user object +func (u *NewUser) SetRecoveryQuestion(questionIn string, answerIn string) { + + if questionIn != "" && answerIn != "" { + recovery := new(recoveryQuestion) + + recovery.Question = questionIn + recovery.Answer = answerIn + + var cred *credentials + if u.Credentials == nil { + cred = new(credentials) + } else { + cred = u.Credentials + } + cred.RecoveryQuestion = recovery + u.Credentials = cred + + } +} + +func (u User) String() string { + return stringify(u) + // return fmt.Sprintf("ID: %v \tLogin: %v", u.ID, u.Profile.Login) +} + +// GetByID returns a user object for a specific OKTA ID. +// Generally the id input string is the cryptic OKTA key value from User.ID. However, the OKTA API may accept other values like "me", or login shortname +func (s *UsersService) GetByID(id string) (*User, *Response, error) { + u := fmt.Sprintf("users/%v", id) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + user := new(User) + resp, err := s.client.Do(req, user) + if err != nil { + return nil, resp, err + } + + return user, resp, err +} + +// UserListFilterOptions is a struct that you can populate which will "filter" user searches +// the exported struct fields should allow you to do different filters based on what is allowed in the OKTA API. +// The filter OKTA API is limited in the fields it can search +// NOTE: In the current form you can't add parenthesis and ordering +// OKTA API Supports only a limited number of properties: +// status, lastUpdated, id, profile.login, profile.email, profile.firstName, and profile.lastName. +// http://developer.okta.com/docs/api/resources/users.html#list-users-with-a-filter +type UserListFilterOptions struct { + Limit int `url:"limit,omitempty"` + EmailEqualTo string `url:"-"` + LoginEqualTo string `url:"-"` + StatusEqualTo string `url:"-"` + IDEqualTo string `url:"-"` + + FirstNameEqualTo string `url:"-"` + LastNameEqualTo string `url:"-"` + // API documenation says you can search with "starts with" but these don't work + + // FirstNameStartsWith string `url:"-"` + // LastNameStartsWith string `url:"-"` + + // This will be built by internal - may not need to export + FilterString string `url:"filter,omitempty"` + NextURL *url.URL `url:"-"` + GetAllPages bool `url:"-"` + NumberOfPages int `url:"-"` + LastUpdated dateFilter `url:"-"` +} + +// PopulateGroups will populate the groups a user is a member of. You pass in a pointer to an existing users +func (s *UsersService) PopulateGroups(user *User) (*Response, error) { + u := fmt.Sprintf("users/%v/groups", user.ID) + req, err := s.client.NewRequest("GET", u, nil) + + if err != nil { + return nil, err + } + // TODO: If user has more than 200 groups this will only return those first 200 + resp, err := s.client.Do(req, &user.Groups) + if err != nil { + return resp, err + } + + return resp, err +} + +// PopulateEnrolledFactors will populate the Enrolled MFA Factors a user is a member of. +// You pass in a pointer to an existing users +// http://developer.okta.com/docs/api/resources/factors.html#list-enrolled-factors +func (s *UsersService) PopulateEnrolledFactors(user *User) (*Response, error) { + u := fmt.Sprintf("users/%v/factors", user.ID) + req, err := s.client.NewRequest("GET", u, nil) + + if err != nil { + return nil, err + } + // TODO: If user has more than 200 groups this will only return those first 200 + resp, err := s.client.Do(req, &user.MFAFactors) + if err != nil { + return resp, err + } + + return resp, err +} + +// List users with status of LOCKED_OUT +// filter=status eq "LOCKED_OUT" +// List users updated after 06/01/2013 but before 01/01/2014 +// filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and lastUpdated lt "2014-01-01T00:00:00.000Z" +// List users updated after 06/01/2013 but before 01/01/2014 with a status of ACTIVE +// filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and lastUpdated lt "2014-01-01T00:00:00.000Z" and status eq "ACTIVE" +// TODO - Currently no way to do parenthesis +// List users updated after 06/01/2013 but with a status of LOCKED_OUT or RECOVERY +// filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and (status eq "LOCKED_OUT" or status eq "RECOVERY") + +// OTKA API docs: http://developer.okta.com/docs/api/resources/users.html#list-users-with-a-filter + +func appendToFilterString(currFilterString string, appendFilterKey string, appendFilterOperator string, appendFilterValue string) (rs string) { + if currFilterString != "" { + rs = fmt.Sprintf("%v and %v %v \"%v\"", currFilterString, appendFilterKey, appendFilterOperator, appendFilterValue) + } else { + rs = fmt.Sprintf("%v %v \"%v\"", appendFilterKey, appendFilterOperator, appendFilterValue) + } + + return rs +} + +// ListWithFilter will use the input UserListFilterOptions to find users and return a paged result set +func (s *UsersService) ListWithFilter(opt *UserListFilterOptions) ([]User, *Response, error) { + var u string + var err error + + pagesRetreived := 0 + + if opt.NextURL != nil { + u = opt.NextURL.String() + } else { + if opt.EmailEqualTo != "" { + opt.FilterString = appendToFilterString(opt.FilterString, profileEmailFilter, FilterEqualOperator, opt.EmailEqualTo) + } + if opt.LoginEqualTo != "" { + opt.FilterString = appendToFilterString(opt.FilterString, profileLoginFilter, FilterEqualOperator, opt.LoginEqualTo) + } + + if opt.StatusEqualTo != "" { + opt.FilterString = appendToFilterString(opt.FilterString, profileStatusFilter, FilterEqualOperator, opt.StatusEqualTo) + } + + if opt.IDEqualTo != "" { + opt.FilterString = appendToFilterString(opt.FilterString, profileIDFilter, FilterEqualOperator, opt.IDEqualTo) + } + + if opt.FirstNameEqualTo != "" { + opt.FilterString = appendToFilterString(opt.FilterString, profileFirstNameFilter, FilterEqualOperator, opt.FirstNameEqualTo) + } + + if opt.LastNameEqualTo != "" { + opt.FilterString = appendToFilterString(opt.FilterString, profileLastNameFilter, FilterEqualOperator, opt.LastNameEqualTo) + } + + // API documenation says you can search with "starts with" but these don't work + // if opt.FirstNameStartsWith != "" { + // opt.FilterString = appendToFilterString(opt.FilterString, profileFirstNameFilter, filterStartsWithOperator, opt.FirstNameStartsWith) + // } + + // if opt.LastNameStartsWith != "" { + // opt.FilterString = appendToFilterString(opt.FilterString, profileLastNameFilter, filterStartsWithOperator, opt.LastNameStartsWith) + // } + + if !opt.LastUpdated.Value.IsZero() { + opt.FilterString = appendToFilterString(opt.FilterString, profileLastUpdatedFilter, opt.LastUpdated.Operator, opt.LastUpdated.Value.UTC().Format(oktaFilterTimeFormat)) + } + + if opt.Limit == 0 { + opt.Limit = defaultLimit + } + + u, err = addOptions("users", opt) + + } + + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + users := make([]User, 1) + resp, err := s.client.Do(req, &users) + if err != nil { + return nil, resp, err + } + + pagesRetreived++ + + if (opt.NumberOfPages > 0 && pagesRetreived < opt.NumberOfPages) || opt.GetAllPages { + + for { + + if pagesRetreived == opt.NumberOfPages { + break + } + if resp.NextURL != nil { + var userPage []User + pageOption := new(UserListFilterOptions) + pageOption.NextURL = resp.NextURL + pageOption.NumberOfPages = 1 + pageOption.Limit = opt.Limit + + userPage, resp, err = s.ListWithFilter(pageOption) + if err != nil { + return users, resp, err + } else { + users = append(users, userPage...) + pagesRetreived++ + } + } else { + break + } + } + } + return users, resp, err +} + +// Create - Creates a new user. You must pass in a "newUser" object created from Users.NewUser() +// There are many differnt reasons that OKTA may reject the request so you have to check the error messages +func (s *UsersService) Create(userIn NewUser, createAsActive bool) (*User, *Response, error) { + + u := fmt.Sprintf("users?activate=%v", createAsActive) + + req, err := s.client.NewRequest("POST", u, userIn) + + if err != nil { + return nil, nil, err + } + + newUser := new(User) + resp, err := s.client.Do(req, newUser) + if err != nil { + return nil, resp, err + } + + return newUser, resp, err +} + +// Activate Activates a user. You can have OKTA send an email by including a "sendEmail=true" +// If you pass in sendEmail=false, then activationResponse.ActivationURL will have a string URL that +// can be sent to the end user. You can discard response if sendEmail=true +func (s *UsersService) Activate(id string, sendEmail bool) (*activationResponse, *Response, error) { + u := fmt.Sprintf("users/%v/lifecycle/activate?sendEmail=%v", id, sendEmail) + + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, nil, err + } + + activationInfo := new(activationResponse) + resp, err := s.client.Do(req, activationInfo) + + if err != nil { + return nil, resp, err + } + + return activationInfo, resp, err +} + +// Deactivate - Deactivates a user +func (s *UsersService) Deactivate(id string) (*Response, error) { + u := fmt.Sprintf("users/%v/lifecycle/deactivate", id) + + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, err + } + resp, err := s.client.Do(req, nil) + + if err != nil { + return resp, err + } + + return resp, err +} + +// Suspend - Suspends a user - If user is NOT active an Error will come back based on OKTA API: +// http://developer.okta.com/docs/api/resources/users.html#suspend-user +func (s *UsersService) Suspend(id string) (*Response, error) { + u := fmt.Sprintf("users/%v/lifecycle/suspend", id) + + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, err + } + resp, err := s.client.Do(req, nil) + + if err != nil { + return resp, err + } + + return resp, err +} + +// Unsuspend - Unsuspends a user - If user is NOT SUSPENDED, an Error will come back based on OKTA API: +// http://developer.okta.com/docs/api/resources/users.html#unsuspend-user +func (s *UsersService) Unsuspend(id string) (*Response, error) { + u := fmt.Sprintf("users/%v/lifecycle/unsuspend", id) + + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, err + } + resp, err := s.client.Do(req, nil) + + if err != nil { + return resp, err + } + + return resp, err +} + +// Unlock - Unlocks a user - Per docs, only for OKTA Mastered Account +// http://developer.okta.com/docs/api/resources/users.html#unlock-user +func (s *UsersService) Unlock(id string) (*Response, error) { + u := fmt.Sprintf("users/%v/lifecycle/unlock", id) + + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, err + } + resp, err := s.client.Do(req, nil) + + if err != nil { + return resp, err + } + + return resp, err +} + +// SetPassword - Sets a user password to an Admin provided String +func (s *UsersService) SetPassword(id string, newPassword string) (*User, *Response, error) { + + if id == "" || newPassword == "" { + return nil, nil, errors.New("please provide a User ID and Password") + } + + passwordUpdate := new(newPasswordSet) + + pass := new(passwordValue) + pass.Value = newPassword + + passwordUpdate.Credentials.Password = pass + + u := fmt.Sprintf("users/%v", id) + req, err := s.client.NewRequest("POST", u, passwordUpdate) + if err != nil { + return nil, nil, err + } + + user := new(User) + resp, err := s.client.Do(req, user) + if err != nil { + return nil, resp, err + } + + return user, resp, err +} + +// ResetPassword - Generates a one-time token (OTT) that can be used to reset a user’s password. +// The OTT link can be automatically emailed to the user or returned to the API caller and distributed using a custom flow. +// http://developer.okta.com/docs/api/resources/users.html#reset-password +// If you pass in sendEmail=false, then resetPasswordResponse.resetPasswordUrl will have a string URL that +// can be sent to the end user. You can discard response if sendEmail=true +func (s *UsersService) ResetPassword(id string, sendEmail bool) (*resetPasswordResponse, *Response, error) { + u := fmt.Sprintf("users/%v/lifecycle/reset_password?sendEmail=%v", id, sendEmail) + + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, nil, err + } + + resetInfo := new(resetPasswordResponse) + resp, err := s.client.Do(req, resetInfo) + + if err != nil { + return nil, resp, err + } + + return resetInfo, resp, err +} + +// PopulateMFAFactors will populate the MFA Factors a user is a member of. You pass in a pointer to an existing users +func (s *UsersService) PopulateMFAFactors(user *User) (*Response, error) { + u := fmt.Sprintf("users/%v/factors", user.ID) + + req, err := s.client.NewRequest("GET", u, nil) + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, &user.MFAFactors) + if err != nil { + return resp, err + } + + return resp, err +} diff --git a/vendor/github.com/sstarcher/go-okta/README.md b/vendor/github.com/sstarcher/go-okta/README.md deleted file mode 100644 index 28e880c3ade5..000000000000 --- a/vendor/github.com/sstarcher/go-okta/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Okta golang client -================ - -[![CircleCI](https://circleci.com/gh/sstarcher/job-reaper.svg?style=svg)](https://circleci.com/gh/sstarcher/go-okta) - - -Basic Okta HTTP client - - diff --git a/vendor/github.com/sstarcher/go-okta/api.go b/vendor/github.com/sstarcher/go-okta/api.go deleted file mode 100644 index 1236f07445c0..000000000000 --- a/vendor/github.com/sstarcher/go-okta/api.go +++ /dev/null @@ -1,123 +0,0 @@ -package okta - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" -) - -// Client to access okta -type Client struct { - client *http.Client - org string - Url string - ApiToken string -} - -// errorResponse is an error wrapper for the okta response -type errorResponse struct { - HTTPCode int - Response ErrorResponse - Endpoint string -} - -func (e *errorResponse) Error() string { - return fmt.Sprintf("Error hitting api endpoint %s %s", e.Endpoint, e.Response.ErrorCode) -} - -// NewClient object for calling okta -func NewClient(org string) *Client { - client := Client{ - client: &http.Client{}, - org: org, - Url: "okta.com", - } - - return &client -} - -// Authenticate with okta using username and password -func (c *Client) Authenticate(username, password string) (*AuthnResponse, error) { - var request = &AuthnRequest{ - Username: username, - Password: password, - } - - var response = &AuthnResponse{} - err := c.call("authn", "POST", request, response) - return response, err -} - -// Session takes a session token and always fails -func (c *Client) Session(sessionToken string) (*SessionResponse, error) { - var request = &SessionRequest{ - SessionToken: sessionToken, - } - - var response = &SessionResponse{} - err := c.call("sessions", "POST", request, response) - return response, err -} - -// User takes a user id and returns data about that user -func (c *Client) User(userID string) (*User, error) { - - var response = &User{} - err := c.call("users/"+userID, "GET", nil, response) - return response, err -} - -// Groups takes a user id and returns the groups the user belongs to -func (c *Client) Groups(userID string) (*Groups, error) { - - var response = &Groups{} - err := c.call("users/"+userID+"/groups", "GET", nil, response) - return response, err -} - -func (c *Client) call(endpoint, method string, request, response interface{}) error { - data, _ := json.Marshal(request) - - var url = "https://" + c.org + "." + c.Url + "/api/v1/" + endpoint - req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) - if err != nil { - return err - } - - req.Header.Add("Accept", `application/json`) - req.Header.Add("Content-Type", `application/json`) - if c.ApiToken != "" { - req.Header.Add("Authorization", "SSWS "+c.ApiToken) - } - - resp, err := c.client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - - if resp.StatusCode == http.StatusOK { - err := json.Unmarshal(body, &response) - if err != nil { - return err - } - } else { - var errors ErrorResponse - err = json.Unmarshal(body, &errors) - - return &errorResponse{ - HTTPCode: resp.StatusCode, - Response: errors, - Endpoint: url, - } - } - - return nil -} diff --git a/vendor/github.com/sstarcher/go-okta/authn.go b/vendor/github.com/sstarcher/go-okta/authn.go deleted file mode 100644 index f0cc8eb9ca3d..000000000000 --- a/vendor/github.com/sstarcher/go-okta/authn.go +++ /dev/null @@ -1,45 +0,0 @@ -package okta - -import ( - "time" -) - -type ErrorResponse struct { - ErrorCode string `json:"errorCode"` - ErrorSummary string `json:"errorSummary"` - ErrorLink string `json:"errorLink"` - ErrorID string `json:"errorId"` - ErrorCauses []struct { - ErrorSummary string `json:"errorSummary"` - } `json:"errorCauses"` -} - -type AuthnRequest struct { - Username string `json:"username"` - Password string `json:"password"` - RelayState string `json:"relayState"` - Options struct { - MultiOptionalFactorEnroll bool `json:"multiOptionalFactorEnroll"` - WarnBeforePasswordExpired bool `json:"warnBeforePasswordExpired"` - } `json:"options"` -} - -type AuthnResponse struct { - ExpiresAt time.Time `json:"expiresAt"` - Status string `json:"status"` - RelayState string `json:"relayState"` - SessionToken string `json:"sessionToken"` - Embedded struct { - User struct { - ID string `json:"id"` - PasswordChanged time.Time `json:"passwordChanged"` - Profile struct { - Login string `json:"login"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Locale string `json:"locale"` - TimeZone string `json:"timeZone"` - } `json:"profile"` - } `json:"user"` - } `json:"_embedded"` -} diff --git a/vendor/github.com/sstarcher/go-okta/circleci.yml b/vendor/github.com/sstarcher/go-okta/circleci.yml deleted file mode 100644 index 20db0cf2b9b9..000000000000 --- a/vendor/github.com/sstarcher/go-okta/circleci.yml +++ /dev/null @@ -1,19 +0,0 @@ -machine: - environment: - GOPATH: "${HOME}/.go_workspace" - IMPORT_PATH: "${GOPATH}/src/github.com/${CIRCLE_PROJECT_USERNAME}" - APP_PATH: "${IMPORT_PATH}/${CIRCLE_PROJECT_REPONAME}" - -dependencies: - override: - - sudo add-apt-repository ppa:masterminds/glide -y - - sudo apt-get update - - sudo apt-get install glide -y - -test: - pre: - - mkdir -p "$IMPORT_PATH" - - ln -sf "$(pwd)" "${APP_PATH}" - - cd "${APP_PATH}" && glide install - override: - - cd "${APP_PATH}" && go test -cover $(glide nv) diff --git a/vendor/github.com/sstarcher/go-okta/glide.lock b/vendor/github.com/sstarcher/go-okta/glide.lock deleted file mode 100644 index ea836401749d..000000000000 --- a/vendor/github.com/sstarcher/go-okta/glide.lock +++ /dev/null @@ -1,4 +0,0 @@ -hash: acc035e4a3e5e3ed975f4233cc66fdbf3af5eb7bc2b5b337a26f730abf86e4b7 -updated: 2016-09-28T11:14:46.44318819-04:00 -imports: [] -testImports: [] diff --git a/vendor/github.com/sstarcher/go-okta/glide.yaml b/vendor/github.com/sstarcher/go-okta/glide.yaml deleted file mode 100644 index 79792630af75..000000000000 --- a/vendor/github.com/sstarcher/go-okta/glide.yaml +++ /dev/null @@ -1,2 +0,0 @@ -package: github.com/sstarcher/go-okta -import: [] diff --git a/vendor/github.com/sstarcher/go-okta/sessions.go b/vendor/github.com/sstarcher/go-okta/sessions.go deleted file mode 100644 index b974683f1553..000000000000 --- a/vendor/github.com/sstarcher/go-okta/sessions.go +++ /dev/null @@ -1,46 +0,0 @@ -package okta - -import ( - "time" -) - -type SessionRequest struct { - SessionToken string `json:"sessionToken"` -} - -type SessionResponse struct { - ID string `json:"id"` - Login string `json:"login"` - UserID string `json:"userId"` - ExpiresAt time.Time `json:"expiresAt"` - Status string `json:"status"` - LastPasswordVerification time.Time `json:"lastPasswordVerification"` - LastFactorVerification interface{} `json:"lastFactorVerification"` - Amr []string `json:"amr"` - Idp struct { - ID string `json:"id"` - Type string `json:"type"` - } `json:"idp"` - MfaActive bool `json:"mfaActive"` - Links struct { - Self struct { - Href string `json:"href"` - Hints struct { - Allow []string `json:"allow"` - } `json:"hints"` - } `json:"self"` - Refresh struct { - Href string `json:"href"` - Hints struct { - Allow []string `json:"allow"` - } `json:"hints"` - } `json:"refresh"` - User struct { - Name string `json:"name"` - Href string `json:"href"` - Hints struct { - Allow []string `json:"allow"` - } `json:"hints"` - } `json:"user"` - } `json:"_links"` -} diff --git a/vendor/github.com/sstarcher/go-okta/users.go b/vendor/github.com/sstarcher/go-okta/users.go deleted file mode 100644 index 850d4b97cf6f..000000000000 --- a/vendor/github.com/sstarcher/go-okta/users.go +++ /dev/null @@ -1,83 +0,0 @@ -package okta - -import ( - "time" -) - -type User struct { - ID string `json:"id"` - Status string `json:"status"` - Created *time.Time `json:"created"` - Activated *time.Time `json:"activated"` - StatusChanged *time.Time `json:"statusChanged"` - LastLogin *time.Time `json:"lastLogin"` - LastUpdated *time.Time `json:"lastUpdated"` - PasswordChanged *time.Time `json:"passwordChanged"` - Profile struct { - Login string `json:"login"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - NickName string `json:"nickName"` - DisplayName string `json:"displayName"` - Email string `json:"email"` - SecondEmail string `json:"secondEmail"` - ProfileURL string `json:"profileUrl"` - PreferredLanguage string `json:"preferredLanguage"` - UserType string `json:"userType"` - Organization string `json:"organization"` - Title string `json:"title"` - Division string `json:"division"` - Department string `json:"department"` - CostCenter string `json:"costCenter"` - EmployeeNumber string `json:"employeeNumber"` - MobilePhone string `json:"mobilePhone"` - PrimaryPhone string `json:"primaryPhone"` - StreetAddress string `json:"streetAddress"` - City string `json:"city"` - State string `json:"state"` - ZipCode string `json:"zipCode"` - CountryCode string `json:"countryCode"` - } `json:"profile"` - Credentials struct { - Password struct { - } `json:"password"` - RecoveryQuestion struct { - Question string `json:"question"` - } `json:"recovery_question"` - Provider struct { - Type string `json:"type"` - Name string `json:"name"` - } `json:"provider"` - } `json:"credentials"` - Links struct { - ResetPassword struct { - Href string `json:"href"` - } `json:"resetPassword"` - ResetFactors struct { - Href string `json:"href"` - } `json:"resetFactors"` - ExpirePassword struct { - Href string `json:"href"` - } `json:"expirePassword"` - ForgotPassword struct { - Href string `json:"href"` - } `json:"forgotPassword"` - ChangeRecoveryQuestion struct { - Href string `json:"href"` - } `json:"changeRecoveryQuestion"` - Deactivate struct { - Href string `json:"href"` - } `json:"deactivate"` - ChangePassword struct { - Href string `json:"href"` - } `json:"changePassword"` - } `json:"_links"` -} - -type Groups []struct { - ID string `json:"id"` - Profile struct { - Name string `json:"name"` - Description string `json:"description"` - } `json:"profile"` -} diff --git a/vendor/vendor.json b/vendor/vendor.json index bc75586e09a4..f507f1aa7d14 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -446,6 +446,12 @@ "revision": "61153c768f31ee5f130071d08fc82b85208528de", "revisionTime": "2017-07-11T19:02:43Z" }, + { + "checksumSHA1": "QZtBo/fc3zeQFxPFgPVMyDiw70M=", + "path": "github.com/chrismalek/oktasdk-go/okta", + "revision": "7d4ce0a254ec5f9eda3397523f6cf183e1d46c5e", + "revisionTime": "2017-02-07T05:01:14Z" + }, { "checksumSHA1": "Ymghbn2vkOAdT9rNQxKR2qNuxtA=", "path": "github.com/circonus-labs/circonus-gometrics", @@ -1358,12 +1364,6 @@ "revision": "e49c59d69b89746796d48156991108c80faf1d7d", "revisionTime": "2017-07-18T14:06:42Z" }, - { - "checksumSHA1": "7b7psq20O8IOCr885W2Ld6a3KTc=", - "path": "github.com/sstarcher/go-okta", - "revision": "64b3cb9e3a7b6d0c4e4432576c873e492d152666", - "revisionTime": "2017-04-28T20:44:25Z" - }, { "checksumSHA1": "9Zw986fuQM/hCoVd8vmHoSM+8sU=", "path": "github.com/ugorji/go/codec", diff --git a/website/source/api/auth/okta/index.html.md b/website/source/api/auth/okta/index.html.md index 6eff597b59cb..efa149e16408 100644 --- a/website/source/api/auth/okta/index.html.md +++ b/website/source/api/auth/okta/index.html.md @@ -27,11 +27,12 @@ distinction between the `create` and `update` capabilities inside ACL policies. ### Parameters -- `organization` `(string: )` - Okta organization to authenticate - against. -- `token` `(string: "")` - Okta admin API token. -- `base_url` `(string: "")` - The API endpoint to use. Useful if you are using - Okta development accounts. +- `org_name` `(string: )` - Name of the organization to be used in the + Okta API. +- `api_token` `(string: )` - Okta API key. +- `production` `(bool: true)` - If set, production API URL prefix will be used + to communicate with Okta and if not set, a preview production API URL prefix + will be used. Defaults to true. - `ttl` `(string: "")` - Duration after which authentication will be expired. - `max_ttl` `(string: "")` - Maximum duration after which authentication will be expired. @@ -40,8 +41,8 @@ distinction between the `create` and `update` capabilities inside ACL policies. ```json { - "organization": "example", - "token": "abc123" + "org_name": "example", + "api_token": "abc123" } ``` @@ -80,9 +81,9 @@ $ curl \ "lease_duration": 0, "renewable": false, "data": { - "organization": "example", - "token": "abc123", - "base_url": "", + "org_name": "example", + "api_token": "abc123", + "production": true, "ttl": "", "max_ttl": "" }, diff --git a/website/source/api/secret/pki/index.html.md b/website/source/api/secret/pki/index.html.md index ba0b7f66607a..4f6a350b6d81 100644 --- a/website/source/api/secret/pki/index.html.md +++ b/website/source/api/secret/pki/index.html.md @@ -41,6 +41,7 @@ update your API calls accordingly. * [Generate Root](#generate-root) * [Delete Root](#delete-root) * [Sign Intermediate](#sign-intermediate) +* [Sign Self-Issued](#sign-self-issued) * [Sign Certificate](#sign-certificate) * [Sign Verbatim](#sign-verbatim) * [Tidy](#tidy) @@ -1073,7 +1074,6 @@ verbatim. { "csr": "...", "common_name": "example.com" - } ``` @@ -1103,6 +1103,63 @@ $ curl \ "auth": null } ``` +## Sign Self-Issued + +This endpoint uses the configured CA certificate to sign a self-issued +certificate (which will usually be a self-signed certificate as well). + +**_This is an extremely privileged endpoint_**. The given certificate will be +signed as-is with only minimal validation performed (is it a CA cert, and is it +actually self-issued). The only values that will be changed will be the +authority key ID and, if set, any distribution points. + +This is generally only needed for root certificate rolling. If you don't know +whether you need this endpoint, you most likely should be using a different +endpoint (such as `sign-intermediate`). + +This endpoint requires `sudo` capability. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `POST` | `/pki/root/sign-self-issued` | `200 application/json` | + +### Parameters + +- `certificate` `(string: )` – Specifies the PEM-encoded self-issued certificate. + +### Sample Payload + +```json +{ + "certificate": "..." +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + https://vault.rocks/v1/pki/root/sign-self-issued +``` + +### Sample Response + +```json +{ + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE-----\n", + "issuing_ca": "-----BEGIN CERTIFICATE-----\nMIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV\n...\nG/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==\n-----END CERTIFICATE-----\n", + }, + "auth": null +} +``` + ## Sign Certificate diff --git a/website/source/api/system/auth.html.md b/website/source/api/system/auth.html.md index 01b793b40985..36ca6646c882 100644 --- a/website/source/api/system/auth.html.md +++ b/website/source/api/system/auth.html.md @@ -74,6 +74,18 @@ For example, mounting the "foo" auth backend will make it accessible at - `type` `(string: )` – Specifies the name of the authentication backend type, such as "github" or "token". +- `config` `(map: nil)` – Specifies configuration options for + this mount. These are the possible values: + + - `plugin_name` + + The plugin_name can be provided in the config map or as a top-level option, + with the former taking precedence. + +- `plugin_name` `(string: "")` – Specifies the name of the auth plugin to + use based from the name in the plugin catalog. Applies only to plugin + backends. + Additionally, the following options are allowed in Vault open-source, but relevant functionality is only supported in Vault Enterprise: @@ -81,9 +93,6 @@ relevant functionality is only supported in Vault Enterprise: only. Local mounts are not replicated nor (if a secondary) removed by replication. -- `plugin_name` `(string: "")` – Specifies the name of the auth plugin to - use based from the name in the plugin catalog. - ### Sample Payload ```json diff --git a/website/source/api/system/mounts.html.md b/website/source/api/system/mounts.html.md index 94f6ab8708ef..46e1b22c33a8 100644 --- a/website/source/api/system/mounts.html.md +++ b/website/source/api/system/mounts.html.md @@ -74,16 +74,22 @@ This endpoint mounts a new secret backend at the given path. mount. - `config` `(map: nil)` – Specifies configuration options for - this mount. This is an object with three possible values: + this mount. This is an object with four possible values: - `default_lease_ttl` - `max_lease_ttl` - `force_no_cache` - `plugin_name` - These control the default and maximum lease time-to-live, and force - disabling backend caching respectively. If set on a specific mount, this - overrides the global defaults. + These control the default and maximum lease time-to-live, force + disabling backend caching, and option plugin name for plugin backends + respectively. The first three options override the global defaults if + set on a specific mount. The plugin_name can be provided in the config + map or as a top-level option, with the former taking precedence. + +- `plugin_name` `(string: "")` – Specifies the name of the plugin to + use based from the name in the plugin catalog. Applies only to plugin + backends. Additionally, the following options are allowed in Vault open-source, but relevant functionality is only supported in Vault Enterprise: