Skip to content

Commit ded89f1

Browse files
committed
fix: expiry date for main token, and documentation for rotating token
1 parent 38501d1 commit ded89f1

9 files changed

+111
-45
lines changed

README.md

+32-11
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ The current authentication model requires providing Vault with a Gitlab Token.
3838

3939
### Config
4040

41-
| Property | Required | Default value | Sensitive | Description |
42-
|:-------------------------:|:--------:|:-------------:|:---------:|:----------------------------------------------------------------------------------------------------------------------------------|
43-
| token | yes | n/a | yes | The token to access Gitlab API |
44-
| base_url | yes | n/a | no | The address to access Gitlab |
45-
| auto_rotate_token | no | no | no | Should we autorotate the token when it's close to expiry? (Experimental) |
46-
| revoke_auto_rotated_token | no | no | no | Should we revoke the auto-rotated token after a new one has been generated? |
47-
| auto_rotate_before | no | 24h | no | How much time should be remaining on the token validity before we should rotate it? Minimum can be set to 24h and maximum to 730h |
41+
| Property | Required | Default value | Sensitive | Description |
42+
|:-------------------------:|:--------:|:-------------:|:---------:|:----------------------------------------------------------------------------------------------------------------------------------------------|
43+
| token | yes | n/a | yes | The token to access Gitlab API, it will not show when you do a read, as it's a sensitive value. Instead it will display it's SHA1 hash value. |
44+
| base_url | yes | n/a | no | The address to access Gitlab |
45+
| auto_rotate_token | no | no | no | Should we autorotate the token when it's close to expiry? (Experimental) |
46+
| revoke_auto_rotated_token | no | no | no | Should we revoke the auto-rotated token after a new one has been generated? |
47+
| auto_rotate_before | no | 24h | no | How much time should be remaining on the token validity before we should rotate it? Minimum can be set to 24h and maximum to 730h |
4848

4949
### Role
5050

@@ -120,20 +120,33 @@ If you use Vault to manage the tokens the minimal TTL you can use is `1h`, by se
120120
The command bellow will set up the config backend with a max TTL of 48h.
121121

122122
```shell
123-
$ vault write gitlab/config base_url=https://gitlab.example.com token=gitlab-super-secret-token
123+
$ vault write gitlab/config base_url=https://gitlab.example.com token=gitlab-super-secret-token auto_rotate_token=false revoke_auto_rotated_token=false auto_rotate_before=48h
124+
$ vault read gitlab/config
125+
Key Value
126+
--- -----
127+
auto_rotate_before 48h0m0s
128+
auto_rotate_token false
129+
base_url https://gitlab.example.com
130+
token_id 107
131+
token_expires_at 2025-03-29T00:00:00Z
132+
token_sha1_hash 1014647cd9bbf359d926fcacdf78e184db9dbedc
124133
```
125134

126135
You may also need to configure the Max/Default TTL for a token that can be issued by setting:
127136

128-
Max TTL: 1 year
129-
Default TTL: 1 week
137+
Max TTL: `1 year`
138+
Default TTL: `1 week`
130139

131140
```shell
132141
$ vault secrets tune -max-lease-ttl=8784h -default-lease-ttl=168h gitlab/
133142
```
134143

135144
Check https://developer.hashicorp.com/vault/docs/commands/secrets/tune for more information.
136145

146+
There is a periodic func that runs that is responsible for autorotation and main token expiry time.
147+
So in the beginning you may see `token_expires_at n/a`. But when the function runs it will update itself
148+
with the correct expiry date and the corresponding `token_id`.
149+
137150
### Roles
138151

139152
This will create three roles, one of each type.
@@ -255,7 +268,15 @@ If the original token that has been supplied to the backend is not expired. We c
255268
to force a rotation of the main token. This would create a new token with the same expiration as the original token.
256269

257270
```shell
258-
vault put gitlab/config/rotate
271+
$ vault read gitlab/config/rotate
272+
Key Value
273+
--- -----
274+
auto_rotate_before 48h0m0s
275+
auto_rotate_token false
276+
base_url https://gitlab.example.com
277+
token_expires_at 2025-03-29T00:00:00Z
278+
token_id 110
279+
token_sha1_hash b8ff3f9e560f29d15f756fc92a3b1d6602aaae55
259280
```
260281

261282
## TODO

backend.go

+52-4
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package gitlab
22

33
import (
44
"context"
5+
"errors"
56
"strings"
67
"sync"
8+
"time"
79

810
"github.com/hashicorp/vault/sdk/framework"
911
"github.com/hashicorp/vault/sdk/helper/locksutil"
@@ -101,37 +103,83 @@ func (b *Backend) periodicFunc(ctx context.Context, request *logical.Request) er
101103
return nil
102104
}
103105

104-
if !config.AutoRotateToken {
105-
return nil
106+
// if there is no expiry date on the token fetch it
107+
if config.TokenExpiresAt.IsZero() {
108+
err = errors.Join(err, b.updateMainTokenExpiryTime(ctx, request, config))
109+
}
110+
111+
// If we need to autorotate the token, initiate the procedure to autorotate the token
112+
if config.AutoRotateToken {
113+
err = errors.Join(err, b.checkAndRotateConfigToken(ctx, request, config))
114+
}
115+
116+
return err
117+
}
118+
119+
func (b *Backend) updateMainTokenExpiryTime(ctx context.Context, request *logical.Request, config *EntryConfig) error {
120+
b.Logger().Debug("Running updateMainTokenExpiryTime")
121+
var client Client
122+
var err error
123+
124+
if client, err = b.getClient(ctx, request.Storage); err != nil {
125+
return err
106126
}
107127

108-
return b.checkAndRotateConfigToken(ctx, request, config)
128+
if config.TokenExpiresAt.IsZero() {
129+
b.Logger().Warn("Main token expiry information is empty, updating")
130+
var entryToken *EntryToken
131+
// we need to fetch the token expiration information
132+
entryToken, err = client.CurrentTokenInfo()
133+
if err != nil {
134+
return err
135+
}
136+
// and update the information so we can do the checks
137+
config.TokenExpiresAt = *entryToken.ExpiresAt
138+
config.TokenId = entryToken.TokenID
139+
err = func() error {
140+
b.lockClientMutex.Lock()
141+
defer b.lockClientMutex.Unlock()
142+
return saveConfig(ctx, *config, request.Storage)
143+
}()
144+
if err != nil {
145+
return err
146+
}
147+
b.Logger().Info("Main token expiry information updated", "token_id", entryToken.TokenID, "token_expires_at", entryToken.ExpiresAt.Format(time.RFC3339))
148+
}
149+
return err
109150
}
110151

111152
// Invalidate invalidates the key if required
112153
func (b *Backend) Invalidate(ctx context.Context, key string) {
113154
b.Logger().Debug("Backend invalidate", "key", key)
114155
if key == PathConfigStorage {
115-
b.Logger().Warn("gitlab config changed, reinitializing the gitlab client")
156+
b.Logger().Warn("Gitlab config changed, reinitializing the gitlab client")
116157
b.lockClientMutex.Lock()
117158
defer b.lockClientMutex.Unlock()
118159
b.client = nil
119160
}
120161
}
121162

122163
func (b *Backend) SetClient(client Client) {
164+
if client == nil {
165+
b.Logger().Debug("Setting a nil client")
166+
return
167+
}
168+
b.Logger().Debug("Setting a new client")
123169
b.client = client
124170
}
125171

126172
func (b *Backend) getClient(ctx context.Context, s logical.Storage) (Client, error) {
127173
if b.client != nil && b.client.Valid() {
174+
b.Logger().Debug("Returning existing gitlab client")
128175
return b.client, nil
129176
}
130177

131178
b.lockClientMutex.RLock()
132179
defer b.lockClientMutex.RUnlock()
133180
config, err := getConfig(ctx, s)
134181
if err != nil {
182+
b.Logger().Error("Failed to retrieve configuration", "error", err.Error())
135183
return nil, err
136184
}
137185

entry_config.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package gitlab
22

33
import (
44
"context"
5+
"crypto/sha1"
6+
"fmt"
57
"time"
68

79
"github.com/hashicorp/vault/sdk/logical"
810
)
911

1012
type EntryConfig struct {
13+
TokenId int `json:"token_id" yaml:"token_id" mapstructure:"token_id"`
1114
BaseURL string `json:"base_url" structs:"base_url" mapstructure:"base_url"`
1215
Token string `json:"token" structs:"token" mapstructure:"token"`
1316
AutoRotateToken bool `json:"auto_rotate_token" structs:"auto_rotate_token" mapstructure:"auto_rotate_token"`
@@ -24,10 +27,11 @@ func (e EntryConfig) LogicalResponseData() map[string]any {
2427

2528
return map[string]any{
2629
"base_url": e.BaseURL,
27-
"token": e.Token,
2830
"auto_rotate_token": e.AutoRotateToken,
2931
"auto_rotate_before": e.AutoRotateBefore.String(),
32+
"token_id": e.TokenId,
3033
"token_expires_at": tokenExpiresAt,
34+
"token_sha1_hash": fmt.Sprintf("%x", sha1.Sum([]byte(e.Token))),
3135
}
3236
}
3337

gitlab_client.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,16 @@ func NewGitlabClient(config *EntryConfig, httpClient *http.Client) (client Clien
232232
return nil, fmt.Errorf("configure the backend first, config: %w", ErrNilValue)
233233
}
234234

235-
if "" == strings.TrimSpace(config.BaseURL) || "" == strings.TrimSpace(config.Token) {
236-
return nil, fmt.Errorf("base url or token is empty: %w", ErrInvalidValue)
235+
if "" == strings.TrimSpace(config.BaseURL) {
236+
err = errors.Join(err, fmt.Errorf("gitlab base url: %w", ErrInvalidValue))
237+
}
238+
239+
if "" == strings.TrimSpace(config.Token) {
240+
err = errors.Join(err, fmt.Errorf("gitlab token: %w", ErrInvalidValue))
241+
}
242+
243+
if err != nil {
244+
return nil, err
237245
}
238246

239247
var opts = []g.ClientOptionFunc{

path_config.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,6 @@ func (b *Backend) pathConfigWrite(ctx context.Context, req *logical.Request, dat
140140

141141
b.lockClientMutex.Lock()
142142
defer b.lockClientMutex.Unlock()
143-
144143
err = saveConfig(ctx, config, req.Storage)
145144
if err != nil {
146145
return nil, err
@@ -155,7 +154,7 @@ func (b *Backend) pathConfigWrite(ctx context.Context, req *logical.Request, dat
155154
})
156155

157156
b.SetClient(nil)
158-
b.Logger().Debug("Wrote new config", "base_url", config.BaseURL)
157+
b.Logger().Debug("Wrote new config", "base_url", config.BaseURL, "auto_rotate_token", config.AutoRotateToken, "revoke_auto_rotated_token", config.RevokeAutoRotatedToken, "auto_rotate_before", config.AutoRotateBefore)
159158
return &logical.Response{
160159
Data: config.LogicalResponseData(),
161160
Warnings: warnings,

path_config_rotate.go

+6-22
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,13 @@ func pathConfigTokenRotate(b *Backend) *framework.Path {
3131
}
3232

3333
func (b *Backend) checkAndRotateConfigToken(ctx context.Context, request *logical.Request, config *EntryConfig) error {
34-
var client Client
3534
var err error
35+
b.Logger().Debug("Running checkAndRotateConfigToken")
3636

37-
if client, err = b.getClient(ctx, request.Storage); err != nil {
37+
if err = b.updateMainTokenExpiryTime(ctx, request, config); err != nil {
3838
return err
3939
}
4040

41-
if config.TokenExpiresAt.IsZero() {
42-
var entryToken *EntryToken
43-
// we need to fetch the token expiration information
44-
entryToken, err = client.CurrentTokenInfo()
45-
if err != nil {
46-
return err
47-
}
48-
// and update the information so we can do the checks
49-
config.TokenExpiresAt = *entryToken.ExpiresAt
50-
err = func() error {
51-
b.lockClientMutex.Lock()
52-
defer b.lockClientMutex.Unlock()
53-
return saveConfig(ctx, *config, request.Storage)
54-
}()
55-
if err != nil {
56-
return err
57-
}
58-
}
59-
6041
if time.Until(config.TokenExpiresAt) > config.AutoRotateBefore {
6142
b.Logger().Debug("Nothing to do it's not yet time to rotate the token")
6243
return nil
@@ -67,13 +48,15 @@ func (b *Backend) checkAndRotateConfigToken(ctx context.Context, request *logica
6748
}
6849

6950
func (b *Backend) pathConfigTokenRotate(ctx context.Context, request *logical.Request, data *framework.FieldData) (*logical.Response, error) {
51+
b.Logger().Debug("Running pathConfigTokenRotate")
7052
var config *EntryConfig
7153
var client Client
7254
var err error
7355

7456
b.lockClientMutex.RLock()
7557
if config, err = getConfig(ctx, request.Storage); err != nil {
7658
b.lockClientMutex.RUnlock()
59+
b.Logger().Error("Failed to fetch configuration", "error", err.Error())
7760
return nil, err
7861
}
7962
b.lockClientMutex.RUnlock()
@@ -90,14 +73,15 @@ func (b *Backend) pathConfigTokenRotate(ctx context.Context, request *logical.Re
9073
var entryToken, oldToken *EntryToken
9174
entryToken, oldToken, err = client.RotateCurrentToken(config.RevokeAutoRotatedToken)
9275
if err != nil {
93-
b.Logger().Error("failed to rotate main token", "err", err)
76+
b.Logger().Error("Failed to rotate main token", "err", err)
9477
return nil, err
9578
}
9679

9780
config.Token = entryToken.Token
9881
if entryToken.ExpiresAt != nil {
9982
config.TokenExpiresAt = *entryToken.ExpiresAt
10083
}
84+
config.TokenId = entryToken.TokenID
10185
b.lockClientMutex.Lock()
10286
defer b.lockClientMutex.Unlock()
10387
err = saveConfig(ctx, *config, request.Storage)

path_config_rotate_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import (
66
"testing"
77

88
"github.com/hashicorp/vault/sdk/logical"
9-
gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab"
109
"github.com/stretchr/testify/require"
10+
11+
gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab"
1112
)
1213

1314
func TestPathConfigRotate(t *testing.T) {

path_config_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func TestPathConfig(t *testing.T) {
6565
require.NoError(t, err)
6666
require.NotNil(t, resp)
6767
require.NoError(t, resp.Error())
68-
assert.EqualValues(t, "token", resp.Data["token"])
68+
assert.NotEmpty(t, resp.Data["token_sha1_hash"])
6969
assert.NotEmpty(t, resp.Data["base_url"])
7070
require.Len(t, events.eventsProcessed, 1)
7171

path_config_token_autorotate_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ func TestPathConfig_AutoRotateToken(t *testing.T) {
141141
b, l, err := getBackendWithConfig(map[string]any{"token": "token"})
142142
require.NoError(t, err)
143143

144+
b.SetClient(newInMemoryClient(true))
144145
err = b.PeriodicFunc(context.Background(), &logical.Request{Storage: l})
145146
require.NoError(t, err)
146147
})
@@ -176,7 +177,7 @@ func TestPathConfig_AutoRotateToken(t *testing.T) {
176177
require.NotNil(t, resp)
177178
require.NoError(t, resp.Error())
178179
require.NotEmpty(t, resp.Data)
179-
require.EqualValues(t, "new token", resp.Data["token"])
180+
require.NotEmpty(t, resp.Data["token_sha1_hash"])
180181
require.NotEmpty(t, resp.Data["token_expires_at"])
181182

182183
events.expectEvents(t, []expectedEvent{

0 commit comments

Comments
 (0)