diff --git a/command/commands.go b/command/commands.go index 395b47a07e20..e219decf8926 100644 --- a/command/commands.go +++ b/command/commands.go @@ -27,6 +27,7 @@ import ( credAliCloud "github.com/hashicorp/vault-plugin-auth-alicloud" credCentrify "github.com/hashicorp/vault-plugin-auth-centrify" credGcp "github.com/hashicorp/vault-plugin-auth-gcp/plugin" + credOIDC "github.com/hashicorp/vault-plugin-auth-jwt" credAws "github.com/hashicorp/vault/builtin/credential/aws" credCert "github.com/hashicorp/vault/builtin/credential/cert" credGitHub "github.com/hashicorp/vault/builtin/credential/github" @@ -177,6 +178,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { "gcp": &credGcp.CLIHandler{}, "github": &credGitHub.CLIHandler{}, "ldap": &credLdap.CLIHandler{}, + "oidc": &credOIDC.CLIHandler{}, "okta": &credOkta.CLIHandler{}, "radius": &credUserpass.CLIHandler{ DefaultMount: "radius", diff --git a/helper/builtinplugins/registry.go b/helper/builtinplugins/registry.go index 2ab5985c0d2d..1183f7626680 100644 --- a/helper/builtinplugins/registry.go +++ b/helper/builtinplugins/registry.go @@ -73,6 +73,7 @@ func newRegistry() *registry { "jwt": credJWT.Factory, "kubernetes": credKube.Factory, "ldap": credLdap.Factory, + "oidc": credJWT.Factory, "okta": credOkta.Factory, "radius": credRadius.Factory, "userpass": credUserpass.Factory, diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/Gopkg.lock b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/Gopkg.lock index c936288aaab1..9ae1a539743a 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/Gopkg.lock +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/Gopkg.lock @@ -15,12 +15,12 @@ version = "1.1" [[projects]] - branch = "master" - digest = "1:6bf6d532e503d9526d46e69aff04d11632c8c1e28b847dbd226babc1689aa723" + digest = "1:c47f4964978e211c6e566596ec6246c329912ea92e9bb99c00798bb4564c5b09" name = "github.com/armon/go-radix" packages = ["."] pruneopts = "UT" - revision = "7fddfc383310abc091d79a27f116d30cf0424032" + revision = "1a2de0c21c94309923825da3df33a4381872c795" + version = "v1.0.0" [[projects]] digest = "1:f6e5e1bc64c2908167e6aa9a1fe0c084d515132a1c63ad5b6c84036aa06dc0c1" @@ -39,7 +39,7 @@ version = "v1.0.1" [[projects]] - digest = "1:17fe264ee908afc795734e8c4e63db2accabaf57326dbf21763a7d6b86096260" + digest = "1:4c0989ca0bcd10799064318923b9bc2db6b4d6338dd75f3f2d86c3511aaaf5cf" name = "github.com/golang/protobuf" packages = [ "proto", @@ -49,8 +49,8 @@ "ptypes/timestamp", ] pruneopts = "UT" - revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" - version = "v1.1.0" + revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" + version = "v1.2.0" [[projects]] branch = "master" @@ -62,106 +62,108 @@ [[projects]] branch = "master" - digest = "1:d1971637b21871ec2033a44ca87c99c5608a7340cb34ec75fab8d2ab503276c9" + digest = "1:0ade334594e69404d80d9d323445d2297ff8161637f9b2d347cc6973d2d6f05b" name = "github.com/hashicorp/errwrap" packages = ["."] pruneopts = "UT" - revision = "d6c0cd88035724dd42e0f335ae30161c20575ecc" + revision = "8a6fb523712970c966eefc6b39ed2c5e74880354" [[projects]] branch = "master" - digest = "1:77cb3be9b21ba7f1a4701e870c84ea8b66e7d74c7c8951c58155fdadae9414ec" + digest = "1:f47d6109c2034cb16bd62b220e18afd5aa9d5a1630fe5d937ad96a4fb7cbb277" name = "github.com/hashicorp/go-cleanhttp" packages = ["."] pruneopts = "UT" - revision = "d5fe4b57a186c716b0e00b8c301cbd9b4182694d" + revision = "e8ab9daed8d1ddd2d3c4efba338fe2eeae2e4f18" [[projects]] branch = "master" - digest = "1:e8d99882caa8c74d68f340ddb9bba3f7e433117ce57c3e52501edfa7e195d2c7" + digest = "1:0876aeb6edb07e20b6b0ce1d346655cb63dbe0a26ccfb47b68a9b7697709777b" name = "github.com/hashicorp/go-hclog" packages = ["."] pruneopts = "UT" - revision = "ff2cf002a8dd750586d91dddd4470c341f981fe1" + revision = "4783caec6f2e5cdd47fab8b2bb47ce2ce5c546b7" [[projects]] - branch = "master" - digest = "1:2394f5a25132b3868eff44599cc28d44bdd0330806e34c495d754dd052df612b" + digest = "1:2be5a35f0c5b35162c41bb24971e5dcf6ce825403296ee435429cdcc4e1e847e" name = "github.com/hashicorp/go-immutable-radix" packages = ["."] pruneopts = "UT" - revision = "7f3cd4390caab3250a57f30efdb2a65dd7649ecf" + revision = "27df80928bb34bb1b0d6d0e01b9e679902e7a6b5" + version = "v1.0.0" [[projects]] - branch = "master" - digest = "1:46fb6a9f1b9667f32ac93e08b1da118b2c666991424ea12e848b05d4fe5155ef" + digest = "1:f668349b83f7d779567c880550534addeca7ebadfdcf44b0b9c39be61864b4b7" name = "github.com/hashicorp/go-multierror" packages = ["."] pruneopts = "UT" - revision = "3d5d8f294aa03d8e98859feac328afbdf1ae0703" + revision = "886a7fbe3eb1c874d46f623bfa70af45f425b3d1" + version = "v1.0.0" [[projects]] branch = "master" - digest = "1:20f78c1cf1b6fe6c55ba1407350d6fc7dc77d1591f8106ba693c28014a1a1b37" + digest = "1:77a6108b8eb3cd0feac4eeb3e032f36c8fdfe9497671952fd9eb682b9c503158" name = "github.com/hashicorp/go-plugin" - packages = ["."] + packages = [ + ".", + "internal/proto", + ] pruneopts = "UT" - revision = "a4620f9913d19f03a6bf19b2f304daaaf83ea130" + revision = "362c99b11937c6a84686ee5726a8170e921ab406" [[projects]] - branch = "master" - digest = "1:183f00c472fb9b2446659618eebf4899872fa267b92f926539411abdc8b941df" + digest = "1:d260503602063d71718eb21f85c02133ad5eac894c2a6f0e0546b7dc017dc97e" name = "github.com/hashicorp/go-retryablehttp" packages = ["."] pruneopts = "UT" - revision = "e651d75abec6fbd4f2c09508f72ae7af8a8b7171" + revision = "73489d0a1476f0c9e6fb03f9c39241523a496dfd" + version = "v0.5.2" [[projects]] - branch = "master" - digest = "1:45aad874d3c7d5e8610427c81870fb54970b981692930ec2a319ce4cb89d7a00" + digest = "1:a54ada9beb59fdc35b69322979e870ff0b780e03f4dc309c4c8674b94927df75" name = "github.com/hashicorp/go-rootcerts" packages = ["."] pruneopts = "UT" - revision = "6bb64b370b90e7ef1fa532be9e591a81c3493e00" + revision = "63503fb4e1eca22f9ae0f90b49c5d5538a0e87eb" + version = "v1.0.0" [[projects]] branch = "master" - digest = "1:14f2005c31ddf99c4a0f36fc440f8d1ac43224194c7c4a904b3c8f4ba5654d0b" + digest = "1:3c4c27026ab6a3218dbde897568f651c81062e2ee6e617e57ae46ca95bb1db6b" name = "github.com/hashicorp/go-sockaddr" packages = ["."] pruneopts = "UT" - revision = "6d291a969b86c4b633730bfc6b8b9d64c3aafed9" + revision = "3aed17b5ee41761cc2b04f2a94c7107d428967e5" [[projects]] - branch = "master" - digest = "1:354978aad16c56c27f57e5b152224806d87902e4935da3b03e18263d82ae77aa" + digest = "1:f14364057165381ea296e49f8870a9ffce2b8a95e34d6ae06c759106aaef428c" name = "github.com/hashicorp/go-uuid" packages = ["."] pruneopts = "UT" - revision = "27454136f0364f2d44b1276c552d69105cf8c498" + revision = "4f571afc59f3043a65f8fe6bf46d887b10a01d43" + version = "v1.0.1" [[projects]] - branch = "master" - digest = "1:32c0e96a63bd093eccf37db757fb314be5996f34de93969321c2cbef893a7bd6" + digest = "1:950caca7dfcf796419232ba996c9c3539d09f26af27ba848c4508e604c13efbb" name = "github.com/hashicorp/go-version" packages = ["."] pruneopts = "UT" - revision = "270f2f71b1ee587f3b609f00f422b76a6b28f348" + revision = "d40cf49b3a77bba84a7afdbd7f1dc295d114efb1" + version = "v1.1.0" [[projects]] - branch = "master" - digest = "1:cf296baa185baae04a9a7004efee8511d08e2f5f51d4cbe5375da89722d681db" + digest = "1:8ec8d88c248041a6df5f6574b87bc00e7e0b493881dad2e7ef47b11dc69093b5" name = "github.com/hashicorp/golang-lru" packages = [ ".", "simplelru", ] pruneopts = "UT" - revision = "0fb14efe8c47ae851c0034ed7a448854d3d34cf3" + revision = "20f1fb78b0740ba8c3cb143a61e86ba5c8669768" + version = "v0.5.0" [[projects]] - branch = "master" - digest = "1:12247a2e99a060cc692f6680e5272c8adf0b8f572e6bce0d7095e624c958a240" + digest = "1:ea40c24cdbacd054a6ae9de03e62c5f252479b96c716375aace5c120d68647c8" name = "github.com/hashicorp/hcl" packages = [ ".", @@ -175,11 +177,12 @@ "json/token", ] pruneopts = "UT" - revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" + revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241" + version = "v1.0.0" [[projects]] branch = "master" - digest = "1:d00de8725219a569ffbb5dd1042e4ced1f3b5ccee2b07218371f71026cc7609a" + digest = "1:f5bdd7b0d06bfa965cefa9c52af7f556bd079ff4328d67c89f6afdf4be7eabbe" name = "github.com/hashicorp/vault" packages = [ "api", @@ -187,9 +190,11 @@ "helper/cidrutil", "helper/compressutil", "helper/consts", + "helper/cryptoutil", "helper/errutil", "helper/hclutil", "helper/jsonutil", + "helper/license", "helper/locksutil", "helper/logging", "helper/mlock", @@ -209,39 +214,47 @@ "version", ] pruneopts = "UT" - revision = "8655d167084028d627f687ddc25d0c71307eb5be" + revision = "b16527d791ba46f74a608527b328957618aa0ae6" [[projects]] branch = "master" - digest = "1:89658943622e6bc5e76b4da027ee9583fa0b321db0c797bd554edab96c1ca2b1" + digest = "1:a4826c308e84f5f161b90b54a814f0be7d112b80164b9b884698a6903ea47ab3" name = "github.com/hashicorp/yamux" packages = ["."] pruneopts = "UT" - revision = "3520598351bb3500a49ae9563f5539666ae0a27c" + revision = "2f1d1f20f75d5404f53b9edf6b53ed5505508675" [[projects]] - branch = "master" - digest = "1:c7354463195544b1ab3c1f1fadb41430947f5d28dfbf2cdbd38268c5717a5a03" + digest = "1:5d231480e1c64a726869bc4142d270184c419749d34f167646baa21008eb0a79" name = "github.com/mitchellh/go-homedir" packages = ["."] pruneopts = "UT" - revision = "58046073cbffe2f25d425fe1331102f55cf719de" + revision = "af06845cf3004701891bf4fdb884bfe4920b3727" + version = "v1.1.0" [[projects]] - branch = "master" - digest = "1:cae1afe858922bd10e9573b87130f730a6e4183a00eba79920d6656629468bfa" + digest = "1:42eb1f52b84a06820cedc9baec2e710bfbda3ee6dac6cdb97f8b9a5066134ec6" name = "github.com/mitchellh/go-testing-interface" packages = ["."] pruneopts = "UT" - revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28" + revision = "6d0b8010fcc857872e42fc6c931227569016843c" + version = "v1.0.0" [[projects]] - branch = "master" - digest = "1:5ab79470a1d0fb19b041a624415612f8236b3c06070161a910562f2b2d064355" + digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318" name = "github.com/mitchellh/mapstructure" packages = ["."] pruneopts = "UT" - revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac" + revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" + version = "v1.1.2" + +[[projects]] + branch = "master" + digest = "1:302de3c669b04a566d4e99760d6fb35a22177fc14c7a9284e8b3cf6e9fe3f28a" + name = "github.com/mitchellh/pointerstructure" + packages = ["."] + pruneopts = "UT" + revision = "f2329fcfa9e280bdb5a3f2544aec815a508ad72f" [[projects]] digest = "1:9ec6cf1df5ad1d55cf41a43b6b1e7e118a91bade4f68ff4303379343e40c0e25" @@ -251,6 +264,25 @@ revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39" version = "v1.0.0" +[[projects]] + digest = "1:808cdddf087fb64baeae67b8dfaee2069034d9704923a3cb8bd96a995421a625" + name = "github.com/patrickmn/go-cache" + packages = ["."] + pruneopts = "UT" + revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0" + version = "v2.1.0" + +[[projects]] + digest = "1:c7a5e79396b6eb570159df7a1d487ce5775bf43b7907976fbef6de544ea160ad" + name = "github.com/pierrec/lz4" + packages = [ + ".", + "internal/xxh32", + ] + pruneopts = "UT" + revision = "473cd7ce01a1113208073166464b98819526150e" + version = "v2.0.8" + [[projects]] branch = "master" digest = "1:bd9efe4e0b0f768302a1e2f0c22458149278de533e521206e5ddc71848c269a0" @@ -263,28 +295,29 @@ revision = "1555304b9b35fdd2b425bccf1a5613677705e7d0" [[projects]] - digest = "1:0e792eea6c96ec55ff302ef33886acbaa5006e900fefe82689e88d96439dcd84" + digest = "1:6baa565fe16f8657cf93469b2b8a6c61a277827734400d27e44d589547297279" name = "github.com/ryanuber/go-glob" packages = ["."] pruneopts = "UT" - revision = "572520ed46dbddaed19ea3d9541bdd0494163693" - version = "v0.1" + revision = "51a8f68e6c24dc43f1e371749c89a267de4ebc53" + version = "v1.0.0" [[projects]] branch = "master" - digest = "1:b8fa1ff0fc20983395978b3f771bb10438accbfe19326b02e236c1d4bf1c91b2" + digest = "1:5bce6a1c0d1492cef01d74084ddbac09c4bbc4cbc1db3fdd0c138ed9bc945bf8" name = "golang.org/x/crypto" packages = [ + "blake2b", "ed25519", "ed25519/internal/edwards25519", "pbkdf2", ] pruneopts = "UT" - revision = "de0752318171da717af4ce24d0a2e8626afaeb11" + revision = "193df9c0f06f8bb35fba505183eaf0acc0136505" [[projects]] branch = "master" - digest = "1:3c4175c2711d67096567fc2d84a83464d6ff58119af3efc89983339d64144cb0" + digest = "1:9d2f08c64693fbe7177b5980f80c35672c80f12be79bb3bc86948b934d70e4ee" name = "golang.org/x/net" packages = [ "context", @@ -297,26 +330,29 @@ "trace", ] pruneopts = "UT" - revision = "aaf60122140d3fcf75376d319f0554393160eb50" + revision = "65e2d4e15006aab9813ff8769e768bbf4bb667a0" [[projects]] branch = "master" - digest = "1:af19f6e6c369bf51ef226e989034cd88a45083173c02ac4d7ab74c9a90d356b7" + digest = "1:e007b54f54cbd4214aa6d97a67d57bc2539991adb4e22ea92c482bbece8de469" name = "golang.org/x/oauth2" packages = [ ".", "internal", ] pruneopts = "UT" - revision = "3d292e4d0cdc3a0113e6d207bb137145ef1de42f" + revision = "99b60b757ec124ebb7d6b7e97f153b19c10ce163" [[projects]] branch = "master" - digest = "1:05662433b3a13c921587a6e622b5722072edff83211efd1cd79eeaeedfd83f07" + digest = "1:c9e49928119661a681af4037236af47654d6bd421c0af184962c890d0a61e0fb" name = "golang.org/x/sys" - packages = ["unix"] + packages = [ + "cpu", + "unix", + ] pruneopts = "UT" - revision = "1c9583448a9c3aa0f9a6a5241bf73c0bd8aafded" + revision = "3b5209105503162ded1863c307ac66fec31120dd" [[projects]] digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" @@ -343,14 +379,14 @@ [[projects]] branch = "master" - digest = "1:c9e7a4b4d47c0ed205d257648b0e5b0440880cb728506e318f8ac7cd36270bc4" + digest = "1:9fdc2b55e8e0fafe4b41884091e51e77344f7dc511c5acedcfd98200003bff90" name = "golang.org/x/time" packages = ["rate"] pruneopts = "UT" - revision = "fbb02b2291d28baffd63558aa44b4b56f178d650" + revision = "85acf8d2951cb2a3bde7632f9ff273ef0379bcbd" [[projects]] - digest = "1:328b5e4f197d928c444a51a75385f4b978915c0e75521f0ad6a3db976c97a7d3" + digest = "1:6f3bd49ddf2e104e52062774d797714371fac1b8bddfd8e124ce78e6b2264a10" name = "google.golang.org/appengine" packages = [ "internal", @@ -362,8 +398,8 @@ "urlfetch", ] pruneopts = "UT" - revision = "b1f26356af11148e710935ed1ac8a7f5702c7612" - version = "v1.1.0" + revision = "e9657d882bb81064595ca3b56cbe2546bbabf7b1" + version = "v1.4.0" [[projects]] branch = "master" @@ -371,19 +407,21 @@ name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] pruneopts = "UT" - revision = "d0a8f471bba2dbb160885b0000d814ee5d559bad" + revision = "4b09977fb92221987e99d190c8f88f2c92727a29" [[projects]] - digest = "1:047efbc3c9a51f3002b0002f92543857d372654a676fb6b01931982cd80467dd" + digest = "1:a887a56d0ff92cf05b4bb6004b46fc6e64d3fb6aca4eaeb1466bdce183ba5004" name = "google.golang.org/grpc" packages = [ ".", "balancer", "balancer/base", "balancer/roundrobin", + "binarylog/grpc_binarylog_v1", "codes", "connectivity", "credentials", + "credentials/internal", "encoding", "encoding/proto", "grpclog", @@ -391,9 +429,12 @@ "health/grpc_health_v1", "internal", "internal/backoff", + "internal/binarylog", "internal/channelz", "internal/envconfig", "internal/grpcrand", + "internal/grpcsync", + "internal/syscall", "internal/transport", "keepalive", "metadata", @@ -407,11 +448,11 @@ "tap", ] pruneopts = "UT" - revision = "32fb0ac620c32ba40a4626ddf94d90d12cce3455" - version = "v1.14.0" + revision = "a02b0774206b209466313a0b525d2c738fe407eb" + version = "v1.18.0" [[projects]] - digest = "1:b57bb9a6a2a03558d63166f1afc3c0c4f91ad137f63bf2bee995e9baeb976a9c" + digest = "1:a4cde1eec9a17eb2399a50c6e1a9fe3fde039994de058f9dbf6592d157bfe97b" name = "gopkg.in/square/go-jose.v2" packages = [ ".", @@ -420,8 +461,8 @@ "jwt", ] pruneopts = "UT" - revision = "8254d6c783765f38c8675fae4427a1fe73fbd09d" - version = "v2.1.8" + revision = "e94fb177d3668d35ab39c61cbb2f311550557e83" + version = "v2.2.2" [solve-meta] analyzer-name = "dep" @@ -433,6 +474,8 @@ "github.com/hashicorp/go-cleanhttp", "github.com/hashicorp/go-hclog", "github.com/hashicorp/go-sockaddr", + "github.com/hashicorp/go-uuid", + "github.com/hashicorp/vault/api", "github.com/hashicorp/vault/helper/certutil", "github.com/hashicorp/vault/helper/cidrutil", "github.com/hashicorp/vault/helper/logging", @@ -443,6 +486,8 @@ "github.com/hashicorp/vault/logical", "github.com/hashicorp/vault/logical/framework", "github.com/hashicorp/vault/logical/plugin", + "github.com/mitchellh/pointerstructure", + "github.com/patrickmn/go-cache", "golang.org/x/oauth2", "gopkg.in/square/go-jose.v2", "gopkg.in/square/go-jose.v2/jwt", diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/backend.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/backend.go index c906bd8c8ead..c1d328bdf29e 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/backend.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/backend.go @@ -3,10 +3,12 @@ package jwtauth import ( "context" "sync" + "time" oidc "github.com/coreos/go-oidc" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" + cache "github.com/patrickmn/go-cache" ) const ( @@ -16,7 +18,7 @@ const ( // Factory is used by framework func Factory(ctx context.Context, c *logical.BackendConfig) (logical.Backend, error) { - b := backend(c) + b := backend() if err := b.Setup(ctx, c); err != nil { return nil, err } @@ -29,14 +31,16 @@ type jwtAuthBackend struct { l sync.RWMutex provider *oidc.Provider cachedConfig *jwtConfig + oidcStates *cache.Cache providerCtx context.Context providerCtxCancel context.CancelFunc } -func backend(c *logical.BackendConfig) *jwtAuthBackend { +func backend() *jwtAuthBackend { b := new(jwtAuthBackend) b.providerCtx, b.providerCtxCancel = context.WithCancel(context.Background()) + b.oidcStates = cache.New(oidcStateTimeout, 1*time.Minute) b.Backend = &framework.Backend{ AuthRenew: b.pathLoginRenew, @@ -46,6 +50,9 @@ func backend(c *logical.BackendConfig) *jwtAuthBackend { PathsSpecial: &logical.Paths{ Unauthenticated: []string{ "login", + "oidc/auth_url", + "oidc/callback", + "ui", // TODO: remove when Vault UI is ready }, SealWrapStorage: []string{ "config", @@ -57,7 +64,9 @@ func backend(c *logical.BackendConfig) *jwtAuthBackend { pathRoleList(b), pathRole(b), pathConfig(b), + pathUI(b), // TODO: remove when Vault UI is ready }, + pathOIDC(b), ), Clean: b.cleanup, } diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/claims.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/claims.go new file mode 100644 index 000000000000..473349bc3ab2 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/claims.go @@ -0,0 +1,65 @@ +package jwtauth + +import ( + "fmt" + "strings" + + log "github.com/hashicorp/go-hclog" + "github.com/mitchellh/pointerstructure" +) + +// getClaim returns a claim value from allClaims given a provided claim string. +// If this string is a valid JSONPointer, it will be interpreted as such to locate +// the claim. Otherwise, the claim string will be used directly. +func getClaim(logger log.Logger, allClaims map[string]interface{}, claim string) interface{} { + var val interface{} + var err error + + if !strings.HasPrefix(claim, "/") { + val = allClaims[claim] + } else { + val, err = pointerstructure.Get(allClaims, claim) + if err != nil { + logger.Warn(fmt.Sprintf("unable to locate %s in claims: %s", claim, err.Error())) + return nil + } + } + + // The claims unmarshalled by go-oidc don't use UseNumber, so there will + // be mismatches if they're coming in as float64 since Vault's config will + // be represented as json.Number. If the operator can coerce claims data to + // be in string form, there is no problem. Alternatively, we could try to + // intelligently convert float64 to json.Number, e.g.: + // + // switch v := val.(type) { + // case float64: + // val = json.Number(strconv.Itoa(int(v))) + // } + // + // Or we fork and/or PR go-oidc. + + return val +} + +// extractMetadata builds a metadata map from a set of claims and claims mappings. +// The referenced claims must be strings and the claims mappings must be of the structure: +// +// { +// "/some/claim/pointer": "metadata_key1", +// "another_claim": "metadata_key2", +// ... +// } +func extractMetadata(logger log.Logger, allClaims map[string]interface{}, claimMappings map[string]string) (map[string]string, error) { + metadata := make(map[string]string) + for source, target := range claimMappings { + if value := getClaim(logger, allClaims, source); value != nil { + strValue, ok := value.(string) + if !ok { + return nil, fmt.Errorf("error converting claim '%s' to string", source) + } + + metadata[target] = strValue + } + } + return metadata, nil +} diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/cli.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/cli.go new file mode 100644 index 000000000000..a8b221d261a5 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/cli.go @@ -0,0 +1,502 @@ +package jwtauth + +import ( + "errors" + "fmt" + "net/http" + "os" + "os/exec" + "os/signal" + "regexp" + "runtime" + "strings" + + "github.com/hashicorp/vault/api" +) + +const defaultMount = "oidc" +const defaultPort = "8300" + +type CLIHandler struct{} + +type loginResp struct { + secret *api.Secret + err error +} + +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { + // handle ctrl-c while waiting for the callback + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + defer signal.Stop(ch) + + doneCh := make(chan loginResp) + + mount, ok := m["mount"] + if !ok { + mount = defaultMount + } + + port, ok := m["port"] + if !ok { + port = defaultPort + } + + role := m["role"] + if role == "" { + return nil, errors.New("a 'role' must be specified") + } + + secret, err := fetchAuthURL(c, role, mount, port) + if err != nil { + return nil, err + } + + authURL := secret.Data["auth_url"].(string) + if authURL == "" { + return nil, errors.New(fmt.Sprintf("Unable to authorize role %q. Check Vault logs for more information.", role)) + } + + fmt.Fprintf(os.Stderr, "Complete the login via your OIDC provider. Launching browser to:\n\n %s\n\n\n", authURL) + if err := openURL(authURL); err != nil { + fmt.Fprintf(os.Stderr, "Error attempting to automatically open browser: '%s'.\nPlease visit the authorization URL manually.", err) + } + + // Set up callback handler + http.HandleFunc(fmt.Sprintf("/v1/auth/%s/oidc/callback", mount), func(w http.ResponseWriter, req *http.Request) { + var response string + + query := req.URL.Query() + code := query.Get("code") + state := query.Get("state") + data := map[string][]string{ + "code": {code}, + "state": {state}, + } + + secret, err := c.Logical().ReadWithData(fmt.Sprintf("auth/%s/oidc/callback", mount), data) + if err != nil { + summary, detail := parseError(err) + response = errorHTML(summary, detail) + } else { + response = successHTML + } + + w.Write([]byte(response)) + doneCh <- loginResp{secret, err} + }) + + // Start local server + go func() { + if err := http.ListenAndServe(":"+port, nil); err != nil && err != http.ErrServerClosed { + fmt.Fprintf(os.Stderr, "Error listening for callback: %v\n\n", err.Error()) + } + }() + + // Wait for either the callback to finish or SIGINT to be received + select { + case s := <-doneCh: + return s.secret, s.err + case <-ch: + return nil, errors.New("interrupted") + } +} + +func fetchAuthURL(c *api.Client, role, mount, port string) (*api.Secret, error) { + data := map[string]interface{}{ + "role": role, + "redirect_uri": fmt.Sprintf("http://localhost:%s/v1/auth/%s/oidc/callback", port, mount), + } + + return c.Logical().Write(fmt.Sprintf("auth/%s/oidc/auth_url", mount), data) +} + +// openURL opens the specified URL in the default browser of the user. +// Source: https://stackoverflow.com/a/39324149/453290 +func openURL(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, url) + return exec.Command(cmd, args...).Start() +} + +// parseError converts error from the API into summary and detailed portions. +func parseError(err error) (string, string) { + headers := []string{errNoResponse, errLoginFailed, errTokenVerification} + summary := "Login error" + detail := "" + + re := regexp.MustCompile(`(?s)Errors:.*\* *(.*)`) + + errorParts := re.FindStringSubmatch(err.Error()) + switch len(errorParts) { + case 0: + summary = "" + case 1: + detail = errorParts[0] + case 2: + for _, h := range headers { + if strings.HasPrefix(errorParts[1], h) { + summary = h + detail = strings.TrimSpace(errorParts[1][len(h):]) + break + } + } + if detail == "" { + detail = errorParts[1] + } + } + + return summary, detail + +} + +func errorHTML(summary, detail string) string { + const html = ` + + + + + + + +HashiCorp Vault + + + +
+
+ +
+ + + +
+
+ %s +
+

+ %s +

+
+
+
+ +

Not sure how to get started?

+

+ Check out beginner and advanced guides on HashiCorp Vault at the HashiCorp Learn site or read more in the official documentation. +

+ + + + + + + Get started with Vault + + + + + + + + + + View the official Vault documentation + +
+
+ + + +` + return fmt.Sprintf(html, summary, detail) +} + +// Help method for OIDC cli +func (h *CLIHandler) Help() string { + help := ` +Usage: vault login -method=oidc [CONFIG K=V...] + + The OIDC auth method allows users to authenticate using an OIDC provider. + The provider must be configured as part of a role by the operator. + + Authenticate using role "engineering": + + $ vault login -method=oidc role=engineering + Complete the login via your OIDC provider. Launching browser to: + + https://accounts.google.com/o/oauth2/v2/... + + The default browser will be opened for the user to complete the login. Alternatively, + the user may visit the provided URL directly. + +Configuration: + + role= + Vault role of type "OIDC" to use for authentication. + + port= + Optional localhost port to use for OIDC callback (default: 8300). +` + + return strings.TrimSpace(help) +} + +const successHTML = ` + + + + + + Vault Authentication Succeeded + + + +
+
+ +
+ +
+
+ Signed in via your OIDC provider +
+

+ You can now close this window and start using Vault. +

+
+
+
+

Not sure how to get started?

+

+ Check out beginner and advanced guides on HashiCorp Vault at the HashiCorp Learn site or read more in the official documentation. +

+ + + + + + + Get started with Vault + + + + + + + + View the official Vault documentation + +
+
+ + +` diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_config.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_config.go index 5019cbd603de..3fc200df4cc2 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_config.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_config.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "crypto/x509" "errors" + "fmt" "net/http" "context" @@ -29,19 +30,44 @@ func pathConfig(b *jwtAuthBackend) *framework.Path { Type: framework.TypeString, Description: "The CA certificate or chain of certificates, in PEM format, to use to validate conections to the OIDC Discovery URL. If not set, system certificates are used.", }, + "oidc_client_id": { + Type: framework.TypeString, + Description: "The OAuth Client ID configured with your OIDC provider.", + }, + "oidc_client_secret": { + Type: framework.TypeString, + Description: "The OAuth Client Secret configured with your OIDC provider.", + DisplaySensitive: true, + }, + "default_role": { + Type: framework.TypeString, + Description: "The default role to use if none is provided during login. If not set, a role is required during login.", + }, "jwt_validation_pubkeys": { Type: framework.TypeCommaStringSlice, Description: `A list of PEM-encoded public keys to use to authenticate signatures locally. Cannot be used with "oidc_discovery_url".`, }, + "jwt_supported_algs": { + Type: framework.TypeCommaStringSlice, + Description: `A list of supported signing algorithms. Defaults to RS256.`, + }, "bound_issuer": { Type: framework.TypeString, Description: "The value against which to match the 'iss' claim in a JWT. Optional.", }, }, - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathConfigRead, - logical.UpdateOperation: b.pathConfigWrite, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: b.pathConfigRead, + Summary: "Read the current JWT authentication backend configuration.", + }, + + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.pathConfigWrite, + Summary: "Configure the JWT authentication backend.", + Description: confHelpDesc, + }, }, HelpSynopsis: confHelpSyn, @@ -98,7 +124,11 @@ func (b *jwtAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reques Data: map[string]interface{}{ "oidc_discovery_url": config.OIDCDiscoveryURL, "oidc_discovery_ca_pem": config.OIDCDiscoveryCAPEM, + "oidc_client_id": config.OIDCClientID, + "oidc_client_secret": config.OIDCClientSecret, + "default_role": config.DefaultRole, "jwt_validation_pubkeys": config.JWTValidationPubKeys, + "jwt_supported_algs": config.JWTSupportedAlgs, "bound_issuer": config.BoundIssuer, }, } @@ -110,7 +140,11 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque config := &jwtConfig{ OIDCDiscoveryURL: d.Get("oidc_discovery_url").(string), OIDCDiscoveryCAPEM: d.Get("oidc_discovery_ca_pem").(string), + OIDCClientID: d.Get("oidc_client_id").(string), + OIDCClientSecret: d.Get("oidc_client_secret").(string), + DefaultRole: d.Get("default_role").(string), JWTValidationPubKeys: d.Get("jwt_validation_pubkeys").([]string), + JWTSupportedAlgs: d.Get("jwt_supported_algs").([]string), BoundIssuer: d.Get("bound_issuer").(string), } @@ -120,12 +154,19 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque config.OIDCDiscoveryURL != "" && len(config.JWTValidationPubKeys) != 0: return logical.ErrorResponse("exactly one of 'oidc_discovery_url' and 'jwt_validation_pubkeys' must be set"), nil + case config.OIDCClientID != "" && config.OIDCClientSecret == "", + config.OIDCClientID == "" && config.OIDCClientSecret != "": + return logical.ErrorResponse("both 'oidc_client_id' and 'oidc_client_secret' must be set for OIDC"), nil + case config.OIDCDiscoveryURL != "": _, err := b.createProvider(config) if err != nil { return logical.ErrorResponse(errwrap.Wrapf("error checking discovery URL: {{err}}", err).Error()), nil } + case config.OIDCClientID != "" && config.OIDCDiscoveryURL == "": + return logical.ErrorResponse("'oidc_discovery_url' must be set for OIDC"), nil + case len(config.JWTValidationPubKeys) != 0: for _, v := range config.JWTValidationPubKeys { if _, err := certutil.ParsePublicKeyPEM([]byte(v)); err != nil { @@ -137,6 +178,14 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque return nil, errors.New("unknown condition") } + for _, a := range config.JWTSupportedAlgs { + switch a { + case oidc.RS256, oidc.RS384, oidc.RS512, oidc.ES256, oidc.ES384, oidc.ES512, oidc.PS256, oidc.PS384, oidc.PS512: + default: + return logical.ErrorResponse(fmt.Sprintf("Invalid supported algorithm: %s", a)), nil + } + } + entry, err := logical.StorageEntryJSON(configPath, config) if err != nil { return nil, err @@ -181,8 +230,12 @@ func (b *jwtAuthBackend) createProvider(config *jwtConfig) (*oidc.Provider, erro type jwtConfig struct { OIDCDiscoveryURL string `json:"oidc_discovery_url"` OIDCDiscoveryCAPEM string `json:"oidc_discovery_ca_pem"` + OIDCClientID string `json:"oidc_client_id"` + OIDCClientSecret string `json:"oidc_client_secret"` JWTValidationPubKeys []string `json:"jwt_validation_pubkeys"` + JWTSupportedAlgs []string `json:"jwt_supported_algs"` BoundIssuer string `json:"bound_issuer"` + DefaultRole string `json:"default_role"` ParsedJWTPubKeys []interface{} `json:"-"` } diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_login.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_login.go index 868fe9d094e4..70d7e943ad30 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_login.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_login.go @@ -29,9 +29,14 @@ func pathLogin(b *jwtAuthBackend) *framework.Path { }, }, - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.pathLogin, - logical.AliasLookaheadOperation: b.pathLogin, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.pathLogin, + Summary: pathLoginHelpSyn, + }, + logical.AliasLookaheadOperation: &framework.PathOperation{ + Callback: b.pathLogin, + }, }, HelpSynopsis: pathLoginHelpSyn, @@ -40,13 +45,19 @@ func pathLogin(b *jwtAuthBackend) *framework.Path { } func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - token := d.Get("jwt").(string) - if len(token) == 0 { - return logical.ErrorResponse("missing token"), nil + config, err := b.config(ctx, req.Storage) + if err != nil { + return nil, err + } + if config == nil { + return logical.ErrorResponse("could not load configuration"), nil } roleName := d.Get("role").(string) - if len(roleName) == 0 { + if roleName == "" { + roleName = config.DefaultRole + } + if roleName == "" { return logical.ErrorResponse("missing role"), nil } @@ -55,19 +66,16 @@ func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d return nil, err } if role == nil { - return logical.ErrorResponse("role could not be found"), nil + return logical.ErrorResponse("role %q could not be found", roleName), nil } - if req.Connection != nil && !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, role.BoundCIDRs) { - return logical.ErrorResponse("request originated from invalid CIDR"), nil + token := d.Get("jwt").(string) + if len(token) == 0 { + return logical.ErrorResponse("missing token"), nil } - config, err := b.config(ctx, req.Storage) - if err != nil { - return nil, err - } - if config == nil { - return logical.ErrorResponse("could not load configuration"), nil + if req.Connection != nil && !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, role.BoundCIDRs) { + return logical.ErrorResponse("request originated from invalid CIDR"), nil } // Here is where things diverge. If it is using OIDC Discovery, validate @@ -130,118 +138,37 @@ func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d } case config.OIDCDiscoveryURL != "": - provider, err := b.getProvider(ctx, config) + allClaims, err = b.verifyToken(ctx, config, role, token) if err != nil { - return nil, errwrap.Wrapf("error getting provider for login operation: {{err}}", err) - } - - verifier := provider.Verifier(&oidc.Config{ - SkipClientIDCheck: true, - }) - - idToken, err := verifier.Verify(ctx, token) - if err != nil { - return logical.ErrorResponse(errwrap.Wrapf("error validating signature: {{err}}", err).Error()), nil - } - - if err := idToken.Claims(&allClaims); err != nil { - return logical.ErrorResponse(errwrap.Wrapf("unable to successfully parse all claims from token: {{err}}", err).Error()), nil - } - - if role.BoundSubject != "" && role.BoundSubject != idToken.Subject { - return logical.ErrorResponse("sub claim does not match bound subject"), nil - } - if len(role.BoundAudiences) != 0 { - var found bool - for _, v := range role.BoundAudiences { - if strutil.StrListContains(idToken.Audience, v) { - found = true - break - } - } - if !found { - return logical.ErrorResponse("aud claim does not match any bound audience"), nil - } + return logical.ErrorResponse(err.Error()), nil } default: return nil, errors.New("unhandled case during login") } - userClaimRaw, ok := allClaims[role.UserClaim] - if !ok { - return logical.ErrorResponse(fmt.Sprintf("%q claim not found in token", role.UserClaim)), nil - } - userName, ok := userClaimRaw.(string) - if !ok { - return logical.ErrorResponse(fmt.Sprintf("%q claim could not be converted to string", role.UserClaim)), nil + alias, groupAliases, err := b.createIdentity(allClaims, role) + if err != nil { + return logical.ErrorResponse(err.Error()), nil } - var groupAliases []*logical.Alias - if role.GroupsClaim != "" { - mapPath, err := parseClaimWithDelimiters(role.GroupsClaim, role.GroupsClaimDelimiterPattern) - if err != nil { - return logical.ErrorResponse(errwrap.Wrapf("error parsing delimiters for groups claim: {{err}}", err).Error()), nil - } - if len(mapPath) < 1 { - return logical.ErrorResponse("unexpected length 0 of claims path after parsing groups claim against delimiters"), nil - } - var claimKey string - claimMap := allClaims - for i, key := range mapPath { - if i == len(mapPath)-1 { - claimKey = key - break - } - nextMapRaw, ok := claimMap[key] - if !ok { - return logical.ErrorResponse(fmt.Sprintf("map via key %q not found while navigating group claim delimiters", key)), nil - } - nextMap, ok := nextMapRaw.(map[string]interface{}) - if !ok { - return logical.ErrorResponse(fmt.Sprintf("key %q does not reference a map while navigating group claim delimiters", key)), nil - } - claimMap = nextMap - } - - groupsClaimRaw, ok := claimMap[claimKey] - if !ok { - return logical.ErrorResponse(fmt.Sprintf("%q claim not found in token", role.GroupsClaim)), nil - } - groups, ok := groupsClaimRaw.([]interface{}) - if !ok { - return logical.ErrorResponse(fmt.Sprintf("%q claim could not be converted to string list", role.GroupsClaim)), nil - } - for _, groupRaw := range groups { - group, ok := groupRaw.(string) - if !ok { - return logical.ErrorResponse(fmt.Sprintf("value %v in groups claim could not be parsed as string", groupRaw)), nil - } - if group == "" { - continue - } - groupAliases = append(groupAliases, &logical.Alias{ - Name: group, - }) - } + tokenMetadata := map[string]string{"role": roleName} + for k, v := range alias.Metadata { + tokenMetadata[k] = v } resp := &logical.Response{ Auth: &logical.Auth{ - Policies: role.Policies, - DisplayName: userName, - Period: role.Period, - NumUses: role.NumUses, - Alias: &logical.Alias{ - Name: userName, - }, + Policies: role.Policies, + DisplayName: alias.Name, + Period: role.Period, + NumUses: role.NumUses, + Alias: alias, GroupAliases: groupAliases, InternalData: map[string]interface{}{ "role": roleName, }, - Metadata: map[string]string{ - "role": roleName, - }, + Metadata: tokenMetadata, LeaseOptions: logical.LeaseOptions{ Renewable: true, TTL: role.TTL, @@ -276,6 +203,120 @@ func (b *jwtAuthBackend) pathLoginRenew(ctx context.Context, req *logical.Reques return resp, nil } +func (b *jwtAuthBackend) verifyToken(ctx context.Context, config *jwtConfig, role *jwtRole, rawToken string) (map[string]interface{}, error) { + allClaims := make(map[string]interface{}) + + provider, err := b.getProvider(ctx, config) + if err != nil { + return nil, errwrap.Wrapf("error getting provider for login operation: {{err}}", err) + } + + oidcConfig := &oidc.Config{ + SupportedSigningAlgs: config.JWTSupportedAlgs, + } + + if role.RoleType == "oidc" { + oidcConfig.ClientID = config.OIDCClientID + } else { + oidcConfig.SkipClientIDCheck = true + } + verifier := provider.Verifier(oidcConfig) + + idToken, err := verifier.Verify(ctx, rawToken) + if err != nil { + return nil, errwrap.Wrapf("error validating signature: {{err}}", err) + } + + if err := idToken.Claims(&allClaims); err != nil { + return nil, errwrap.Wrapf("unable to successfully parse all claims from token: {{err}}", err) + } + + if role.BoundSubject != "" && role.BoundSubject != idToken.Subject { + return nil, errors.New("sub claim does not match bound subject") + } + if len(role.BoundAudiences) > 0 { + var found bool + for _, v := range role.BoundAudiences { + if strutil.StrListContains(idToken.Audience, v) { + found = true + break + } + } + if !found { + return nil, errors.New("aud claim does not match any bound audience") + } + } + + if len(role.BoundClaims) > 0 { + for claim, expValue := range role.BoundClaims { + actValue := getClaim(b.Logger(), allClaims, claim) + if actValue == nil { + return nil, fmt.Errorf("claim is missing: %s", claim) + } + + if expValue != actValue { + return nil, fmt.Errorf("claim '%s' does not match associated bound claim", claim) + } + } + } + + return allClaims, nil +} + +// createIdentity creates an alias and set of groups aliass based on the role +// definition and received claims. +func (b *jwtAuthBackend) createIdentity(allClaims map[string]interface{}, role *jwtRole) (*logical.Alias, []*logical.Alias, error) { + userClaimRaw, ok := allClaims[role.UserClaim] + if !ok { + return nil, nil, fmt.Errorf("claim %q not found in token", role.UserClaim) + } + userName, ok := userClaimRaw.(string) + if !ok { + return nil, nil, fmt.Errorf("claim %q could not be converted to string", role.UserClaim) + } + + metadata, err := extractMetadata(b.Logger(), allClaims, role.ClaimMappings) + if err != nil { + return nil, nil, err + } + + alias := &logical.Alias{ + Name: userName, + Metadata: metadata, + } + + var groupAliases []*logical.Alias + + if role.GroupsClaim == "" { + return alias, groupAliases, nil + } + + groupsClaimRaw := getClaim(b.Logger(), allClaims, role.GroupsClaim) + + if groupsClaimRaw == nil { + return nil, nil, fmt.Errorf("%q claim not found in token", role.GroupsClaim) + } + groups, ok := groupsClaimRaw.([]interface{}) + + if !ok { + return nil, nil, fmt.Errorf("%q claim could not be converted to string list", role.GroupsClaim) + } + for _, groupRaw := range groups { + group, ok := groupRaw.(string) + if !ok { + return nil, nil, fmt.Errorf("value %v in groups claim could not be parsed as string", groupRaw) + } + if group == "" { + continue + } + groupAliases = append(groupAliases, &logical.Alias{ + Name: group, + }) + } + + return alias, groupAliases, nil +} + const ( pathLoginHelpSyn = ` Authenticates to Vault using a JWT (or OIDC) token. diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_oidc.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_oidc.go new file mode 100644 index 000000000000..b437ed6e5474 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_oidc.go @@ -0,0 +1,303 @@ +package jwtauth + +import ( + "context" + "fmt" + "strings" + "time" + + oidc "github.com/coreos/go-oidc" + "github.com/hashicorp/errwrap" + uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/strutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "golang.org/x/oauth2" +) + +var oidcStateTimeout = 10 * time.Minute + +// OIDC error prefixes. This are searched for specifically by the UI, so any +// changes to them must be aligned with a UI change. +const errLoginFailed = "Vault login failed." +const errNoResponse = "No response from provider." +const errTokenVerification = "Token verification failed." + +// oidcState is created when an authURL is requested. The state identifier is +// passed throughout the OAuth process. +type oidcState struct { + rolename string + nonce string + redirectURI string +} + +func pathOIDC(b *jwtAuthBackend) []*framework.Path { + return []*framework.Path{ + { + Pattern: `oidc/callback`, + Fields: map[string]*framework.FieldSchema{ + "state": { + Type: framework.TypeString, + }, + "code": { + Type: framework.TypeString, + }, + }, + + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: b.pathCallback, + Summary: "Callback endpoint to complete an OIDC login.", + }, + }, + }, + { + Pattern: `oidc/auth_url`, + Fields: map[string]*framework.FieldSchema{ + "role": { + Type: framework.TypeLowerCaseString, + Description: "The role to issue an OIDC authorization URL against.", + }, + "redirect_uri": { + Type: framework.TypeString, + Description: "The OAuth redirect_uri to use in the authorization URL.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.authURL, + Summary: "Request an authorization URL to start an OIDC login flow.", + }, + }, + }, + } +} + +func (b *jwtAuthBackend) pathCallback(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + state := b.verifyState(d.Get("state").(string)) + if state == nil { + return logical.ErrorResponse(errLoginFailed + " Expired or missing OAuth state."), nil + } + + roleName := state.rolename + role, err := b.role(ctx, req.Storage, roleName) + if err != nil { + return nil, err + } + if role == nil { + return logical.ErrorResponse(errLoginFailed + " Role could not be found"), nil + } + + config, err := b.config(ctx, req.Storage) + if err != nil { + return nil, err + } + if config == nil { + return logical.ErrorResponse(errLoginFailed + " Could not load configuration"), nil + } + + provider, err := b.getProvider(ctx, config) + if err != nil { + return nil, errwrap.Wrapf(errLoginFailed+" Error getting provider for login operation: {{err}}", err) + } + + var oauth2Config = oauth2.Config{ + ClientID: config.OIDCClientID, + ClientSecret: config.OIDCClientSecret, + RedirectURL: state.redirectURI, + Endpoint: provider.Endpoint(), + Scopes: []string{oidc.ScopeOpenID}, + } + + code := d.Get("code").(string) + if code == "" { + return logical.ErrorResponse(errLoginFailed + " OAuth code parameter not provided"), nil + } + + oauth2Token, err := oauth2Config.Exchange(ctx, code) + if err != nil { + return logical.ErrorResponse(errLoginFailed+" Error exchanging oidc code: %q.", err.Error()), nil + } + + // Extract the ID Token from OAuth2 token. + rawToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + return logical.ErrorResponse(errTokenVerification + " No id_token found in response."), nil + } + + // Parse and verify ID Token payload. + allClaims, err := b.verifyToken(ctx, config, role, rawToken) + if err != nil { + return logical.ErrorResponse("%s %s", errTokenVerification, err.Error()), nil + } + + // Attempt to fetch information from the /userinfo endpoint and merge it with + // the existing claims data. A failure to fetch additional information from this + // endpoint will not invalidate the authorization flow. + if userinfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token)); err == nil { + _ = userinfo.Claims(&allClaims) + } else { + logFunc := b.Logger().Warn + if strings.Contains(err.Error(), "user info endpoint is not supported") { + logFunc = b.Logger().Info + } + logFunc("error reading /userinfo endpoint", "error", err) + } + + if allClaims["nonce"] != state.nonce { + return logical.ErrorResponse(errTokenVerification + " Invalid ID token nonce."), nil + } + delete(allClaims, "nonce") + + alias, groupAliases, err := b.createIdentity(allClaims, role) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + tokenMetadata := map[string]string{"role": roleName} + for k, v := range alias.Metadata { + tokenMetadata[k] = v + } + + resp := &logical.Response{ + Auth: &logical.Auth{ + Policies: role.Policies, + DisplayName: alias.Name, + Period: role.Period, + NumUses: role.NumUses, + Alias: alias, + GroupAliases: groupAliases, + InternalData: map[string]interface{}{ + "role": roleName, + }, + Metadata: tokenMetadata, + LeaseOptions: logical.LeaseOptions{ + Renewable: true, + TTL: role.TTL, + MaxTTL: role.MaxTTL, + }, + BoundCIDRs: role.BoundCIDRs, + }, + } + + return resp, nil +} + +// authURL returns a URL used for redirection to receive an authorization code. +// This path requires a role name, or that a default_role has been configured. +// Because this endpoint is unauthenticated, the response to invalid or non-OIDC +// roles is intentionally non-descriptive and will simply be an empty string. +func (b *jwtAuthBackend) authURL(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + logger := b.Logger() + + // default response for most error/invalid conditions + resp := &logical.Response{ + Data: map[string]interface{}{ + "auth_url": "", + }, + } + + config, err := b.config(ctx, req.Storage) + if err != nil { + logger.Warn("error loading configuration", "error", err) + return resp, nil + } + + if config == nil { + logger.Warn("nil configuration") + return resp, nil + } + + roleName := d.Get("role").(string) + if roleName == "" { + roleName = config.DefaultRole + if roleName == "" { + return logical.ErrorResponse("missing role"), nil + } + } + + redirectURI := d.Get("redirect_uri").(string) + if redirectURI == "" { + return logical.ErrorResponse("missing redirect_uri"), nil + } + + role, err := b.role(ctx, req.Storage, roleName) + if err != nil { + logger.Warn("error loading role", "error", err) + return resp, nil + } + + if role == nil || role.RoleType != "oidc" { + logger.Warn("invalid role type", "role type", role) + return resp, nil + } + + if !strutil.StrListContains(role.AllowedRedirectURIs, redirectURI) { + logger.Warn("unauthorized redirect_uri", "redirect_uri", redirectURI) + return resp, nil + } + + provider, err := b.getProvider(ctx, config) + if err != nil { + logger.Warn("error getting provider for login operation", "error", err) + return resp, nil + } + + // "openid" is a required scope for OpenID Connect flows + scopes := append([]string{oidc.ScopeOpenID}, role.OIDCScopes...) + + // Configure an OpenID Connect aware OAuth2 client + oauth2Config := oauth2.Config{ + ClientID: config.OIDCClientID, + ClientSecret: config.OIDCClientSecret, + RedirectURL: redirectURI, + Endpoint: provider.Endpoint(), + Scopes: scopes, + } + + stateID, nonce, err := b.createState(roleName, redirectURI) + if err != nil { + logger.Warn("error generating OAuth state", "error", err) + return resp, nil + } + + resp.Data["auth_url"] = oauth2Config.AuthCodeURL(stateID, oidc.Nonce(nonce)) + + return resp, nil +} + +// createState make an expiring state object, associated with a random state ID +// that is passed throughout the OAuth process. A nonce is also included in the +// auth process, and for simplicity will be identical in length/format as the state ID. +func (b *jwtAuthBackend) createState(rolename, redirectURI string) (string, string, error) { + // Get enough bytes for 2 160-bit IDs (per rfc6749#section-10.10) + bytes, err := uuid.GenerateRandomBytes(2 * 20) + if err != nil { + return "", "", err + } + + stateID := fmt.Sprintf("%x", bytes[:20]) + nonce := fmt.Sprintf("%x", bytes[20:]) + + b.oidcStates.SetDefault(stateID, &oidcState{ + rolename: rolename, + nonce: nonce, + redirectURI: redirectURI, + }) + + return stateID, nonce, nil +} + +// verifyState tests whether the provided state ID is valid and returns the +// associated state object if so. A nil state is returned if the ID is not found +// or expired. The state should only ever be retrieved once and is deleted as +// part of this request. +func (b *jwtAuthBackend) verifyState(stateID string) *oidcState { + defer b.oidcStates.Delete(stateID) + + if stateRaw, ok := b.oidcStates.Get(stateID); ok { + return stateRaw.(*oidcState) + } + + return nil +} diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_role.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_role.go index 9ca1fec415f8..6eb701601c4e 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_role.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_role.go @@ -7,19 +7,25 @@ import ( "strings" "time" - "github.com/hashicorp/errwrap" sockaddr "github.com/hashicorp/go-sockaddr" "github.com/hashicorp/vault/helper/parseutil" "github.com/hashicorp/vault/helper/policyutil" + "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) +var reservedMetadata = []string{"role"} + func pathRoleList(b *jwtAuthBackend) *framework.Path { return &framework.Path{ Pattern: "role/?", - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ListOperation: b.pathRoleList, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: b.pathRoleList, + Summary: strings.TrimSpace(roleHelp["role-list"][0]), + Description: strings.TrimSpace(roleHelp["role-list"][1]), + }, }, HelpSynopsis: strings.TrimSpace(roleHelp["role-list"][0]), HelpDescription: strings.TrimSpace(roleHelp["role-list"][1]), @@ -35,6 +41,10 @@ func pathRole(b *jwtAuthBackend) *framework.Path { Type: framework.TypeLowerCaseString, Description: "Name of the role.", }, + "role_type": { + Type: framework.TypeString, + Description: "Type of the role, either 'jwt' or 'oidc'.", + }, "policies": { Type: framework.TypeCommaStringSlice, Description: "List of policies on the role.", @@ -68,6 +78,14 @@ TTL will be set to the value of this parameter.`, Type: framework.TypeCommaStringSlice, Description: `Comma-separated list of 'aud' claims that are valid for login; any match is sufficient`, }, + "bound_claims": { + Type: framework.TypeMap, + Description: `Map of claims/values which must match for login`, + }, + "claim_mappings": { + Type: framework.TypeKVPairs, + Description: `Mappings of claims (key) that will be copied to a metadata field (value)`, + }, "user_claim": { Type: framework.TypeString, Description: `The claim to use for the Identity entity alias name`, @@ -76,22 +94,43 @@ TTL will be set to the value of this parameter.`, Type: framework.TypeString, Description: `The claim to use for the Identity group alias names`, }, - "groups_claim_delimiter_pattern": { - Type: framework.TypeString, - Description: `A pattern of delimiters used to allow the groups_claim to live outside of the top-level JWT structure. For instance, a "groups_claim" of "meta/user.name/groups" with this field set to "//" will expect nested structures named "meta", "user.name", and "groups". If this field was set to "/./" the groups information would expect to be via nested structures of "meta", "user", "name", and "groups".`, - }, "bound_cidrs": { Type: framework.TypeCommaStringSlice, Description: `Comma-separated list of IP CIDRS that are allowed to authenticate against this role`, }, + "oidc_scopes": { + Type: framework.TypeCommaStringSlice, + Description: `Comma-separated list of OIDC scopes`, + }, + "allowed_redirect_uris": { + Type: framework.TypeCommaStringSlice, + Description: `Comma-separated list of allowed values for redirect_uri`, + }, }, ExistenceCheck: b.pathRoleExistenceCheck, - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.CreateOperation: b.pathRoleCreateUpdate, - logical.UpdateOperation: b.pathRoleCreateUpdate, - logical.ReadOperation: b.pathRoleRead, - logical.DeleteOperation: b.pathRoleDelete, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: b.pathRoleRead, + Summary: "Read an existing role.", + }, + + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.pathRoleCreateUpdate, + Summary: strings.TrimSpace(roleHelp["role"][0]), + Description: strings.TrimSpace(roleHelp["role"][1]), + }, + + logical.CreateOperation: &framework.PathOperation{ + Callback: b.pathRoleCreateUpdate, + Summary: strings.TrimSpace(roleHelp["role"][0]), + Description: strings.TrimSpace(roleHelp["role"][1]), + }, + + logical.DeleteOperation: &framework.PathOperation{ + Callback: b.pathRoleDelete, + Summary: "Delete an existing role.", + }, }, HelpSynopsis: strings.TrimSpace(roleHelp["role"][0]), HelpDescription: strings.TrimSpace(roleHelp["role"][1]), @@ -99,6 +138,8 @@ authenticate against this role`, } type jwtRole struct { + RoleType string `json:"role_type"` + // Policies that are to be required by the token to access this role Policies []string `json:"policies"` @@ -119,12 +160,15 @@ type jwtRole struct { Period time.Duration `json:"period"` // Role binding properties - BoundAudiences []string `json:"bound_audiences"` - BoundSubject string `json:"bound_subject"` - BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"` - UserClaim string `json:"user_claim"` - GroupsClaim string `json:"groups_claim"` - GroupsClaimDelimiterPattern string `json:"groups_claim_delimiter_pattern"` + BoundAudiences []string `json:"bound_audiences"` + BoundSubject string `json:"bound_subject"` + BoundClaims map[string]interface{} `json:"bound_claims"` + ClaimMappings map[string]string `json:"claim_mappings"` + BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"` + UserClaim string `json:"user_claim"` + GroupsClaim string `json:"groups_claim"` + OIDCScopes []string `json:"oidc_scopes"` + AllowedRedirectURIs []string `json:"allowed_redirect_uris"` } // role takes a storage backend and the name and returns the role's storage @@ -182,17 +226,20 @@ func (b *jwtAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request, // Create a map of data to be returned resp := &logical.Response{ Data: map[string]interface{}{ - "policies": role.Policies, - "num_uses": role.NumUses, - "period": int64(role.Period.Seconds()), - "ttl": int64(role.TTL.Seconds()), - "max_ttl": int64(role.MaxTTL.Seconds()), - "bound_audiences": role.BoundAudiences, - "bound_subject": role.BoundSubject, - "bound_cidrs": role.BoundCIDRs, - "user_claim": role.UserClaim, - "groups_claim": role.GroupsClaim, - "groups_claim_delimiter_pattern": role.GroupsClaimDelimiterPattern, + "role_type": role.RoleType, + "policies": role.Policies, + "num_uses": role.NumUses, + "period": int64(role.Period.Seconds()), + "ttl": int64(role.TTL.Seconds()), + "max_ttl": int64(role.MaxTTL.Seconds()), + "bound_audiences": role.BoundAudiences, + "bound_subject": role.BoundSubject, + "bound_cidrs": role.BoundCIDRs, + "bound_claims": role.BoundClaims, + "claim_mappings": role.ClaimMappings, + "user_claim": role.UserClaim, + "groups_claim": role.GroupsClaim, + "allowed_redirect_uris": role.AllowedRedirectURIs, }, } @@ -236,6 +283,15 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical. role = new(jwtRole) } + roleType := data.Get("role_type").(string) + if roleType == "" { + roleType = "jwt" + } + if roleType != "jwt" && roleType != "oidc" { + return logical.ErrorResponse("invalid 'role_type': %s", roleType), nil + } + role.RoleType = roleType + if policiesRaw, ok := data.GetOk("policies"); ok { role.Policies = policyutil.ParsePolicies(policiesRaw) } @@ -287,6 +343,29 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical. role.BoundCIDRs = parsedCIDRs } + if boundClaimsRaw, ok := data.GetOk("bound_claims"); ok { + role.BoundClaims = boundClaimsRaw.(map[string]interface{}) + } + + if claimMappingsRaw, ok := data.GetOk("claim_mappings"); ok { + claimMappings := claimMappingsRaw.(map[string]string) + + // sanity check mappings for duplicates and collision with reserved names + targets := make(map[string]bool) + for _, metadataKey := range claimMappings { + if strutil.StrListContains(reservedMetadata, metadataKey) { + return logical.ErrorResponse("metadata key '%s' is reserved and may not be a mapping destination", metadataKey), nil + } + + if targets[metadataKey] { + return logical.ErrorResponse("multiple keys are mapped to metadata key '%s'", metadataKey), nil + } + targets[metadataKey] = true + } + + role.ClaimMappings = claimMappings + } + if userClaim, ok := data.GetOk("user_claim"); ok { role.UserClaim = userClaim.(string) } @@ -298,21 +377,24 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical. role.GroupsClaim = groupsClaim.(string) } - if groupsClaimDelimiterPattern, ok := data.GetOk("groups_claim_delimiter_pattern"); ok { - role.GroupsClaimDelimiterPattern = groupsClaimDelimiterPattern.(string) + if oidcScopes, ok := data.GetOk("oidc_scopes"); ok { + role.OIDCScopes = oidcScopes.([]string) } - // Validate claim/delims - if role.GroupsClaim != "" { - if _, err := parseClaimWithDelimiters(role.GroupsClaim, role.GroupsClaimDelimiterPattern); err != nil { - return logical.ErrorResponse(errwrap.Wrapf("error validating delimiters for groups claim: {{err}}", err).Error()), nil - } + allowedRedirectURIs := data.Get("allowed_redirect_uris").([]string) + if roleType == "oidc" && len(allowedRedirectURIs) == 0 { + return logical.ErrorResponse("'allowed_redirect_uris' must be set"), nil } + role.AllowedRedirectURIs = allowedRedirectURIs - if len(role.BoundAudiences) == 0 && - len(role.BoundCIDRs) == 0 && - role.BoundSubject == "" { - return logical.ErrorResponse("must have at least one bound constraint when creating/updating a role"), nil + // OIDC verifcation will enforce that the audience match the configured client_id. + // For other methods, require at least one bound constraint. + if roleType != "oidc" { + if len(role.BoundAudiences) == 0 && + len(role.BoundCIDRs) == 0 && + role.BoundSubject == "" { + return logical.ErrorResponse("must have at least one bound constraint when creating/updating a role"), nil + } } // Check that the TTL value provided is less than the MaxTTL. @@ -340,32 +422,6 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical. return resp, nil } -// parseClaimWithDelimiters parses a given claim string and ensures that we can -// separate it out into a "map path" -func parseClaimWithDelimiters(claim, delimiters string) ([]string, error) { - if delimiters == "" { - return []string{claim}, nil - } - var ret []string - for _, runeVal := range delimiters { - idx := strings.IndexRune(claim, runeVal) - switch idx { - case -1: - return nil, fmt.Errorf("could not find instance of %q delimiter in claim", string(runeVal)) - case 0: - return nil, fmt.Errorf("instance of %q delimiter in claim is at beginning of claim string", string(runeVal)) - case len(claim) - 1: - return nil, fmt.Errorf("instance of %q delimiter in claim is at end of claim string", string(runeVal)) - default: - ret = append(ret, claim[:idx]) - claim = claim[idx+1:] - } - } - ret = append(ret, claim) - - return ret, nil -} - // roleStorageEntry stores all the options that are set on an role var roleHelp = map[string][2]string{ "role-list": { diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_ui.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_ui.go new file mode 100644 index 000000000000..7930f9ef8be5 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_ui.go @@ -0,0 +1,37 @@ +// A throwaway file for super simple testing via a UI +package jwtauth + +import ( + "context" + "io/ioutil" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathUI(b *jwtAuthBackend) *framework.Path { + return &framework.Path{ + Pattern: `ui$`, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathUI, + }, + } +} + +func (b *jwtAuthBackend) pathUI(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + data, err := ioutil.ReadFile("test_ui.html") + if err != nil { + panic(err) + } + + resp := &logical.Response{ + Data: map[string]interface{}{ + logical.HTTPStatusCode: 200, + logical.HTTPRawBody: string(data), + logical.HTTPContentType: "text/html", + }, + } + + return resp, nil +} diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/test_ui.html b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/test_ui.html new file mode 100644 index 000000000000..dd6502ed73f1 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/test_ui.html @@ -0,0 +1,37 @@ + + + +Role:
+
+ + + + + + diff --git a/vendor/github.com/mitchellh/pointerstructure/README.md b/vendor/github.com/mitchellh/pointerstructure/README.md new file mode 100644 index 000000000000..13e3358557f3 --- /dev/null +++ b/vendor/github.com/mitchellh/pointerstructure/README.md @@ -0,0 +1,74 @@ +# pointerstructure [![GoDoc](https://godoc.org/github.com/mitchellh/pointerstructure?status.svg)](https://godoc.org/github.com/mitchellh/pointerstructure) + +pointerstructure is a Go library for identifying a specific value within +any Go structure using a string syntax. + +pointerstructure is based on +[JSON Pointer (RFC 6901)](https://tools.ietf.org/html/rfc6901), but +reimplemented for Go. + +The goal of pointerstructure is to provide a single, well-known format +for addressing a specific value. This can be useful for user provided +input on structures, diffs of structures, etc. + +## Features + + * Get the value for an address + + * Set the value for an address within an existing structure + + * Delete the value at an address + + * Sorting a list of addresses + +## Installation + +Standard `go get`: + +``` +$ go get github.com/mitchellh/pointerstructure +``` + +## Usage & Example + +For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/pointerstructure). + +A quick code example is shown below: + +```go +complex := map[string]interface{}{ + "alice": 42, + "bob": []interface{}{ + map[string]interface{}{ + "name": "Bob", + }, + }, +} + +value, err := pointerstructure.Get(complex, "/bob/0/name") +if err != nil { + panic(err) +} + +fmt.Printf("%s", value) +// Output: +// Bob +``` + +Continuing the example above, you can also set values: + +```go +value, err = pointerstructure.Set(complex, "/bob/0/name", "Alice") +if err != nil { + panic(err) +} + +value, err = pointerstructure.Get(complex, "/bob/0/name") +if err != nil { + panic(err) +} + +fmt.Printf("%s", value) +// Output: +// Alice +``` diff --git a/vendor/github.com/mitchellh/pointerstructure/delete.go b/vendor/github.com/mitchellh/pointerstructure/delete.go new file mode 100644 index 000000000000..5ed6b4bffc39 --- /dev/null +++ b/vendor/github.com/mitchellh/pointerstructure/delete.go @@ -0,0 +1,112 @@ +package pointerstructure + +import ( + "fmt" + "reflect" +) + +// Delete deletes the value specified by the pointer p in structure s. +// +// When deleting a slice index, all other elements will be shifted to +// the left. This is specified in RFC6902 (JSON Patch) and not RFC6901 since +// RFC6901 doesn't specify operations on pointers. If you don't want to +// shift elements, you should use Set to set the slice index to the zero value. +// +// The structures s must have non-zero values set up to this pointer. +// For example, if deleting "/bob/0/name", then "/bob/0" must be set already. +// +// The returned value is potentially a new value if this pointer represents +// the root document. Otherwise, the returned value will always be s. +func (p *Pointer) Delete(s interface{}) (interface{}, error) { + // if we represent the root doc, we've deleted everything + if len(p.Parts) == 0 { + return nil, nil + } + + // Save the original since this is going to be our return value + originalS := s + + // Get the parent value + var err error + s, err = p.Parent().Get(s) + if err != nil { + return nil, err + } + + // Map for lookup of getter to call for type + funcMap := map[reflect.Kind]deleteFunc{ + reflect.Array: p.deleteSlice, + reflect.Map: p.deleteMap, + reflect.Slice: p.deleteSlice, + } + + val := reflect.ValueOf(s) + for val.Kind() == reflect.Interface { + val = val.Elem() + } + + for val.Kind() == reflect.Ptr { + val = reflect.Indirect(val) + } + + f, ok := funcMap[val.Kind()] + if !ok { + return nil, fmt.Errorf("delete %s: invalid value kind: %s", p, val.Kind()) + } + + result, err := f(originalS, val) + if err != nil { + return nil, fmt.Errorf("delete %s: %s", p, err) + } + + return result, nil +} + +type deleteFunc func(interface{}, reflect.Value) (interface{}, error) + +func (p *Pointer) deleteMap(root interface{}, m reflect.Value) (interface{}, error) { + part := p.Parts[len(p.Parts)-1] + key, err := coerce(reflect.ValueOf(part), m.Type().Key()) + if err != nil { + return root, err + } + + // Delete the key + var elem reflect.Value + m.SetMapIndex(key, elem) + return root, nil +} + +func (p *Pointer) deleteSlice(root interface{}, s reflect.Value) (interface{}, error) { + // Coerce the key to an int + part := p.Parts[len(p.Parts)-1] + idxVal, err := coerce(reflect.ValueOf(part), reflect.TypeOf(42)) + if err != nil { + return root, err + } + idx := int(idxVal.Int()) + + // Verify we're within bounds + if idx < 0 || idx >= s.Len() { + return root, fmt.Errorf( + "index %d is out of range (length = %d)", idx, s.Len()) + } + + // Mimicing the following with reflection to do this: + // + // copy(a[i:], a[i+1:]) + // a[len(a)-1] = nil // or the zero value of T + // a = a[:len(a)-1] + + // copy(a[i:], a[i+1:]) + reflect.Copy(s.Slice(idx, s.Len()), s.Slice(idx+1, s.Len())) + + // a[len(a)-1] = nil // or the zero value of T + s.Index(s.Len() - 1).Set(reflect.Zero(s.Type().Elem())) + + // a = a[:len(a)-1] + s = s.Slice(0, s.Len()-1) + + // set the slice back on the parent + return p.Parent().Set(root, s.Interface()) +} diff --git a/vendor/github.com/mitchellh/pointerstructure/get.go b/vendor/github.com/mitchellh/pointerstructure/get.go new file mode 100644 index 000000000000..15137c1570ef --- /dev/null +++ b/vendor/github.com/mitchellh/pointerstructure/get.go @@ -0,0 +1,91 @@ +package pointerstructure + +import ( + "fmt" + "reflect" +) + +// Get reads the value out of the total value v. +func (p *Pointer) Get(v interface{}) (interface{}, error) { + // fast-path the empty address case to avoid reflect.ValueOf below + if len(p.Parts) == 0 { + return v, nil + } + + // Map for lookup of getter to call for type + funcMap := map[reflect.Kind]func(string, reflect.Value) (reflect.Value, error){ + reflect.Array: p.getSlice, + reflect.Map: p.getMap, + reflect.Slice: p.getSlice, + } + + currentVal := reflect.ValueOf(v) + for i, part := range p.Parts { + for currentVal.Kind() == reflect.Interface { + currentVal = currentVal.Elem() + } + + for currentVal.Kind() == reflect.Ptr { + currentVal = reflect.Indirect(currentVal) + } + + f, ok := funcMap[currentVal.Kind()] + if !ok { + return nil, fmt.Errorf( + "%s: at part %d, invalid value kind: %s", p, i, currentVal.Kind()) + } + + var err error + currentVal, err = f(part, currentVal) + if err != nil { + return nil, fmt.Errorf("%s at part %d: %s", p, i, err) + } + } + + return currentVal.Interface(), nil +} + +func (p *Pointer) getMap(part string, m reflect.Value) (reflect.Value, error) { + var zeroValue reflect.Value + + // Coerce the string part to the correct key type + key, err := coerce(reflect.ValueOf(part), m.Type().Key()) + if err != nil { + return zeroValue, err + } + + // Verify that the key exists + found := false + for _, k := range m.MapKeys() { + if k.Interface() == key.Interface() { + found = true + break + } + } + if !found { + return zeroValue, fmt.Errorf("couldn't find key %#v", key.Interface()) + } + + // Get the key + return m.MapIndex(key), nil +} + +func (p *Pointer) getSlice(part string, v reflect.Value) (reflect.Value, error) { + var zeroValue reflect.Value + + // Coerce the key to an int + idxVal, err := coerce(reflect.ValueOf(part), reflect.TypeOf(42)) + if err != nil { + return zeroValue, err + } + idx := int(idxVal.Int()) + + // Verify we're within bounds + if idx < 0 || idx >= v.Len() { + return zeroValue, fmt.Errorf( + "index %d is out of range (length = %d)", idx, v.Len()) + } + + // Get the key + return v.Index(idx), nil +} diff --git a/vendor/github.com/mitchellh/pointerstructure/parse.go b/vendor/github.com/mitchellh/pointerstructure/parse.go new file mode 100644 index 000000000000..c34e8d465fd7 --- /dev/null +++ b/vendor/github.com/mitchellh/pointerstructure/parse.go @@ -0,0 +1,57 @@ +package pointerstructure + +import ( + "fmt" + "strings" +) + +// Parse parses a pointer from the input string. The input string +// is expected to follow the format specified by RFC 6901: '/'-separated +// parts. Each part can contain escape codes to contain '/' or '~'. +func Parse(input string) (*Pointer, error) { + // Special case the empty case + if input == "" { + return &Pointer{}, nil + } + + // We expect the first character to be "/" + if input[0] != '/' { + return nil, fmt.Errorf( + "parse Go pointer %q: first char must be '/'", input) + } + + // Trim out the first slash so we don't have to +1 every index + input = input[1:] + + // Parse out all the parts + var parts []string + lastSlash := -1 + for i, r := range input { + if r == '/' { + parts = append(parts, input[lastSlash+1:i]) + lastSlash = i + } + } + + // Add last part + parts = append(parts, input[lastSlash+1:]) + + // Process each part for string replacement + for i, p := range parts { + // Replace ~1 followed by ~0 as specified by the RFC + parts[i] = strings.Replace( + strings.Replace(p, "~1", "/", -1), "~0", "~", -1) + } + + return &Pointer{Parts: parts}, nil +} + +// MustParse is like Parse but panics if the input cannot be parsed. +func MustParse(input string) *Pointer { + p, err := Parse(input) + if err != nil { + panic(err) + } + + return p +} diff --git a/vendor/github.com/mitchellh/pointerstructure/pointer.go b/vendor/github.com/mitchellh/pointerstructure/pointer.go new file mode 100644 index 000000000000..3a8f88b918fb --- /dev/null +++ b/vendor/github.com/mitchellh/pointerstructure/pointer.go @@ -0,0 +1,123 @@ +// Package pointerstructure provides functions for identifying a specific +// value within any Go structure using a string syntax. +// +// The syntax used is based on JSON Pointer (RFC 6901). +package pointerstructure + +import ( + "fmt" + "reflect" + "strings" + + "github.com/mitchellh/mapstructure" +) + +// Pointer represents a pointer to a specific value. You can construct +// a pointer manually or use Parse. +type Pointer struct { + // Parts are the pointer parts. No escape codes are processed here. + // The values are expected to be exact. If you have escape codes, use + // the Parse functions. + Parts []string +} + +// Get reads the value at the given pointer. +// +// This is a shorthand for calling Parse on the pointer and then calling Get +// on that result. An error will be returned if the value cannot be found or +// there is an error with the format of pointer. +func Get(value interface{}, pointer string) (interface{}, error) { + p, err := Parse(pointer) + if err != nil { + return nil, err + } + + return p.Get(value) +} + +// Set sets the value at the given pointer. +// +// This is a shorthand for calling Parse on the pointer and then calling Set +// on that result. An error will be returned if the value cannot be found or +// there is an error with the format of pointer. +// +// Set returns the complete document, which might change if the pointer value +// points to the root (""). +func Set(doc interface{}, pointer string, value interface{}) (interface{}, error) { + p, err := Parse(pointer) + if err != nil { + return nil, err + } + + return p.Set(doc, value) +} + +// String returns the string value that can be sent back to Parse to get +// the same Pointer result. +func (p *Pointer) String() string { + if len(p.Parts) == 0 { + return "" + } + + // Copy the parts so we can convert back the escapes + result := make([]string, len(p.Parts)) + copy(result, p.Parts) + for i, p := range p.Parts { + result[i] = strings.Replace( + strings.Replace(p, "~", "~0", -1), "/", "~1", -1) + + } + + return "/" + strings.Join(result, "/") +} + +// Parent returns a pointer to the parent element of this pointer. +// +// If Pointer represents the root (empty parts), a pointer representing +// the root is returned. Therefore, to check for the root, IsRoot() should be +// called. +func (p *Pointer) Parent() *Pointer { + // If this is root, then we just return a new root pointer. We allocate + // a new one though so this can still be modified. + if p.IsRoot() { + return &Pointer{} + } + + parts := make([]string, len(p.Parts)-1) + copy(parts, p.Parts[:len(p.Parts)-1]) + return &Pointer{ + Parts: parts, + } +} + +// IsRoot returns true if this pointer represents the root document. +func (p *Pointer) IsRoot() bool { + return len(p.Parts) == 0 +} + +// coerce is a helper to coerce a value to a specific type if it must +// and if its possible. If it isn't possible, an error is returned. +func coerce(value reflect.Value, to reflect.Type) (reflect.Value, error) { + // If the value is already assignable to the type, then let it go + if value.Type().AssignableTo(to) { + return value, nil + } + + // If a direct conversion is possible, do that + if value.Type().ConvertibleTo(to) { + return value.Convert(to), nil + } + + // Create a new value to hold our result + result := reflect.New(to) + + // Decode + if err := mapstructure.WeakDecode(value.Interface(), result.Interface()); err != nil { + return result, fmt.Errorf( + "couldn't convert value %#v to type %s", + value.Interface(), to.String()) + } + + // We need to indirect the value since reflect.New always creates a pointer + return reflect.Indirect(result), nil +} diff --git a/vendor/github.com/mitchellh/pointerstructure/set.go b/vendor/github.com/mitchellh/pointerstructure/set.go new file mode 100644 index 000000000000..a396ac62f295 --- /dev/null +++ b/vendor/github.com/mitchellh/pointerstructure/set.go @@ -0,0 +1,122 @@ +package pointerstructure + +import ( + "fmt" + "reflect" +) + +// Set writes a value v to the pointer p in structure s. +// +// The structures s must have non-zero values set up to this pointer. +// For example, if setting "/bob/0/name", then "/bob/0" must be set already. +// +// The returned value is potentially a new value if this pointer represents +// the root document. Otherwise, the returned value will always be s. +func (p *Pointer) Set(s, v interface{}) (interface{}, error) { + // if we represent the root doc, return that + if len(p.Parts) == 0 { + return v, nil + } + + // Save the original since this is going to be our return value + originalS := s + + // Get the parent value + var err error + s, err = p.Parent().Get(s) + if err != nil { + return nil, err + } + + // Map for lookup of getter to call for type + funcMap := map[reflect.Kind]setFunc{ + reflect.Array: p.setSlice, + reflect.Map: p.setMap, + reflect.Slice: p.setSlice, + } + + val := reflect.ValueOf(s) + for val.Kind() == reflect.Interface { + val = val.Elem() + } + + for val.Kind() == reflect.Ptr { + val = reflect.Indirect(val) + } + + f, ok := funcMap[val.Kind()] + if !ok { + return nil, fmt.Errorf("set %s: invalid value kind: %s", p, val.Kind()) + } + + result, err := f(originalS, val, reflect.ValueOf(v)) + if err != nil { + return nil, fmt.Errorf("set %s: %s", p, err) + } + + return result, nil +} + +type setFunc func(interface{}, reflect.Value, reflect.Value) (interface{}, error) + +func (p *Pointer) setMap(root interface{}, m, value reflect.Value) (interface{}, error) { + part := p.Parts[len(p.Parts)-1] + key, err := coerce(reflect.ValueOf(part), m.Type().Key()) + if err != nil { + return root, err + } + + elem, err := coerce(value, m.Type().Elem()) + if err != nil { + return root, err + } + + // Set the key + m.SetMapIndex(key, elem) + return root, nil +} + +func (p *Pointer) setSlice(root interface{}, s, value reflect.Value) (interface{}, error) { + // Coerce the value, we'll need that no matter what + value, err := coerce(value, s.Type().Elem()) + if err != nil { + return root, err + } + + // If the part is the special "-", that means to append it (RFC6901 4.) + part := p.Parts[len(p.Parts)-1] + if part == "-" { + return p.setSliceAppend(root, s, value) + } + + // Coerce the key to an int + idxVal, err := coerce(reflect.ValueOf(part), reflect.TypeOf(42)) + if err != nil { + return root, err + } + idx := int(idxVal.Int()) + + // Verify we're within bounds + if idx < 0 || idx >= s.Len() { + return root, fmt.Errorf( + "index %d is out of range (length = %d)", idx, s.Len()) + } + + // Set the key + s.Index(idx).Set(value) + return root, nil +} + +func (p *Pointer) setSliceAppend(root interface{}, s, value reflect.Value) (interface{}, error) { + // Coerce the value, we'll need that no matter what. This should + // be a no-op since we expect it to be done already, but there is + // a fast-path check for that in coerce so do it anyways. + value, err := coerce(value, s.Type().Elem()) + if err != nil { + return root, err + } + + // We can assume "s" is the parent of pointer value. We need to actually + // write s back because Append can return a new slice. + return p.Parent().Set(root, reflect.Append(s, value).Interface()) +} diff --git a/vendor/github.com/mitchellh/pointerstructure/sort.go b/vendor/github.com/mitchellh/pointerstructure/sort.go new file mode 100644 index 000000000000..886d1183c69a --- /dev/null +++ b/vendor/github.com/mitchellh/pointerstructure/sort.go @@ -0,0 +1,42 @@ +package pointerstructure + +import ( + "sort" +) + +// Sort does an in-place sort of the pointers so that they are in order +// of least specific to most specific alphabetized. For example: +// "/foo", "/foo/0", "/qux" +// +// This ordering is ideal for applying the changes in a way that ensures +// that parents are set first. +func Sort(p []*Pointer) { sort.Sort(PointerSlice(p)) } + +// PointerSlice is a slice of pointers that adheres to sort.Interface +type PointerSlice []*Pointer + +func (p PointerSlice) Len() int { return len(p) } +func (p PointerSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p PointerSlice) Less(i, j int) bool { + // Equal number of parts, do a string compare per part + for idx, ival := range p[i].Parts { + // If we're passed the length of p[j] parts, then we're done + if idx >= len(p[j].Parts) { + break + } + + // Compare the values if they're not equal + jval := p[j].Parts[idx] + if ival != jval { + return ival < jval + } + } + + // Equal prefix, take the shorter + if len(p[i].Parts) != len(p[j].Parts) { + return len(p[i].Parts) < len(p[j].Parts) + } + + // Equal, it doesn't matter + return false +} diff --git a/vendor/vendor.json b/vendor/vendor.json index f6ddcf764d2d..316b1bd97069 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1409,10 +1409,12 @@ "revisionTime": "2018-12-10T20:01:33Z" }, { - "checksumSHA1": "tt3FtyjXgdBI9Mb43UL4LtOZmAk=", + "checksumSHA1": "86jzaGc3dRpZ5BKQPFP7ecasQfg=", "path": "github.com/hashicorp/vault-plugin-auth-jwt", - "revision": "f428c77917331c1b87dae2dd37016bd1dd4c55da", - "revisionTime": "2018-10-31T19:59:42Z" + "revision": "bf17a88bb5c43eb2cbdc08011cd76ecec028521c", + "revisionTime": "2019-02-07T06:35:46Z", + "version": "=oidc-cli", + "versionExact": "oidc-cli" }, { "checksumSHA1": "Ldg2jQeyPrpAupyQq4lRVN+jfFY=", @@ -1788,6 +1790,12 @@ "revision": "3536a929edddb9a5b34bd6861dc4a9647cb459fe", "revisionTime": "2018-10-05T04:51:35Z" }, + { + "checksumSHA1": "31atAEqGt+z8hZgyVZZokEeM6dM=", + "path": "github.com/mitchellh/pointerstructure", + "revision": "f2329fcfa9e280bdb5a3f2544aec815a508ad72f", + "revisionTime": "2017-02-05T20:42:03Z" + }, { "checksumSHA1": "nxuST3bjBv5uDVPzrX9wdruOwv0=", "path": "github.com/mitchellh/reflectwalk",