Skip to content

Commit

Permalink
GitLab Delegated Joining (#22705) (#23191)
Browse files Browse the repository at this point in the history
* Add type for GitLab ProvisionToken

* Add default behaviour for domain

* Add IDTokenClaims for GitLab

* Add gitlab token source and token validator

* Thread GitLab support through auth and tbot packages

* Adjust cluster name fetching in token validator

* Initialize GitLab token validator in auth

* Improve comment on `sub`

* Working GitLab CI delegated joining

* Add additional token rule fields

* Add checking for new configuration fields

* add additional test cases for validation of gitlab config struct

* Add TestAuth_RegisterUsingToken_GitLab

* Add tests for IDTokenSource

* Fix imports

* Add tests for GitLab Token Validator

* Fix some comments that were incomplete

* Add license headers
  • Loading branch information
strideynet authored Mar 17, 2023
1 parent 49e4d69 commit fe5bff3
Show file tree
Hide file tree
Showing 18 changed files with 3,881 additions and 1,838 deletions.
43 changes: 43 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,8 @@ message ProvisionTokenSpecV2 {
ProvisionTokenSpecV2Kubernetes Kubernetes = 10 [(gogoproto.jsontag) = "kubernetes,omitempty"];
// Azure allows the configuration of options specific to the "azure" join method.
ProvisionTokenSpecV2Azure Azure = 11 [(gogoproto.jsontag) = "azure,omitempty"];
// GitLab allows the configuration of options specific to the "gitlab" join method.
ProvisionTokenSpecV2GitLab GitLab = 12 [(gogoproto.jsontag) = "gitlab,omitempty"];
}

// ProvisionTokenSpecV2Github contains the GitHub-specific part of the
Expand Down Expand Up @@ -1182,6 +1184,47 @@ message ProvisionTokenSpecV2GitHub {
string EnterpriseServerHost = 2 [(gogoproto.jsontag) = "enterprise_server_host,omitempty"];
}

// ProvisionTokenSpecV2GitLab contains the GitLab-specific part of the
// ProvisionTokenSpecV2
message ProvisionTokenSpecV2GitLab {
message Rule {
// Sub roughly uniquely identifies the workload. Example:
// `project_path:mygroup/my-project:ref_type:branch:ref:main`
// project_path:{group}/{project}:ref_type:{type}:ref:{branch_name}
string Sub = 1 [(gogoproto.jsontag) = "sub,omitempty"];
// Ref allows access to be limited to jobs triggered by a specific git ref.
// Ensure this is used in combination with ref_type.
string Ref = 2 [(gogoproto.jsontag) = "ref,omitempty"];
// RefType allows access to be limited to jobs triggered by a specific git
// ref type. Example:
// `branch` or `tag`
string RefType = 3 [(gogoproto.jsontag) = "ref_type,omitempty"];
// NamespacePath is used to limit access to jobs in a group or user's
// projects.
// Example:
// `mygroup`
string NamespacePath = 4 [(gogoproto.jsontag) = "namespace_path,omitempty"];
// ProjectPath is used to limit access to jobs belonging to an individual
// project. Example:
// `mygroup/myproject`
string ProjectPath = 5 [(gogoproto.jsontag) = "project_path,omitempty"];
// PipelineSource limits access by the job pipeline source type.
// https://docs.gitlab.com/ee/ci/jobs/job_control.html#common-if-clauses-for-rules
// Example: `web`
string PipelineSource = 6 [(gogoproto.jsontag) = "pipeline_source,omitempty"];
// Environment limits access by the environment the job deploys to
// (if one is associated)
string Environment = 7 [(gogoproto.jsontag) = "environment,omitempty"];
}
// Allow is a list of TokenRules, nodes using this token must match one
// allow rule to use this token.
repeated Rule Allow = 1 [(gogoproto.jsontag) = "allow,omitempty"];
// Domain is the domain of your GitLab instance. This will default to
// `gitlab.com` - but can be set to the domain of your self-hosted GitLab
// e.g `gitlab.example.com`.
string Domain = 2 [(gogoproto.jsontag) = "domain,omitempty"];
}

// ProvisionTokenSpecV2CircleCI contains the CircleCI-specific part of the
// ProvisionTokenSpecV2
message ProvisionTokenSpecV2CircleCI {
Expand Down
46 changes: 46 additions & 0 deletions api/types/provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ const (
// JoinMethodAzure indicates that the node will join with the Azure join
// method.
JoinMethodAzure JoinMethod = "azure"
// JoinMethodGitLab indicates that the node will join with the GitLab
// join method. Documentation regarding implementation of this
// can be found in lib/gitlab
JoinMethodGitLab JoinMethod = "gitlab"
)

var JoinMethods = []JoinMethod{
Expand All @@ -65,6 +69,7 @@ var JoinMethods = []JoinMethod{
JoinMethodCircleCI,
JoinMethodKubernetes,
JoinMethodAzure,
JoinMethodGitLab,
}

func ValidateJoinMethod(method JoinMethod) error {
Expand Down Expand Up @@ -267,6 +272,17 @@ func (p *ProvisionTokenV2) CheckAndSetDefaults() error {
if err := providerCfg.checkAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
case JoinMethodGitLab:
providerCfg := p.Spec.GitLab
if providerCfg == nil {
return trace.BadParameter(
`"gitlab" configuration must be provided for the join method %q`,
JoinMethodGitLab,
)
}
if err := providerCfg.checkAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
default:
return trace.BadParameter("unknown join method %q", p.Spec.JoinMethod)
}
Expand Down Expand Up @@ -567,3 +583,33 @@ func (a *ProvisionTokenSpecV2Azure) checkAndSetDefaults() error {
}
return nil
}

const defaultGitLabDomain = "gitlab.com"

func (a *ProvisionTokenSpecV2GitLab) checkAndSetDefaults() error {
if len(a.Allow) == 0 {
return trace.BadParameter(
"the %q join method requires defined gitlab allow rules",
JoinMethodGitLab,
)
}
for _, allowRule := range a.Allow {
if allowRule.Sub == "" && allowRule.NamespacePath == "" && allowRule.ProjectPath == "" {
return trace.BadParameter(
"the %q join method requires allow rules with at least 'sub', 'project_path' or 'namespace_path' to ensure security.",
JoinMethodGitLab,
)
}
}

if a.Domain == "" {
a.Domain = defaultGitLabDomain
} else {
if strings.Contains(a.Domain, "/") {
return trace.BadParameter(
"'spec.gitlab.domain' should not contain the scheme or path",
)
}
}
return nil
}
152 changes: 151 additions & 1 deletion api/types/provisioning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (

"github.com/gravitational/trace"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/api/defaults"
)

func TestProvisionTokenV2_CheckAndSetDefaults(t *testing.T) {
Expand Down Expand Up @@ -448,6 +450,154 @@ func TestProvisionTokenV2_CheckAndSetDefaults(t *testing.T) {
},
expectedErr: &trace.BadParameterError{},
},
{
desc: "gitlab empty allow rules",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodGitLab,
GitLab: &ProvisionTokenSpecV2GitLab{
Allow: []*ProvisionTokenSpecV2GitLab_Rule{},
},
},
},
expectedErr: &trace.BadParameterError{},
},
{
desc: "gitlab missing config",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodGitLab,
GitLab: nil,
},
},
expectedErr: &trace.BadParameterError{},
},
{
desc: "gitlab empty allow rule",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodGitLab,
GitLab: &ProvisionTokenSpecV2GitLab{
Allow: []*ProvisionTokenSpecV2GitLab_Rule{
{},
},
},
},
},
expectedErr: &trace.BadParameterError{},
},
{
desc: "gitlab defaults",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodGitLab,
GitLab: &ProvisionTokenSpecV2GitLab{
Allow: []*ProvisionTokenSpecV2GitLab_Rule{
{
Sub: "asub",
},
},
},
},
},
expected: &ProvisionTokenV2{
Kind: KindToken,
Version: V2,
Metadata: Metadata{
Name: "test",
Namespace: defaults.Namespace,
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodGitLab,
GitLab: &ProvisionTokenSpecV2GitLab{
Allow: []*ProvisionTokenSpecV2GitLab_Rule{
{
Sub: "asub",
},
},
Domain: defaultGitLabDomain,
},
},
},
},
{
desc: "overridden domain",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodGitLab,
GitLab: &ProvisionTokenSpecV2GitLab{
Allow: []*ProvisionTokenSpecV2GitLab_Rule{
{
Sub: "asub",
},
},
Domain: "gitlab.example.com",
},
},
},
expected: &ProvisionTokenV2{
Kind: KindToken,
Version: V2,
Metadata: Metadata{
Name: "test",
Namespace: defaults.Namespace,
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodGitLab,
GitLab: &ProvisionTokenSpecV2GitLab{
Allow: []*ProvisionTokenSpecV2GitLab_Rule{
{
Sub: "asub",
},
},
Domain: "gitlab.example.com",
},
},
},
},
{
desc: "invalid overridden domain",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodGitLab,
GitLab: &ProvisionTokenSpecV2GitLab{
Allow: []*ProvisionTokenSpecV2GitLab_Rule{
{
Sub: "asub",
},
},
Domain: "http://gitlab.example.com",
},
},
},
expectedErr: &trace.BadParameterError{},
},
}

for _, tc := range testcases {
Expand All @@ -459,7 +609,7 @@ func TestProvisionTokenV2_CheckAndSetDefaults(t *testing.T) {
}
require.NoError(t, err)
if tc.expected != nil {
require.Equal(t, tc.token, tc.expected)
require.Equal(t, tc.expected, tc.token)
}
})
}
Expand Down
Loading

0 comments on commit fe5bff3

Please sign in to comment.