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

Add Project Connection setup for Vault OAuth provider. #91

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ci: test ## Run all the CI targets
start-docker: ## Starts up docker container running TeamCity Server
@test -d $(TEAMCITY_DATA_DIR) || bsdtar xfz $(INTEGRATION_TEST_DIR)/teamcity_data.tar.gz -C $(INTEGRATION_TEST_DIR)
@curl -sL https://download.octopusdeploy.com/octopus-teamcity/4.42.1/Octopus.TeamCity.zip -o $(TEAMCITY_DATA_DIR)/plugins/Octopus.TeamCity.zip
@curl -sL "https://plugins.jetbrains.com/plugin/download?rel=true&updateId=100790" -o ${TEAMCITY_DATA_DIR}/plugins/teamcity-hashicorp-vault-plugin.zip
@test -n "$$(docker ps -q -f name=$(CONTAINER_NAME))" || docker run --rm -d \
--name $(CONTAINER_NAME) \
-v $(PWD)/$(TEAMCITY_DATA_DIR):/data/teamcity_server/datadir \
Expand Down
11 changes: 10 additions & 1 deletion teamcity/locator.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package teamcity

import "net/url"
import (
"fmt"
"net/url"
)

//Locator represents a arbitraty locator to be used when querying resources, such as id:, type:, or key:
//These are used in GET requests within the URL so must be properly escaped
Expand All @@ -26,6 +29,12 @@ func LocatorType(id string) Locator {
return Locator(url.QueryEscape("type:") + id)
}

//LocatorTypeProvider creates a locator for a Project Feature by Type and Provider
func LocatorTypeProvider(featureType string, featureProvider string) Locator {
queryString := fmt.Sprintf("type:%s,property(name:providerType,value:%s)", featureType, featureProvider)
return Locator(url.QueryEscape(queryString))
}

func (l Locator) String() string {
return string(l)
}
9 changes: 9 additions & 0 deletions teamcity/locator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@ func Test_LocatorId(t *testing.T) {

assert.Equal(t, "id%3A_Root", actual)
}

func Test_LocatorTypeProvider(t *testing.T) {
sut := LocatorTypeProvider("OAuthProvider", "teamcity-vault")
actual := sut.String()

expected := "type%3AOAuthProvider%2Cproperty%28name%3AproviderType%2Cvalue%3Ateamcity-vault%29"

assert.Equal(t, expected, actual)
}
20 changes: 18 additions & 2 deletions teamcity/project_feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (s *ProjectFeatureService) Create(feature ProjectFeature) (ProjectFeature,
return nil, fmt.Errorf("feature is nil")
}
if feature.ProjectID() != s.ProjectID {
return nil, fmt.Errorf("given ProjectFeature for project %q to ProjectFeatureService for project %q.", feature.ProjectID(), s.ProjectID)
return nil, fmt.Errorf("given ProjectFeature for project %q to ProjectFeatureService for project %q", feature.ProjectID(), s.ProjectID)
}

requestBody := &projectFeatureJSON{
Expand Down Expand Up @@ -114,7 +114,7 @@ func (s *ProjectFeatureService) GetByID(id string) (ProjectFeature, error) {
return s.parseProjectFeatureJSONResponse(out)
}

// GetByType returns a single ProjectFeature for the current project by it's typw.
// GetByType returns a single ProjectFeature for the current project by it's type.
func (s *ProjectFeatureService) GetByType(id string) (ProjectFeature, error) {
var out projectFeatureJSON

Expand All @@ -127,6 +127,20 @@ func (s *ProjectFeatureService) GetByType(id string) (ProjectFeature, error) {
return s.parseProjectFeatureJSONResponse(out)
}

// GetByTypeAndProvider returns any matching ProjectFeature's for the current project
// that match the supplied Type and Provider
func (s *ProjectFeatureService) GetByTypeAndProvider(featureType string, featureProvider string) (ProjectFeature, error) {
var out projectFeatureJSON

loc := LocatorTypeProvider(featureType, featureProvider)
url := fmt.Sprintf("projects/%s/projectFeatures/%s", s.ProjectID, loc)
if err := s.restHelper.get(url, &out, "projectFeature"); err != nil {
return nil, err
}

return s.parseProjectFeatureJSONResponse(out)
}

// Update updated an existing a ProjectFeature under the current project.
func (s *ProjectFeatureService) Update(feature ProjectFeature) (ProjectFeature, error) {
if feature == nil {
Expand All @@ -152,6 +166,8 @@ func (s *ProjectFeatureService) Update(feature ProjectFeature) (ProjectFeature,

func (s *ProjectFeatureService) parseProjectFeatureJSONResponse(feature projectFeatureJSON) (ProjectFeature, error) {
switch feature.Type {
case "OAuthProvider":
return loadConnectionProviderVault(s.ProjectID, feature)
case "versionedSettings":
return loadProjectFeatureVersionedSettings(s.ProjectID, feature)
default:
Expand Down
161 changes: 161 additions & 0 deletions teamcity/project_feature_connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package teamcity
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I suggest we break the feature_connection from the specific Vault types here?
Perhaps a project_connection_vault_oauth.go would be more suitable.


import (
"fmt"
"strconv"
)

// ConnectionType represents the TeamCity connection type
type ConnectionType string

const (
// ConnectionTypeOAuthProvider resprents an OAuth Provider connection type
ConnectionTypeOAuthProvider ConnectionType = "OAuthProvider"
)

// ConnectionProviderType represents the OAuth provider type
type ConnectionProviderType string

const (
// ConnectionProviderTypeVault represents an Hashicorp Vault provider type
ConnectionProviderTypeVault ConnectionProviderType = "teamcity-vault"
)

// ConnectionProviderVaultAuthMethod represents the Vault auth method
type ConnectionProviderVaultAuthMethod string

const (
// ConnectionProviderVaultAuthMethodIAM represents the IAM auth method
ConnectionProviderVaultAuthMethodIAM ConnectionProviderVaultAuthMethod = "iam"
// ConnectionProviderVaultAuthMethodApprole represents the approle auth method
ConnectionProviderVaultAuthMethodApprole ConnectionProviderVaultAuthMethod = "approle"
)

// ConnectionProviderVaultOptions is the required options for the Vault OAuth provider
type ConnectionProviderVaultOptions struct {
AuthMethod ConnectionProviderVaultAuthMethod
DisplayName string
Endpoint string
FailOnError bool
Namespace string
ProviderType ConnectionProviderType
RoleID string
SecretID string
URL string
VaultNamespace string
}

// ConnectionProviderVault defines the Vault Connection details
type ConnectionProviderVault struct {
id string
projectID string

Options ConnectionProviderVaultOptions
}

// NewProjectConnectionVault creates a new Vault OAuth Provider connection feature
func NewProjectConnectionVault(projectID string, options ConnectionProviderVaultOptions) *ConnectionProviderVault {
return &ConnectionProviderVault{
projectID: projectID,
Options: options,
}
}

// ID returns the ID of this project feature
func (f *ConnectionProviderVault) ID() string {
return f.id
}

// SetID sets the ID of this project feature
func (f *ConnectionProviderVault) SetID(value string) {
f.id = value
}

// Type represents the type of this project feature as a string
func (f *ConnectionProviderVault) Type() string {
return "OAuthProvider"
}

// ProjectID represents the ID of the project the project feature is assigned to.
func (f *ConnectionProviderVault) ProjectID() string {
return f.projectID
}

// SetProjectID sets the ID of the project the project feature is assigned to.
func (f *ConnectionProviderVault) SetProjectID(value string) {
f.projectID = value
}

// Properties returns all properties for the Vault OAuth Provider project feature
func (f *ConnectionProviderVault) Properties() *Properties {
return NewProperties(
NewProperty("auth-method", string(f.Options.AuthMethod)),
NewProperty("displayName", string(f.Options.DisplayName)),
NewProperty("endpoint", string(f.Options.Endpoint)),
NewProperty("fail-on-error", fmt.Sprintf("%t", f.Options.FailOnError)),
NewProperty("namespace", string(f.Options.Namespace)),
NewProperty("providerType", string(ConnectionProviderTypeVault)),
NewProperty("role-id", string(f.Options.RoleID)),
NewProperty("secure:secret-id", string(f.Options.SecretID)),
NewProperty("url", string(f.Options.URL)),
NewProperty("vault-namespace", string(f.Options.VaultNamespace)),
)
}

func loadConnectionProviderVault(projectID string, feature projectFeatureJSON) (ProjectFeature, error) {
settings := &ConnectionProviderVault{
id: feature.ID,
projectID: projectID,
Options: ConnectionProviderVaultOptions{},
}

// stringProperties := []string{"displayName", "endpoint", "namespace", "role-id", "url", "vault-namespace"}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these comments expected?

// for _, property := range stringProperties {
// if encodedValue, ok := feature.Properties.GetOk("displayName"); ok {
// settings.Options.DisplayName = encodedValue
// }
// }

if encodedValue, ok := feature.Properties.GetOk("displayName"); ok {
settings.Options.DisplayName = encodedValue
}

if encodedValue, ok := feature.Properties.GetOk("endpoint"); ok {
settings.Options.Endpoint = encodedValue
}

if encodedValue, ok := feature.Properties.GetOk("namespace"); ok {
settings.Options.Namespace = encodedValue
}

if encodedValue, ok := feature.Properties.GetOk("role-id"); ok {
settings.Options.RoleID = encodedValue
}

if encodedValue, ok := feature.Properties.GetOk("url"); ok {
settings.Options.URL = encodedValue
}

if encodedValue, ok := feature.Properties.GetOk("vault-namespace"); ok {
settings.Options.VaultNamespace = encodedValue
}

if encodedValue, ok := feature.Properties.GetOk("fail-on-error"); ok {
v, err := strconv.ParseBool(encodedValue)
if err != nil {
return nil, err
}

settings.Options.FailOnError = v
}

if encodedValue, ok := feature.Properties.GetOk("auth-method"); ok {
settings.Options.AuthMethod = ConnectionProviderVaultAuthMethod(encodedValue)
}

if encodedValue, ok := feature.Properties.GetOk("providerType"); ok {
settings.Options.ProviderType = ConnectionProviderType(encodedValue)
}

return settings, nil
}
Loading