Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial version of group access token #50

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![codecov-badge]][codecov]
![go-version-badge]

This is a backend plugin to be used with Vault. This plugin generates [Gitlab Project Access Tokens][pat]
This is a backend plugin to be used with Vault. This plugin generates [Project Access Tokens][pat] and [Group Access Tokens][gat]

- [Requirements](#requirements)
- [Getting Started](#getting-started)
Expand All @@ -17,12 +17,12 @@ This is a backend plugin to be used with Vault. This plugin generates [Gitlab Pr

## Requirements

- Gitlab instance with **13.10** or later for API compatibility
- Gitlab instance with **13.10** or later for API compatibility for [Project Access Tokens][pat] and **14.7** or later for [Group Access Tokens][gat]
- You need **14.1** or later to have access level
- Self-managed instances on Free and above. Or, GitLab SaaS Premium and above
- a token of a user with maintainer or higher permission in a project
- a token of a user with maintainer or higher permission in a project or group

- Lifting API rate limit for the user whose token will be used in this plugin to generate/revoke project access tokens. Admin of self-hosted can check [this doc][lift rate limit] to allow specific users to bypass authenticated request rate limiting. For SaaS Gitlab, I have not confirmed how to lift API limit yet.
- Lifting API rate limit for the user whose token will be used in this plugin to generate/revoke project/group access tokens. Admin of self-hosted can check [this doc][lift rate limit] to allow specific users to bypass authenticated request rate limiting. For SaaS Gitlab, I have not confirmed how to lift API limit yet.

## Getting Started

Expand Down Expand Up @@ -143,6 +143,7 @@ Please refer [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF
[Apache Software License version 2.0](LICENSE)

[pat]: https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html
[gat]: https://docs.gitlab.com/ee/user/group/settings/group_access_tokens.html
[lift rate limit]: https://docs.gitlab.com/ee/user/admin_area/settings/user_and_ip_rate_limits.html#allow-specific-users-to-bypass-authenticated-request-rate-limiting
[vault-plugin-secrets-artifactory]: https://github.com/splunk/vault-plugin-secrets-artifactory
[vault plugin]:https://www.vaultproject.io/docs/internals/plugins.html
Expand Down
2 changes: 1 addition & 1 deletion docs/backlogs.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ For comprehensive CI,

## Acceptance Testing

Running test against real servers doesn't seem good idea. Create an isolated environment by spinning up vault and gitlab in docker in CI. Then, run full suite of testing there. *Self-hosted GitLab has project access token available from free version*
Running test against real servers doesn't seem good idea. Create an isolated environment by spinning up vault and gitlab in docker in CI. Then, run full suite of testing there. *Self-hosted GitLab has project/group access token available from free version*

[granular control on token expiry]: https://gitlab.com/gitlab-org/gitlab/-/issues/335535
11 changes: 6 additions & 5 deletions docs/design-principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ This plugin supports two ways to generate a token in `/token` path
1. At root of `/token` path, a user requests a token by passing parameters.
2. (WIP): A user predefines roles with parameters. Then, a user can request a role's token at `/token/:<role-name>`

Parameters are same from Gitlab's [Project Access Token API]
Parameters are same from Gitlab's [Project Access Token API] and [Group Access Token API], make sure to pass `type` field with project/group

path `/token`

- Create/Update: generate a project access token with given parameters
- Create/Update: generate a project/group access token with given parameters

path `/roles/:<role_name>`

Expand All @@ -24,7 +24,7 @@ path `/roles/:<role_name>`

path `/token/:<role_name>`

- Create/Update: generate a project access token with stored parameters for the role
- Create/Update: generate a project/group access token with stored parameters for the role

## Things to Note

Expand All @@ -35,8 +35,9 @@ There are 2 kinds of access control in this plugins.
1. permissions attaches to the configured token
1. Vault resource access control - path access and capabilities

Root `/token` path can be used to request a project access token for any projects and any scopes as long as the configured token to generate access tokens have necessary permissions in these projects. 2nd kind of access token can't limit parameters to pass.
Root `/token` path can be used to request a project/group access token for any projects/groups and any scopes as long as the configured token to generate access tokens have necessary permissions in these projects/groups. 2nd kind of access token can't limit parameters to pass.

With that being said, it's better to use **roles**, which predefines a project and scopes; then, requesting a project access token for a role. You can further limit access to path via 2nd kind of access control imposed by Vault
With that being said, it's better to use **roles**, which predefines a project/groups and scopes; then, requesting a project/group access token for a role. You can further limit access to path via 2nd kind of access control imposed by Vault

[Project Access Token API]: https://docs.gitlab.com/ee/api/resource_access_tokens.html
[Group Access Token API]: https://docs.gitlab.com/ee/user/group/settings/group_access_tokens.html
2 changes: 2 additions & 0 deletions plugin/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import "github.com/xanzy/go-gitlab"

type PAT = gitlab.ProjectAccessToken

type GAT = gitlab.GroupAccessToken

const (
pathPatternConfig = "config"
pathPatternToken = "token"
Expand Down
20 changes: 20 additions & 0 deletions plugin/gitlab_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Client interface {
// ListProjectAccessToken(int) ([]*PAT, error)
CreateProjectAccessToken(*BaseTokenStorageEntry, *time.Time) (*PAT, error)
// RevokeProjectAccessToken(*BaseTokenStorageEntry) error
CreateGroupAccessToken(*BaseTokenStorageEntry, *time.Time) (*GAT, error)
Valid() bool
}

Expand Down Expand Up @@ -90,3 +91,22 @@ func (gc *gitlabClient) CreateProjectAccessToken(tokenStorage *BaseTokenStorageE
// func (gc *gitlabClient) RevokeProjectAccessToken(tokenStorage *BaseTokenStorageEntry) error {
// return nil
// }

func (gc *gitlabClient) CreateGroupAccessToken(tokenStorage *BaseTokenStorageEntry, expiresAt *time.Time) (*GAT, error) {
opt := gitlab.CreateGroupAccessTokenOptions{
Name: &tokenStorage.Name,
Scopes: &tokenStorage.Scopes,
}
if expiresAt != nil {
expiration := gitlab.ISOTime(*expiresAt)
opt.ExpiresAt = &expiration
}
if tokenStorage.AccessLevel != 0 {
opt.AccessLevel = (*gitlab.AccessLevelValue)(&tokenStorage.AccessLevel)
}
gat, _, err := gc.client.GroupAccessTokens.CreateGroupAccessToken(tokenStorage.ID, &opt)
if err != nil {
return nil, err
}
return gat, nil
}
4 changes: 4 additions & 0 deletions plugin/gitlab_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,7 @@ func (ac *mockGitlabClient) CreateProjectAccessToken(tokenStorage *BaseTokenStor
// func (ac *mockGitlabClient) RevokeProjectAccessToken(tokenStorage *BaseTokenStorageEntry) error {
// return nil
// }

func (ac *mockGitlabClient) CreateGroupAccessToken(tokenStorage *BaseTokenStorageEntry, expiresAt *time.Time) (*GAT, error) {
return nil, nil
}
2 changes: 1 addition & 1 deletion plugin/path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ Configure the Gitlab backend.
`

const pathConfigHelpDesc = `
The Gitlab backend requires credentials for creating a project access token.
The Gitlab backend requires credentials for creating an access token.
This endpoint is used to configure those credentials as well as default values
for the backend in general.
`
Expand Down
23 changes: 14 additions & 9 deletions plugin/path_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ var roleSchema = map[string]*framework.FieldSchema{
},
"id": {
Type: framework.TypeInt,
Description: "Project ID to create a project access token for",
Description: "Project/Group ID to create an access token for",
},
"name": {
Type: framework.TypeString,
Description: "The name of the project access token",
Description: "The name of the access token",
},
"scopes": {
Type: framework.TypeCommaStringSlice,
Expand All @@ -49,7 +49,11 @@ var roleSchema = map[string]*framework.FieldSchema{
},
"access_level": {
Type: framework.TypeInt,
Description: "access level of project access token",
Description: "access level of access token",
},
"token_type": {
Type: framework.TypeString,
Description: "access token type",
},
}

Expand All @@ -61,6 +65,7 @@ func roleDetail(role *RoleStorageEntry) map[string]interface{} {
"scopes": role.BaseTokenStorage.Scopes,
"access_level": role.BaseTokenStorage.AccessLevel,
"token_ttl": int64(role.TokenTTL / time.Second),
"token_type": role.BaseTokenStorage.TokenType,
}
}

Expand Down Expand Up @@ -89,10 +94,10 @@ func (b *GitlabBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.R
role.retrieve(data)
config, err := getConfig(ctx, req.Storage)
if err != nil {
return logical.ErrorResponse("failed to obtain artifactory config - %s", err.Error()), nil
return logical.ErrorResponse("failed to obtain gitlab config - %s", err.Error()), nil
}
if config == nil {
return logical.ErrorResponse("artifactory backend configuration has not been set up"), nil
return logical.ErrorResponse("gitlab backend configuration has not been set up"), nil
}
err = role.assertValid(config.MaxTTL)
if err != nil {
Expand All @@ -106,7 +111,7 @@ func (b *GitlabBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.R
return logical.ErrorResponse(err.Error()), nil
}
b.Logger().Debug("successfully create role", "role_name", roleName, "id", role.BaseTokenStorage.ID,
"name", role.BaseTokenStorage.Name, "scopes", role.BaseTokenStorage.Scopes)
"name", role.BaseTokenStorage.Name, "scopes", role.BaseTokenStorage.Scopes, "token_type", role.BaseTokenStorage.TokenType)

return &logical.Response{
Data: roleDetail(role),
Expand Down Expand Up @@ -209,10 +214,10 @@ func pathRoleList(b *GitlabBackend) []*framework.Path {
return paths
}

const pathRoleHelpSyn = `Create a role with parameters that are used to generate a project access token.`
const pathRoleHelpSyn = `Create a role with parameters that are used to generate an access token.`
const pathRoleHelpDesc = `
This path allows you to create a role whose parameters will be used to generate a project access token.
You must supply a project id to generate a token for, a name, which will be used as a name field in Gitlab,
This path allows you to create a role whose parameters will be used to generate an access token.
You must supply a project/group id to generate a token for, a name, which will be used as a name field in Gitlab,
and scopes for the generated project access token.
`

Expand Down
10 changes: 7 additions & 3 deletions plugin/path_role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func TestPathRole(t *testing.T) {
"name": "role-test",
"scopes": []string{"api", "read_repository"},
"access_level": 30,
"token_type": "project",
}
t.Run("successful", func(t *testing.T) {
roleName := "successful"
Expand Down Expand Up @@ -92,6 +93,7 @@ func TestPathRole(t *testing.T) {
"id": -1,
"token_ttl": fmt.Sprintf("%dh", 30*24),
"access_level": 31,
"token_type": "foo",
}
resp, err := testRoleCreate(t, backend, storage, roleName, d)
require.NoError(t, err)
Expand All @@ -102,6 +104,7 @@ func TestPathRole(t *testing.T) {
require.Contains(t, resp.Data["error"], "scopes are empty")
require.Contains(t, resp.Data["error"], "exceeds configured maximum ttl")
require.Contains(t, resp.Data["error"], "invalid access level")
require.Contains(t, resp.Data["error"], "token_type must be either")
})
}

Expand All @@ -116,9 +119,10 @@ func TestPathRoleList(t *testing.T) {
}
testConfigUpdate(t, backend, storage, conf)
data := map[string]interface{}{
"id": 1,
"name": "role-test",
"scopes": []string{"api", "read_repository"},
"id": 1,
"name": "role-test",
"scopes": []string{"api", "read_repository"},
"token_type": "project",
}

var listResp map[string]interface{}
Expand Down
44 changes: 31 additions & 13 deletions plugin/path_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import (
var accessTokenSchema = map[string]*framework.FieldSchema{
"id": {
Type: framework.TypeInt,
Description: "Project ID to create a project access token for",
Description: "Project/Group ID to create an access token for",
},
"name": {
Type: framework.TypeString,
Description: "The name of the project access token",
Description: "The name of the access token",
},
"scopes": {
Type: framework.TypeCommaStringSlice,
Expand All @@ -43,11 +43,15 @@ var accessTokenSchema = map[string]*framework.FieldSchema{
},
"access_level": {
Type: framework.TypeInt,
Description: "access level of project access token",
Description: "access level of access token",
},
"token_type": {
Type: framework.TypeString,
Description: "access token type",
},
}

func tokenDetails(pat *PAT) map[string]interface{} {
func projectTokenDetails(pat *PAT) map[string]interface{} {
d := map[string]interface{}{
"token": pat.Token,
"id": pat.ID,
Expand All @@ -60,6 +64,19 @@ func tokenDetails(pat *PAT) map[string]interface{} {
}
return d
}
func groupTokenDetails(gat *GAT) map[string]interface{} {
d := map[string]interface{}{
"token": gat.Token,
"id": gat.ID,
"name": gat.Name,
"scopes": gat.Scopes,
"access_level": gat.AccessLevel,
}
if gat.ExpiresAt != nil {
d["expires_at"] = time.Time(*gat.ExpiresAt)
}
return d
}

func (b *GitlabBackend) pathTokenCreate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
gc, err := b.getClient(ctx, req.Storage)
Expand All @@ -72,10 +89,10 @@ func (b *GitlabBackend) pathTokenCreate(ctx context.Context, req *logical.Reques

config, err := getConfig(ctx, req.Storage)
if err != nil {
return logical.ErrorResponse("failed to obtain artifactory config - %s", err.Error()), nil
return logical.ErrorResponse("failed to obtain gitlab config - %s", err.Error()), nil
}
if config == nil {
return logical.ErrorResponse("artifactory backend configuration has not been set up"), nil
return logical.ErrorResponse("gitlab backend configuration has not been set up"), nil
}
err = tokenStorage.assertValid(config.MaxTTL)
if err != nil {
Expand All @@ -84,11 +101,12 @@ func (b *GitlabBackend) pathTokenCreate(ctx context.Context, req *logical.Reques

b.Logger().Debug("generating access token", "id", tokenStorage.BaseTokenStorage.ID,
"name", tokenStorage.BaseTokenStorage.Name, "scopes", tokenStorage.BaseTokenStorage.Scopes)
pat, err := gc.CreateProjectAccessToken(&tokenStorage.BaseTokenStorage, tokenStorage.ExpiresAt)

d, err := tokenStorage.BaseTokenStorage.createAccessToken(gc, *tokenStorage.ExpiresAt)
if err != nil {
return logical.ErrorResponse("Failed to create a token - " + err.Error()), nil
}
return &logical.Response{Data: tokenDetails(pat)}, nil
return &logical.Response{Data: d}, nil
}

// set up the paths for the roles within vault
Expand All @@ -101,7 +119,7 @@ func pathToken(b *GitlabBackend) []*framework.Path {
logical.CreateOperation: &framework.PathOperation{

Callback: b.pathTokenCreate,
Summary: "Create a project access token",
Summary: "Create an access token",
Examples: tokenExamples,
},
logical.UpdateOperation: &framework.PathOperation{
Expand All @@ -116,18 +134,18 @@ func pathToken(b *GitlabBackend) []*framework.Path {
return paths
}

const pathTokenHelpSyn = `Generate a project access token for a given project with token name, scopes.`
const pathTokenHelpSyn = `Generate an access token for a given project/group with token name, scopes.`
const pathTokenHelpDesc = `
This path allows you to generate a project access token. You must supply a project id to generate a token for, a name, which
This path allows you to generate an access token. You must supply a project/group id to generate a token for, a name, which
will be used as a name field in Gitlab, and scopes for the generated project access token.
`

var tokenExamples = []framework.RequestExample{
{
Description: "Create a project access token",
Description: "Create an access token",
Data: map[string]interface{}{
"id": 1,
"name": "MyProjectAccessToken",
"name": "MyAccessToken",
"scopes": []string{"read_api", "read_repository"},
},
},
Expand Down
15 changes: 7 additions & 8 deletions plugin/path_token_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,11 @@ func (b *GitlabBackend) pathRoleTokenCreate(ctx context.Context, req *logical.Re

expiresAt := time.Now().UTC().Add(role.TokenTTL)
b.Logger().Debug("generating access token for a role", "role_name", role.RoleName, "expires_at", expiresAt)
pat, err := gc.CreateProjectAccessToken(&role.BaseTokenStorage, &expiresAt)
d, err := role.BaseTokenStorage.createAccessToken(gc, expiresAt)
if err != nil {
return logical.ErrorResponse("Failed to create a token - " + err.Error()), nil
}

return &logical.Response{Data: tokenDetails(pat)}, nil
return &logical.Response{Data: d}, nil
}

// set up the paths for the roles within vault
Expand All @@ -63,7 +62,7 @@ func pathRoleToken(b *GitlabBackend) []*framework.Path {
logical.CreateOperation: &framework.PathOperation{

Callback: b.pathRoleTokenCreate,
Summary: "Create a project access token based on a predefined role",
Summary: "Create an access token based on a predefined role",
Examples: roleTokenExamples,
},
logical.UpdateOperation: &framework.PathOperation{
Expand All @@ -78,15 +77,15 @@ func pathRoleToken(b *GitlabBackend) []*framework.Path {
return paths
}

const pathRoleTokenHelpSyn = `Generate a project access token for a given project based on a predefined role`
const pathRoleTokenHelpSyn = `Generate an access token for a given project/group based on a predefined role`
const pathRoleTokenHelpDesc = `
This path allows you to generate a project access token based on a predefined role. You must create a role beforehand in /roles/ path,
whose parameters are used to generate a project access token.
This path allows you to generate an access token based on a predefined role. You must create a role beforehand in /roles/ path,
whose parameters are used to generate an access token.
`

var roleTokenExamples = []framework.RequestExample{
{
Description: "Create a project access token based on a predefined role",
Description: "Create an access token based on a predefined role",
Data: map[string]interface{}{
"role_name": "MyRole",
},
Expand Down
Loading