2
0
Эх сурвалжийг харах

add internal cred exchange mechanism

Alexander Belanger 4 жил өмнө
parent
commit
4debab0f75

+ 24 - 0
api/server/handlers/credentials/get_credentials_ce.go

@@ -0,0 +1,24 @@
+// +build !ee
+
+package credentials
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+)
+
+type GetCredentialsHandler struct {
+	handlers.PorterHandlerReader
+	handlers.Unavailable
+}
+
+func NewGetCredentialsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) http.Handler {
+	return handlers.NewUnavailable(config, "invite_update_role")
+}

+ 21 - 0
api/server/handlers/credentials/get_credentials_ee.go

@@ -0,0 +1,21 @@
+// +build ee
+
+package credentials
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/ee/api/server/handlers/credentials"
+)
+
+var NewGetCredentialsHandler func(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) http.Handler
+
+func init() {
+	NewGetCredentialsHandler = credentials.NewCredentialsGetHandler
+}

+ 26 - 0
api/server/router/base.go

@@ -2,6 +2,7 @@ package router
 
 import (
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/credentials"
 	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/handlers/healthcheck"
 	"github.com/porter-dev/porter/api/server/handlers/metadata"
@@ -485,5 +486,30 @@ func GetBaseRoutes(
 		Router:   r,
 	})
 
+	// GET /api/internal/credentials
+	getCredentialsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/internal/credentials",
+			},
+			Scopes: []types.PermissionScope{},
+		},
+	)
+
+	getCredentialsHandler := credentials.NewGetCredentialsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getCredentialsEndpoint,
+		Handler:  getCredentialsHandler,
+		Router:   r,
+	})
+
 	return routes
 }

+ 140 - 0
ee/api/server/handlers/credentials/get_credentials.go

@@ -0,0 +1,140 @@
+package credentials
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/ee/api/types"
+	"github.com/porter-dev/porter/ee/integrations/vault"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository/credentials"
+	"github.com/porter-dev/porter/internal/repository/gorm"
+	"golang.org/x/crypto/bcrypt"
+)
+
+type CredentialsGetHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewCredentialsGetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) http.Handler {
+	return &CredentialsGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *CredentialsGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// read the request to get the token id and hashed token
+	req := &types.CredentialsExchangeRequest{}
+
+	// populate the request from the headers
+	req.CredExchangeToken = r.Header.Get("X-Porter-Token")
+	tokID, err := strconv.ParseUint(r.Header.Get("X-Porter-Token-ID"), 10, 64)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	req.CredExchangeID = uint(tokID)
+	req.VaultToken = r.Header.Get("X-Vault-Token")
+
+	// read the access token in the header, check against DB
+	ceToken, err := c.Repo().CredentialsExchangeToken().ReadCredentialsExchangeToken(req.CredExchangeID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	// TODO: verify hashed token!!
+	if valid, err := verifyToken(req.CredExchangeToken, ceToken); !valid {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	resp := &types.CredentialsExchangeResponse{}
+	repo := c.Repo()
+
+	// if the request contains a vault token, use that vault token to construct a new repository
+	// that will query vault using the passed in token
+	if req.VaultToken != "" {
+		// read the vault token in the header, create new vault client with this token
+		conf := c.Config().DBConf
+		vaultClient := vault.NewClient(conf.VaultServerURL, req.VaultToken, conf.VaultPrefix)
+
+		var key [32]byte
+
+		for i, b := range []byte(conf.EncryptionKey) {
+			key[i] = b
+		}
+
+		// use this vault client for the repo
+		repo = gorm.NewRepository(c.Config().DB, &key, vaultClient)
+	}
+
+	if ceToken.DOCredentialID != 0 {
+		doInt, err := repo.OAuthIntegration().ReadOAuthIntegration(ceToken.ProjectID, ceToken.DOCredentialID)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+			return
+		}
+
+		resp.DO = &credentials.OAuthCredential{
+			ClientID:     doInt.ClientID,
+			AccessToken:  doInt.AccessToken,
+			RefreshToken: doInt.RefreshToken,
+		}
+	} else if ceToken.GCPCredentialID != 0 {
+		gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(ceToken.ProjectID, ceToken.GCPCredentialID)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+			return
+		}
+
+		resp.GCP = &credentials.GCPCredential{
+			GCPKeyData: gcpInt.GCPKeyData,
+		}
+	} else if ceToken.AWSCredentialID != 0 {
+		awsInt, err := repo.AWSIntegration().ReadAWSIntegration(ceToken.ProjectID, ceToken.AWSCredentialID)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+			return
+		}
+
+		resp.AWS = &credentials.AWSCredential{
+			AWSAccessKeyID:     awsInt.AWSAccessKeyID,
+			AWSClusterID:       awsInt.AWSClusterID,
+			AWSSecretAccessKey: awsInt.AWSSecretAccessKey,
+			AWSSessionToken:    awsInt.AWSSessionToken,
+		}
+	}
+
+	// return the decrypted credentials
+	c.WriteResult(w, r, resp)
+}
+
+func verifyToken(reqToken string, ceToken *models.CredentialsExchangeToken) (bool, error) {
+	// make sure the token is still valid and has not expired
+	if ceToken.IsExpired() {
+		return false, fmt.Errorf("token is expired")
+	}
+
+	// make sure the token is correct
+	if err := bcrypt.CompareHashAndPassword([]byte(ceToken.Token), []byte(reqToken)); err != nil {
+		return false, fmt.Errorf("verify token failed: %s", err)
+	}
+
+	return true, nil
+}

+ 17 - 0
ee/api/types/cred_exchange.go

@@ -0,0 +1,17 @@
+package types
+
+import "github.com/porter-dev/porter/internal/repository/credentials"
+
+type CredentialsExchangeRequest struct {
+	CredExchangeID    uint
+	CredExchangeToken string
+
+	// (optional) Vault token, if required
+	VaultToken string
+}
+
+type CredentialsExchangeResponse struct {
+	DO  *credentials.OAuthCredential `json:"do,omitempty"`
+	GCP *credentials.GCPCredential   `json:"gcp,omitempty"`
+	AWS *credentials.AWSCredential   `json:"aws,omitempty"`
+}

+ 27 - 4
ee/integrations/vault/types.go

@@ -8,7 +8,7 @@ type CreateVaultSecretRequest struct {
 	Data interface{} `json:"data"`
 }
 
-type GetVaultSecretGenericResponse struct {
+type VaultGetResponse struct {
 	RequestID string `json:"request_id"`
 }
 
@@ -19,7 +19,7 @@ type VaultMetadata struct {
 }
 
 type GetOAuthCredentialResponse struct {
-	*GetVaultSecretGenericResponse
+	*VaultGetResponse
 	Data *GetOAuthCredentialData `json:"data"`
 }
 
@@ -29,7 +29,7 @@ type GetOAuthCredentialData struct {
 }
 
 type GetGCPCredentialResponse struct {
-	*GetVaultSecretGenericResponse
+	*VaultGetResponse
 	Data *GetGCPCredentialData `json:"data"`
 }
 
@@ -39,7 +39,7 @@ type GetGCPCredentialData struct {
 }
 
 type GetAWSCredentialResponse struct {
-	*GetVaultSecretGenericResponse
+	*VaultGetResponse
 	Data *GetAWSCredentialData `json:"data"`
 }
 
@@ -47,3 +47,26 @@ type GetAWSCredentialData struct {
 	Metadata *VaultMetadata             `json:"metadata"`
 	Data     *credentials.AWSCredential `json:"data"`
 }
+
+type CreatePolicyRequest struct {
+	Policy string `json:"policy"`
+}
+
+type CreateTokenRequest struct {
+	Policies []string `json:"policies"`
+	Meta     Meta     `json:"meta"`
+	TTL      string   `json:"ttl"`
+}
+
+type Meta struct {
+	User string `json:"user"`
+}
+
+type CreateTokenResponse struct {
+	*VaultGetResponse
+	Auth *TokenAuth `json:"auth"`
+}
+
+type TokenAuth struct {
+	Token string `json:"client_token"`
+}

+ 66 - 9
ee/integrations/vault/vault.go

@@ -40,13 +40,13 @@ func (c *Client) WriteOAuthCredential(
 		Data: data,
 	}
 
-	return c.postRequest(c.getOAuthCredentialPath(oauthIntegration), reqData, nil)
+	return c.postRequest(fmt.Sprintf("/v1/%s", c.getOAuthCredentialPath(oauthIntegration)), reqData, nil)
 }
 
 func (c *Client) GetOAuthCredential(oauthIntegration *integrations.OAuthIntegration) (*credentials.OAuthCredential, error) {
 	resp := &GetOAuthCredentialResponse{}
 
-	err := c.getRequest(c.getOAuthCredentialPath(oauthIntegration), resp)
+	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getOAuthCredentialPath(oauthIntegration)), resp)
 
 	if err != nil {
 		return nil, err
@@ -55,9 +55,16 @@ func (c *Client) GetOAuthCredential(oauthIntegration *integrations.OAuthIntegrat
 	return resp.Data.Data, nil
 }
 
+func (c *Client) CreateOAuthToken(oauthIntegration *integrations.OAuthIntegration) (string, error) {
+	credPath := c.getOAuthCredentialPath(oauthIntegration)
+	policyName := fmt.Sprintf("access-%d-oauth-%d", oauthIntegration.ProjectID, oauthIntegration.ID)
+
+	return c.getToken(credPath, policyName)
+}
+
 func (c *Client) getOAuthCredentialPath(oauthIntegration *integrations.OAuthIntegration) string {
 	return fmt.Sprintf(
-		"/v1/kv/data/secret/%s/%d/oauth/%d",
+		"kv/data/secret/%s/%d/oauth/%d",
 		c.secretPrefix,
 		oauthIntegration.ProjectID,
 		oauthIntegration.ID,
@@ -71,13 +78,13 @@ func (c *Client) WriteGCPCredential(
 		Data: data,
 	}
 
-	return c.postRequest(c.getGCPCredentialPath(gcpIntegration), reqData, nil)
+	return c.postRequest(fmt.Sprintf("/v1/%s", c.getGCPCredentialPath(gcpIntegration)), reqData, nil)
 }
 
 func (c *Client) GetGCPCredential(gcpIntegration *integrations.GCPIntegration) (*credentials.GCPCredential, error) {
 	resp := &GetGCPCredentialResponse{}
 
-	err := c.getRequest(c.getGCPCredentialPath(gcpIntegration), resp)
+	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getGCPCredentialPath(gcpIntegration)), resp)
 
 	if err != nil {
 		return nil, err
@@ -86,9 +93,16 @@ func (c *Client) GetGCPCredential(gcpIntegration *integrations.GCPIntegration) (
 	return resp.Data.Data, nil
 }
 
+func (c *Client) CreateGCPToken(gcpIntegration *integrations.GCPIntegration) (string, error) {
+	credPath := c.getGCPCredentialPath(gcpIntegration)
+	policyName := fmt.Sprintf("access-%d-gcp-%d", gcpIntegration.ProjectID, gcpIntegration.ID)
+
+	return c.getToken(credPath, policyName)
+}
+
 func (c *Client) getGCPCredentialPath(gcpIntegration *integrations.GCPIntegration) string {
 	return fmt.Sprintf(
-		"/v1/kv/data/secret/%s/%d/gcp/%d",
+		"kv/data/secret/%s/%d/gcp/%d",
 		c.secretPrefix,
 		gcpIntegration.ProjectID,
 		gcpIntegration.ID,
@@ -102,13 +116,13 @@ func (c *Client) WriteAWSCredential(
 		Data: data,
 	}
 
-	return c.postRequest(c.getAWSCredentialPath(awsIntegration), reqData, nil)
+	return c.postRequest(fmt.Sprintf("/v1/%s", c.getAWSCredentialPath(awsIntegration)), reqData, nil)
 }
 
 func (c *Client) GetAWSCredential(awsIntegration *integrations.AWSIntegration) (*credentials.AWSCredential, error) {
 	resp := &GetAWSCredentialResponse{}
 
-	err := c.getRequest(c.getAWSCredentialPath(awsIntegration), resp)
+	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getAWSCredentialPath(awsIntegration)), resp)
 
 	if err != nil {
 		return nil, err
@@ -117,15 +131,58 @@ func (c *Client) GetAWSCredential(awsIntegration *integrations.AWSIntegration) (
 	return resp.Data.Data, nil
 }
 
+func (c *Client) CreateAWSToken(awsIntegration *integrations.AWSIntegration) (string, error) {
+	credPath := c.getAWSCredentialPath(awsIntegration)
+	policyName := fmt.Sprintf("access-%d-aws-%d", awsIntegration.ProjectID, awsIntegration.ID)
+
+	return c.getToken(credPath, policyName)
+}
+
 func (c *Client) getAWSCredentialPath(awsIntegration *integrations.AWSIntegration) string {
 	return fmt.Sprintf(
-		"/v1/kv/data/secret/%s/%d/aws/%d",
+		"kv/data/secret/%s/%d/aws/%d",
 		c.secretPrefix,
 		awsIntegration.ProjectID,
 		awsIntegration.ID,
 	)
 }
 
+const readOnlyPolicyTemplate = `path "%s" {
+  capabilities = ["read"]
+}`
+
+func (c *Client) getToken(credPath, policyName string) (string, error) {
+	policy := fmt.Sprintf(readOnlyPolicyTemplate, credPath)
+
+	policyReq := &CreatePolicyRequest{
+		Policy: policy,
+	}
+
+	err := c.postRequest(fmt.Sprintf("/v1/sys/policy/%s", policyName), policyReq, nil)
+
+	if err != nil {
+		return "", err
+	}
+
+	tokenReq := &CreateTokenRequest{
+		Policies: []string{policyName},
+		Meta: Meta{
+			User: policyName,
+		},
+		TTL: "6h",
+	}
+
+	tokenResp := &CreateTokenResponse{}
+
+	err = c.postRequest("/v1/auth/token/create", tokenReq, tokenResp)
+
+	if err != nil {
+		return "", err
+	}
+
+	return tokenResp.Auth.Token, nil
+}
+
 func (c *Client) postRequest(path string, data interface{}, dst interface{}) error {
 	return c.writeRequest("POST", path, data, dst)
 }

+ 24 - 0
internal/models/cred_exchange_token.go

@@ -0,0 +1,24 @@
+package models
+
+import (
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type CredentialsExchangeToken struct {
+	gorm.Model
+
+	ProjectID uint
+	Token     []byte
+	Expiry    *time.Time
+
+	DOCredentialID  uint
+	AWSCredentialID uint
+	GCPCredentialID uint
+}
+
+func (t *CredentialsExchangeToken) IsExpired() bool {
+	timeLeft := t.Expiry.Sub(time.Now())
+	return timeLeft < 0
+}

+ 8 - 0
internal/repository/cred_exchange_token.go

@@ -0,0 +1,8 @@
+package repository
+
+import "github.com/porter-dev/porter/internal/models"
+
+type CredentialsExchangeTokenRepository interface {
+	CreateCredentialsExchangeToken(ceToken *models.CredentialsExchangeToken) (*models.CredentialsExchangeToken, error)
+	ReadCredentialsExchangeToken(id uint) (*models.CredentialsExchangeToken, error)
+}

+ 34 - 0
internal/repository/gorm/cred_exchange_token.go

@@ -0,0 +1,34 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// CredentialsExchangeTokenRepository uses gorm.DB for querying the database
+type CredentialsExchangeTokenRepository struct {
+	db *gorm.DB
+}
+
+func NewCredentialsExchangeTokenRepository(db *gorm.DB) repository.CredentialsExchangeTokenRepository {
+	return &CredentialsExchangeTokenRepository{db}
+}
+
+func (repo *CredentialsExchangeTokenRepository) CreateCredentialsExchangeToken(ceToken *models.CredentialsExchangeToken) (*models.CredentialsExchangeToken, error) {
+	if err := repo.db.Create(ceToken).Error; err != nil {
+		return nil, err
+	}
+
+	return ceToken, nil
+}
+
+func (repo *CredentialsExchangeTokenRepository) ReadCredentialsExchangeToken(id uint) (*models.CredentialsExchangeToken, error) {
+	ceToken := &models.CredentialsExchangeToken{}
+
+	if err := repo.db.Where("id = ?", id).First(&ceToken).Error; err != nil {
+		return nil, err
+	}
+
+	return ceToken, nil
+}

+ 6 - 0
internal/repository/gorm/repository.go

@@ -33,6 +33,7 @@ type GormRepository struct {
 	notificationConfig        repository.NotificationConfigRepository
 	event                     repository.EventRepository
 	projectUsage              repository.ProjectUsageRepository
+	ceToken                   repository.CredentialsExchangeTokenRepository
 }
 
 func (t *GormRepository) User() repository.UserRepository {
@@ -139,6 +140,10 @@ func (t *GormRepository) ProjectUsage() repository.ProjectUsageRepository {
 	return t.projectUsage
 }
 
+func (t *GormRepository) CredentialsExchangeToken() repository.CredentialsExchangeTokenRepository {
+	return t.ceToken
+}
+
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.CredentialStorage) repository.Repository {
@@ -169,5 +174,6 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		notificationConfig:        NewNotificationConfigRepository(db),
 		event:                     NewEventRepository(db),
 		projectUsage:              NewProjectUsageRepository(db),
+		ceToken:                   NewCredentialsExchangeTokenRepository(db),
 	}
 }

+ 1 - 0
internal/repository/repository.go

@@ -27,4 +27,5 @@ type Repository interface {
 	NotificationConfig() NotificationConfigRepository
 	Event() EventRepository
 	ProjectUsage() ProjectUsageRepository
+	CredentialsExchangeToken() CredentialsExchangeTokenRepository
 }

+ 45 - 0
internal/repository/test/cred_exchange_token.go

@@ -0,0 +1,45 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+type CredentialsExchangeTokenRepository struct {
+	canQuery bool
+	ceTokens []*models.CredentialsExchangeToken
+}
+
+func NewCredentialsExchangeTokenRepository(canQuery bool) repository.CredentialsExchangeTokenRepository {
+	return &CredentialsExchangeTokenRepository{canQuery, []*models.CredentialsExchangeToken{}}
+}
+
+func (repo *CredentialsExchangeTokenRepository) CreateCredentialsExchangeToken(
+	a *models.CredentialsExchangeToken,
+) (*models.CredentialsExchangeToken, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.ceTokens = append(repo.ceTokens, a)
+	a.ID = uint(len(repo.ceTokens))
+
+	return a, nil
+}
+
+// ReadPWResetToken gets an auth code object specified by the unique code
+func (repo *CredentialsExchangeTokenRepository) ReadCredentialsExchangeToken(id uint) (*models.CredentialsExchangeToken, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.ceTokens) || repo.ceTokens[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.ceTokens[index], nil
+}

+ 6 - 0
internal/repository/test/repository.go

@@ -31,6 +31,7 @@ type TestRepository struct {
 	notificationConfig        repository.NotificationConfigRepository
 	event                     repository.EventRepository
 	projectUsage              repository.ProjectUsageRepository
+	ceToken                   repository.CredentialsExchangeTokenRepository
 }
 
 func (t *TestRepository) User() repository.UserRepository {
@@ -137,6 +138,10 @@ func (t *TestRepository) ProjectUsage() repository.ProjectUsageRepository {
 	return t.projectUsage
 }
 
+func (t *TestRepository) CredentialsExchangeToken() repository.CredentialsExchangeTokenRepository {
+	return t.ceToken
+}
+
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(canQuery bool, failingMethods ...string) repository.Repository {
@@ -167,5 +172,6 @@ func NewRepository(canQuery bool, failingMethods ...string) repository.Repositor
 		notificationConfig:        NewNotificationConfigRepository(canQuery),
 		event:                     NewEventRepository(canQuery),
 		projectUsage:              NewProjectUsageRepository(canQuery),
+		ceToken:                   NewCredentialsExchangeTokenRepository(canQuery),
 	}
 }