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();