Skip to content

Commit

Permalink
feat: allow us to specify multiple configurations and config per role (
Browse files Browse the repository at this point in the history
  • Loading branch information
ilijamt authored Oct 12, 2024
1 parent ad8577b commit 2747f87
Show file tree
Hide file tree
Showing 35 changed files with 2,052 additions and 196 deletions.
86 changes: 70 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,53 @@ To learn specifically about how plugins work, see documentation on [Vault plugin
## GitLab

- GitLab CE/EE - Self Managed
- gitlab.com (cannot use personal access token)
- Dedicated Instance (cannot use personal access token)
- gitlab.com (cannot use personal access token, and user service account)
- Dedicated Instance (cannot use personal access token, and user service account)

### Setup

Before we can use this plugin we need to create an access token that will have rights to do what we need to.

## Paths

For a list of the available endpoints you can check bellow or by running the command `vault path-help gitlab` for your version after you've mounted it.

```shell
$ vault path-help gitlab
## DESCRIPTION

The Gitlab Access token auth Backend dynamically generates private
and group tokens.

After mounting this Backend, credentials to manage Gitlab tokens must be configured
with the "^config/(?P<config_name>\w(([\w-.@]+)?\w)?)$" endpoints.

## PATHS

The following paths are supported by this backend. To view help for
any of the paths below, use the help command with any route matching
the path pattern. Note that depending on the policy of your auth token,
you may or may not be able to access certain paths.

^config/(?P<config_name>\w(([\w-.@]+)?\w)?)$
Configure the Gitlab Access Tokens Backend.
^config/(?P<config_name>\w(([\w-.]+)?\w)?)/rotate$
Rotate the gitlab token for this configuration.
^config?/?$
Lists existing configs
^roles/(?P<role_name>\w(([\w-.@]+)?\w)?)$
Create a role with parameters that are used to generate a various access tokens.
^roles?/?$
Lists existing roles
^token/(?P<role_name>\w(([\w-.@]+)?\w)?)$
Generate an access token based on the specified role
```
## Security Model
The current authentication model requires providing Vault with a Gitlab Token.
Expand Down Expand Up @@ -91,6 +131,8 @@ The following data points can be used within your token name template. These are
* access_level
* scopes
* token_type
* role_name
* config_name
* gitlab_revokes_token
* unix_timestamp_utc
Expand Down Expand Up @@ -167,30 +209,34 @@ If you use Vault to manage the tokens the minimal TTL you can use is `1h`, by se
The command bellow will set up the config backend with a max TTL of 48h.
```shell
$ vault write gitlab/config base_url=https://gitlab.example.com token=gitlab-super-secret-token auto_rotate_token=false auto_rotate_before=48h type=self-managed
$ vault read gitlab/config
$ vault write gitlab/config/default base_url=https://gitlab.example.com token=gitlab-super-secret-token auto_rotate_token=false auto_rotate_before=48h type=self-managed
$ vault read gitlab/config/default
Key Value
--- -----
auto_rotate_before 48h0m0s
auto_rotate_token false
base_url https://gitlab.example.com
base_url http://localhost:8080
name default
scopes api, read_api, read_user, sudo, admin_mode, create_runner, k8s_proxy, read_repository, write_repository, ai_features, read_service_ping
token_created_at 2024-07-11T18:53:26Z
token_expires_at 2025-07-11T00:00:00Z
token_id 1
token_expires_at 2025-03-29T00:00:00Z
token_sha1_hash 9441e6e07d77a2d5601ab5d7cac5868d358d885c
type self-managed
```
After initial setup should you wish to change any value you can do so by using the patch command for example
```shell
$ vault patch gitlab/config type=saas auto_rotate_token=true auto_rotate_before=64h token=glpat-secret-admin-token
$ vault patch gitlab/config/default type=saas auto_rotate_token=true auto_rotate_before=64h token=glpat-secret-admin-token
Key Value
--- -----
auto_rotate_before 64h0m0s
auto_rotate_token true
base_url https://gitlab.example.com
base_url http://localhost:8080
name default
scopes api, read_api, read_user, sudo, admin_mode, create_runner, k8s_proxy, read_repository, write_repository, ai_features, read_service_ping
token_created_at 2024-07-11T18:53:26Z
token_created_at 2024-07-11T18:53:46Z
token_expires_at 2025-07-11T00:00:00Z
token_id 2
token_sha1_hash c6e762667cadb936f0c8439b0d240661a270eba1
Expand Down Expand Up @@ -264,16 +310,24 @@ If the original token that has been supplied to the backend is not expired. We c
to force a rotation of the main token. This would create a new token with the same expiration as the original token.
```shell
$ vault write -f gitlab/config/rotate
$ vault write -f gitlab/config/default/rotate
Key Value
--- -----
auto_rotate_before 48h0m0s
auto_rotate_token false
base_url https://gitlab.example.com
token_expires_at 2025-03-29T00:00:00Z
token_id 110
token_sha1_hash b8ff3f9e560f29d15f756fc92a3b1d6602aaae55
lease_id gitlab/config/default/rotate/Ils8dp7PDXSWgb5dF4I2NPPS
lease_duration 768h
lease_renewable false
auto_rotate_before 64h0m0s
auto_rotate_token true
base_url http://localhost:8080
name default
scopes api, read_api, read_user, sudo, admin_mode, create_runner, k8s_proxy, read_repository, write_repository, ai_features, read_service_ping
token_created_at 2024-07-11T18:53:46Z
token_expires_at 2024-10-12T20:02:11Z
token_id 76
token_sha1_hash 10e413692b345809f6b4db57c5d38fadaacbf9be
type saas
```
## Upgrading
```shell
Expand Down
81 changes: 48 additions & 33 deletions backend.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package gitlab

import (
"cmp"
"context"
"errors"
"fmt"
"net/http"
"strings"
"sync"
Expand All @@ -21,14 +23,15 @@ The Gitlab Access token auth Backend dynamically generates private
and group tokens.
After mounting this Backend, credentials to manage Gitlab tokens must be configured
with the "config/" endpoints.
with the "^config/(?P<config_name>\w(([\w-.@]+)?\w)?)$" endpoints.
`
)

// Factory returns expected new Backend as logical.Backend
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
var b = &Backend{
roleLocks: locksutil.CreateLocks(),
clients: sync.Map{},
}

b.Backend = &framework.Backend{
Expand All @@ -53,6 +56,7 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
Paths: framework.PathAppend(
[]*framework.Path{
pathConfig(b),
pathListConfig(b),
pathConfigTokenRotate(b),
pathListRoles(b),
pathRoles(b),
Expand All @@ -63,7 +67,6 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
PeriodicFunc: b.periodicFunc,
}

b.SetClient(nil)
var err = b.Setup(ctx, conf)
return b, err
}
Expand All @@ -72,7 +75,7 @@ type Backend struct {
*framework.Backend

// The client that we can use to create and revoke the access tokens
client Client
clients sync.Map

// Mutex to protect access to gitlab clients and client configs, a change to the gitlab client config
// would invalidate the gitlab client, so it will need to be reinitialized
Expand All @@ -82,27 +85,30 @@ type Backend struct {
roleLocks []*locksutil.LockEntry
}

func (b *Backend) periodicFunc(ctx context.Context, request *logical.Request) error {
func (b *Backend) periodicFunc(ctx context.Context, req *logical.Request) (err error) {
b.Logger().Debug("Periodic action executing")

if !b.WriteSafeReplicationState() {
return nil
}
if b.WriteSafeReplicationState() {
var config *EntryConfig

var config *EntryConfig
var err error

b.lockClientMutex.Lock()
unlockLockClientMutex := sync.OnceFunc(func() { b.lockClientMutex.Unlock() })
defer unlockLockClientMutex()
if config, err = getConfig(ctx, request.Storage); err == nil {
unlockLockClientMutex()
if config == nil {
return nil
}
// If we need to autorotate the token, initiate the procedure to autorotate the token
if config.AutoRotateToken {
err = errors.Join(err, b.checkAndRotateConfigToken(ctx, request, config))
b.lockClientMutex.Lock()
unlockLockClientMutex := sync.OnceFunc(func() { b.lockClientMutex.Unlock() })
defer unlockLockClientMutex()

var configs []string
configs, err = req.Storage.List(ctx, fmt.Sprintf("%s/", PathConfigStorage))

for _, name := range configs {
if config, err = getConfig(ctx, req.Storage, name); err == nil {
b.Logger().Debug("Trying to rotate the config", "name", name)
unlockLockClientMutex()
if config != nil {
// If we need to autorotate the token, initiate the procedure to autorotate the token
if config.AutoRotateToken {
err = errors.Join(err, b.checkAndRotateConfigToken(ctx, req, config))
}
}
}
}
}

Expand All @@ -112,37 +118,46 @@ func (b *Backend) periodicFunc(ctx context.Context, request *logical.Request) er
// Invalidate invalidates the key if required
func (b *Backend) Invalidate(ctx context.Context, key string) {
b.Logger().Debug("Backend invalidate", "key", key)
if key == PathConfigStorage {
b.Logger().Warn("Gitlab config changed, reinitializing the gitlab client")
if strings.HasPrefix(key, PathConfigStorage) {
parts := strings.SplitN(key, "/", 2)
var name = parts[1]
b.Logger().Warn(fmt.Sprintf("Gitlab config for %s changed, reinitializing the gitlab client", name))
b.lockClientMutex.Lock()
defer b.lockClientMutex.Unlock()
b.client = nil
b.clients.Delete(name)
}
}

func (b *Backend) GetClient() Client {
return b.client
func (b *Backend) GetClient(name string) Client {
if client, ok := b.clients.Load(cmp.Or(name, DefaultConfigName)); ok {
return client.(Client)
}
return nil
}

func (b *Backend) SetClient(client Client) {
func (b *Backend) SetClient(client Client, name string) {
name = cmp.Or(name, DefaultConfigName)
if client == nil {
b.Logger().Debug("Setting a nil client")
return
}
b.Logger().Debug("Setting a new client")
b.client = client
b.clients.Store(name, client)
}

func (b *Backend) getClient(ctx context.Context, s logical.Storage) (client Client, err error) {
if b.client != nil && b.client.Valid() {
func (b *Backend) getClient(ctx context.Context, s logical.Storage, name string) (client Client, err error) {
if c, ok := b.clients.Load(cmp.Or(name, DefaultConfigName)); ok {
client = c.(Client)
}
if client != nil && client.Valid() {
b.Logger().Debug("Returning existing gitlab client")
return b.client, nil
return client, nil
}

b.lockClientMutex.RLock()
defer b.lockClientMutex.RUnlock()
var config *EntryConfig
config, err = getConfig(ctx, s)
config, err = getConfig(ctx, s, name)
if err != nil {
b.Logger().Error("Failed to retrieve configuration", "error", err.Error())
return nil, err
Expand All @@ -152,7 +167,7 @@ func (b *Backend) getClient(ctx context.Context, s logical.Storage) (client Clie
httpClient, _ = HttpClientFromContext(ctx)
if client, _ = GitlabClientFromContext(ctx); client == nil {
if client, err = NewGitlabClient(config, httpClient, b.Logger()); err == nil {
b.SetClient(client)
b.SetClient(client, name)
}
}
return client, err
Expand Down
17 changes: 8 additions & 9 deletions backend_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package gitlab_test

import (
"reflect"
"fmt"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -16,13 +16,12 @@ func TestBackend(t *testing.T) {
b, _, err = getBackend(ctx)
require.NoError(t, err)
require.NotNil(t, b)
fv := reflect.ValueOf(b).Elem().FieldByName("client")
require.True(t, fv.IsNil())
b.SetClient(newInMemoryClient(true))
require.False(t, fv.IsNil())
b.Invalidate(ctx, gitlab.PathConfigStorage)
require.True(t, fv.IsNil())
b.SetClient(newInMemoryClient(true))
require.False(t, fv.IsNil())
require.Nil(t, b.GetClient(gitlab.DefaultConfigName))
b.SetClient(newInMemoryClient(true), gitlab.DefaultConfigName)
require.NotNil(t, b.GetClient(gitlab.DefaultConfigName))
b.Invalidate(ctx, fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName))
require.Nil(t, b.GetClient(gitlab.DefaultConfigName))
b.SetClient(newInMemoryClient(true), gitlab.DefaultConfigName)
require.NotNil(t, b.GetClient(gitlab.DefaultConfigName))
require.EqualValues(t, gitlab.Version, b.PluginVersion().Version)
}
1 change: 1 addition & 0 deletions defs.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
DefaultAutoRotateBeforeMaxTTL = 730 * time.Hour
ctxKeyHttpClient = contextKey("vpsg-ctx-key-http-client")
ctxKeyGitlabClient = contextKey("vpsg-ctx-key-gitlab-client")
DefaultConfigName = "default"
)

func HttpClientNewContext(ctx context.Context, httpClient *http.Client) context.Context {
Expand Down
12 changes: 7 additions & 5 deletions entry_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import (
type EntryConfig struct {
TokenId int `json:"token_id" yaml:"token_id" mapstructure:"token_id"`
BaseURL string `json:"base_url" structs:"base_url" mapstructure:"base_url"`
Token string `json:"token" structs:"token" mapstructure:"token" validate:"min=10,max=40"`
Token string `json:"token" structs:"token" mapstructure:"token"`
AutoRotateToken bool `json:"auto_rotate_token" structs:"auto_rotate_token" mapstructure:"auto_rotate_token"`
AutoRotateBefore time.Duration `json:"auto_rotate_before" structs:"auto_rotate_before" mapstructure:"auto_rotate_before"`
TokenCreatedAt time.Time `json:"token_created_at" structs:"token_created_at" mapstructure:"token_created_at"`
TokenExpiresAt time.Time `json:"token_expires_at" structs:"token_expires_at" mapstructure:"token_expires_at"`
Scopes []string `json:"scopes" structs:"scopes" mapstructure:"scopes"`
Type Type `json:"type" structs:"type" mapstructure:"type" validate:"gitlab-type"`
Type Type `json:"type" structs:"type" mapstructure:"type"`
Name string `json:"name" structs:"name" mapstructure:"name"`
}

func (e *EntryConfig) Merge(data *framework.FieldData) (warnings []string, changes map[string]string, err error) {
Expand Down Expand Up @@ -167,15 +168,16 @@ func (e *EntryConfig) LogicalResponseData() map[string]any {
"token_sha1_hash": fmt.Sprintf("%x", sha1.Sum([]byte(e.Token))),
"scopes": strings.Join(e.Scopes, ", "),
"type": e.Type.String(),
"name": e.Name,
}
}

func getConfig(ctx context.Context, s logical.Storage) (cfg *EntryConfig, err error) {
func getConfig(ctx context.Context, s logical.Storage, name string) (cfg *EntryConfig, err error) {
if s == nil {
return nil, fmt.Errorf("%w: local.Storage", ErrNilValue)
}
var entry *logical.StorageEntry
if entry, err = s.Get(ctx, PathConfigStorage); err == nil {
if entry, err = s.Get(ctx, fmt.Sprintf("%s/%s", PathConfigStorage, name)); err == nil {
if entry == nil {
return nil, nil
}
Expand All @@ -187,7 +189,7 @@ func getConfig(ctx context.Context, s logical.Storage) (cfg *EntryConfig, err er

func saveConfig(ctx context.Context, config EntryConfig, s logical.Storage) (err error) {
var storageEntry *logical.StorageEntry
if storageEntry, err = logical.StorageEntryJSON(PathConfigStorage, config); err == nil {
if storageEntry, err = logical.StorageEntryJSON(fmt.Sprintf("%s/%s", PathConfigStorage, config.Name), config); err == nil {
err = s.Put(ctx, storageEntry)
}
return err
Expand Down
Loading

0 comments on commit 2747f87

Please sign in to comment.