diff --git a/README.md b/README.md index 7f24514..c46aa08 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ Congratulations, GitLab is now fully configured! Let's move on to the Vault side * OAuth callback URL: It should be the root address of your Vault instance. * eg: `https://my_vault_server.local`. * CI token: put in the previously generated PAT from GitLab. + * (*Starting v1.18.1*) (*Optional*) Vault Service Token: an optional Vault service token that will be used to read + loaded policies in your Vault . See [VST Configuration](#vault-service-token-configuration) for more details. Vault is now configured, you should be able to log in to Vault using your GitLab credentials now! @@ -119,6 +121,54 @@ concatenate the user role after it. A few examples to fully understand it: Every matching Vault policy will be loaded to the user token. +### Vault Service Token Configuration + +Added in v1.18.1 and onwards, the Vault Service Token allows you to filter the generated policies based on your GitLab +access, by removing any policy that does not really exist in Vault. This feature drastically reduce token size, and also +your audit logs if you enabled them. + +You must provide a token that never expires OR a token that can be renewed indefinitely. The backend will refresh every +two minutes the list of ACLs registered in Vault, and will also renew the token accordingly. We highly recommend to +create a dedicated token for this usage with a very specific policy, in order to limit any risk if this token got +stolen. + +Create an ACL policy named `token-viewer`, with the following content: + +```hcl +# Allow people to list all ACLs +path "sys/policies/acl" { + capabilities = ["list"] +} + +# Allow tokens to look up their own properties +path "auth/token/lookup-self" { + capabilities = ["read"] +} + +# Allow tokens to renew themselves +path "auth/token/renew-self" { + capabilities = ["update"] +} + +# Allow tokens to revoke themselves +path "auth/token/revoke-self" { + capabilities = ["update"] +} +``` + +This policy is very restrictive, and only allows token to list ACL on Vault (but not see them), and perform basic +operations on its own token (lookup/renew/revoke). + +Then, using a root account, create a new token with this policy only : +`vault token create -policy="token-viewer" -period=24h -no-default-policy` + +This token will have no privileges but the ones you defined in your `token-viewer` policy, ensuring a very limited risk +for your passwords. + +If no Vault Service Token is provided, the program will load all the policies based on your rights. + +If you provide a token, please note that **users must have to log-out and login again when privileges changes.** + _____________ ## Using the connector (as a user) diff --git a/patches/v1.18.1.patch b/patches/v1.18.1.patch index 7acb6a7..858a322 100644 --- a/patches/v1.18.1.patch +++ b/patches/v1.18.1.patch @@ -1,16 +1,23 @@ diff --git a/builtin/credential/gitlab/backend.go b/builtin/credential/gitlab/backend.go new file mode 100644 -index 0000000000..77fed07c34 +index 0000000000..cbd16802d2 --- /dev/null +++ b/builtin/credential/gitlab/backend.go -@@ -0,0 +1,52 @@ +@@ -0,0 +1,161 @@ +package gitlab + +import ( + "context" ++ "encoding/json" ++ "fmt" ++ "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" ++ "io" + mathrand "math/rand" ++ "net/http" ++ "net/url" ++ "time" +) + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -25,9 +32,104 @@ index 0000000000..77fed07c34 + for i := range b.CipherKey { + b.CipherKey[i] = letterBytes[mathrand.Intn(len(letterBytes))] + } ++ ++ go b.startScheduler(ctx, conf) ++ + return b, nil +} + ++func (b *backend) startScheduler(ctx context.Context, conf *logical.BackendConfig) { ++ for { ++ config, err := b.Config(ctx, conf.StorageView) ++ if err != nil { ++ fmt.Printf("Error while loading configuration: %v", err) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ ++ if config == nil || config.VaultServiceToken == "" { ++ fmt.Println("Vault service token not present, disabling.") ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ ++ headers := make(http.Header) ++ headers.Add("X-Vault-Token", config.VaultServiceToken) ++ ++ u, err := url.Parse(fmt.Sprintf("%s/v1/sys/policies/acl", config.CallbackURL)) ++ if err != nil { ++ fmt.Printf("Error while genering ACL url: %v", err) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ ++ resp, err := cleanhttp.DefaultClient().Do(&http.Request{ ++ Method: "LIST", ++ URL: u, ++ Proto: "HTTP/1.1", ++ ProtoMajor: 1, ++ ProtoMinor: 1, ++ Header: headers, ++ Host: u.Host, ++ }) ++ if err != nil { ++ if err != nil { ++ fmt.Printf("Error while creating request: %v", err) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ } ++ if resp.StatusCode != http.StatusOK { ++ if err != nil { ++ fmt.Printf("Invalid response code: %d", resp.StatusCode) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ } ++ ++ var result VaultPoliciesResponse ++ data2, _ := io.ReadAll(resp.Body) ++ _ = json.Unmarshal(data2, &result) ++ ++ b.vaultPolicies = result.Data.Keys ++ ++ // Renew ++ ++ u, err = url.Parse(fmt.Sprintf("%s/v1/auth/token/renew-self", config.CallbackURL)) ++ if err != nil { ++ fmt.Printf("Error while genering renew url: %v", err) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ ++ resp, err = cleanhttp.DefaultClient().Do(&http.Request{ ++ Method: "PUT", ++ URL: u, ++ Proto: "HTTP/1.1", ++ ProtoMajor: 1, ++ ProtoMinor: 1, ++ Header: headers, ++ Host: u.Host, ++ }) ++ if err != nil { ++ if err != nil { ++ fmt.Printf("Error while creating renew request: %v", err) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ } ++ if resp.StatusCode != http.StatusOK { ++ if err != nil { ++ fmt.Printf("Invalid renew response code: %d", resp.StatusCode) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ } ++ ++ time.Sleep(time.Minute * 2) ++ } ++} ++ +// Backend constructor +func Backend() *backend { + @@ -45,7 +147,8 @@ index 0000000000..77fed07c34 + +type backend struct { + *framework.Backend -+ CipherKey []byte ++ CipherKey []byte ++ vaultPolicies []string +} + +const backendHelp = ` @@ -56,6 +159,12 @@ index 0000000000..77fed07c34 +After enabling the credential provider, use the "config" route to +configure it. +` ++ ++type VaultPoliciesResponse struct { ++ Data struct { ++ Keys []string `json:"keys"` ++ } `json:"data"` ++} diff --git a/builtin/credential/gitlab/backend_test.go b/builtin/credential/gitlab/backend_test.go new file mode 100644 index 0000000000..a93e106d27 @@ -413,10 +522,10 @@ index 0000000000..3e2b4b4ca5 +} diff --git a/builtin/credential/gitlab/path_config.go b/builtin/credential/gitlab/path_config.go new file mode 100644 -index 0000000000..23c5f25256 +index 0000000000..c378f6e9f4 --- /dev/null +++ b/builtin/credential/gitlab/path_config.go -@@ -0,0 +1,194 @@ +@@ -0,0 +1,202 @@ +package gitlab + +import ( @@ -463,6 +572,11 @@ index 0000000000..23c5f25256 + Description: "The CI token API to use.", + Default: "", + }, ++ "vault_service_token": { ++ Type: framework.TypeString, ++ Description: "Vault service token.", ++ Default: "", ++ }, + }, + + Operations: map[logical.Operation]framework.OperationHandler{ @@ -499,6 +613,7 @@ index 0000000000..23c5f25256 + appSecret := data.Get("app_secret").(string) + callbackURL := data.Get("callback_url").(string) + ciToken := data.Get("ci_token").(string) ++ vault_service_token := data.Get("vault_service_token").(string) + if len(callbackURL) > 0 { + _, err := url.Parse(callbackURL) + if err != nil { @@ -506,12 +621,13 @@ index 0000000000..23c5f25256 + } + } + entry, err := logical.StorageEntryJSON("config", config{ -+ BaseURL: baseURL, -+ MinAccessLevel: minAccessLevel, -+ AppID: appID, -+ AppSecret: appSecret, -+ CallbackURL: callbackURL, -+ CIToken: ciToken, ++ BaseURL: baseURL, ++ MinAccessLevel: minAccessLevel, ++ AppID: appID, ++ AppSecret: appSecret, ++ CallbackURL: callbackURL, ++ CIToken: ciToken, ++ VaultServiceToken: vault_service_token, + }) + + if err != nil { @@ -604,19 +720,20 @@ index 0000000000..23c5f25256 +} + +type config struct { -+ BaseURL string `json:"baseURL" structs:"baseURL" mapstructure:"baseURL"` -+ MinAccessLevel string `json:"minAccessLevel" structs:"minAccessLevel" mapstructure:"minAccessLevel"` -+ AppID string `json:"appID" structs:"appID" mapstructure:"appID"` -+ AppSecret string `json:"appSecret" structs:"appSecret" mapstructure:"appSecret"` -+ CallbackURL string `json:"callbackURL" structs:"callbackURL" mapstructure:"callbackURL"` -+ CIToken string `json:"ciToken" structs:"ciToken" mapstructure:"ciToken"` ++ BaseURL string `json:"baseURL" structs:"baseURL" mapstructure:"baseURL"` ++ MinAccessLevel string `json:"minAccessLevel" structs:"minAccessLevel" mapstructure:"minAccessLevel"` ++ AppID string `json:"appID" structs:"appID" mapstructure:"appID"` ++ AppSecret string `json:"appSecret" structs:"appSecret" mapstructure:"appSecret"` ++ CallbackURL string `json:"callbackURL" structs:"callbackURL" mapstructure:"callbackURL"` ++ CIToken string `json:"ciToken" structs:"ciToken" mapstructure:"ciToken"` ++ VaultServiceToken string `json:"vaultServiceToken" structs:"vaultServiceToken" mapstructure:"vaultServiceToken"` +} diff --git a/builtin/credential/gitlab/path_login_commons.go b/builtin/credential/gitlab/path_login_commons.go new file mode 100644 -index 0000000000..659205afd6 +index 0000000000..7fbfd96cd6 --- /dev/null +++ b/builtin/credential/gitlab/path_login_commons.go -@@ -0,0 +1,185 @@ +@@ -0,0 +1,201 @@ +package gitlab + +import ( @@ -626,6 +743,7 @@ index 0000000000..659205afd6 + "github.com/hasura/go-graphql-client" + "github.com/xanzy/go-gitlab" + "net/http" ++ "slices" + "strings" +) + @@ -727,7 +845,22 @@ index 0000000000..659205afd6 + access = append(access, b.parseNoneAccessLevelsToMaxRights(access, noneAccessLevels)...) + // parse all rights access inheritance (E.g: owner = owner + maintainer + dev + guest) + access = b.parseMaxRightsToRights(access) -+ return access, nil ++ ++ var shownPolicies []string ++ ++ // If no policy loaded, return all policies. ++ if len(b.vaultPolicies) == 0 { ++ return access, nil ++ } ++ ++ // Else, do a filter ++ for _, policy := range b.vaultPolicies { ++ if slices.Contains(access, policy) { ++ shownPolicies = append(shownPolicies, policy) ++ } ++ } ++ ++ return shownPolicies, nil +} + +// parseGraphQLEdgesToMaxRights parses GraphQL results to MaxAccess rights, also returns "none" access levels @@ -1297,6 +1430,44 @@ index 8db22350cf..18bb2845b9 100644 "kerberos": &credKerb.CLIHandler{}, "ldap": &credLdap.CLIHandler{}, "oci": &credOCI.CLIHandler{}, +diff --git a/go.mod b/go.mod +index 3e3bd5e62f..f6e4d98ed6 100644 +--- a/go.mod ++++ b/go.mod +@@ -231,9 +231,11 @@ require ( + require ( + cel.dev/expr v0.15.0 // indirect + cloud.google.com/go/longrunning v0.6.0 // indirect ++ github.com/coder/websocket v1.8.12 // indirect + github.com/containerd/containerd v1.7.20 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/hashicorp/go-secure-stdlib/httputil v0.1.0 // indirect ++ github.com/hasura/go-graphql-client v0.13.1 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect +diff --git a/go.sum b/go.sum +index 6b321976d3..1da5fb2fca 100644 +--- a/go.sum ++++ b/go.sum +@@ -925,6 +925,8 @@ github.com/cockroachdb/cockroach-go/v2 v2.3.8 h1:53yoUo4+EtrC1NrAEgnnad4AS3ntNvG + github.com/cockroachdb/cockroach-go/v2 v2.3.8/go.mod h1:9uH5jK4yQ3ZQUT9IXe4I2fHzMIF5+JC/oOdzTRgJYJk= + github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= + github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= ++github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= ++github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= + github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ= + github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0= + github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +@@ -1605,6 +1607,8 @@ github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyu + github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo= + github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= + github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= ++github.com/hasura/go-graphql-client v0.13.1 h1:kKbjhxhpwz58usVl+Xvgah/TDha5K2akNTRQdsEHN6U= ++github.com/hasura/go-graphql-client v0.13.1/go.mod h1:k7FF7h53C+hSNFRG3++DdVZWIuHdCaTbI7siTJ//zGQ= + github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= + github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= + github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= diff --git a/helper/builtinplugins/registry_full.go b/helper/builtinplugins/registry_full.go index 32bba40487..b4931436e2 100644 --- a/helper/builtinplugins/registry_full.go @@ -1317,6 +1488,256 @@ index 32bba40487..b4931436e2 100644 "kerberos": {Factory: credKerb.Factory}, "kubernetes": {Factory: credKube.Factory}, "ldap": {Factory: credLdap.Factory}, +diff --git a/patch b/patch +new file mode 100644 +index 0000000000..8e7464c0a0 +--- /dev/null ++++ b/patch +@@ -0,0 +1,244 @@ ++diff --git a/command/base_predict.go b/command/base_predict.go ++index ed3edfa30f..70fe7e73d2 100644 ++--- a/command/base_predict.go +++++ b/command/base_predict.go ++@@ -111,6 +111,7 @@ func (b *BaseCommand) PredictVaultAvailableAuths() complete.Predictor { ++ "cert", ++ "gcp", ++ "github", +++ "gitlab", ++ "ldap", ++ "okta", ++ "plugin", ++diff --git a/command/base_predict_test.go b/command/base_predict_test.go ++index 387b8f0b84..477eda9c5f 100644 ++--- a/command/base_predict_test.go +++++ b/command/base_predict_test.go ++@@ -359,6 +359,7 @@ func TestPredict_Plugins(t *testing.T) { ++ "gcp", ++ "gcpkms", ++ "github", +++ "gitlab", ++ "hana-database-plugin", ++ "influxdb-database-plugin", ++ "jwt", ++diff --git a/command/commands_full.go b/command/commands_full.go ++index 8db22350cf..18bb2845b9 100644 ++--- a/command/commands_full.go +++++ b/command/commands_full.go ++@@ -15,6 +15,7 @@ import ( ++ credOCI "github.com/hashicorp/vault-plugin-auth-oci" ++ credAws "github.com/hashicorp/vault/builtin/credential/aws" ++ credGitHub "github.com/hashicorp/vault/builtin/credential/github" +++ credGitlab "github.com/hashicorp/vault/builtin/credential/gitlab" ++ credLdap "github.com/hashicorp/vault/builtin/credential/ldap" ++ credOkta "github.com/hashicorp/vault/builtin/credential/okta" ++ credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" ++@@ -75,6 +76,7 @@ func newFullAddonHandlers() (map[string]physical.Factory, map[string]LoginHandle ++ "cf": &credCF.CLIHandler{}, ++ "gcp": &credGcp.CLIHandler{}, ++ "github": &credGitHub.CLIHandler{}, +++ "gitlab": &credGitlab.CLIHandler{}, ++ "kerberos": &credKerb.CLIHandler{}, ++ "ldap": &credLdap.CLIHandler{}, ++ "oci": &credOCI.CLIHandler{}, ++diff --git a/go.mod b/go.mod ++index 3e3bd5e62f..f6e4d98ed6 100644 ++--- a/go.mod +++++ b/go.mod ++@@ -231,9 +231,11 @@ require ( ++ require ( ++ cel.dev/expr v0.15.0 // indirect ++ cloud.google.com/go/longrunning v0.6.0 // indirect +++ github.com/coder/websocket v1.8.12 // indirect ++ github.com/containerd/containerd v1.7.20 // indirect ++ github.com/fxamacker/cbor/v2 v2.7.0 // indirect ++ github.com/hashicorp/go-secure-stdlib/httputil v0.1.0 // indirect +++ github.com/hasura/go-graphql-client v0.13.1 // indirect ++ github.com/mitchellh/go-testing-interface v1.14.1 // indirect ++ github.com/moby/docker-image-spec v1.3.1 // indirect ++ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect ++diff --git a/go.sum b/go.sum ++index 6b321976d3..1da5fb2fca 100644 ++--- a/go.sum +++++ b/go.sum ++@@ -925,6 +925,8 @@ github.com/cockroachdb/cockroach-go/v2 v2.3.8 h1:53yoUo4+EtrC1NrAEgnnad4AS3ntNvG ++ github.com/cockroachdb/cockroach-go/v2 v2.3.8/go.mod h1:9uH5jK4yQ3ZQUT9IXe4I2fHzMIF5+JC/oOdzTRgJYJk= ++ github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= ++ github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +++github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +++github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= ++ github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ= ++ github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0= ++ github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= ++@@ -1605,6 +1607,8 @@ github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyu ++ github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo= ++ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= ++ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +++github.com/hasura/go-graphql-client v0.13.1 h1:kKbjhxhpwz58usVl+Xvgah/TDha5K2akNTRQdsEHN6U= +++github.com/hasura/go-graphql-client v0.13.1/go.mod h1:k7FF7h53C+hSNFRG3++DdVZWIuHdCaTbI7siTJ//zGQ= ++ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= ++ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= ++ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= ++diff --git a/helper/builtinplugins/registry_full.go b/helper/builtinplugins/registry_full.go ++index 32bba40487..b4931436e2 100644 ++--- a/helper/builtinplugins/registry_full.go +++++ b/helper/builtinplugins/registry_full.go ++@@ -32,6 +32,7 @@ import ( ++ logicalTerraform "github.com/hashicorp/vault-plugin-secrets-terraform" ++ credAws "github.com/hashicorp/vault/builtin/credential/aws" ++ credGitHub "github.com/hashicorp/vault/builtin/credential/github" +++ credGitlab "github.com/hashicorp/vault/builtin/credential/gitlab" ++ credLdap "github.com/hashicorp/vault/builtin/credential/ldap" ++ credOkta "github.com/hashicorp/vault/builtin/credential/okta" ++ credRadius "github.com/hashicorp/vault/builtin/credential/radius" ++@@ -64,6 +65,7 @@ func newFullAddonRegistry() *registry { ++ "cf": {Factory: credCF.Factory}, ++ "gcp": {Factory: credGcp.Factory}, ++ "github": {Factory: credGitHub.Factory}, +++ "gitlab": {Factory: credGitlab.Factory}, ++ "kerberos": {Factory: credKerb.Factory}, ++ "kubernetes": {Factory: credKube.Factory}, ++ "ldap": {Factory: credLdap.Factory}, ++diff --git a/ui/app/adapters/cluster.js b/ui/app/adapters/cluster.js ++index 5a8984ec33..dcc709f242 100644 ++--- a/ui/app/adapters/cluster.js +++++ b/ui/app/adapters/cluster.js ++@@ -180,6 +180,7 @@ export default ApplicationAdapter.extend({ ++ const authURLs = { ++ github: 'login', ++ jwt: 'login', +++ gitlab: username ? `login/${encodeURIComponent(username)}` : 'login', ++ oidc: 'login', ++ userpass: `login/${encodeURIComponent(username)}`, ++ ldap: `login/${encodeURIComponent(username)}`, ++diff --git a/ui/app/components/auth-form.js b/ui/app/components/auth-form.js ++index d87161b290..7d8307af7c 100644 ++--- a/ui/app/components/auth-form.js +++++ b/ui/app/components/auth-form.js ++@@ -185,8 +185,14 @@ export default Component.extend(DEFAULTS, { ++ this.set('selectedAuth', 'token'); ++ const adapter = this.store.adapterFor('tools'); ++ try { ++- const response = yield adapter.toolAction('unwrap', null, { clientToken: token }); ++- this.set('token', response.auth.client_token); +++ const response = yield adapter.toolAction('unwrap', null, {clientToken: token}); +++ if (response.data.authType) { +++ this.set('selectedAuth', response.data.authType) +++ this.set('token', response.data.token); +++ } else { +++ this.set('selectedAuth', 'token'); +++ this.set('token', response.auth.client_token); +++ } ++ this.send('doSubmit'); ++ } catch (e) { ++ this.set('error', `Token unwrap failed: ${e.errors[0]}`); ++diff --git a/ui/app/helpers/mountable-auth-methods.js b/ui/app/helpers/mountable-auth-methods.js ++index 53d2410d6c..0e0344fc59 100644 ++--- a/ui/app/helpers/mountable-auth-methods.js +++++ b/ui/app/helpers/mountable-auth-methods.js ++@@ -64,6 +64,13 @@ const MOUNTABLE_AUTH_METHODS = [ ++ category: 'cloud', ++ glyph: 'github-color', ++ }, +++ { +++ displayName: 'Gitlab', +++ value: 'gitlab', +++ type: 'gitlab', +++ glyph: 'auth', +++ category: 'cloud', +++ }, ++ { ++ displayName: 'JWT', ++ value: 'jwt', ++diff --git a/ui/app/helpers/supported-auth-backends.js b/ui/app/helpers/supported-auth-backends.js ++index 4c9185cf48..c75ca7fc0e 100644 ++--- a/ui/app/helpers/supported-auth-backends.js +++++ b/ui/app/helpers/supported-auth-backends.js ++@@ -76,6 +76,14 @@ const SUPPORTED_AUTH_BACKENDS = [ ++ displayNamePath: ['metadata.org', 'metadata.username'], ++ formAttributes: ['token'], ++ }, +++ { +++ type: 'gitlab', +++ typeDisplay: 'Gitlab', +++ description: 'Gitlab authentication.', +++ tokenPath: 'client_token', +++ displayNamePath: 'metadata.username', +++ formAttributes: ['token'], +++ }, ++ ]; ++ ++ const ENTERPRISE_AUTH_METHODS = [ ++diff --git a/ui/app/helpers/tabs-for-auth-section.js b/ui/app/helpers/tabs-for-auth-section.js ++index 77a1b92388..44f940d800 100644 ++--- a/ui/app/helpers/tabs-for-auth-section.js +++++ b/ui/app/helpers/tabs-for-auth-section.js ++@@ -39,6 +39,18 @@ const TABS_FOR_SETTINGS = { ++ routeParams: ['configuration'], ++ }, ++ ], +++ gitlab: [ +++ { +++ label: 'Configuration', +++ routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'], +++ }, +++ ], +++ gitlab: [ +++ { +++ label: 'Configuration', +++ routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'], +++ }, +++ ], ++ gcp: [ ++ { ++ label: 'Configuration', ++diff --git a/ui/app/routes/vault/cluster/settings/auth/configure/section.js b/ui/app/routes/vault/cluster/settings/auth/configure/section.js ++index 0111f636ee..da91583f10 100644 ++--- a/ui/app/routes/vault/cluster/settings/auth/configure/section.js +++++ b/ui/app/routes/vault/cluster/settings/auth/configure/section.js ++@@ -23,6 +23,7 @@ export default Route.extend(UnloadModelRoute, { ++ 'aws-roletag-denylist': 'auth-config/aws/roletag-denylist', ++ 'azure-configuration': 'auth-config/azure', ++ 'github-configuration': 'auth-config/github', +++ 'gitlab-configuration': 'auth-config/gitlab', ++ 'gcp-configuration': 'auth-config/gcp', ++ 'jwt-configuration': 'auth-config/jwt', ++ 'oidc-configuration': 'auth-config/oidc', ++diff --git a/ui/app/templates/components/auth-form.hbs b/ui/app/templates/components/auth-form.hbs ++index f9fee4c71f..8010db7b6f 100644 ++--- a/ui/app/templates/components/auth-form.hbs +++++ b/ui/app/templates/components/auth-form.hbs ++@@ -118,6 +118,19 @@ ++ /> ++ ++ +++ {{else if (eq this.providerName "gitlab")}} +++
+++ +++ +++ +++
+++ +++
+++ +++
+++
++ {{else if (eq this.providerName "token")}} ++
++ ++diff --git a/ui/tests/acceptance/settings/auth/configure/section-test.js b/ui/tests/acceptance/settings/auth/configure/section-test.js ++index b6533273c0..f0f968f666 100644 ++--- a/ui/tests/acceptance/settings/auth/configure/section-test.js +++++ b/ui/tests/acceptance/settings/auth/configure/section-test.js ++@@ -56,7 +56,7 @@ module('Acceptance | settings/auth/configure/section', function (hooks) { ++ ); ++ }); ++ ++- for (const type of ['aws', 'azure', 'gcp', 'github', 'kubernetes']) { +++ for (const type of ['aws', 'azure', 'gcp', 'github', 'gitlab', 'kubernetes']) { ++ test(`it shows tabs for auth method: ${type}`, async function (assert) { ++ const path = `${type}-showtab-${this.uid}`; ++ await cli.toggle(); diff --git a/ui/app/adapters/auth-config/gitlab.js b/ui/app/adapters/auth-config/gitlab.js new file mode 100644 index 0000000000..21f5624ac4 diff --git a/patches/v1.18.2.patch b/patches/v1.18.2.patch new file mode 100644 index 0000000..16bd5b6 --- /dev/null +++ b/patches/v1.18.2.patch @@ -0,0 +1,1971 @@ +diff --git a/builtin/credential/gitlab/backend.go b/builtin/credential/gitlab/backend.go +new file mode 100644 +index 0000000000..cbd16802d2 +--- /dev/null ++++ b/builtin/credential/gitlab/backend.go +@@ -0,0 +1,161 @@ ++package gitlab ++ ++import ( ++ "context" ++ "encoding/json" ++ "fmt" ++ "github.com/hashicorp/go-cleanhttp" ++ "github.com/hashicorp/vault/sdk/framework" ++ "github.com/hashicorp/vault/sdk/logical" ++ "io" ++ mathrand "math/rand" ++ "net/http" ++ "net/url" ++ "time" ++) ++ ++const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ++ ++// Factory of gitlab backend ++func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { ++ b := Backend() ++ if err := b.Setup(ctx, conf); err != nil { ++ return nil, err ++ } ++ b.CipherKey = make([]byte, 16) ++ for i := range b.CipherKey { ++ b.CipherKey[i] = letterBytes[mathrand.Intn(len(letterBytes))] ++ } ++ ++ go b.startScheduler(ctx, conf) ++ ++ return b, nil ++} ++ ++func (b *backend) startScheduler(ctx context.Context, conf *logical.BackendConfig) { ++ for { ++ config, err := b.Config(ctx, conf.StorageView) ++ if err != nil { ++ fmt.Printf("Error while loading configuration: %v", err) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ ++ if config == nil || config.VaultServiceToken == "" { ++ fmt.Println("Vault service token not present, disabling.") ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ ++ headers := make(http.Header) ++ headers.Add("X-Vault-Token", config.VaultServiceToken) ++ ++ u, err := url.Parse(fmt.Sprintf("%s/v1/sys/policies/acl", config.CallbackURL)) ++ if err != nil { ++ fmt.Printf("Error while genering ACL url: %v", err) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ ++ resp, err := cleanhttp.DefaultClient().Do(&http.Request{ ++ Method: "LIST", ++ URL: u, ++ Proto: "HTTP/1.1", ++ ProtoMajor: 1, ++ ProtoMinor: 1, ++ Header: headers, ++ Host: u.Host, ++ }) ++ if err != nil { ++ if err != nil { ++ fmt.Printf("Error while creating request: %v", err) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ } ++ if resp.StatusCode != http.StatusOK { ++ if err != nil { ++ fmt.Printf("Invalid response code: %d", resp.StatusCode) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ } ++ ++ var result VaultPoliciesResponse ++ data2, _ := io.ReadAll(resp.Body) ++ _ = json.Unmarshal(data2, &result) ++ ++ b.vaultPolicies = result.Data.Keys ++ ++ // Renew ++ ++ u, err = url.Parse(fmt.Sprintf("%s/v1/auth/token/renew-self", config.CallbackURL)) ++ if err != nil { ++ fmt.Printf("Error while genering renew url: %v", err) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ ++ resp, err = cleanhttp.DefaultClient().Do(&http.Request{ ++ Method: "PUT", ++ URL: u, ++ Proto: "HTTP/1.1", ++ ProtoMajor: 1, ++ ProtoMinor: 1, ++ Header: headers, ++ Host: u.Host, ++ }) ++ if err != nil { ++ if err != nil { ++ fmt.Printf("Error while creating renew request: %v", err) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ } ++ if resp.StatusCode != http.StatusOK { ++ if err != nil { ++ fmt.Printf("Invalid renew response code: %d", resp.StatusCode) ++ time.Sleep(time.Minute * 2) ++ continue ++ } ++ } ++ ++ time.Sleep(time.Minute * 2) ++ } ++} ++ ++// Backend constructor ++func Backend() *backend { ++ ++ var b backend ++ ++ b.Backend = &framework.Backend{ ++ Help: backendHelp, ++ PathsSpecial: &logical.Paths{Unauthenticated: []string{"login", "login/*", "oauth", "ci"}}, ++ Paths: append([]*framework.Path{pathConfig(&b), pathLoginToken(&b), pathOauthLogin(&b), pathLoginJob(&b)}), ++ BackendType: logical.TypeCredential, ++ } ++ ++ return &b ++} ++ ++type backend struct { ++ *framework.Backend ++ CipherKey []byte ++ vaultPolicies []string ++} ++ ++const backendHelp = ` ++The Gitlab credential provider allows authentication via Gitlab. ++ ++Users provide a personal access token to log in, and the credential ++provider maps the user to a set of Vault policies according to the groups he is part of. ++After enabling the credential provider, use the "config" route to ++configure it. ++` ++ ++type VaultPoliciesResponse struct { ++ Data struct { ++ Keys []string `json:"keys"` ++ } `json:"data"` ++} +diff --git a/builtin/credential/gitlab/backend_test.go b/builtin/credential/gitlab/backend_test.go +new file mode 100644 +index 0000000000..a93e106d27 +--- /dev/null ++++ b/builtin/credential/gitlab/backend_test.go +@@ -0,0 +1,164 @@ ++package gitlab ++ ++import ( ++ "context" ++ "os" ++ "strings" ++ "testing" ++ ++ logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" ++ "github.com/hashicorp/vault/sdk/logical" ++) ++ ++func TestBackend_Config(t *testing.T) { ++ b, err := Factory(context.Background(), &logical.BackendConfig{ ++ Logger: nil, ++ }) ++ if err != nil { ++ t.Fatalf("Unable to create backend: %s", err) ++ } ++ ++ loginData := map[string]interface{}{ ++ // This token has to be replaced with a working token for the test to work. ++ "token": os.Getenv("GITLAB_TOKEN"), ++ } ++ configData := map[string]interface{}{ ++ "group": os.Getenv("GITLAB_GROUP"), ++ } ++ ++ logicaltest.Test(t, logicaltest.TestCase{ ++ PreCheck: func() { testAccPreCheck(t) }, ++ LogicalBackend: b, ++ Steps: []logicaltest.TestStep{ ++ testConfigWrite(t, loginData), ++ testLoginWrite(t, configData, false), ++ }, ++ }) ++} ++ ++func testLoginWrite(t *testing.T, d map[string]interface{}, expectFail bool) logicaltest.TestStep { ++ return logicaltest.TestStep{ ++ Operation: logical.UpdateOperation, ++ Path: "login", ++ ErrorOk: true, ++ Data: d, ++ Check: func(resp *logical.Response) error { ++ if resp.IsError() && expectFail { ++ return nil ++ } ++ return nil ++ }, ++ } ++} ++ ++func testConfigWrite(t *testing.T, d map[string]interface{}) logicaltest.TestStep { ++ return logicaltest.TestStep{ ++ Operation: logical.UpdateOperation, ++ Path: "config", ++ Data: d, ++ } ++} ++ ++func TestBackend_basic(t *testing.T) { ++ b, err := Factory(context.Background(), &logical.BackendConfig{ ++ Logger: nil, ++ }) ++ if err != nil { ++ t.Fatalf("Unable to create backend: %s", err) ++ } ++ ++ logicaltest.Test(t, logicaltest.TestCase{ ++ PreCheck: func() { testAccPreCheck(t) }, ++ LogicalBackend: b, ++ Steps: []logicaltest.TestStep{ ++ testAccStepConfig(t, false), ++ testAccMap(t, "default", "fakepol"), ++ testAccMap(t, "oWnErs", "fakepol"), ++ testAccLogin(t, []string{"default", "fakepol"}), ++ testAccStepConfig(t, true), ++ testAccMap(t, "default", "fakepol"), ++ testAccMap(t, "oWnErs", "fakepol"), ++ testAccLogin(t, []string{"default", "fakepol"}), ++ testAccStepConfigWithBaseURL(t), ++ testAccMap(t, "default", "fakepol"), ++ testAccMap(t, "oWnErs", "fakepol"), ++ testAccLogin(t, []string{"default", "fakepol"}), ++ testAccMap(t, "default", "fakepol"), ++ testAccStepConfig(t, true), ++ mapUserToPolicy(t, os.Getenv("GITLAB_USER"), "userpolicy"), ++ testAccLogin(t, []string{"default", "fakepol", "userpolicy"}), ++ }, ++ }) ++} ++ ++func testAccPreCheck(t *testing.T) { ++ if v := os.Getenv("GITLAB_TOKEN"); v == "" { ++ t.Skip("GITLAB_TOKEN must be set for acceptance tests") ++ } ++ ++ if v := os.Getenv("GITLAB_GROUP"); v == "" { ++ t.Skip("GITLAB_GROUP must be set for acceptance tests") ++ } ++ ++ if v := os.Getenv("GITLAB_BASEURL"); v == "" { ++ t.Skip("GITLAB_BASEURL must be set for acceptance tests (use 'https://gitlab.com/api/v4/' if you don't know what you're doing)") ++ } ++} ++ ++func testAccStepConfig(t *testing.T, upper bool) logicaltest.TestStep { ++ ts := logicaltest.TestStep{ ++ Operation: logical.UpdateOperation, ++ Path: "config", ++ Data: map[string]interface{}{ ++ "organization": os.Getenv("GITLAB_GROUP"), ++ }, ++ } ++ if upper { ++ ts.Data["organization"] = strings.ToUpper(os.Getenv("GITLAB_GROUP")) ++ } ++ return ts ++} ++ ++func testAccStepConfigWithBaseURL(t *testing.T) logicaltest.TestStep { ++ return logicaltest.TestStep{ ++ Operation: logical.UpdateOperation, ++ Path: "config", ++ Data: map[string]interface{}{ ++ "organization": os.Getenv("GITLAB_GROUP"), ++ "base_url": os.Getenv("GITLAB_BASEURL"), ++ }, ++ } ++} ++ ++func testAccMap(t *testing.T, k string, v string) logicaltest.TestStep { ++ return logicaltest.TestStep{ ++ Operation: logical.UpdateOperation, ++ Path: "map/teams/" + k, ++ Data: map[string]interface{}{ ++ "value": v, ++ }, ++ } ++} ++ ++func mapUserToPolicy(t *testing.T, k string, v string) logicaltest.TestStep { ++ return logicaltest.TestStep{ ++ Operation: logical.UpdateOperation, ++ Path: "map/users/" + k, ++ Data: map[string]interface{}{ ++ "value": v, ++ }, ++ } ++} ++ ++func testAccLogin(t *testing.T, policies []string) logicaltest.TestStep { ++ return logicaltest.TestStep{ ++ Operation: logical.UpdateOperation, ++ Path: "login", ++ Data: map[string]interface{}{ ++ "token": os.Getenv("GITLAB_TOKEN"), ++ }, ++ Unauthenticated: true, ++ ++ Check: logicaltest.TestCheckAuth(policies), ++ } ++} +diff --git a/builtin/credential/gitlab/cli.go b/builtin/credential/gitlab/cli.go +new file mode 100644 +index 0000000000..705f7a16dd +--- /dev/null ++++ b/builtin/credential/gitlab/cli.go +@@ -0,0 +1,98 @@ ++package gitlab ++ ++import ( ++"fmt" ++"io" ++"os" ++"strings" ++ ++"github.com/hashicorp/errwrap" ++"github.com/hashicorp/vault/api" ++"github.com/hashicorp/vault/sdk/helper/password" ++) ++ ++// CLIHandler structure ++type CLIHandler struct { ++ // for tests ++ testStdout io.Writer ++} ++ ++// Auth return secret token ++func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { ++ mount, ok := m["mount"] ++ if !ok { ++ mount = "gitlab" ++ } ++ ++ // Extract or prompt for token ++ token := m["token"] ++ if token == "" { ++ token = os.Getenv("VAULT_AUTH_GITLAB_TOKEN") ++ } ++ if token == "" { ++ // Override the output ++ stdout := h.testStdout ++ if stdout == nil { ++ stdout = os.Stderr ++ } ++ ++ var err error ++ fmt.Fprintf(stdout, "Gitlab Access Token (will be hidden): ") ++ token, err = password.Read(os.Stdin) ++ fmt.Fprintf(stdout, "\n") ++ if err != nil { ++ if err == password.ErrInterrupted { ++ return nil, fmt.Errorf("user interrupted") ++ } ++ ++ return nil, errwrap.Wrapf("An error occurred attempting to "+ ++ "ask for a token. The raw error message is shown below, but usually "+ ++ "this is because you attempted to pipe a value into the command or "+ ++ "you are executing outside of a terminal (tty). If you want to pipe "+ ++ "the value, pass \"-\" as the argument to read from stdin. The raw "+ ++ "error was: {{err}}", err) ++ } ++ } ++ ++ path := fmt.Sprintf("auth/%s/login", mount) ++ secret, err := c.Logical().Write(path, map[string]interface{}{ ++ "token": strings.TrimSpace(token), ++ }) ++ if err != nil { ++ return nil, err ++ } ++ if secret == nil { ++ return nil, fmt.Errorf("empty response from credential provider") ++ } ++ ++ return secret, nil ++} ++ ++// Help return help message ++func (h *CLIHandler) Help() string { ++ help := ` ++Usage: vault login -method=gitlab [CONFIG K=V...] ++ ++ The Gitlab auth method allows users to authenticate using a Gitlab ++ access token. Users can generate a personal access token from the ++ settings page on their Gitlab account. ++ ++ Authenticate using a Gitlab token: ++ ++ $ vault login -method=gitlab token=abcd1234 ++ ++Configuration: ++ ++ mount= ++ Path where the Gitlab credential method is mounted. This is usually ++ provided via the -path flag in the "vault login" command, but it can be ++ specified here as well. If specified here, it takes precedence over the ++ value for -path. The default value is "gitlab". ++ ++ token= ++ Gitlab access token to use for authentication. If not provided, ++ Vault will prompt for the value. ++` ++ ++ return strings.TrimSpace(help) ++} +diff --git a/builtin/credential/gitlab/clients.go b/builtin/credential/gitlab/clients.go +new file mode 100644 +index 0000000000..c3212de492 +--- /dev/null ++++ b/builtin/credential/gitlab/clients.go +@@ -0,0 +1,40 @@ ++package gitlab ++ ++import ( ++ "errors" ++ "github.com/xanzy/go-gitlab" ++ "strconv" ++ "strings" ++) ++ ++func (b *backend) TokenClient(baseUrl string, token string) (*gitlab.Client, error) { ++ if strings.HasPrefix(token, "OAuth-") { ++ return gitlab.NewOAuthClient(strings.TrimPrefix(token, "OAuth-"), gitlab.WithBaseURL(baseUrl)) ++ } ++ return gitlab.NewClient(token, gitlab.WithBaseURL(baseUrl)) ++} ++ ++// -------------------------------- ++ ++func (b *backend) JobClient(baseURL, CIToken, project, job, commit, token string) (*gitlab.Client, error) { ++ client, err := gitlab.NewClient(CIToken, gitlab.WithBaseURL(baseURL)) ++ if err != nil { ++ return nil, err ++ } ++ ++ jobID, err := strconv.Atoi(job) ++ if err != nil { ++ return nil, err ++ } ++ ++ j, _, err := client.Jobs.GetJob(project, jobID) ++ if err != nil { ++ return nil, err ++ } ++ ++ if j.Status != string(gitlab.Running) || j.Commit.ID != commit { ++ return nil, errors.New("invalid job arguments") ++ } ++ ++ return client, nil ++} +diff --git a/builtin/credential/gitlab/cmd/gitlab/main.go b/builtin/credential/gitlab/cmd/gitlab/main.go +new file mode 100644 +index 0000000000..3e2b4b4ca5 +--- /dev/null ++++ b/builtin/credential/gitlab/cmd/gitlab/main.go +@@ -0,0 +1,29 @@ ++package main ++ ++import ( ++ "github.com/hashicorp/vault/api" ++ "github.com/hashicorp/vault/builtin/credential/gitlab" ++ "os" ++ ++ hclog "github.com/hashicorp/go-hclog" ++ "github.com/hashicorp/vault/sdk/plugin" ++) ++ ++func main() { ++ apiClientMeta := &api.PluginAPIClientMeta{} ++ flags := apiClientMeta.FlagSet() ++ flags.Parse(os.Args[1:]) ++ ++ tlsConfig := apiClientMeta.GetTLSConfig() ++ tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) ++ ++ if err := plugin.Serve(&plugin.ServeOpts{ ++ BackendFactoryFunc: gitlab.Factory, ++ TLSProviderFunc: tlsProviderFunc, ++ }); err != nil { ++ logger := hclog.New(&hclog.LoggerOptions{}) ++ ++ logger.Error("plugin shutting down", "error", err) ++ os.Exit(1) ++ } ++} +diff --git a/builtin/credential/gitlab/path_config.go b/builtin/credential/gitlab/path_config.go +new file mode 100644 +index 0000000000..c378f6e9f4 +--- /dev/null ++++ b/builtin/credential/gitlab/path_config.go +@@ -0,0 +1,202 @@ ++package gitlab ++ ++import ( ++ "context" ++ "fmt" ++ "github.com/hashicorp/errwrap" ++ "github.com/hashicorp/vault/sdk/framework" ++ "github.com/hashicorp/vault/sdk/logical" ++ "github.com/xanzy/go-gitlab" ++ "net/http" ++ "net/url" ++) ++ ++func pathConfig(b *backend) *framework.Path { ++ return &framework.Path{ ++ Pattern: "config", ++ Fields: map[string]*framework.FieldSchema{ ++ "base_url": { ++ Type: framework.TypeString, ++ Description: "The Gitlab API endpoint to use.", ++ }, ++ "min_access_level": { ++ Type: framework.TypeString, ++ Description: "The minimal project access level that users must have", ++ Default: "guest", ++ }, ++ "app_id": { ++ Type: framework.TypeString, ++ Description: "The OAuth appId", ++ Default: "", ++ }, ++ "app_secret": { ++ Type: framework.TypeString, ++ Description: "The OAuth appSecret", ++ Default: "", ++ }, ++ "callback_url": { ++ Type: framework.TypeString, ++ Description: "The Vault OAuth API endpoint to use.", ++ Default: "", ++ }, ++ "ci_token": { ++ Type: framework.TypeString, ++ Description: "The CI token API to use.", ++ Default: "", ++ }, ++ "vault_service_token": { ++ Type: framework.TypeString, ++ Description: "Vault service token.", ++ Default: "", ++ }, ++ }, ++ ++ Operations: map[logical.Operation]framework.OperationHandler{ ++ logical.UpdateOperation: &framework.PathOperation{ ++ Callback: b.pathConfigWrite, ++ Responses: map[int][]framework.Response{ ++ http.StatusOK: {{ ++ Description: http.StatusText(http.StatusOK), ++ }}, ++ }, ++ }, ++ logical.ReadOperation: &framework.PathOperation{ ++ Callback: b.pathConfigRead, ++ Responses: map[int][]framework.Response{ ++ http.StatusOK: {{ ++ Description: http.StatusText(http.StatusOK), ++ }}, ++ }, ++ }, ++ }, ++ } ++} ++ ++func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { ++ baseURL := data.Get("base_url").(string) ++ if len(baseURL) > 0 { ++ _, err := url.Parse(baseURL) ++ if err != nil { ++ return logical.ErrorResponse(fmt.Sprintf("Error parsing given base_url: %s", err)), nil ++ } ++ } ++ minAccessLevel := data.Get("min_access_level").(string) ++ appID := data.Get("app_id").(string) ++ appSecret := data.Get("app_secret").(string) ++ callbackURL := data.Get("callback_url").(string) ++ ciToken := data.Get("ci_token").(string) ++ vault_service_token := data.Get("vault_service_token").(string) ++ if len(callbackURL) > 0 { ++ _, err := url.Parse(callbackURL) ++ if err != nil { ++ return logical.ErrorResponse(fmt.Sprintf("Error parsing given callback_url: %s", err)), nil ++ } ++ } ++ entry, err := logical.StorageEntryJSON("config", config{ ++ BaseURL: baseURL, ++ MinAccessLevel: minAccessLevel, ++ AppID: appID, ++ AppSecret: appSecret, ++ CallbackURL: callbackURL, ++ CIToken: ciToken, ++ VaultServiceToken: vault_service_token, ++ }) ++ ++ if err != nil { ++ return nil, err ++ } ++ ++ if err := req.Storage.Put(ctx, entry); err != nil { ++ return nil, err ++ } ++ ++ return nil, nil ++} ++ ++func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { ++ config, err := b.Config(ctx, req.Storage) ++ if err != nil { ++ return nil, err ++ } ++ ++ if config == nil { ++ return nil, fmt.Errorf("configuration object not found") ++ } ++ ++ resp := &logical.Response{ ++ Data: map[string]interface{}{ ++ "base_url": config.BaseURL, ++ "min_access_level": config.MinAccessLevel, ++ "app_id": config.AppID, ++ "app_secret": config.AppSecret, ++ "callback_url": config.CallbackURL, ++ "ci_token": config.CIToken, ++ }, ++ } ++ return resp, nil ++} ++ ++// Config returns the configuration for this backend. ++func (b *backend) Config(ctx context.Context, s logical.Storage) (*config, error) { ++ entry, err := s.Get(ctx, "config") ++ if err != nil { ++ return nil, err ++ } ++ ++ var result config ++ if entry != nil { ++ if err := entry.DecodeJSON(&result); err != nil { ++ return nil, errwrap.Wrapf("error reading configuration: {{err}}", err) ++ } ++ } ++ ++ return &result, nil ++} ++ ++func (b *backend) AccessLevelValue(level string) *gitlab.AccessLevelValue { ++ if level == "" { ++ return gitlab.AccessLevel(gitlab.OwnerPermission) ++ } ++ return gitlab.AccessLevel(accessLevelNameToValue[level]) ++} ++ ++// AllAccessValuesFromLevel Meaning for a owner access, we also have access to developer, guest, etc. ++func (b *backend) AllAccessValuesFromLevel(level string) []string { ++ var accessLevelValues []string ++ ++ var start = int(*b.AccessLevelValue(level)) ++ for k, v := range accessLevelNameToValue { ++ if int(v) <= start && v != 0 { // not adding "none" level access ++ accessLevelValues = append(accessLevelValues, k) ++ } ++ } ++ return accessLevelValues ++} ++ ++func (b *backend) AccessLevelValueToString(level gitlab.AccessLevelValue) string { ++ for k, v := range accessLevelNameToValue { ++ if v == level { ++ return k ++ } ++ } ++ return "none" ++} ++ ++var accessLevelNameToValue = map[string]gitlab.AccessLevelValue{ ++ "none": gitlab.NoPermissions, ++ "guest": gitlab.GuestPermissions, ++ "reporter": gitlab.ReporterPermissions, ++ "developer": gitlab.DeveloperPermissions, ++ "maintainer": gitlab.MaintainerPermissions, ++ "owner": gitlab.OwnerPermissions, ++} ++ ++type config struct { ++ BaseURL string `json:"baseURL" structs:"baseURL" mapstructure:"baseURL"` ++ MinAccessLevel string `json:"minAccessLevel" structs:"minAccessLevel" mapstructure:"minAccessLevel"` ++ AppID string `json:"appID" structs:"appID" mapstructure:"appID"` ++ AppSecret string `json:"appSecret" structs:"appSecret" mapstructure:"appSecret"` ++ CallbackURL string `json:"callbackURL" structs:"callbackURL" mapstructure:"callbackURL"` ++ CIToken string `json:"ciToken" structs:"ciToken" mapstructure:"ciToken"` ++ VaultServiceToken string `json:"vaultServiceToken" structs:"vaultServiceToken" mapstructure:"vaultServiceToken"` ++} +diff --git a/builtin/credential/gitlab/path_login_commons.go b/builtin/credential/gitlab/path_login_commons.go +new file mode 100644 +index 0000000000..7fbfd96cd6 +--- /dev/null ++++ b/builtin/credential/gitlab/path_login_commons.go +@@ -0,0 +1,201 @@ ++package gitlab ++ ++import ( ++ "context" ++ "fmt" ++ "github.com/hashicorp/vault/sdk/logical" ++ "github.com/hasura/go-graphql-client" ++ "github.com/xanzy/go-gitlab" ++ "net/http" ++ "slices" ++ "strings" ++) ++ ++func (b *backend) pathLoginOk(verifyResp *verifyCredentialsResp, internalData map[string]interface{}) *logical.Response { ++ resp := &logical.Response{ ++ Auth: &logical.Auth{ ++ InternalData: internalData, ++ Metadata: map[string]string{ ++ "username": verifyResp.Username, ++ }, ++ DisplayName: verifyResp.Username, ++ LeaseOptions: logical.LeaseOptions{ ++ Renewable: false, ++ }, ++ Alias: &logical.Alias{ ++ Name: verifyResp.Username, ++ }, ++ EntityID: verifyResp.Username, ++ }, ++ } ++ ++ if verifyResp.IsAdmin { ++ if b.Logger().IsDebug() { ++ b.Logger().Debug("User " + verifyResp.Username + " is admin") ++ } ++ resp.Auth.Policies = append(resp.Auth.Policies, "admins") ++ } ++ ++ for _, name := range verifyResp.Rights { ++ resp.Auth.GroupAliases = append(resp.Auth.GroupAliases, &logical.Alias{Name: name}) ++ resp.Auth.Policies = append(resp.Auth.Policies, name) ++ } ++ ++ return resp ++} ++ ++func (b *backend) parseAdminRights(username string, isAdmin bool, access []string) (*verifyCredentialsResp, error) { ++ if isAdmin { ++ if b.Logger().IsDebug() { ++ b.Logger().Debug("User " + username + " is admin") ++ } ++ access = append(access, "admins") ++ } ++ ++ return &verifyCredentialsResp{ ++ Username: username, ++ Rights: access, ++ IsAdmin: isAdmin, ++ }, nil ++} ++ ++func (b *backend) getProjectsAndGroupsAccess(ctx context.Context, req *logical.Request, client *gitlab.Client, userToken string) (access []string, err error) { ++ config, err := b.Config(ctx, req.Storage) ++ if err != nil { ++ return nil, err ++ } ++ var noneAccessLevels []string ++ minAccessLevel := int(accessLevelNameToValue[config.MinAccessLevel]) ++ lastCursorProjects := "" ++ nextPageProjects := true ++ lastCursorGroups := "" ++ nextPageGroups := true ++ graphQLClient := graphql.NewClient(fmt.Sprintf("%s://%s/api/graphql", client.BaseURL().Scheme, client.BaseURL().Host), http.DefaultClient).WithRequestModifier(func(r *http.Request) { ++ r.Header.Set("Authorization", "Bearer "+userToken) ++ }) ++ ++ for nextPageProjects || nextPageGroups { ++ var query graphQLProjectsGroups ++ variables := map[string]interface{}{ ++ "afterProjects": lastCursorProjects, ++ "afterGroups": lastCursorGroups, ++ } ++ err := graphQLClient.Query(context.Background(), &query, variables) ++ if err != nil { ++ b.Logger().Error("Something went wrong while getting query " + err.Error()) ++ break ++ } ++ ++ // parse GraphQL groups and projects response ++ if nextPageProjects { ++ var tmpMaxRights []string ++ ++ nextPageProjects = query.Projects.PageInfo.HasNextPage ++ tmpMaxRights, _, lastCursorProjects = b.parseGraphQLEdgesToMaxRights(query.Projects.Edges, minAccessLevel) ++ access = append(access, tmpMaxRights...) ++ } ++ // we can only get group direct membership, so we keep "none" access groups ++ if nextPageGroups { ++ var tmpMaxRights, tmpNone []string ++ ++ nextPageGroups = query.Groups.PageInfo.HasNextPage ++ tmpMaxRights, tmpNone, lastCursorGroups = b.parseGraphQLEdgesToMaxRights(query.Groups.Edges, minAccessLevel) ++ access = append(access, tmpMaxRights...) ++ noneAccessLevels = append(noneAccessLevels, tmpNone...) ++ } ++ ++ } ++ // parse "none" access groups to their MaxAccess according to root groups ++ access = append(access, b.parseNoneAccessLevelsToMaxRights(access, noneAccessLevels)...) ++ // parse all rights access inheritance (E.g: owner = owner + maintainer + dev + guest) ++ access = b.parseMaxRightsToRights(access) ++ ++ var shownPolicies []string ++ ++ // If no policy loaded, return all policies. ++ if len(b.vaultPolicies) == 0 { ++ return access, nil ++ } ++ ++ // Else, do a filter ++ for _, policy := range b.vaultPolicies { ++ if slices.Contains(access, policy) { ++ shownPolicies = append(shownPolicies, policy) ++ } ++ } ++ ++ return shownPolicies, nil ++} ++ ++// parseGraphQLEdgesToMaxRights parses GraphQL results to MaxAccess rights, also returns "none" access levels ++func (b *backend) parseGraphQLEdgesToMaxRights(edges []graphQLEdge, minAccessLevel int) (rights []string, nonAccessLevels []string, lastCursor string) { ++ for _, e := range edges { ++ accessLevelInt := int(e.Node.MaxAccessLevel.IntegerValue) ++ if accessLevelInt >= minAccessLevel && accessLevelInt > 0 { ++ rights = append(rights, fmt.Sprintf("%s:%s", strings.ReplaceAll(strings.ReplaceAll(e.Node.FullPath, "/", ":"), "-", "_"), b.AccessLevelValueToString(e.Node.MaxAccessLevel.IntegerValue))) ++ } ++ if accessLevelInt == 0 { ++ nonAccessLevels = append(nonAccessLevels, strings.ReplaceAll(strings.ReplaceAll(e.Node.FullPath, "/", ":"), "-", "_")) ++ } ++ lastCursor = e.Cursor ++ } ++ return ++} ++ ++// parseNoneAccessLevelsToMaxRights parses "none" access level rights to MaxAccess rights if user has inherited rights ++func (b *backend) parseNoneAccessLevelsToMaxRights(access, noneAccess []string) (rights []string) { ++ for _, value := range noneAccess { ++ for _, aValue := range access { ++ splitAValue := strings.Split(aValue, ":") ++ aValueWithoutAccess, _ := strings.CutSuffix(aValue, ":"+splitAValue[len(splitAValue)-1]) // remove last :access ++ if strings.HasPrefix(value, aValueWithoutAccess) { ++ rights = append(rights, fmt.Sprintf("%s:%s", value, splitAValue[len(splitAValue)-1])) ++ break // only has one per group / project ++ } ++ } ++ } ++ return ++} ++ ++// parseMaxRightsToRights parses MaxAccess rights to all rights ++func (b *backend) parseMaxRightsToRights(maxRights []string) (rights []string) { ++ for _, r := range maxRights { ++ rSplit := strings.Split(r, ":") ++ rWithoutAccess, _ := strings.CutSuffix(r, ":"+rSplit[len(rSplit)-1]) // remove last :access ++ for _, level := range b.AllAccessValuesFromLevel(rSplit[len(rSplit)-1]) { ++ rights = append(rights, fmt.Sprintf("%s:%s", rWithoutAccess, level)) ++ } ++ } ++ return ++} ++ ++type verifyCredentialsResp struct { ++ Username string ++ Rights []string ++ IsAdmin bool ++} ++ ++type graphQLProjectsGroups struct { ++ Projects struct { ++ PageInfo struct { ++ HasNextPage bool `json:"hasNextPage"` ++ } `json:"pageInfo"` ++ Edges []graphQLEdge ++ } `graphql:"projects(membership:true,after:$afterProjects)"` ++ Groups struct { ++ PageInfo struct { ++ HasNextPage bool `json:"hasNextPage"` ++ } `json:"pageInfo"` ++ Edges []graphQLEdge ++ } `graphql:"groups(after:$afterGroups)"` ++} ++ ++type graphQLEdge struct { ++ Cursor string ++ Node struct { ++ FullPath string `json:"fullPath"` ++ MaxAccessLevel struct { ++ IntegerValue gitlab.AccessLevelValue `json:"integerValue"` ++ } `json:"maxAccessLevel"` ++ } ++} +diff --git a/builtin/credential/gitlab/path_login_job.go b/builtin/credential/gitlab/path_login_job.go +new file mode 100644 +index 0000000000..16727802ea +--- /dev/null ++++ b/builtin/credential/gitlab/path_login_job.go +@@ -0,0 +1,139 @@ ++package gitlab ++ ++import ( ++ "context" ++ "encoding/json" ++ "errors" ++ "fmt" ++ "github.com/hashicorp/go-cleanhttp" ++ "github.com/hashicorp/vault/sdk/framework" ++ "github.com/hashicorp/vault/sdk/logical" ++ "github.com/xanzy/go-gitlab" ++ "io" ++ "net/http" ++ "net/url" ++ "time" ++) ++ ++func pathLoginJob(b *backend) *framework.Path { ++ return &framework.Path{ ++ Pattern: `ci`, ++ Fields: map[string]*framework.FieldSchema{ ++ "token": {Type: framework.TypeString, Description: "Gitlab Job token"}, ++ }, ++ Operations: map[logical.Operation]framework.OperationHandler{ ++ logical.UpdateOperation: &framework.PathOperation{ ++ Callback: b.pathLoginByJob, ++ Responses: map[int][]framework.Response{ ++ http.StatusOK: {{ ++ Description: http.StatusText(http.StatusOK), ++ }}, ++ }, ++ }, ++ }, ++ } ++} ++ ++func (b *backend) pathLoginByJob(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { ++ ++ config, err := b.Config(ctx, req.Storage) ++ if err != nil { ++ return nil, err ++ } ++ ++ if config.CIToken == "" { ++ return nil, fmt.Errorf("config CI access disabled") ++ } ++ ++ // Get Job ++ job, err := b.getJobByToken(config, data.Get("token").(string)) ++ if err != nil { ++ return nil, err ++ } ++ ++ // Admin client ++ client, err := b.TokenClient(config.BaseURL, config.CIToken) ++ if err != nil { ++ return nil, err ++ } ++ ++ // Generate impersonation token for this user ++ name := "vault-connexion" ++ scopes := []string{"read_api"} ++ expTime := time.Now().Add(time.Hour * 24) ++ token, _, err := client.Users.CreateImpersonationToken(job.User.ID, &gitlab.CreateImpersonationTokenOptions{ ++ Name: &name, ++ Scopes: &scopes, ++ ExpiresAt: &expTime, ++ }) ++ if err != nil { ++ return nil, err ++ } ++ ++ // Get rights ++ userClient, err := b.TokenClient(config.BaseURL, token.Token) ++ if err != nil { ++ return nil, err ++ } ++ ++ user, _, err := userClient.Users.CurrentUser() ++ if err != nil { ++ return nil, err ++ } ++ ++ access, err := b.getProjectsAndGroupsAccess(ctx, req, userClient, token.Token) ++ if err != nil { ++ return nil, err ++ } ++ ++ // Revoke impersonation ++ _, err = client.Users.RevokeImpersonationToken(job.User.ID, token.ID) ++ if err != nil { ++ return nil, err ++ } ++ ++ // Finalize connection ++ if verifyResponse, err := b.parseAdminRights(user.Username, user.IsAdmin, access); err != nil { ++ return nil, err ++ } else { ++ return b.pathLoginOk(verifyResponse, map[string]interface{}{"token": data.Get("token").(string)}), nil ++ } ++} ++ ++func (b *backend) getJobByToken(config *config, jobToken string) (job gitlab.Job, err error) { ++ // Verify that job is running, and user id. ++ u, err := url.Parse(fmt.Sprintf("%s/api/v4/job", config.BaseURL)) ++ if err != nil { ++ return ++ } ++ ++ headers := make(http.Header) ++ headers.Add("JOB-TOKEN", jobToken) ++ ++ resp, err := cleanhttp.DefaultClient().Do(&http.Request{ ++ Method: "GET", ++ URL: u, ++ Proto: "HTTP/1.1", ++ ProtoMajor: 1, ++ ProtoMinor: 1, ++ Header: headers, ++ Host: u.Host, ++ }) ++ if err != nil { ++ return ++ } ++ if resp.StatusCode != http.StatusOK { ++ err = errors.New(resp.Status) ++ return ++ } ++ ++ data2, _ := io.ReadAll(resp.Body) ++ _ = json.Unmarshal(data2, &job) ++ ++ if job.Status != "running" { ++ err = errors.New("job is not running anymore ; could not generate token") ++ return ++ } ++ ++ return ++} +diff --git a/builtin/credential/gitlab/path_login_oauth.go b/builtin/credential/gitlab/path_login_oauth.go +new file mode 100644 +index 0000000000..262d0f7872 +--- /dev/null ++++ b/builtin/credential/gitlab/path_login_oauth.go +@@ -0,0 +1,209 @@ ++package gitlab ++ ++import ( ++ "context" ++ "crypto/aes" ++ "crypto/cipher" ++ "crypto/rand" ++ "encoding/base64" ++ "errors" ++ "fmt" ++ "github.com/hashicorp/vault/sdk/framework" ++ "github.com/hashicorp/vault/sdk/logical" ++ "github.com/xanzy/go-gitlab" ++ "golang.org/x/oauth2" ++ "io" ++ "net/http" ++ "net/url" ++ "strconv" ++ "time" ++) ++ ++func pathOauthLogin(b *backend) *framework.Path { ++ return &framework.Path{ ++ Pattern: `oauth`, ++ Fields: map[string]*framework.FieldSchema{ ++ "code": {Type: framework.TypeString, Description: "Gitlab API code"}, ++ "state": {Type: framework.TypeString, Description: "Gitlab API state", Default: ""}, ++ }, ++ ++ Operations: map[logical.Operation]framework.OperationHandler{ ++ logical.UpdateOperation: &framework.PathOperation{ ++ Callback: b.pathOauthLogin, ++ Responses: map[int][]framework.Response{ ++ http.StatusOK: {{ ++ Description: http.StatusText(http.StatusOK), ++ }}, ++ }, ++ }, ++ logical.ReadOperation: &framework.PathOperation{ ++ Callback: b.pathOauthLogin, ++ Responses: map[int][]framework.Response{ ++ http.StatusOK: {{ ++ Description: http.StatusText(http.StatusOK), ++ }}, ++ }, ++ }, ++ }, ++ } ++} ++ ++func (b *backend) pathOauthLogin(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { ++ config, err := b.Config(ctx, req.Storage) ++ if err != nil { ++ return nil, err ++ } ++ ++ if config.AppSecret == "" || config.AppID == "" || config.CallbackURL == "" { ++ return nil, fmt.Errorf("config OAuth disabled") ++ } ++ ++ baseURL, _ := url.Parse(config.BaseURL) ++ callbackURL, _ := url.Parse(config.CallbackURL) ++ ++ oauth2Conf := &oauth2.Config{ ++ ClientID: config.AppID, ++ ClientSecret: config.AppSecret, ++ Endpoint: oauth2.Endpoint{ ++ AuthURL: fmt.Sprintf("%s://%s/oauth/authorize", baseURL.Scheme, baseURL.Host), ++ TokenURL: fmt.Sprintf("%s://%s/oauth/token", baseURL.Scheme, baseURL.Host), ++ }, ++ Scopes: []string{"api", "read_user"}, ++ RedirectURL: fmt.Sprintf("%s://%s/v1/%s%s", callbackURL.Scheme, callbackURL.Host, req.MountPoint, req.Path), ++ } ++ ++ code, _ := data.GetOk("code") ++ ++ if code != nil { ++ state := data.Get("state") ++ err = b.CheckState(state.(string)) ++ if err != nil { ++ return nil, err ++ } ++ ++ token, err := oauth2Conf.Exchange(ctx, code.(string)) ++ if err != nil { ++ return nil, err ++ } ++ client, err := b.TokenClient(config.BaseURL, "OAuth-"+token.AccessToken) ++ if err != nil { ++ return nil, err ++ } ++ ++ user, _, err := client.Users.CurrentUser() ++ if err != nil { ++ return nil, err ++ } ++ ++ // Admin client ++ adminClient, err := b.TokenClient(config.BaseURL, config.CIToken) ++ if err != nil { ++ return nil, err ++ } ++ ++ // Generate impersonation token for this user ++ name := "vault-connexion" ++ scopes := []string{"read_api"} ++ expTime := time.Now().Add(time.Hour * 24) ++ userToken, _, err := adminClient.Users.CreateImpersonationToken(user.ID, &gitlab.CreateImpersonationTokenOptions{ ++ Name: &name, ++ Scopes: &scopes, ++ ExpiresAt: &expTime, ++ }) ++ if err != nil { ++ return nil, err ++ } ++ ++ access, err := b.getProjectsAndGroupsAccess(ctx, req, client, userToken.Token) ++ if err != nil { ++ b.Logger().Error("ERROR : ", err.Error()) ++ return nil, err ++ } ++ ++ // Revoke impersonation ++ _, err = adminClient.Users.RevokeImpersonationToken(user.ID, userToken.ID) ++ if err != nil { ++ return nil, err ++ } ++ ++ if verifyResponse, err := b.parseAdminRights(user.Username, user.IsAdmin, access); err != nil { ++ return nil, err ++ } else { ++ response := b.pathLoginOk(verifyResponse, map[string]interface{}{ ++ "token": token.AccessToken, ++ }) ++ wrappedResponse, err := b.System().ResponseWrapData(ctx, map[string]interface{}{ ++ "authType": "gitlab", ++ "token": "OAuth-" + token.AccessToken, ++ }, time.Second*60, false) ++ if err != nil { ++ return nil, err ++ } ++ response.Redirect = "/ui/vault/auth?with=gitlab&wrapped_token=" + wrappedResponse.Token ++ return response, nil ++ } ++ } else { ++ state, err := b.State() ++ if err != nil { ++ return nil, err ++ } ++ return &logical.Response{ ++ Redirect: oauth2Conf.AuthCodeURL(state, oauth2.AccessTypeOffline), ++ }, nil ++ } ++} ++ ++func (b *backend) State() (encoded string, err error) { ++ plainText := []byte(strconv.FormatInt(time.Now().UnixNano(), 10)) ++ ++ block, err := aes.NewCipher(b.CipherKey) ++ if err != nil { ++ return ++ } ++ ++ //IV needs to be unique, but doesn't have to be secure. ++ //It's common to put it at the beginning of the ciphertext. ++ cipherText := make([]byte, aes.BlockSize+len(plainText)) ++ iv := cipherText[:aes.BlockSize] ++ if _, err = io.ReadFull(rand.Reader, iv); err != nil { ++ return ++ } ++ ++ stream := cipher.NewCFBEncrypter(block, iv) ++ stream.XORKeyStream(cipherText[aes.BlockSize:], plainText) ++ ++ //returns to base64 encoded string ++ encoded = base64.URLEncoding.EncodeToString(cipherText) ++ return ++} ++ ++func (b *backend) CheckState(secureState string) (err error) { ++ now := time.Now().UnixNano() ++ cipherText, err := base64.URLEncoding.DecodeString(secureState) ++ if err != nil { ++ return ++ } ++ ++ block, err := aes.NewCipher(b.CipherKey) ++ if err != nil { ++ return ++ } ++ ++ if len(cipherText) < aes.BlockSize { ++ err = errors.New("illegal State") ++ return ++ } ++ ++ iv := cipherText[:aes.BlockSize] ++ cipherText = cipherText[aes.BlockSize:] ++ ++ stream := cipher.NewCFBDecrypter(block, iv) ++ // XORKeyStream can work in-place if the two arguments are the same. ++ stream.XORKeyStream(cipherText, cipherText) ++ ++ decoded, err := strconv.ParseInt(string(cipherText), 10, 64) ++ if err == nil && (decoded > now || now-decoded > 60*int64(time.Second)) { ++ err = errors.New("illegal State") ++ } ++ return ++} +diff --git a/builtin/credential/gitlab/path_login_tokn.go b/builtin/credential/gitlab/path_login_tokn.go +new file mode 100644 +index 0000000000..b7c24d82be +--- /dev/null ++++ b/builtin/credential/gitlab/path_login_tokn.go +@@ -0,0 +1,85 @@ ++package gitlab ++ ++import ( ++ "context" ++ "github.com/hashicorp/vault/sdk/framework" ++ "github.com/hashicorp/vault/sdk/logical" ++ "github.com/xanzy/go-gitlab" ++ "net/http" ++ "time" ++) ++ ++func pathLoginToken(b *backend) *framework.Path { ++ return &framework.Path{ ++ Pattern: `login`, ++ Fields: map[string]*framework.FieldSchema{ ++ "token": {Type: framework.TypeString, Description: "Gitlab API token"}, ++ }, ++ ++ Operations: map[logical.Operation]framework.OperationHandler{ ++ logical.UpdateOperation: &framework.PathOperation{ ++ Callback: b.pathLoginByToken, ++ Responses: map[int][]framework.Response{ ++ http.StatusOK: {{ ++ Description: http.StatusText(http.StatusOK), ++ }}, ++ }, ++ }, ++ }, ++ } ++} ++ ++func (b *backend) pathLoginByToken(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { ++ config, err := b.Config(ctx, req.Storage) ++ if err != nil { ++ return nil, err ++ } ++ ++ client, err := b.TokenClient(config.BaseURL, data.Get("token").(string)) ++ if err != nil { ++ return nil, err ++ } ++ ++ user, _, err := client.Users.CurrentUser() ++ if err != nil { ++ return nil, err ++ } ++ ++ // Admin client ++ adminClient, err := b.TokenClient(config.BaseURL, config.CIToken) ++ if err != nil { ++ return nil, err ++ } ++ ++ // Generate impersonation token for this user ++ name := "vault-connexion" ++ scopes := []string{"read_api"} ++ expTime := time.Now().Add(time.Hour * 24) ++ userToken, _, err := adminClient.Users.CreateImpersonationToken(user.ID, &gitlab.CreateImpersonationTokenOptions{ ++ Name: &name, ++ Scopes: &scopes, ++ ExpiresAt: &expTime, ++ }) ++ if err != nil { ++ return nil, err ++ } ++ ++ access, err := b.getProjectsAndGroupsAccess(ctx, req, client, userToken.Token) ++ if err != nil { ++ return nil, err ++ } ++ ++ // Revoke impersonation ++ _, err = adminClient.Users.RevokeImpersonationToken(user.ID, userToken.ID) ++ if err != nil { ++ return nil, err ++ } ++ ++ if verifyResponse, err := b.parseAdminRights(user.Username, user.IsAdmin, access); err != nil { ++ return nil, err ++ } else { ++ return b.pathLoginOk(verifyResponse, map[string]interface{}{ ++ "token": data.Get("token").(string), ++ }), nil ++ } ++} +diff --git a/command/base_predict.go b/command/base_predict.go +index ed3edfa30f..70fe7e73d2 100644 +--- a/command/base_predict.go ++++ b/command/base_predict.go +@@ -111,6 +111,7 @@ func (b *BaseCommand) PredictVaultAvailableAuths() complete.Predictor { + "cert", + "gcp", + "github", ++ "gitlab", + "ldap", + "okta", + "plugin", +diff --git a/command/base_predict_test.go b/command/base_predict_test.go +index 387b8f0b84..477eda9c5f 100644 +--- a/command/base_predict_test.go ++++ b/command/base_predict_test.go +@@ -359,6 +359,7 @@ func TestPredict_Plugins(t *testing.T) { + "gcp", + "gcpkms", + "github", ++ "gitlab", + "hana-database-plugin", + "influxdb-database-plugin", + "jwt", +diff --git a/command/commands_full.go b/command/commands_full.go +index 8db22350cf..18bb2845b9 100644 +--- a/command/commands_full.go ++++ b/command/commands_full.go +@@ -15,6 +15,7 @@ import ( + credOCI "github.com/hashicorp/vault-plugin-auth-oci" + credAws "github.com/hashicorp/vault/builtin/credential/aws" + credGitHub "github.com/hashicorp/vault/builtin/credential/github" ++ credGitlab "github.com/hashicorp/vault/builtin/credential/gitlab" + credLdap "github.com/hashicorp/vault/builtin/credential/ldap" + credOkta "github.com/hashicorp/vault/builtin/credential/okta" + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" +@@ -75,6 +76,7 @@ func newFullAddonHandlers() (map[string]physical.Factory, map[string]LoginHandle + "cf": &credCF.CLIHandler{}, + "gcp": &credGcp.CLIHandler{}, + "github": &credGitHub.CLIHandler{}, ++ "gitlab": &credGitlab.CLIHandler{}, + "kerberos": &credKerb.CLIHandler{}, + "ldap": &credLdap.CLIHandler{}, + "oci": &credOCI.CLIHandler{}, +diff --git a/go.mod b/go.mod +index 60d5fddaae..4a5f128e93 100644 +--- a/go.mod ++++ b/go.mod +@@ -231,9 +231,11 @@ require ( + require ( + cel.dev/expr v0.15.0 // indirect + cloud.google.com/go/longrunning v0.6.0 // indirect ++ github.com/coder/websocket v1.8.12 // indirect + github.com/containerd/containerd v1.7.20 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/hashicorp/go-secure-stdlib/httputil v0.1.0 // indirect ++ github.com/hasura/go-graphql-client v0.13.1 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect +diff --git a/go.sum b/go.sum +index 540b38a0b7..62ea393074 100644 +--- a/go.sum ++++ b/go.sum +@@ -925,6 +925,8 @@ github.com/cockroachdb/cockroach-go/v2 v2.3.8 h1:53yoUo4+EtrC1NrAEgnnad4AS3ntNvG + github.com/cockroachdb/cockroach-go/v2 v2.3.8/go.mod h1:9uH5jK4yQ3ZQUT9IXe4I2fHzMIF5+JC/oOdzTRgJYJk= + github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= + github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= ++github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= ++github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= + github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ= + github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0= + github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +@@ -1605,6 +1607,8 @@ github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyu + github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo= + github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= + github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= ++github.com/hasura/go-graphql-client v0.13.1 h1:kKbjhxhpwz58usVl+Xvgah/TDha5K2akNTRQdsEHN6U= ++github.com/hasura/go-graphql-client v0.13.1/go.mod h1:k7FF7h53C+hSNFRG3++DdVZWIuHdCaTbI7siTJ//zGQ= + github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= + github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= + github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +diff --git a/helper/builtinplugins/registry_full.go b/helper/builtinplugins/registry_full.go +index ac56d139df..3e72a3a2da 100644 +--- a/helper/builtinplugins/registry_full.go ++++ b/helper/builtinplugins/registry_full.go +@@ -32,6 +32,7 @@ import ( + logicalTerraform "github.com/hashicorp/vault-plugin-secrets-terraform" + credAws "github.com/hashicorp/vault/builtin/credential/aws" + credGitHub "github.com/hashicorp/vault/builtin/credential/github" ++ credGitlab "github.com/hashicorp/vault/builtin/credential/gitlab" + credLdap "github.com/hashicorp/vault/builtin/credential/ldap" + credOkta "github.com/hashicorp/vault/builtin/credential/okta" + credRadius "github.com/hashicorp/vault/builtin/credential/radius" +@@ -70,6 +71,7 @@ func newFullAddonRegistry() *registry { + pluginconsts.AuthTypeLDAP: {Factory: credLdap.Factory}, + pluginconsts.AuthTypeOCI: {Factory: credOCI.Factory}, + pluginconsts.AuthTypeOkta: {Factory: credOkta.Factory}, ++ pluginconsts.AuthTypeGitlab: {Factory: credGitlab.Factory}, + pluginconsts.AuthTypePCF: { + Factory: credCF.Factory, + DeprecationStatus: consts.Deprecated, +diff --git a/helper/pluginconsts/plugin_consts.go b/helper/pluginconsts/plugin_consts.go +index 37d1f2b966..71410157b5 100644 +--- a/helper/pluginconsts/plugin_consts.go ++++ b/helper/pluginconsts/plugin_consts.go +@@ -19,6 +19,7 @@ const ( + AuthTypeOkta = "okta" + AuthTypePCF = "pcf" + AuthTypeRadius = "radius" ++ AuthTypeGitlab = "gitlab" + AuthTypeToken = "token" + AuthTypeCert = "cert" + AuthTypeOIDC = "oidc" +diff --git a/patch b/patch +new file mode 100644 +index 0000000000..58bc018e71 +--- /dev/null ++++ b/patch +@@ -0,0 +1,244 @@ ++diff --git a/command/base_predict.go b/command/base_predict.go ++index ed3edfa30f..70fe7e73d2 100644 ++--- a/command/base_predict.go +++++ b/command/base_predict.go ++@@ -111,6 +111,7 @@ func (b *BaseCommand) PredictVaultAvailableAuths() complete.Predictor { ++ "cert", ++ "gcp", ++ "github", +++ "gitlab", ++ "ldap", ++ "okta", ++ "plugin", ++diff --git a/command/base_predict_test.go b/command/base_predict_test.go ++index 387b8f0b84..477eda9c5f 100644 ++--- a/command/base_predict_test.go +++++ b/command/base_predict_test.go ++@@ -359,6 +359,7 @@ func TestPredict_Plugins(t *testing.T) { ++ "gcp", ++ "gcpkms", ++ "github", +++ "gitlab", ++ "hana-database-plugin", ++ "influxdb-database-plugin", ++ "jwt", ++diff --git a/command/commands_full.go b/command/commands_full.go ++index 8db22350cf..18bb2845b9 100644 ++--- a/command/commands_full.go +++++ b/command/commands_full.go ++@@ -15,6 +15,7 @@ import ( ++ credOCI "github.com/hashicorp/vault-plugin-auth-oci" ++ credAws "github.com/hashicorp/vault/builtin/credential/aws" ++ credGitHub "github.com/hashicorp/vault/builtin/credential/github" +++ credGitlab "github.com/hashicorp/vault/builtin/credential/gitlab" ++ credLdap "github.com/hashicorp/vault/builtin/credential/ldap" ++ credOkta "github.com/hashicorp/vault/builtin/credential/okta" ++ credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" ++@@ -75,6 +76,7 @@ func newFullAddonHandlers() (map[string]physical.Factory, map[string]LoginHandle ++ "cf": &credCF.CLIHandler{}, ++ "gcp": &credGcp.CLIHandler{}, ++ "github": &credGitHub.CLIHandler{}, +++ "gitlab": &credGitlab.CLIHandler{}, ++ "kerberos": &credKerb.CLIHandler{}, ++ "ldap": &credLdap.CLIHandler{}, ++ "oci": &credOCI.CLIHandler{}, ++diff --git a/go.mod b/go.mod ++index 3e3bd5e62f..f6e4d98ed6 100644 ++--- a/go.mod +++++ b/go.mod ++@@ -231,9 +231,11 @@ require ( ++ require ( ++ cel.dev/expr v0.15.0 // indirect ++ cloud.google.com/go/longrunning v0.6.0 // indirect +++ github.com/coder/websocket v1.8.12 // indirect ++ github.com/containerd/containerd v1.7.20 // indirect ++ github.com/fxamacker/cbor/v2 v2.7.0 // indirect ++ github.com/hashicorp/go-secure-stdlib/httputil v0.1.0 // indirect +++ github.com/hasura/go-graphql-client v0.13.1 // indirect ++ github.com/mitchellh/go-testing-interface v1.14.1 // indirect ++ github.com/moby/docker-image-spec v1.3.1 // indirect ++ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect ++diff --git a/go.sum b/go.sum ++index 6b321976d3..1da5fb2fca 100644 ++--- a/go.sum +++++ b/go.sum ++@@ -925,6 +925,8 @@ github.com/cockroachdb/cockroach-go/v2 v2.3.8 h1:53yoUo4+EtrC1NrAEgnnad4AS3ntNvG ++ github.com/cockroachdb/cockroach-go/v2 v2.3.8/go.mod h1:9uH5jK4yQ3ZQUT9IXe4I2fHzMIF5+JC/oOdzTRgJYJk= ++ github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= ++ github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +++github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +++github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= ++ github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ= ++ github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0= ++ github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= ++@@ -1605,6 +1607,8 @@ github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyu ++ github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo= ++ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= ++ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +++github.com/hasura/go-graphql-client v0.13.1 h1:kKbjhxhpwz58usVl+Xvgah/TDha5K2akNTRQdsEHN6U= +++github.com/hasura/go-graphql-client v0.13.1/go.mod h1:k7FF7h53C+hSNFRG3++DdVZWIuHdCaTbI7siTJ//zGQ= ++ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= ++ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= ++ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= ++diff --git a/helper/builtinplugins/registry_full.go b/helper/builtinplugins/registry_full.go ++index 32bba40487..b4931436e2 100644 ++--- a/helper/builtinplugins/registry_full.go +++++ b/helper/builtinplugins/registry_full.go ++@@ -32,6 +32,7 @@ import ( ++ logicalTerraform "github.com/hashicorp/vault-plugin-secrets-terraform" ++ credAws "github.com/hashicorp/vault/builtin/credential/aws" ++ credGitHub "github.com/hashicorp/vault/builtin/credential/github" +++ credGitlab "github.com/hashicorp/vault/builtin/credential/gitlab" ++ credLdap "github.com/hashicorp/vault/builtin/credential/ldap" ++ credOkta "github.com/hashicorp/vault/builtin/credential/okta" ++ credRadius "github.com/hashicorp/vault/builtin/credential/radius" ++@@ -64,6 +65,7 @@ func newFullAddonRegistry() *registry { ++ "cf": {Factory: credCF.Factory}, ++ "gcp": {Factory: credGcp.Factory}, ++ "github": {Factory: credGitHub.Factory}, +++ "gitlab": {Factory: credGitlab.Factory}, ++ "kerberos": {Factory: credKerb.Factory}, ++ "kubernetes": {Factory: credKube.Factory}, ++ "ldap": {Factory: credLdap.Factory}, ++diff --git a/ui/app/adapters/cluster.js b/ui/app/adapters/cluster.js ++index 5a8984ec33..dcc709f242 100644 ++--- a/ui/app/adapters/cluster.js +++++ b/ui/app/adapters/cluster.js ++@@ -180,6 +180,7 @@ export default ApplicationAdapter.extend({ ++ const authURLs = { ++ github: 'login', ++ jwt: 'login', +++ gitlab: username ? `login/${encodeURIComponent(username)}` : 'login', ++ oidc: 'login', ++ userpass: `login/${encodeURIComponent(username)}`, ++ ldap: `login/${encodeURIComponent(username)}`, ++diff --git a/ui/app/components/auth-form.js b/ui/app/components/auth-form.js ++index d87161b290..7d8307af7c 100644 ++--- a/ui/app/components/auth-form.js +++++ b/ui/app/components/auth-form.js ++@@ -185,8 +185,14 @@ export default Component.extend(DEFAULTS, { ++ this.set('selectedAuth', 'token'); ++ const adapter = this.store.adapterFor('tools'); ++ try { ++- const response = yield adapter.toolAction('unwrap', null, { clientToken: token }); ++- this.set('token', response.auth.client_token); +++ const response = yield adapter.toolAction('unwrap', null, {clientToken: token}); +++ if (response.data.authType) { +++ this.set('selectedAuth', response.data.authType) +++ this.set('token', response.data.token); +++ } else { +++ this.set('selectedAuth', 'token'); +++ this.set('token', response.auth.client_token); +++ } ++ this.send('doSubmit'); ++ } catch (e) { ++ this.set('error', `Token unwrap failed: ${e.errors[0]}`); ++diff --git a/ui/app/helpers/mountable-auth-methods.js b/ui/app/helpers/mountable-auth-methods.js ++index 53d2410d6c..0e0344fc59 100644 ++--- a/ui/app/helpers/mountable-auth-methods.js +++++ b/ui/app/helpers/mountable-auth-methods.js ++@@ -64,6 +64,13 @@ const MOUNTABLE_AUTH_METHODS = [ ++ category: 'cloud', ++ glyph: 'github-color', ++ }, +++ { +++ displayName: 'Gitlab', +++ value: 'gitlab', +++ type: 'gitlab', +++ glyph: 'auth', +++ category: 'cloud', +++ }, ++ { ++ displayName: 'JWT', ++ value: 'jwt', ++diff --git a/ui/app/helpers/supported-auth-backends.js b/ui/app/helpers/supported-auth-backends.js ++index 4c9185cf48..c75ca7fc0e 100644 ++--- a/ui/app/helpers/supported-auth-backends.js +++++ b/ui/app/helpers/supported-auth-backends.js ++@@ -76,6 +76,14 @@ const SUPPORTED_AUTH_BACKENDS = [ ++ displayNamePath: ['metadata.org', 'metadata.username'], ++ formAttributes: ['token'], ++ }, +++ { +++ type: 'gitlab', +++ typeDisplay: 'Gitlab', +++ description: 'Gitlab authentication.', +++ tokenPath: 'client_token', +++ displayNamePath: 'metadata.username', +++ formAttributes: ['token'], +++ }, ++ ]; ++ ++ const ENTERPRISE_AUTH_METHODS = [ ++diff --git a/ui/app/helpers/tabs-for-auth-section.js b/ui/app/helpers/tabs-for-auth-section.js ++index 77a1b92388..44f940d800 100644 ++--- a/ui/app/helpers/tabs-for-auth-section.js +++++ b/ui/app/helpers/tabs-for-auth-section.js ++@@ -39,6 +39,18 @@ const TABS_FOR_SETTINGS = { ++ routeParams: ['configuration'], ++ }, ++ ], +++ gitlab: [ +++ { +++ label: 'Configuration', +++ routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'], +++ }, +++ ], +++ gitlab: [ +++ { +++ label: 'Configuration', +++ routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'], +++ }, +++ ], ++ gcp: [ ++ { ++ label: 'Configuration', ++diff --git a/ui/app/routes/vault/cluster/settings/auth/configure/section.js b/ui/app/routes/vault/cluster/settings/auth/configure/section.js ++index 0111f636ee..da91583f10 100644 ++--- a/ui/app/routes/vault/cluster/settings/auth/configure/section.js +++++ b/ui/app/routes/vault/cluster/settings/auth/configure/section.js ++@@ -23,6 +23,7 @@ export default Route.extend(UnloadModelRoute, { ++ 'aws-roletag-denylist': 'auth-config/aws/roletag-denylist', ++ 'azure-configuration': 'auth-config/azure', ++ 'github-configuration': 'auth-config/github', +++ 'gitlab-configuration': 'auth-config/gitlab', ++ 'gcp-configuration': 'auth-config/gcp', ++ 'jwt-configuration': 'auth-config/jwt', ++ 'oidc-configuration': 'auth-config/oidc', ++diff --git a/ui/app/templates/components/auth-form.hbs b/ui/app/templates/components/auth-form.hbs ++index f9fee4c71f..8010db7b6f 100644 ++--- a/ui/app/templates/components/auth-form.hbs +++++ b/ui/app/templates/components/auth-form.hbs ++@@ -118,6 +118,19 @@ ++ /> ++
++ +++ {{else if (eq this.providerName "gitlab")}} +++
+++ +++ +++ +++
+++ +++
+++ +++
+++
++ {{else if (eq this.providerName "token")}} ++
++ ++diff --git a/ui/tests/acceptance/settings/auth/configure/section-test.js b/ui/tests/acceptance/settings/auth/configure/section-test.js ++index b6533273c0..f0f968f666 100644 ++--- a/ui/tests/acceptance/settings/auth/configure/section-test.js +++++ b/ui/tests/acceptance/settings/auth/configure/section-test.js ++@@ -56,7 +56,7 @@ module('Acceptance | settings/auth/configure/section', function (hooks) { ++ ); ++ }); ++ ++- for (const type of ['aws', 'azure', 'gcp', 'github', 'kubernetes']) { +++ for (const type of ['aws', 'azure', 'gcp', 'github', 'gitlab', 'kubernetes']) { ++ test(`it shows tabs for auth method: ${type}`, async function (assert) { ++ const path = `${type}-showtab-${this.uid}`; ++ await cli.toggle(); +diff --git a/ui/app/adapters/auth-config/gitlab.js b/ui/app/adapters/auth-config/gitlab.js +new file mode 100644 +index 0000000000..21f5624ac4 +--- /dev/null ++++ b/ui/app/adapters/auth-config/gitlab.js +@@ -0,0 +1,2 @@ ++import AuthConfig from './_base'; ++export default AuthConfig.extend(); +diff --git a/ui/app/adapters/cluster.js b/ui/app/adapters/cluster.js +index 5a8984ec33..dcc709f242 100644 +--- a/ui/app/adapters/cluster.js ++++ b/ui/app/adapters/cluster.js +@@ -180,6 +180,7 @@ export default ApplicationAdapter.extend({ + const authURLs = { + github: 'login', + jwt: 'login', ++ gitlab: username ? `login/${encodeURIComponent(username)}` : 'login', + oidc: 'login', + userpass: `login/${encodeURIComponent(username)}`, + ldap: `login/${encodeURIComponent(username)}`, +diff --git a/ui/app/components/auth-form.js b/ui/app/components/auth-form.js +index d87161b290..7d8307af7c 100644 +--- a/ui/app/components/auth-form.js ++++ b/ui/app/components/auth-form.js +@@ -185,8 +185,14 @@ export default Component.extend(DEFAULTS, { + this.set('selectedAuth', 'token'); + const adapter = this.store.adapterFor('tools'); + try { +- const response = yield adapter.toolAction('unwrap', null, { clientToken: token }); +- this.set('token', response.auth.client_token); ++ const response = yield adapter.toolAction('unwrap', null, {clientToken: token}); ++ if (response.data.authType) { ++ this.set('selectedAuth', response.data.authType) ++ this.set('token', response.data.token); ++ } else { ++ this.set('selectedAuth', 'token'); ++ this.set('token', response.auth.client_token); ++ } + this.send('doSubmit'); + } catch (e) { + this.set('error', `Token unwrap failed: ${e.errors[0]}`); +diff --git a/ui/app/helpers/mountable-auth-methods.js b/ui/app/helpers/mountable-auth-methods.js +index 53d2410d6c..0e0344fc59 100644 +--- a/ui/app/helpers/mountable-auth-methods.js ++++ b/ui/app/helpers/mountable-auth-methods.js +@@ -64,6 +64,13 @@ const MOUNTABLE_AUTH_METHODS = [ + category: 'cloud', + glyph: 'github-color', + }, ++ { ++ displayName: 'Gitlab', ++ value: 'gitlab', ++ type: 'gitlab', ++ glyph: 'auth', ++ category: 'cloud', ++ }, + { + displayName: 'JWT', + value: 'jwt', +diff --git a/ui/app/helpers/supported-auth-backends.js b/ui/app/helpers/supported-auth-backends.js +index 4c9185cf48..c75ca7fc0e 100644 +--- a/ui/app/helpers/supported-auth-backends.js ++++ b/ui/app/helpers/supported-auth-backends.js +@@ -76,6 +76,14 @@ const SUPPORTED_AUTH_BACKENDS = [ + displayNamePath: ['metadata.org', 'metadata.username'], + formAttributes: ['token'], + }, ++ { ++ type: 'gitlab', ++ typeDisplay: 'Gitlab', ++ description: 'Gitlab authentication.', ++ tokenPath: 'client_token', ++ displayNamePath: 'metadata.username', ++ formAttributes: ['token'], ++ }, + ]; + + const ENTERPRISE_AUTH_METHODS = [ +diff --git a/ui/app/helpers/tabs-for-auth-section.js b/ui/app/helpers/tabs-for-auth-section.js +index 77a1b92388..44f940d800 100644 +--- a/ui/app/helpers/tabs-for-auth-section.js ++++ b/ui/app/helpers/tabs-for-auth-section.js +@@ -39,6 +39,18 @@ const TABS_FOR_SETTINGS = { + routeParams: ['configuration'], + }, + ], ++ gitlab: [ ++ { ++ label: 'Configuration', ++ routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'], ++ }, ++ ], ++ gitlab: [ ++ { ++ label: 'Configuration', ++ routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'], ++ }, ++ ], + gcp: [ + { + label: 'Configuration', +diff --git a/ui/app/models/auth-config/gitlab.js b/ui/app/models/auth-config/gitlab.js +new file mode 100644 +index 0000000000..91d52cbcc3 +--- /dev/null ++++ b/ui/app/models/auth-config/gitlab.js +@@ -0,0 +1,39 @@ ++import { computed } from '@ember/object'; ++import DS from 'ember-data'; ++ ++import AuthConfig from '../auth-config'; ++import fieldToAttrs from 'vault/utils/field-to-attrs'; ++ ++const { attr } = DS; ++ ++export default AuthConfig.extend({ ++ baseURL: attr('string', { ++ label: 'Base URL', ++ }), ++ minAccessLevel: attr('string', { ++ label: 'Minimal Access Level', ++ defaultValue: 'developer', ++ possibleValues: ['none', 'guest', 'reporter', 'developer', 'maintainer', 'owner'] ++ }), ++ appID: attr('string', { ++ label: 'Oauth Application ID', ++ }), ++ appSecret: attr('string', { ++ label: 'Oauth Application Secret', ++ }), ++ callbackURL: attr('string', { ++ label: 'Oauth Callback URL', ++ }), ++ ciToken: attr('string', { ++ label: 'CI token', ++ }), ++ ++ fieldGroups: computed(function() { ++ const groups = [{ ++ 'Gitlab Options': ['baseURL', 'minAccessLevel', 'appID', 'appSecret', 'callbackURL', 'ciToken'], ++ }, ]; ++ ++ return fieldToAttrs(this, groups); ++ }), ++ ++}); +diff --git a/ui/app/routes/vault/cluster/settings/auth/configure/section.js b/ui/app/routes/vault/cluster/settings/auth/configure/section.js +index 0111f636ee..da91583f10 100644 +--- a/ui/app/routes/vault/cluster/settings/auth/configure/section.js ++++ b/ui/app/routes/vault/cluster/settings/auth/configure/section.js +@@ -23,6 +23,7 @@ export default Route.extend(UnloadModelRoute, { + 'aws-roletag-denylist': 'auth-config/aws/roletag-denylist', + 'azure-configuration': 'auth-config/azure', + 'github-configuration': 'auth-config/github', ++ 'gitlab-configuration': 'auth-config/gitlab', + 'gcp-configuration': 'auth-config/gcp', + 'jwt-configuration': 'auth-config/jwt', + 'oidc-configuration': 'auth-config/oidc', +diff --git a/ui/app/templates/components/auth-form.hbs b/ui/app/templates/components/auth-form.hbs +index f9fee4c71f..8010db7b6f 100644 +--- a/ui/app/templates/components/auth-form.hbs ++++ b/ui/app/templates/components/auth-form.hbs +@@ -118,6 +118,19 @@ + /> +
+ ++ {{else if (eq this.providerName "gitlab")}} ++
++ ++ ++ ++
++ ++
++ ++
++
+ {{else if (eq this.providerName "token")}} +
+ +diff --git a/ui/app/templates/components/wizard/gitlab-method.hbs b/ui/app/templates/components/wizard/gitlab-method.hbs +new file mode 100644 +index 0000000000..f3d95a1762 +--- /dev/null ++++ b/ui/app/templates/components/wizard/gitlab-method.hbs +@@ -0,0 +1,10 @@ ++ ++

++ The Gitlab auth method can be used to authenticate with Vault using a Gitlab access token. ++

++
+diff --git a/ui/public/eco/gitlab.svg b/ui/public/eco/gitlab.svg +new file mode 100644 +index 0000000000..95a22f1017 +--- /dev/null ++++ b/ui/public/eco/gitlab.svg +@@ -0,0 +1 @@ ++ +\ No newline at end of file +diff --git a/ui/tests/acceptance/settings/auth/configure/section-test.js b/ui/tests/acceptance/settings/auth/configure/section-test.js +index b6533273c0..f0f968f666 100644 +--- a/ui/tests/acceptance/settings/auth/configure/section-test.js ++++ b/ui/tests/acceptance/settings/auth/configure/section-test.js +@@ -56,7 +56,7 @@ module('Acceptance | settings/auth/configure/section', function (hooks) { + ); + }); + +- for (const type of ['aws', 'azure', 'gcp', 'github', 'kubernetes']) { ++ for (const type of ['aws', 'azure', 'gcp', 'github', 'gitlab', 'kubernetes']) { + test(`it shows tabs for auth method: ${type}`, async function (assert) { + const path = `${type}-showtab-${this.uid}`; + await cli.toggle();