From 94028656de04cd809ee6aa968f93656a83b7f7e0 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Wed, 19 Jul 2023 12:42:37 -0400 Subject: [PATCH] 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. --- internal/hcp/api/client.go | 118 +++++++++++++++++++++----------- internal/hcp/api/client_test.go | 4 +- internal/hcp/env/env.go | 4 ++ internal/hcp/env/variables.go | 1 + 4 files changed, 84 insertions(+), 43 deletions(-) diff --git a/internal/hcp/api/client.go b/internal/hcp/api/client.go index 21a07156c..89d4a39b4 100644 --- a/internal/hcp/api/client.go +++ b/internal/hcp/api/client.go @@ -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) { diff --git a/internal/hcp/api/client_test.go b/internal/hcp/api/client_test.go index b1cdbff61..466b390b8 100644 --- a/internal/hcp/api/client_test.go +++ b/internal/hcp/api/client_test.go @@ -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, diff --git a/internal/hcp/env/env.go b/internal/hcp/env/env.go index 0cf459015..0186be167 100644 --- a/internal/hcp/env/env.go +++ b/internal/hcp/env/env.go @@ -13,6 +13,10 @@ func HasProjectID() bool { return hasEnvVar(HCPProjectID) } +func HasOrganizationID() bool { + return hasEnvVar(HCPOrganizationID) +} + func HasClientID() bool { return hasEnvVar(HCPClientID) } diff --git a/internal/hcp/env/variables.go b/internal/hcp/env/variables.go index 71258c13b..8c74b600d 100644 --- a/internal/hcp/env/variables.go +++ b/internal/hcp/env/variables.go @@ -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"