hcp: Add support for project level service principals

HCP supports two types of service principals: Organization-level and project-level.
When a user tries to publish to an active HCP Packer registry using a plsp the client
fails when configuring the client due to a API permission error; namely plsp do not have
the permissions to query an org for a list of projects. Setting the HCP_PROJECT_ID does
not resolve the issue because the call to ListProjects is still executed.

This changes updates the client configuration params to obtain both the HCP Organization and
Project IDs that will be used for connecting to the HCP Packer registry. With this change
if a user provides a project Id via the HCP_PROJECT_ID environment variable no call to ListProjects will
be made. Instead the value will be take as is and used to create the connection. A user connecting with
a project level service principals must provide a valid HCP_PROJECT_ID in order to connect.
pull/12520/head
Wilken Rivera 3 years ago
parent 74b5c2aa56
commit 94028656de

@ -7,6 +7,7 @@ package api
import (
"fmt"
"log"
"net/http"
"os"
"time"
@ -44,39 +45,67 @@ func NewClient() (*Client, error) {
}
}
cl, err := httpclient.New(httpclient.Config{
hcpClientCfg := httpclient.Config{
SourceChannel: fmt.Sprintf("packer/%s", version.PackerVersion.FormattedVersion()),
})
if err != nil {
}
if err := hcpClientCfg.Canonicalize(); err != nil {
return nil, &ClientError{
StatusCode: InvalidClientConfig,
Err: err,
}
}
cl, err := httpclient.New(hcpClientCfg)
if err != nil {
return nil, &ClientError{
StatusCode: InvalidClientConfig,
Err: err,
}
}
client := &Client{
Packer: packerSvc.New(cl, nil),
Organization: organizationSvc.New(cl, nil),
Project: projectSvc.New(cl, nil),
}
//if both HCP_ORGANIZATION_ID and HCP_PROJECT_ID are set via env variables the hcpConfig may have all we need already.
if hcpClientCfg.Profile().OrganizationID != "" && hcpClientCfg.Profile().ProjectID != "" {
client.OrganizationID = hcpClientCfg.Profile().OrganizationID
client.ProjectID = hcpClientCfg.Profile().ProjectID
if err := client.loadOrganizationID(); err != nil {
return nil, &ClientError{
StatusCode: InvalidClientConfig,
Err: err,
}
return client, nil
}
if err := client.loadProjectID(); err != nil {
return nil, &ClientError{
StatusCode: InvalidClientConfig,
Err: err,
if client.OrganizationID == "" {
err := client.loadOrganizationID()
if err != nil {
return nil, &ClientError{
StatusCode: InvalidClientConfig,
Err: err,
}
}
}
if client.ProjectID == "" {
err := client.loadProjectID()
if err != nil {
return nil, &ClientError{
StatusCode: InvalidClientConfig,
Err: err,
}
}
}
return client, nil
}
func (c *Client) loadOrganizationID() error {
if c.OrganizationID != "" {
return nil
}
if env.HasOrganizationID() {
c.OrganizationID = os.Getenv(env.HCPOrganizationID)
return nil
}
// Get the organization ID.
listOrgParams := organizationSvc.NewOrganizationServiceListParams()
listOrgResp, err := c.Organization.OrganizationServiceList(listOrgParams, nil)
@ -92,55 +121,62 @@ func (c *Client) loadOrganizationID() error {
}
func (c *Client) loadProjectID() error {
if c.ProjectID != "" {
return nil
}
if env.HasProjectID() {
c.ProjectID = os.Getenv(env.HCPProjectID)
return nil
}
// Get the project using the organization ID.
listProjParams := projectSvc.NewProjectServiceListParams()
listProjParams.ScopeID = &c.OrganizationID
scopeType := string(rmmodels.HashicorpCloudResourcemanagerResourceIDResourceTypeORGANIZATION)
listProjParams.ScopeType = &scopeType
listProjResp, err := c.Project.ProjectServiceList(listProjParams, nil)
if err != nil {
return fmt.Errorf("unable to fetch project id: %v", err)
}
if env.HasProjectID() {
proj, err := findProjectByID(os.Getenv(env.HCPProjectID), listProjResp.Payload.Projects)
if err != nil {
return err
}
c.ProjectID = proj.ID
} else {
if len(listProjResp.Payload.Projects) > 1 {
log.Printf("[WARNING] Multiple HCP projects found, will pick the oldest one by default\n" +
"To specify which project to use, set the HCP_PROJECT_ID environment variable to the one you want to use.")
if err != nil {
//For permission errors our service principle may not have the perms
// to see all projects for an Org; this is the case for project-level service principles.
serviceErr, ok := err.(*projectSvc.ProjectServiceListDefault)
if !ok {
return fmt.Errorf("unable to fetch project list: %v", err)
}
proj, err := findOldestProject(listProjResp.Payload.Projects)
if err != nil {
return err
if serviceErr.Code() == http.StatusForbidden {
return fmt.Errorf("unable to fetch project\n\n"+
"If the provided credentials are tied to a specific project trying setting the %s environment variable to one you want to use.", env.HCPProjectID)
}
}
c.ProjectID = proj.ID
if len(listProjResp.Payload.Projects) > 1 {
log.Printf("[WARNING] Multiple HCP projects found, will pick the oldest one by default\n"+
"To specify which project to use, set the %s environment variable to the one you want to use.", env.HCPProjectID)
}
proj, err := getOldestProject(listProjResp.Payload.Projects)
if err != nil {
return err
}
c.ProjectID = proj.ID
return nil
}
func findOldestProject(projs []*models.HashicorpCloudResourcemanagerProject) (*models.HashicorpCloudResourcemanagerProject, error) {
if len(projs) == 0 {
// getOldestProject retrieves the oldest project from a list based on its created_at time.
func getOldestProject(projects []*models.HashicorpCloudResourcemanagerProject) (*models.HashicorpCloudResourcemanagerProject, error) {
if len(projects) == 0 {
return nil, fmt.Errorf("no project found")
}
proj := projs[0]
for i := 1; i < len(projs); i++ {
nxtProj := projs[i]
if time.Time(nxtProj.CreatedAt).Before(time.Time(proj.CreatedAt)) {
proj = nxtProj
oldestTime := time.Now()
var oldestProj *models.HashicorpCloudResourcemanagerProject
for _, proj := range projects {
projTime := time.Time(proj.CreatedAt)
if projTime.Before(oldestTime) {
oldestProj = proj
oldestTime = projTime
}
}
return proj, nil
return oldestProj, nil
}
func findProjectByID(projID string, projs []*models.HashicorpCloudResourcemanagerProject) (*models.HashicorpCloudResourcemanagerProject, error) {

@ -94,7 +94,7 @@ func TestFindProjectID(t *testing.T) {
}
}
func TestFindOldestProject(t *testing.T) {
func TestGetOldestProject(t *testing.T) {
testcases := []struct {
Name string
ProjectList []*models.HashicorpCloudResourcemanagerProject
@ -151,7 +151,7 @@ func TestFindOldestProject(t *testing.T) {
for _, tt := range testcases {
t.Run(tt.Name, func(t *testing.T) {
proj, err := findOldestProject(tt.ProjectList)
proj, err := getOldestProject(tt.ProjectList)
if (err != nil) != tt.ExpectErr {
t.Errorf("test findProjectByID, expected %t, got %t",
tt.ExpectErr,

@ -13,6 +13,10 @@ func HasProjectID() bool {
return hasEnvVar(HCPProjectID)
}
func HasOrganizationID() bool {
return hasEnvVar(HCPOrganizationID)
}
func HasClientID() bool {
return hasEnvVar(HCPClientID)
}

@ -7,6 +7,7 @@ const (
HCPClientID = "HCP_CLIENT_ID"
HCPClientSecret = "HCP_CLIENT_SECRET"
HCPProjectID = "HCP_PROJECT_ID"
HCPOrganizationID = "HCP_ORGANIZATION_ID"
HCPPackerRegistry = "HCP_PACKER_REGISTRY"
HCPPackerBucket = "HCP_PACKER_BUCKET_NAME"
HCPPackerBuildFingerprint = "HCP_PACKER_BUILD_FINGERPRINT"

Loading…
Cancel
Save