gitlab

package module
v0.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 13, 2024 License: MIT Imports: 19 Imported by: 0

README

Vault Plugin for Gitlab Access Token

Go Report Card Codecov GitHub go.mod Go version (subdirectory of monorepo) GitHub

This is a standalone backend plugin for use with Hashicorp Vault. This plugin allows for Gitlab to generate personal, project and group access tokens. This was created so we can automate the creation/revocation of access tokens through Vault.

Getting Started

This is a Vault plugin and is meant to work with Vault. This guide assumes you have already installed Vault and have a basic understanding of how Vault works.

Otherwise, first read this guide on how to get started with Vault.

To learn specifically about how plugins work, see documentation on Vault plugins.

Setup

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

Security Model

The current authentication model requires providing Vault with a Gitlab Token.

Configuration

Config

Property Required Default value Sensitive Description
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.
base_url yes n/a no The address to access Gitlab
auto_rotate_token no no no Should we autorotate the token when it's close to expiry? (Experimental)
revoke_auto_rotated_token no no no Should we revoke the auto-rotated token after a new one has been generated?
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

Role

Property Required Default value Sensitive Description
path yes n/a no Project/Group path to create an access token for. If the token type is set to personal then write the username here.
name yes n/a no The name of the access token
ttl yes n/a no The TTL of the token
access_level no/yes n/a no Access level of access token (only required for Group and Project access tokens)
scopes no [] no List of scopes
token_type yes n/a no Access token type
gitlab_revokes_token no no no Gitlab revokes the token when it's time. Vault will not revoke the token when the lease expires
ttl

Depending on gitlab_revokes_token the TTL will change.

  • true - 24h <= ttl <= 365 days
  • false - 1h <= ttl <= 365 days
access_level

It's not required if token_type is set to personal.

For a list of available roles check https://docs.gitlab.com/ee/user/permissions.html

scopes

Depending on the type of token you have different scopes:

token_types

Can be

  • personal
  • project
  • group
gitlab_revokes_token

This is a flag that doesn't expire the token when the token used to create the credentials expire. When the vault token used to create gitlab credentials with a TTL longer than the vault token, the new gitlab credentials will expire at the same time with the parent. Setting this up will not call the revoke endpoint on gitlab.

Examples

Setup

Before we can use the plugin we need to register and enable it in Vault.

vault plugin register \
  -sha256=$(sha256sum path/to/plugin/directory/gitlab | cut -d " " -f 1) \
  -command=vault-plugin-secrets-gitlab \
  secret gitlab

vault secrets enable gitlab

Config

Due to how Gitlab manages expiration the minimum is 24h and maximum is 365 days. As per non-expiring-access-tokens and Remove ability to create deprecated non-expiring access tokens. Since Gitlab 16.0 the ability to create non expiring token has been removed.

If you use Vault to manage the tokens the minimal TTL you can use is 1h, by setting gitlab_revokes_token=false.

The command bellow will set up the config backend with a max TTL of 48h.

$ 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
$ vault read gitlab/config
Key                   Value
---                   -----
auto_rotate_before    48h0m0s
auto_rotate_token     false
base_url              https://gitlab.example.com
token_id              107
token_expires_at      2025-03-29T00:00:00Z
token_sha1_hash       1014647cd9bbf359d926fcacdf78e184db9dbedc

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

Max TTL: 1 year Default TTL: 1 week

$ vault secrets tune -max-lease-ttl=8784h -default-lease-ttl=168h gitlab/

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

There is a periodic func that runs that is responsible for autorotation and main token expiry time. So in the beginning you may see token_expires_at n/a. But when the function runs it will update itself with the correct expiry date and the corresponding token_id.

Roles

This will create three roles, one of each type.

# personal access tokens can only be created by Gitlab Administrators (see https://docs.gitlab.com/ee/api/users.html#create-a-personal-access-token)
$ vault write gitlab/roles/personal name=personal-token-name path=username scopes="read_api" token_type=personal ttl=48h

$ vault write gitlab/roles/project name=project-token-name path=group/project scopes="read_api" access_level=guest token_type=project ttl=48h

$ vault write gitlab/roles/group name=group-token-name path=group/subgroup scopes="read_api" access_level=developer token_type=group ttl=48h

Get access tokens

Personal
$ vault read gitlab/token/personal
Key                Value
---                -----
lease_id           gitlab/token/personal/0FrzLFkRKaUNZSfa6WfFqjWK
lease_duration     20h1m37s
lease_renewable    false
access_level       n/a
created_at         2023-08-31T03:58:23.069Z
expires_at         2023-09-01T00:00:00Z
name               vault-generated-personal-access-token-227cb38b
path               username
scopes             [read_api]
token              7mbpSExz7ruyw1QgTjL-

$ vault lease revoke gitlab/token/personal/0FrzLFkRKaUNZSfa6WfFqjWK
All revocation operations queued successfully!
Service accounts

The service account users from Gitlab 16.1 are for all purposes users that don't use seats. So creating a service account and setting the path to the service account user would work the same as on a real user.

$ curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" "https://gitlab/api/v4/service_accounts" | jq .
{
  "id": 2017,
  "username": "service_account_00b069cb73a15d0a7ba8cd67a653599c",
  "name": "Service account user",
  "state": "active",
  "avatar_url": "https://secure.gravatar.com/avatar/6faa2758127182d391be18b4c1e36630?s=80&d=identicon",
  "web_url": "https://gitlab/service_account_00b069cb73a15d0a7ba8cd67a653599c"
}

In this case you would create a role like

$ vault write gitlab/roles/sa name=sa-name path=service_account_00b069cb73a15d0a7ba8cd67a653599c scopes="read_api" token_type=personal token_ttl=24h
$ vault read gitlab/token/sa
vault read gitlab/token/sa

Key                Value
---                -----
lease_id           gitlab/token/sa/oFI2vpUdvykvMgNum6pZReYZ
lease_duration     20h1m37s
lease_renewable    false
access_level       n/a
created_at         2023-08-31T03:58:23.069Z
expires_at         2023-09-01T00:00:00Z
name               vault-generated-personal-access-token-f6417198
path               service_account_00b069cb73a15d0a7ba8cd67a653599c
scopes             [api read_api read_repository read_registry]
token              -senkScjDo-SoGwST9PP
Group
$ vault read gitlab/token/group
Key                Value
---                -----
lease_id           gitlab/token/group/LqmL1MtuIlJ43N8q2L975jm8
lease_duration     20h14s
lease_renewable    false
access_level       developer
created_at         2023-08-31T03:59:46.043Z
expires_at         2023-09-01T00:00:00Z
name               vault-generated-group-access-token-913ab1f9
path               group/subgroup
scopes             [read_api]
token              rSYv4zwgP-2uaFEAsZyd

$ vault lease revoke gitlab/token/group/LqmL1MtuIlJ43N8q2L975jm8
All revocation operations queued successfully!
Project
$ vault read gitlab/token/project
Key                Value
---                -----
lease_id           gitlab/token/project/ZMSOrOHiP77l5kjWXq3zizPA
lease_duration     19h59m6s
lease_renewable    false
access_level       guest
created_at         2023-08-31T04:00:53.613Z
expires_at         2023-09-01T00:00:00Z
name               vault-generated-project-access-token-842113a6
path               group/project
scopes             [read_api]
token              YfRu42VaGGrxshKKwtma

$ vault lease revoke gitlab/token/project/ZMSOrOHiP77l5kjWXq3zizPA
All revocation operations queued successfully!

Revoke all created tokens by this plugin

$ vault lease revoke -prefix gitlab/
All revocation operations queued successfully!

Force rotation of the main token

If the original token that has been supplied to the backend is not expired. We can use the endpoint bellow to force a rotation of the main token. This would create a new token with the same expiration as the original token.

$ vault read gitlab/config/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

TODO

  • Add tests against real Gitlab instance

Documentation

Index

Constants

View Source
const (
	DefaultConfigFieldAccessTokenMaxTTL = 7 * 24 * time.Hour
	DefaultConfigFieldAccessTokenRotate = DefaultAutoRotateBeforeMinTTL
	DefaultRoleFieldAccessTokenMaxTTL   = 24 * time.Hour
	DefaultAccessTokenMinTTL            = 24 * time.Hour
	DefaultAccessTokenMaxPossibleTTL    = 365 * 24 * time.Hour
	DefaultAutoRotateBeforeMinTTL       = 24 * time.Hour
	DefaultAutoRotateBeforeMaxTTL       = 730 * time.Hour
)
View Source
const (
	AccessLevelNoPermissions            = AccessLevel("no_permissions")
	AccessLevelMinimalAccessPermissions = AccessLevel("minimal_access")
	AccessLevelGuestPermissions         = AccessLevel("guest")
	AccessLevelReporterPermissions      = AccessLevel("reporter")
	AccessLevelDeveloperPermissions     = AccessLevel("developer")
	AccessLevelMaintainerPermissions    = AccessLevel("maintainer")
	AccessLevelOwnerPermissions         = AccessLevel("owner")

	AccessLevelUnknown = AccessLevel("")
)
View Source
const (
	// TokenScopeApi grants complete read and write access to the scoped group and related project API, including the Package Registry
	TokenScopeApi = TokenScope("api")
	// TokenScopeReadApi grants read access to the scoped group and related project API, including the Package Registry
	TokenScopeReadApi = TokenScope("read_api")
	// TokenScopeReadRegistry grants read access (pull) to the Container Registry images if any project within expected group is private and authorization is required.
	TokenScopeReadRegistry = TokenScope("read_registry")
	// TokenScopeWriteRegistry grants write access (push) to the Container Registry.
	TokenScopeWriteRegistry = TokenScope("write_registry")
	// TokenScopeReadRepository grants read access (pull) to the Container Registry images if any project within expected group is private and authorization is required
	TokenScopeReadRepository = TokenScope("read_repository")
	// TokenScopeWriteRepository grants read and write access (pull and push) to all repositories within expected group
	TokenScopeWriteRepository = TokenScope("write_repository")
	// TokenScopeCreateRunner grants permission to create runners in expected group
	TokenScopeCreateRunner = TokenScope("create_runner")

	// TokenScopeReadUser grants read-only access to the authenticated user’s profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users.
	TokenScopeReadUser = TokenScope("read_user")
	// TokenScopeSudo grants permission to perform API actions as any user in the system, when authenticated as an administrator.
	TokenScopeSudo = TokenScope("sudo")
	// TokenScopeAdminMode grants permission to perform API actions as an administrator, when Admin Mode is enabled.
	TokenScopeAdminMode = TokenScope("admin_mode")

	TokenScopeUnknown = TokenScope("")
)
View Source
const (
	TokenTypePersonal = TokenType("personal")
	TokenTypeProject  = TokenType("project")
	TokenTypeGroup    = TokenType("group")

	TokenTypeUnknown = TokenType("")
)
View Source
const (
	PathConfigStorage = "config"
)
View Source
const (
	PathRoleStorage = "roles"
)
View Source
const (
	PathTokenRoleStorage = "token"
)

Variables

View Source
var (
	ErrNilValue             = errors.New("nil value")
	ErrInvalidValue         = errors.New("invalid value")
	ErrFieldRequired        = errors.New("required field")
	ErrFieldInvalidValue    = errors.New("invalid value for field")
	ErrBackendNotConfigured = errors.New("backend not configured")
)
View Source
var (
	ErrAccessTokenNotFound = errors.New("access token not found")
	ErrRoleNotFound        = errors.New("role not found")
)
View Source
var (
	ErrUnknownAccessLevel = errors.New("unknown access level")

	ValidAccessLevels = []string{
		AccessLevelNoPermissions.String(),
		AccessLevelMinimalAccessPermissions.String(),
		AccessLevelGuestPermissions.String(),
		AccessLevelReporterPermissions.String(),
		AccessLevelDeveloperPermissions.String(),
		AccessLevelMaintainerPermissions.String(),
		AccessLevelOwnerPermissions.String(),
	}

	ValidPersonalAccessLevels = []string{
		AccessLevelUnknown.String(),
	}
	ValidProjectAccessLevels = []string{
		AccessLevelGuestPermissions.String(),
		AccessLevelReporterPermissions.String(),
		AccessLevelDeveloperPermissions.String(),
		AccessLevelMaintainerPermissions.String(),
		AccessLevelOwnerPermissions.String(),
	}
	ValidGroupAccessLevels = []string{
		AccessLevelGuestPermissions.String(),
		AccessLevelReporterPermissions.String(),
		AccessLevelDeveloperPermissions.String(),
		AccessLevelMaintainerPermissions.String(),
		AccessLevelOwnerPermissions.String(),
	}
)
View Source
var (
	ErrUnknownTokenScope = errors.New("unknown token scope")

	ValidGroupTokenScopes   = validTokenScopes
	ValidProjectTokenScopes = validTokenScopes

	ValidPersonalTokenScopes = []string{
		TokenScopeReadUser.String(),
		TokenScopeSudo.String(),
		TokenScopeAdminMode.String(),
	}
)
View Source
var BuildDate string
View Source
var (
	ErrUnknownTokenType = errors.New("unknown token type")
)
View Source
var FullCommit string
View Source
var Version string = "v0.0.0-dev"

Functions

func Factory

func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error)

Factory returns expected new Backend as logical.Backend

Types

type AccessLevel

type AccessLevel string

func AccessLevelParse

func AccessLevelParse(value string) (AccessLevel, error)

func (AccessLevel) String

func (i AccessLevel) String() string

func (AccessLevel) Value

func (i AccessLevel) Value() int

type Backend

type Backend struct {
	*framework.Backend
	// contains filtered or unexported fields
}

func (*Backend) Invalidate

func (b *Backend) Invalidate(ctx context.Context, key string)

Invalidate invalidates the key if required

func (*Backend) SetClient

func (b *Backend) SetClient(client Client)

type Client

type Client interface {
	Valid() bool

	CurrentTokenInfo() (*EntryToken, error)
	RotateCurrentToken(revokeOldToken bool) (newToken *EntryToken, oldToken *EntryToken, err error)
	CreatePersonalAccessToken(username string, userId int, name string, expiresAt time.Time, scopes []string) (*EntryToken, error)
	CreateGroupAccessToken(groupId string, name string, expiresAt time.Time, scopes []string, accessLevel AccessLevel) (*EntryToken, error)
	CreateProjectAccessToken(projectId string, name string, expiresAt time.Time, scopes []string, accessLevel AccessLevel) (*EntryToken, error)
	RevokePersonalAccessToken(tokenId int) error
	RevokeProjectAccessToken(tokenId int, projectId string) error
	RevokeGroupAccessToken(tokenId int, groupId string) error
	GetUserIdByUsername(username string) (int, error)
}

func NewGitlabClient

func NewGitlabClient(config *EntryConfig, httpClient *http.Client) (client Client, err error)

type EntryConfig added in v0.2.0

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"`
	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"`
	TokenExpiresAt         time.Time     `json:"token_expires_at" structs:"token_expires_at" mapstructure:"token_expires_at"`
	RevokeAutoRotatedToken bool          `json:"revoke_auto_rotated_token" structs:"revoke_auto_rotated_token" mapstructure:"revoke_auto_rotated_token"`
}

func (EntryConfig) LogicalResponseData added in v0.2.0

func (e EntryConfig) LogicalResponseData() map[string]any

type EntryToken

type EntryToken struct {
	TokenID            int         `json:"token_id"`
	UserID             int         `json:"user_id"`
	ParentID           string      `json:"parent_id"`
	Path               string      `json:"path"`
	Name               string      `json:"name"`
	Token              string      `json:"token"`
	TokenType          TokenType   `json:"token_type"`
	CreatedAt          *time.Time  `json:"created_at"`
	ExpiresAt          *time.Time  `json:"expires_at"`
	Scopes             []string    `json:"scopes"`
	AccessLevel        AccessLevel `json:"access_level"` // not used for personal access tokens
	RoleName           string      `json:"role_name"`
	GitlabRevokesToken bool        `json:"gitlab_revokes_token"`
}

func (EntryToken) SecretResponse

func (e EntryToken) SecretResponse() (map[string]any, map[string]any)

type TokenScope

type TokenScope string

func TokenScopeParse

func TokenScopeParse(value string) (TokenScope, error)

func (TokenScope) String

func (i TokenScope) String() string

func (TokenScope) Value

func (i TokenScope) Value() string

type TokenType

type TokenType string

func TokenTypeParse

func TokenTypeParse(value string) (TokenType, error)

func (TokenType) String

func (i TokenType) String() string

func (TokenType) Value

func (i TokenType) Value() string

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL