From 346a16d8514865b3974e15b16ecde629a61f82b6 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Tue, 19 May 2026 12:41:40 -0300 Subject: [PATCH 1/4] Add Application support to certificate agent --- certificate-agent-config.yaml | 7 +- e2e/agent/agent_helpers.go | 159 ++++++-- e2e/agent/certificate_test.go | 272 +++++++++++-- packages/api/api.go | 45 ++- packages/api/model.go | 40 +- packages/cmd/agent.go | 158 ++++++-- packages/cmd/agent_cert_resolution_test.go | 442 ++++++++++++++++++--- 7 files changed, 956 insertions(+), 167 deletions(-) diff --git a/certificate-agent-config.yaml b/certificate-agent-config.yaml index b4778348..5baf8f79 100644 --- a/certificate-agent-config.yaml +++ b/certificate-agent-config.yaml @@ -1,4 +1,4 @@ -version: v1 +version: v2 # v2 issues from a profile attached to a PKI Application. Use v1 for the legacy project-based flow. infisical: address: "https://app.infisical.com/" @@ -11,9 +11,8 @@ auth: remove_client_secret_on_read: false certificates: - # Issue a new certificate from a profile - - profile-name: "my-profile-name" - project-slug: "my-project-slug" + - application-name: "my-application-name" + profile-name: "my-profile-name" # Certificate parameters attributes: diff --git a/e2e/agent/agent_helpers.go b/e2e/agent/agent_helpers.go index 1357442d..18acede1 100644 --- a/e2e/agent/agent_helpers.go +++ b/e2e/agent/agent_helpers.go @@ -25,20 +25,23 @@ import ( ) type CertAgentTestHelper struct { - T *testing.T - ProjectID string - ProjectSlug string - ProfileSlug string - ProfileID string - PolicyID string - CaID string - AdminToken string - InfisicalURL string - TempDir string - ClientID string - ClientSecret string - IdentityClient *client.ClientWithResponses - AdminClient *client.ClientWithResponses + T *testing.T + ProjectID string + ProjectSlug string + ProfileSlug string + ProfileID string + PolicyID string + CaID string + ApplicationID string + ApplicationName string + AdminToken string + IdentityToken string + InfisicalURL string + TempDir string + ClientID string + ClientSecret string + IdentityClient *client.ClientWithResponses + AdminClient *client.ClientWithResponses } func (h *CertAgentTestHelper) CreateInternalCA() { @@ -290,6 +293,94 @@ func (h *CertAgentTestHelper) SetupUniversalAuth(identityID string) { h.ClientSecret = csResp.JSON200.ClientSecret } +func (h *CertAgentTestHelper) doRequestWithToken(method, path string, body interface{}, token string) []byte { + t := h.T + + var bodyReader io.Reader + if body != nil { + jsonBody, err := json.Marshal(body) + require.NoError(t, err) + bodyReader = bytes.NewReader(jsonBody) + } + + url := h.InfisicalURL + "/api" + path + req, err := http.NewRequest(method, url, bodyReader) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + httpClient := &http.Client{Timeout: 30 * time.Second} + resp, err := httpClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.True(t, resp.StatusCode >= 200 && resp.StatusCode < 300, + "API %s %s failed with status %d: %s", method, path, resp.StatusCode, string(respBody)) + + return respBody +} + +func (h *CertAgentTestHelper) SetActiveCertManagerProject() { + h.doRequestWithToken( + "POST", + "/v1/cert-manager/instance/active-project", + map[string]string{"projectId": h.ProjectID}, + h.AdminToken, + ) +} + +func (h *CertAgentTestHelper) CreateApplicationWithProfile(name string) { + t := h.T + + respBody := h.doRequestWithToken( + "POST", + "/v1/cert-manager/applications", + map[string]interface{}{ + "name": name, + "profileIds": []string{h.ProfileID}, + }, + h.IdentityToken, + ) + + var parsed struct { + Application struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"application"` + } + require.NoError(t, json.Unmarshal(respBody, &parsed)) + require.NotEmpty(t, parsed.Application.ID, "create application returned empty id: %s", string(respBody)) + + h.ApplicationID = parsed.Application.ID + h.ApplicationName = parsed.Application.Name + + h.doRequestWithToken( + "PUT", + fmt.Sprintf("/v1/cert-manager/applications/%s/profiles/%s/enrollment/api", h.ApplicationID, h.ProfileID), + map[string]interface{}{"autoRenew": false}, + h.IdentityToken, + ) +} + +func (h *CertAgentTestHelper) AttachProfileWithApiEnrollment(profileID string) { + h.doRequestWithToken( + "POST", + fmt.Sprintf("/v1/cert-manager/applications/%s/profiles", h.ApplicationID), + map[string]interface{}{"profileIds": []string{profileID}}, + h.IdentityToken, + ) + h.doRequestWithToken( + "PUT", + fmt.Sprintf("/v1/cert-manager/applications/%s/profiles/%s/enrollment/api", h.ApplicationID, profileID), + map[string]interface{}{"autoRenew": false}, + h.IdentityToken, + ) +} + func (h *CertAgentTestHelper) CreateAcmeCA(dnsConnectionID, directoryUrl string) { t := h.T ctx := context.Background() @@ -302,7 +393,7 @@ func (h *CertAgentTestHelper) CreateAcmeCA(dnsConnectionID, directoryUrl string) AccountEmail string `json:"accountEmail"` DirectoryUrl string `json:"directoryUrl"` DnsAppConnectionId openapi_types.UUID `json:"dnsAppConnectionId"` - DnsProviderConfig struct { + DnsProviderConfig struct { HostedZoneId string `json:"hostedZoneId"` Provider client.CreateAcmeCertificateAuthorityV1JSONBodyConfigurationDnsProviderConfigProvider `json:"provider"` } `json:"dnsProviderConfig"` @@ -381,7 +472,7 @@ func (h *CertAgentTestHelper) CreateAcmeCARaw(name, dnsConnectionID, directoryUr AccountEmail string `json:"accountEmail"` DirectoryUrl string `json:"directoryUrl"` DnsAppConnectionId openapi_types.UUID `json:"dnsAppConnectionId"` - DnsProviderConfig struct { + DnsProviderConfig struct { HostedZoneId string `json:"hostedZoneId"` Provider client.CreateAcmeCertificateAuthorityV1JSONBodyConfigurationDnsProviderConfigProvider `json:"provider"` } `json:"dnsProviderConfig"` @@ -467,14 +558,15 @@ type agentUniversalAuthConfig struct { } type agentCertificateConfig struct { - ProjectSlug string `yaml:"project-slug"` - ProfileName string `yaml:"profile-name"` - CSR string `yaml:"csr,omitempty"` - CSRPath string `yaml:"csr-path,omitempty"` - Attributes *agentCertificateAttributes `yaml:"attributes,omitempty"` - Lifecycle agentCertificateLifecycle `yaml:"lifecycle"` - FileOutput agentCertificateFileOutput `yaml:"file-output"` - PostHooks *agentCertificatePostHooks `yaml:"post-hooks,omitempty"` + ProjectSlug string `yaml:"project-slug,omitempty"` + ApplicationName string `yaml:"application-name,omitempty"` + ProfileName string `yaml:"profile-name"` + CSR string `yaml:"csr,omitempty"` + CSRPath string `yaml:"csr-path,omitempty"` + Attributes *agentCertificateAttributes `yaml:"attributes,omitempty"` + Lifecycle agentCertificateLifecycle `yaml:"lifecycle"` + FileOutput agentCertificateFileOutput `yaml:"file-output"` + PostHooks *agentCertificatePostHooks `yaml:"post-hooks,omitempty"` } type agentCertificateAttributes struct { @@ -520,10 +612,11 @@ func (h *CertAgentTestHelper) GenerateAgentConfig(opts AgentConfigOptions) strin var certs []agentCertificateConfig for _, cert := range opts.Certificates { c := agentCertificateConfig{ - ProjectSlug: cert.ProjectSlug, - ProfileName: cert.ProfileSlug, - CSR: cert.CSR, - CSRPath: cert.CSRPath, + ProjectSlug: cert.ProjectSlug, + ApplicationName: cert.ApplicationName, + ProfileName: cert.ProfileSlug, + CSR: cert.CSR, + CSRPath: cert.CSRPath, Attributes: &agentCertificateAttributes{ CommonName: cert.CommonName, TTL: cert.TTL, @@ -560,8 +653,12 @@ func (h *CertAgentTestHelper) GenerateAgentConfig(opts AgentConfigOptions) strin certs = append(certs, c) } + version := opts.Version + if version == "" { + version = "v2" + } cfg := agentConfig{ - Version: "v1", + Version: version, Infisical: agentInfisicalConfig{ Address: h.InfisicalURL, }, @@ -586,6 +683,9 @@ func (h *CertAgentTestHelper) GenerateAgentConfig(opts AgentConfigOptions) strin } type AgentConfigOptions struct { + // Version selects the agent schema: "v1" (legacy project-based) or "v2" (application-based). + // Empty defaults to "v2". + Version string ClientIDPath string ClientSecretPath string Certificates []CertificateConfigEntry @@ -593,6 +693,7 @@ type AgentConfigOptions struct { type CertificateConfigEntry struct { ProjectSlug string + ApplicationName string ProfileSlug string CommonName string TTL string diff --git a/e2e/agent/certificate_test.go b/e2e/agent/certificate_test.go index 7c990417..41a6fcdb 100644 --- a/e2e/agent/certificate_test.go +++ b/e2e/agent/certificate_test.go @@ -55,6 +55,7 @@ func setupCertAgentTest(t *testing.T, ctx context.Context, policyOpts ...agentHe helper := &agentHelpers.CertAgentTestHelper{ T: t, AdminToken: infisical.ProvisionResult().Token, + IdentityToken: identityToken, InfisicalURL: infisical.ApiUrl(t), TempDir: t.TempDir(), IdentityClient: identityClient, @@ -76,6 +77,8 @@ func setupCertAgentTest(t *testing.T, ctx context.Context, policyOpts ...agentHe helper.ProjectSlug = projectResp.JSON200.Project.Slug slog.Info("Created cert-manager project", "id", helper.ProjectID, "slug", helper.ProjectSlug) + helper.SetActiveCertManagerProject() + helper.CreateInternalCA() slog.Info("Created internal CA", "id", helper.CaID) @@ -85,6 +88,9 @@ func setupCertAgentTest(t *testing.T, ctx context.Context, policyOpts ...agentHe helper.CreateCertificateProfile("test-profile-" + helpers.RandomSlug(2)) slog.Info("Created certificate profile", "id", helper.ProfileID, "name", helper.ProfileSlug) + helper.CreateApplicationWithProfile("test-app-" + helpers.RandomSlug(2)) + slog.Info("Created application with API enrollment", "id", helper.ApplicationID, "name", helper.ApplicationName) + return helper } @@ -105,7 +111,7 @@ func certAgent_BasicCertificateIssuance(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "test.example.com", TTL: "1h", @@ -176,7 +182,7 @@ func certAgent_CertificateRenewal(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "renew.example.com", TTL: "2m", @@ -261,7 +267,7 @@ func certAgent_PostHookExecution(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "hook.example.com", TTL: "1h", @@ -314,6 +320,7 @@ func certAgent_MultipleCertificates(t *testing.T) { helper.CreateCertificateProfile("second-profile-" + helpers.RandomSlug(2)) secondProfileName := helper.ProfileSlug + helper.AttachProfileWithApiEnrollment(helper.ProfileID) slog.Info("Created second certificate profile", "name", secondProfileName) certDir1 := filepath.Join(helper.TempDir, "cert1") @@ -331,7 +338,7 @@ func certAgent_MultipleCertificates(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: firstProfileName, CommonName: "multi1.example.com", TTL: "1h", @@ -342,7 +349,7 @@ func certAgent_MultipleCertificates(t *testing.T) { ChainPath: chainPath1, }, { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: secondProfileName, CommonName: "multi2.example.com", TTL: "1h", @@ -417,7 +424,7 @@ func certAgent_FilePermissions(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "perms.example.com", TTL: "1h", @@ -484,7 +491,7 @@ func certAgent_AltNames(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "altnames.example.com", TTL: "1h", @@ -552,7 +559,7 @@ func certAgent_CSRBasedIssuance(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "csr-test.example.com", TTL: "1h", @@ -629,7 +636,7 @@ func certAgent_CSRPathBasedIssuance(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "csrpath-test.example.com", TTL: "1h", @@ -696,7 +703,7 @@ func certAgent_AcmeCA_CertificateIssuance(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "acme-test.example.com", TTL: "1h", @@ -784,6 +791,7 @@ func setupAcmeCertAgentTestWithOpts(t *testing.T, ctx context.Context, policyOpt helper := &agentHelpers.CertAgentTestHelper{ T: t, AdminToken: infisical.ProvisionResult().Token, + IdentityToken: identityToken, InfisicalURL: infisical.ApiUrl(t), TempDir: t.TempDir(), IdentityClient: identityClient, @@ -807,6 +815,8 @@ func setupAcmeCertAgentTestWithOpts(t *testing.T, ctx context.Context, policyOpt t.Skip("BDD nock API not available — backend was not built with Dockerfile.dev") } + helper.SetActiveCertManagerProject() + nockCertCount := 1 if len(certCount) > 0 && certCount[0] > 1 { nockCertCount = certCount[0] @@ -820,6 +830,8 @@ func setupAcmeCertAgentTestWithOpts(t *testing.T, ctx context.Context, policyOpt helper.CreateCertificatePolicy("acme-policy-"+helpers.RandomSlug(2), policyOpts...) helper.CreateCertificateProfile("acme-profile-" + helpers.RandomSlug(2)) + helper.CreateApplicationWithProfile("acme-app-" + helpers.RandomSlug(2)) + return helper, connectionID } @@ -945,7 +957,7 @@ func certAgent_AcmeCA_DisabledCA(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "disabled-ca.example.com", TTL: "1h", @@ -1004,6 +1016,7 @@ func certAgent_AcmeCA_MultipleCertificates(t *testing.T) { helper.CreateCertificateProfile("acme-profile2-" + helpers.RandomSlug(2)) secondProfileSlug := helper.ProfileSlug + helper.AttachProfileWithApiEnrollment(helper.ProfileID) certDir1 := filepath.Join(helper.TempDir, "cert1") certDir2 := filepath.Join(helper.TempDir, "cert2") @@ -1020,7 +1033,7 @@ func certAgent_AcmeCA_MultipleCertificates(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: firstProfileSlug, CommonName: "acme-multi1.example.com", TTL: "1h", @@ -1031,7 +1044,7 @@ func certAgent_AcmeCA_MultipleCertificates(t *testing.T) { ChainPath: chainPath1, }, { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: secondProfileSlug, CommonName: "acme-multi2.example.com", TTL: "1h", @@ -1106,7 +1119,7 @@ func certAgent_AcmeCA_PostHookExecution(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "acme-hook.example.com", TTL: "1h", @@ -1184,7 +1197,7 @@ func certAgent_AcmeCA_CSRBasedIssuance(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "acme-csr.example.com", TTL: "1h", @@ -1256,7 +1269,7 @@ func certAgent_IssuanceFailureReporting(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: "nonexistent-profile-" + helpers.RandomSlug(2), CommonName: "failure.example.com", TTL: "1h", @@ -1336,7 +1349,7 @@ func certAgent_CertificateWithFullAttributes(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "fullattrs.example.com", TTL: "1h", @@ -1412,7 +1425,7 @@ func certAgent_Validation_RenewBeforeExpiryExceedsTTL(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "invalid-renew.example.com", TTL: "1h", @@ -1484,7 +1497,7 @@ func certAgent_Validation_BothCSRAndCSRPath(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "both-csr.example.com", TTL: "1h", @@ -1557,7 +1570,7 @@ func certAgent_Validation_InvalidAuthCredentials(t *testing.T) { ClientSecretPath: fakeClientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "badauth.example.com", TTL: "1h", @@ -1620,7 +1633,7 @@ func certAgent_Validation_MissingCertificatePath(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "nopath.example.com", TTL: "1h", @@ -1673,7 +1686,7 @@ func certAgent_Validation_MissingCertificatePath(t *testing.T) { "Stderr should report a path-related validation error, got:\n%s", stderr) } -func certAgent_Validation_NonexistentProjectSlug(t *testing.T) { +func certAgent_Validation_NonexistentApplicationName(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -1690,9 +1703,9 @@ func certAgent_Validation_NonexistentProjectSlug(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: "nonexistent-project-slug", + ApplicationName: "nonexistent-application", ProfileSlug: helper.ProfileSlug, - CommonName: "bad-project.example.com", + CommonName: "bad-app.example.com", TTL: "1h", RenewBeforeExpiry: "10m", StatusCheckInterval: "5s", @@ -1733,7 +1746,7 @@ func certAgent_Validation_NonexistentProjectSlug(t *testing.T) { }) require.True(t, waitResult == helpers.WaitSuccess || waitResult == helpers.WaitBreakEarly, - "Agent should fail with nonexistent project slug") + "Agent should fail with nonexistent application name") stderr := cmd.Stderr() require.Contains(t, stderr, "failed to resolve", @@ -1741,7 +1754,6 @@ func certAgent_Validation_NonexistentProjectSlug(t *testing.T) { } - func certAgent_OnRenewalPostHook(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -1766,7 +1778,7 @@ func certAgent_OnRenewalPostHook(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "renewal-hook.example.com", TTL: "2m", @@ -1850,7 +1862,7 @@ func certAgent_SignatureAlgorithm(t *testing.T) { ClientSecretPath: clientSecretPath, Certificates: []agentHelpers.CertificateConfigEntry{ { - ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, ProfileSlug: helper.ProfileSlug, CommonName: "sigalg.example.com", TTL: "1h", @@ -1903,6 +1915,205 @@ func certAgent_SignatureAlgorithm(t *testing.T) { agentHelpers.VerifyCertificateCommonName(t, certPath, "sigalg.example.com") } +func certAgent_V1LegacyIssuance(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + helper := setupCertAgentTest(t, ctx) + + certDir := filepath.Join(helper.TempDir, "certs") + require.NoError(t, os.MkdirAll(certDir, 0755)) + certPath, keyPath, chainPath := agentHelpers.CertFilePaths(certDir) + + clientIDPath, clientSecretPath := helper.WriteCredentialFiles() + + configPath := helper.GenerateAgentConfig(agentHelpers.AgentConfigOptions{ + Version: "v1", + ClientIDPath: clientIDPath, + ClientSecretPath: clientSecretPath, + Certificates: []agentHelpers.CertificateConfigEntry{ + { + ProjectSlug: helper.ProjectSlug, + ProfileSlug: helper.ProfileSlug, + ApplicationName: helper.ApplicationName, + CommonName: "v1-legacy.example.com", + TTL: "1h", + RenewBeforeExpiry: "10m", + StatusCheckInterval: "5s", + CertPath: certPath, + KeyPath: keyPath, + ChainPath: chainPath, + }, + }, + }) + + cmd := helpers.Command{ + Test: t, + Args: []string{"cert-manager", "agent", "--config", configPath, "--verbose"}, + Env: map[string]string{}, + } + cmd.Start(ctx) + t.Cleanup(func() { + if t.Failed() { + t.Logf("Agent stderr:\n%s", cmd.Stderr()) + t.Logf("Agent stdout:\n%s", cmd.Stdout()) + } + cmd.Stop() + }) + + result := helpers.WaitForStderr(t, helpers.WaitForStderrOptions{ + EnsureCmdRunning: &cmd, + ExpectedString: "certificate management engine starting", + Timeout: 60 * time.Second, + Interval: 2 * time.Second, + }) + require.Equal(t, helpers.WaitSuccess, result, "Agent failed to start cert management engine (v1)") + + result = helpers.WaitForStderr(t, helpers.WaitForStderrOptions{ + EnsureCmdRunning: &cmd, + ExpectedString: "certificate issued successfully", + Timeout: 120 * time.Second, + Interval: 2 * time.Second, + }) + require.Equal(t, helpers.WaitSuccess, result, "v1 legacy certificate was not issued successfully") + + agentHelpers.VerifyCertificateFile(t, certPath) + agentHelpers.VerifyPrivateKeyFile(t, keyPath) + agentHelpers.VerifyChainFile(t, chainPath) + agentHelpers.VerifyCertificateCommonName(t, certPath, "v1-legacy.example.com") +} + +func certAgent_V1ValidationRejectsAppOnly(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + helper := setupCertAgentTest(t, ctx) + + certDir := filepath.Join(helper.TempDir, "certs") + require.NoError(t, os.MkdirAll(certDir, 0755)) + certPath, keyPath, chainPath := agentHelpers.CertFilePaths(certDir) + + clientIDPath, clientSecretPath := helper.WriteCredentialFiles() + + configPath := helper.GenerateAgentConfig(agentHelpers.AgentConfigOptions{ + Version: "v1", + ClientIDPath: clientIDPath, + ClientSecretPath: clientSecretPath, + Certificates: []agentHelpers.CertificateConfigEntry{ + { + ApplicationName: helper.ApplicationName, + ProfileSlug: helper.ProfileSlug, + CommonName: "v1-bad.example.com", + TTL: "1h", + RenewBeforeExpiry: "10m", + StatusCheckInterval: "5s", + CertPath: certPath, + KeyPath: keyPath, + ChainPath: chainPath, + }, + }, + }) + + cmd := helpers.Command{ + Test: t, + Args: []string{"cert-manager", "agent", "--config", configPath, "--verbose"}, + Env: map[string]string{}, + } + cmd.Start(ctx) + t.Cleanup(func() { + if t.Failed() { + t.Logf("Agent stderr:\n%s", cmd.Stderr()) + t.Logf("Agent stdout:\n%s", cmd.Stdout()) + } + cmd.Stop() + }) + + waitResult := helpers.WaitFor(t, helpers.WaitForOptions{ + Timeout: 30 * time.Second, + Interval: 2 * time.Second, + Condition: func() helpers.ConditionResult { + stderr := cmd.Stderr() + if strings.Contains(stderr, "(version v1): must specify either 'certificate-id' or both 'project-slug' and 'profile-name'") { + return helpers.ConditionSuccess + } + if !cmd.IsRunning() { + return helpers.ConditionBreakEarly + } + return helpers.ConditionWait + }, + }) + + require.True(t, waitResult == helpers.WaitSuccess || waitResult == helpers.WaitBreakEarly, + "Agent should reject v1 config that's missing project-slug. stderr:\n%s", cmd.Stderr()) + require.Contains(t, cmd.Stderr(), "(version v1)", + "Stderr should carry the v1-specific validation error") +} + +func certAgent_V2ValidationRejectsProjectSlug(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + helper := setupCertAgentTest(t, ctx) + + certDir := filepath.Join(helper.TempDir, "certs") + require.NoError(t, os.MkdirAll(certDir, 0755)) + certPath, keyPath, chainPath := agentHelpers.CertFilePaths(certDir) + + clientIDPath, clientSecretPath := helper.WriteCredentialFiles() + + configPath := helper.GenerateAgentConfig(agentHelpers.AgentConfigOptions{ + Version: "v2", + ClientIDPath: clientIDPath, + ClientSecretPath: clientSecretPath, + Certificates: []agentHelpers.CertificateConfigEntry{ + { + ProjectSlug: helper.ProjectSlug, + ApplicationName: helper.ApplicationName, + ProfileSlug: helper.ProfileSlug, + CommonName: "v2-bad.example.com", + TTL: "1h", + RenewBeforeExpiry: "10m", + StatusCheckInterval: "5s", + CertPath: certPath, + KeyPath: keyPath, + ChainPath: chainPath, + }, + }, + }) + + cmd := helpers.Command{ + Test: t, + Args: []string{"cert-manager", "agent", "--config", configPath, "--verbose"}, + Env: map[string]string{}, + } + cmd.Start(ctx) + t.Cleanup(func() { + if t.Failed() { + t.Logf("Agent stderr:\n%s", cmd.Stderr()) + t.Logf("Agent stdout:\n%s", cmd.Stdout()) + } + cmd.Stop() + }) + + waitResult := helpers.WaitFor(t, helpers.WaitForOptions{ + Timeout: 30 * time.Second, + Interval: 2 * time.Second, + Condition: func() helpers.ConditionResult { + stderr := cmd.Stderr() + if strings.Contains(stderr, "(version v2): 'project-slug' is not supported") { + return helpers.ConditionSuccess + } + if !cmd.IsRunning() { + return helpers.ConditionBreakEarly + } + return helpers.ConditionWait + }, + }) + + require.True(t, waitResult == helpers.WaitSuccess || waitResult == helpers.WaitBreakEarly, + "Agent should reject v2 config that carries project-slug. stderr:\n%s", cmd.Stderr()) +} + func TestCertAgent_InternalCA(t *testing.T) { t.Run("BasicCertificateIssuance", certAgent_BasicCertificateIssuance) t.Run("CertificateRenewal", certAgent_CertificateRenewal) @@ -1918,7 +2129,10 @@ func TestCertAgent_InternalCA(t *testing.T) { t.Run("Validation_BothCSRAndCSRPath", certAgent_Validation_BothCSRAndCSRPath) t.Run("Validation_InvalidAuthCredentials", certAgent_Validation_InvalidAuthCredentials) t.Run("Validation_MissingCertificatePath", certAgent_Validation_MissingCertificatePath) - t.Run("Validation_NonexistentProjectSlug", certAgent_Validation_NonexistentProjectSlug) + t.Run("Validation_NonexistentApplicationName", certAgent_Validation_NonexistentApplicationName) + t.Run("V1LegacyIssuance", certAgent_V1LegacyIssuance) + t.Run("V1ValidationRejectsAppOnly", certAgent_V1ValidationRejectsAppOnly) + t.Run("V2ValidationRejectsProjectSlug", certAgent_V2ValidationRejectsProjectSlug) t.Run("OnRenewalPostHook", certAgent_OnRenewalPostHook) t.Run("SignatureAlgorithm", certAgent_SignatureAlgorithm) } diff --git a/packages/api/api.go b/packages/api/api.go index f81e36c1..3b349f4d 100644 --- a/packages/api/api.go +++ b/packages/api/api.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "strings" "github.com/Infisical/infisical-merge/packages/config" @@ -343,6 +344,29 @@ func CallGetProjectBySlug(httpClient *resty.Client, slug string) (Project, error return Project(projectResponse), nil } +func CallGetPkiApplicationByName(httpClient *resty.Client, projectId, name string) (PkiApplication, error) { + var applicationResponse GetPkiApplicationResponse + req := httpClient. + R(). + SetResult(&applicationResponse). + SetHeader("User-Agent", USER_AGENT) + + if projectId != "" { + req = req.SetQueryParam("projectId", projectId) + } + + response, err := req.Get(fmt.Sprintf("%v/v1/cert-manager/applications/by-name/%s", config.INFISICAL_URL, url.PathEscape(name))) + if err != nil { + return PkiApplication{}, NewGenericRequestError("CallGetPkiApplicationByName", err) + } + + if response.IsError() { + return PkiApplication{}, NewAPIErrorWithResponse("CallGetPkiApplicationByName", response, nil) + } + + return applicationResponse.Application, nil +} + func CallGetCertificateProfileBySlug(httpClient *resty.Client, projectId, slug string) (CertificateProfile, error) { var profileResponse GetCertificateProfileResponse response, err := httpClient. @@ -350,7 +374,7 @@ func CallGetCertificateProfileBySlug(httpClient *resty.Client, projectId, slug s SetResult(&profileResponse). SetHeader("User-Agent", USER_AGENT). SetQueryParam("projectId", projectId). - Get(fmt.Sprintf("%v/v1/cert-manager/certificate-profiles/slug/%s", config.INFISICAL_URL, slug)) + Get(fmt.Sprintf("%v/v1/cert-manager/certificate-profiles/slug/%s", config.INFISICAL_URL, url.PathEscape(slug))) if err != nil { return CertificateProfile{}, NewGenericRequestError("CallGetCertificateProfileBySlug", err) @@ -363,6 +387,25 @@ func CallGetCertificateProfileBySlug(httpClient *resty.Client, projectId, slug s return profileResponse.CertificateProfile, nil } +func CallListPkiApplicationProfiles(httpClient *resty.Client, applicationId string) ([]PkiApplicationProfile, error) { + var listResponse ListPkiApplicationProfilesResponse + response, err := httpClient. + R(). + SetResult(&listResponse). + SetHeader("User-Agent", USER_AGENT). + Get(fmt.Sprintf("%v/v1/cert-manager/applications/%s/profiles", config.INFISICAL_URL, url.PathEscape(applicationId))) + + if err != nil { + return nil, NewGenericRequestError("CallListPkiApplicationProfiles", err) + } + + if response.IsError() { + return nil, NewAPIErrorWithResponse("CallListPkiApplicationProfiles", response, nil) + } + + return listResponse.Profiles, nil +} + func CallIsAuthenticated(httpClient *resty.Client) bool { var workSpacesResponse GetWorkSpacesResponse response, err := httpClient. diff --git a/packages/api/model.go b/packages/api/model.go index d0bcf2be..05936e1d 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -138,19 +138,38 @@ type GetProjectByIdResponse struct { type GetProjectBySlugResponse Project +type PkiApplication struct { + ID string `json:"id"` + Name string `json:"name"` + ProjectID string `json:"projectId,omitempty"` +} + +type GetPkiApplicationResponse struct { + Application PkiApplication `json:"application"` +} + type CertificateProfile struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - ProjectID string `json:"projectId"` - CaID string `json:"caId"` - CertificateTemplateID string `json:"certificateTemplateId"` + ID string `json:"id"` + Slug string `json:"slug"` + ProjectID string `json:"projectId,omitempty"` + Description string `json:"description,omitempty"` } type GetCertificateProfileResponse struct { CertificateProfile CertificateProfile `json:"certificateProfile"` } +type PkiApplicationProfile struct { + ApplicationID string `json:"applicationId"` + ProfileID string `json:"profileId"` + ProfileSlug string `json:"profileSlug"` + APIConfigID *string `json:"apiConfigId,omitempty"` +} + +type ListPkiApplicationProfilesResponse struct { + Profiles []PkiApplicationProfile `json:"profiles"` +} + type Organization struct { ID string `json:"id"` Name string `json:"name"` @@ -957,9 +976,10 @@ type CertificateAttributes struct { } type IssueCertificateRequest struct { - ProfileID string `json:"profileId"` - CSR string `json:"csr,omitempty"` - Attributes *CertificateAttributes `json:"attributes,omitempty"` + ProfileID string `json:"profileId"` + ApplicationID string `json:"applicationId,omitempty"` + CSR string `json:"csr,omitempty"` + Attributes *CertificateAttributes `json:"attributes,omitempty"` } type CertificateData struct { @@ -986,7 +1006,6 @@ type RetrieveCertificateResponse struct { CommonName string `json:"commonName"` NotBefore time.Time `json:"notBefore"` NotAfter time.Time `json:"notAfter"` - ProjectId string `json:"projectId"` CaId string `json:"caId"` KeyUsages []string `json:"keyUsages"` ExtendedKeyUsages []string `json:"extendedKeyUsages"` @@ -1022,7 +1041,6 @@ type GetCertificateRequestResponse struct { CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` CommonName string `json:"commonName,omitempty"` - ProjectID string `json:"projectId,omitempty"` ProfileID string `json:"profileId,omitempty"` Certificate *string `json:"certificate,omitempty"` IssuingCaCertificate *string `json:"issuingCaCertificate,omitempty"` diff --git a/packages/cmd/agent.go b/packages/cmd/agent.go index f2781057..6f4aef8a 100644 --- a/packages/cmd/agent.go +++ b/packages/cmd/agent.go @@ -219,6 +219,8 @@ type CertificateAttributes struct { type AgentCertificateConfig struct { ProjectName string `yaml:"project-slug,omitempty"` + ApplicationName string `yaml:"application-name,omitempty"` + ApplicationID string `yaml:"-"` ProfileName string `yaml:"profile-name,omitempty"` ProfileID string `yaml:"-"` CertificateID string `yaml:"certificate-id,omitempty"` @@ -800,31 +802,35 @@ func validateAgentConfigVersionCompatibility(config *Config) error { return validateAgentConfigVersionCompatibilityWithMode(config, false) } +const ( + AgentConfigVersionV1 = "v1" + AgentConfigVersionV2 = "v2" +) + func validateAgentConfigVersionCompatibilityWithMode(config *Config, isCertManagerMode bool) error { if config.Version == "" { if len(config.Certificates) > 0 { - return fmt.Errorf("certificates are configured but 'version' field is not specified. Add 'version: v1' to your config") + return fmt.Errorf("certificates are configured but 'version' field is not specified: add 'version: v2' to your config") } return nil } switch config.Version { - case "v1": + case AgentConfigVersionV1, AgentConfigVersionV2: if isCertManagerMode { - return validateCertificateManagementV1ForCertManager(config) - } else { - return validateCertificateManagementV1(config) + return validateCertificateManagementForCertManager(config) } + return validateCertificateManagementForLegacyAgent(config) default: - return fmt.Errorf("unsupported version: %s. Supported versions: v1", config.Version) + return fmt.Errorf("unsupported version: %s. Supported versions: v1, v2", config.Version) } } -func validateCertificateManagementV1(config *Config) error { - return fmt.Errorf("version: v1 is for certificate management. Please use 'infisical cert-manager agent' for certificate configurations") +func validateCertificateManagementForLegacyAgent(config *Config) error { + return fmt.Errorf("version: %s is for certificate management; use 'infisical cert-manager agent' for certificate configurations", config.Version) } -func validateCertificateManagementV1ForCertManager(config *Config) error { +func validateCertificateManagementForCertManager(config *Config) error { if len(config.Certificates) == 0 { return fmt.Errorf("certificate management requires at least one certificate to be configured") } @@ -2007,6 +2013,8 @@ func validateCertificateLifecycleConfig(certificates *[]AgentCertificateConfig) certName = fmt.Sprintf("certificate '%s'", commonName) } else if len(altNames) > 0 { certName = fmt.Sprintf("certificate '%s'", altNames[0]) + } else if cert.ApplicationName != "" && cert.ProfileName != "" { + certName = fmt.Sprintf("certificate '%s/%s'", cert.ApplicationName, cert.ProfileName) } else if cert.ProjectName != "" && cert.ProfileName != "" { certName = fmt.Sprintf("certificate '%s/%s'", cert.ProjectName, cert.ProfileName) } @@ -2032,7 +2040,7 @@ func validateCertificateLifecycleConfig(certificates *[]AgentCertificateConfig) return nil } -func resolveCertificateNameReferences(certificates *[]AgentCertificateConfig, httpClient *resty.Client) error { +func resolveCertificateNameReferences(version string, certificates *[]AgentCertificateConfig, httpClient *resty.Client) error { for i := range *certificates { cert := &(*certificates)[i] @@ -2040,35 +2048,108 @@ func resolveCertificateNameReferences(certificates *[]AgentCertificateConfig, ht continue } - if cert.ProjectName == "" || cert.ProfileName == "" { - return fmt.Errorf("certificate configuration must specify both 'project-slug' and 'profile-name' (or 'certificate-id' to reference an existing certificate)") + switch version { + case AgentConfigVersionV1: + if err := resolveCertificateLegacyReferences(cert, httpClient); err != nil { + return err + } + case AgentConfigVersionV2: + if err := resolveCertificateApplicationReferences(cert, httpClient); err != nil { + return err + } + default: + return fmt.Errorf("unsupported version: %s. Supported versions: v1, v2", version) } + } + return nil +} + +func resolveCertificateLegacyReferences(cert *AgentCertificateConfig, httpClient *resty.Client) error { + project, err := api.CallGetProjectBySlug(httpClient, cert.ProjectName) + if err != nil { + return fmt.Errorf("failed to resolve project slug '%s': %v", cert.ProjectName, err) + } + if project.ID == "" { + return fmt.Errorf("project '%s' returned an empty ID", cert.ProjectName) + } + + profile, err := api.CallGetCertificateProfileBySlug(httpClient, project.ID, cert.ProfileName) + if err != nil { + return fmt.Errorf("failed to resolve profile '%s' in project '%s': %v", cert.ProfileName, cert.ProjectName, err) + } + cert.ProfileID = profile.ID - project, err := api.CallGetProjectBySlug(httpClient, cert.ProjectName) + if cert.ApplicationName != "" { + application, err := api.CallGetPkiApplicationByName(httpClient, project.ID, cert.ApplicationName) if err != nil { - return fmt.Errorf("failed to resolve project name '%s': %v. Please check that the project exists and you have access to it", cert.ProjectName, err) + return fmt.Errorf("failed to resolve application '%s' in project '%s': %v", cert.ApplicationName, cert.ProjectName, err) } - - if project.ID == "" { - return fmt.Errorf("project '%s' was found but returned empty ID. This may indicate a server issue", cert.ProjectName) + if application.ID == "" { + return fmt.Errorf("application '%s' returned an empty ID", cert.ApplicationName) } + cert.ApplicationID = application.ID + } - profile, err := api.CallGetCertificateProfileBySlug(httpClient, project.ID, cert.ProfileName) - if err != nil { - return fmt.Errorf("failed to resolve profile name '%s' in project '%s' (project ID: %s): %v. Please check that the certificate profile exists in this project", cert.ProfileName, cert.ProjectName, project.ID, err) + return nil +} + +func resolveCertificateApplicationReferences(cert *AgentCertificateConfig, httpClient *resty.Client) error { + if cert.ApplicationName == "" || cert.ProfileName == "" { + return fmt.Errorf("certificate configuration must specify either 'certificate-id' or both 'application-name' and 'profile-name'") + } + + application, err := api.CallGetPkiApplicationByName(httpClient, "", cert.ApplicationName) + if err != nil { + return fmt.Errorf("failed to resolve application '%s': %v", cert.ApplicationName, err) + } + if application.ID == "" { + return fmt.Errorf("application '%s' returned an empty ID", cert.ApplicationName) + } + cert.ApplicationID = application.ID + + profiles, err := api.CallListPkiApplicationProfiles(httpClient, application.ID) + if err != nil { + return fmt.Errorf("failed to list profiles for application '%s': %v", cert.ApplicationName, err) + } + + var matched *api.PkiApplicationProfile + for j := range profiles { + if profiles[j].ProfileSlug == cert.ProfileName { + matched = &profiles[j] + break } + } + if matched == nil { + return fmt.Errorf("profile '%s' is not attached to application '%s'", cert.ProfileName, cert.ApplicationName) + } - cert.ProfileID = profile.ID + if matched.APIConfigID == nil || *matched.APIConfigID == "" { + return fmt.Errorf("profile '%s' on application '%s' does not have API enrollment configured", cert.ProfileName, cert.ApplicationName) } + + cert.ProfileID = matched.ProfileID return nil } -func validateCertificateSourceConfig(certificates *[]AgentCertificateConfig) error { +func validateCertificateSourceConfig(version string, certificates *[]AgentCertificateConfig) error { + switch version { + case AgentConfigVersionV1, AgentConfigVersionV2: + default: + return fmt.Errorf("unsupported version: %s. Supported versions: v1, v2", version) + } + for i, cert := range *certificates { certIndex := i + 1 + if cert.HasCertificateID() { - if cert.ProjectName != "" || cert.ProfileName != "" { - return fmt.Errorf("certificate %d: 'certificate-id' cannot be used together with 'project-slug' or 'profile-name'", certIndex) + if cert.ProjectName != "" { + return fmt.Errorf("certificate %d: 'certificate-id' cannot be used together with 'project-slug'", certIndex) + } + if cert.ProfileName != "" { + return fmt.Errorf("certificate %d: 'certificate-id' cannot be used together with 'profile-name'", certIndex) + } + if cert.ApplicationName != "" { + return fmt.Errorf("certificate %d: 'certificate-id' cannot be used together with 'application-name'", certIndex) } if cert.CSR != "" || cert.CSRPath != "" { return fmt.Errorf("certificate %d: 'certificate-id' cannot be used together with 'csr' or 'csr-path'", certIndex) @@ -2079,8 +2160,18 @@ func validateCertificateSourceConfig(certificates *[]AgentCertificateConfig) err continue } - if cert.ProjectName == "" || cert.ProfileName == "" { - return fmt.Errorf("certificate %d: must specify either 'certificate-id' or both 'project-slug' and 'profile-name'", certIndex) + switch version { + case AgentConfigVersionV1: + if cert.ProjectName == "" || cert.ProfileName == "" { + return fmt.Errorf("certificate %d (version v1): must specify either 'certificate-id' or both 'project-slug' and 'profile-name'", certIndex) + } + case AgentConfigVersionV2: + if cert.ProjectName != "" { + return fmt.Errorf("certificate %d (version v2): 'project-slug' is not supported; use 'application-name' + 'profile-name', or set 'version: v1' for the legacy flow", certIndex) + } + if cert.ApplicationName == "" || cert.ProfileName == "" { + return fmt.Errorf("certificate %d (version v2): must specify either 'certificate-id' or both 'application-name' and 'profile-name'", certIndex) + } } } return nil @@ -2311,7 +2402,8 @@ func (tm *AgentManager) IssueCertificate(certificateId int, certificate *AgentCe state := tm.certificateStates[certificateId] request := api.IssueCertificateRequest{ - ProfileID: certificate.ProfileID, + ProfileID: certificate.ProfileID, + ApplicationID: certificate.ApplicationID, } if certificate.CSR != "" { @@ -3240,7 +3332,7 @@ var agentCmd = &cobra.Command{ return } - err = validateCertificateSourceConfig(&agentConfig.Certificates) + err = validateCertificateSourceConfig(agentConfig.Version, &agentConfig.Certificates) if err != nil { log.Error().Msgf("Certificate configuration validation failed: %v", err) return @@ -3369,7 +3461,7 @@ var agentCmd = &cobra.Command{ return } - if err := resolveCertificateNameReferences(&agentConfig.Certificates, httpClient); err != nil { + if err := resolveCertificateNameReferences(agentConfig.Version, &agentConfig.Certificates, httpClient); err != nil { log.Error().Msgf("failed to resolve certificate name references: %v", err) return } @@ -3429,8 +3521,8 @@ var agentCmd = &cobra.Command{ } func validateCertificateOnlyMode(config *Config) error { - if config.Version != "v1" { - return fmt.Errorf("certificate management requires version: v1") + if config.Version != AgentConfigVersionV1 && config.Version != AgentConfigVersionV2 { + return fmt.Errorf("certificate management requires 'version: v1' or 'version: v2'") } if len(config.Certificates) == 0 { @@ -3517,7 +3609,7 @@ var certManagerAgentCmd = &cobra.Command{ return } - err = validateCertificateSourceConfig(&agentConfig.Certificates) + err = validateCertificateSourceConfig(agentConfig.Version, &agentConfig.Certificates) if err != nil { log.Error().Msgf("Certificate configuration validation failed: %v", err) return @@ -3590,7 +3682,7 @@ var certManagerAgentCmd = &cobra.Command{ return } - if err := resolveCertificateNameReferences(&agentConfig.Certificates, httpClient); err != nil { + if err := resolveCertificateNameReferences(agentConfig.Version, &agentConfig.Certificates, httpClient); err != nil { log.Error().Msgf("failed to resolve certificate name references: %v", err) return } diff --git a/packages/cmd/agent_cert_resolution_test.go b/packages/cmd/agent_cert_resolution_test.go index ed8e0dd3..22a27b93 100644 --- a/packages/cmd/agent_cert_resolution_test.go +++ b/packages/cmd/agent_cert_resolution_test.go @@ -13,115 +13,437 @@ import ( "github.com/stretchr/testify/require" ) -func TestResolveCertificateNameReferences(t *testing.T) { +func strPtr(s string) *string { + return &s +} + +func newApplicationServer(t *testing.T, applicationName, applicationID string, profiles []api.PkiApplicationProfile) *httptest.Server { + t.Helper() + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/v1/cert-manager/applications/by-name/" + applicationName: + _ = json.NewEncoder(w).Encode(api.GetPkiApplicationResponse{ + Application: api.PkiApplication{ID: applicationID, Name: applicationName}, + }) + case "/v1/cert-manager/applications/" + applicationID + "/profiles": + _ = json.NewEncoder(w).Encode(api.ListPkiApplicationProfilesResponse{Profiles: profiles}) + default: + http.NotFound(w, r) + } + }), + ) + return server +} + +func withMockInfisicalURL(t *testing.T, url string) { + t.Helper() + orig := config.INFISICAL_URL + config.INFISICAL_URL = url + t.Cleanup(func() { config.INFISICAL_URL = orig }) +} + +func TestResolveCertificateNameReferences_AttachedProfileWithAPIEnrollment(t *testing.T) { const ( - projectSlug = "my-project" - projectID = "proj-uuid-1234" - profileSlug = "crdb" - profileID = "profile-uuid-5678" + applicationName = "my-app" + applicationID = "app-uuid-1" + profileSlug = "crdb" + profileID = "profile-uuid-1" + ) + + server := newApplicationServer(t, applicationName, applicationID, []api.PkiApplicationProfile{ + { + ApplicationID: applicationID, + ProfileID: profileID, + ProfileSlug: profileSlug, + APIConfigID: strPtr("api-cfg-1"), + }, + }) + t.Cleanup(server.Close) + withMockInfisicalURL(t, server.URL) + + certs := []AgentCertificateConfig{ + {ApplicationName: applicationName, ProfileName: profileSlug}, + } + + require.NoError(t, resolveCertificateNameReferences(AgentConfigVersionV2, &certs, resty.New())) + assert.Equal(t, applicationID, certs[0].ApplicationID) + assert.Equal(t, profileID, certs[0].ProfileID) +} + +func TestResolveCertificateNameReferences_MissingAPIEnrollment(t *testing.T) { + const ( + applicationName = "my-app" + applicationID = "app-uuid-2" + profileSlug = "no-api-profile" ) - var gotProjectIDQuery string + server := newApplicationServer(t, applicationName, applicationID, []api.PkiApplicationProfile{ + { + ApplicationID: applicationID, + ProfileID: "profile-uuid-2", + ProfileSlug: profileSlug, + }, + }) + t.Cleanup(server.Close) + withMockInfisicalURL(t, server.URL) + + certs := []AgentCertificateConfig{ + {ApplicationName: applicationName, ProfileName: profileSlug}, + } + + err := resolveCertificateNameReferences(AgentConfigVersionV2, &certs, resty.New()) + require.Error(t, err) + assert.Contains(t, err.Error(), "API enrollment") +} + +func TestResolveCertificateNameReferences_ProfileNotAttached(t *testing.T) { + const ( + applicationName = "my-app" + applicationID = "app-uuid-3" + ) + + server := newApplicationServer(t, applicationName, applicationID, []api.PkiApplicationProfile{ + {ApplicationID: applicationID, ProfileID: "x", ProfileSlug: "other-profile", APIConfigID: strPtr("y")}, + }) + t.Cleanup(server.Close) + withMockInfisicalURL(t, server.URL) + + certs := []AgentCertificateConfig{ + {ApplicationName: applicationName, ProfileName: "missing-profile"}, + } + + err := resolveCertificateNameReferences(AgentConfigVersionV2, &certs, resty.New()) + require.Error(t, err) + assert.Contains(t, err.Error(), "not attached") +} +func TestResolveCertificateNameReferences_MissingFieldsV2(t *testing.T) { + certs := []AgentCertificateConfig{{}} + err := resolveCertificateNameReferences(AgentConfigVersionV2, &certs, resty.New()) + require.Error(t, err) + assert.Contains(t, err.Error(), "application-name") +} + +func TestResolveCertificateNameReferences_UnsupportedVersion(t *testing.T) { + certs := []AgentCertificateConfig{{ApplicationName: "a", ProfileName: "p"}} + err := resolveCertificateNameReferences("v99", &certs, resty.New()) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported version") +} + +func newLegacyServer(t *testing.T, projectSlug, projectID, profileSlug, profileID string, optionalApp *api.PkiApplication) *httptest.Server { + t.Helper() server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/v1/projects/slug/" + projectSlug: - json.NewEncoder(w).Encode(api.Project{ - ID: projectID, - Name: "My Project", - Slug: projectSlug, - }) + _ = json.NewEncoder(w).Encode(api.GetProjectBySlugResponse{ID: projectID, Slug: projectSlug}) case "/v1/cert-manager/certificate-profiles/slug/" + profileSlug: - gotProjectIDQuery = r.URL.Query().Get("projectId") - json.NewEncoder(w).Encode(api.GetCertificateProfileResponse{ - CertificateProfile: api.CertificateProfile{ - ID: profileID, - Name: profileSlug, - ProjectID: projectID, - }, + if r.URL.Query().Get("projectId") != projectID { + http.Error(w, "wrong projectId", http.StatusBadRequest) + return + } + _ = json.NewEncoder(w).Encode(api.GetCertificateProfileResponse{ + CertificateProfile: api.CertificateProfile{ID: profileID, Slug: profileSlug, ProjectID: projectID}, }) default: + if optionalApp != nil && r.URL.Path == "/v1/cert-manager/applications/by-name/"+optionalApp.Name { + if r.URL.Query().Get("projectId") != projectID { + http.Error(w, "wrong projectId", http.StatusBadRequest) + return + } + _ = json.NewEncoder(w).Encode(api.GetPkiApplicationResponse{Application: *optionalApp}) + return + } http.NotFound(w, r) } }), ) - t.Cleanup(server.Close) + return server +} - orig := config.INFISICAL_URL - config.INFISICAL_URL = server.URL - t.Cleanup(func() { config.INFISICAL_URL = orig }) +func TestResolveCertificateNameReferences_LegacyProjectAndProfile(t *testing.T) { + const ( + projectSlug = "my-project" + projectID = "project-uuid-legacy" + profileSlug = "legacy-profile" + profileID = "profile-uuid-legacy" + ) + + server := newLegacyServer(t, projectSlug, projectID, profileSlug, profileID, nil) + t.Cleanup(server.Close) + withMockInfisicalURL(t, server.URL) certs := []AgentCertificateConfig{ {ProjectName: projectSlug, ProfileName: profileSlug}, } - require.NoError(t, resolveCertificateNameReferences(&certs, resty.New())) + require.NoError(t, resolveCertificateNameReferences(AgentConfigVersionV1, &certs, resty.New())) assert.Equal(t, profileID, certs[0].ProfileID) - assert.Equal(t, projectID, gotProjectIDQuery, "profile lookup must pass resolved projectId query param") + assert.Empty(t, certs[0].ApplicationID, "application-id should remain empty when no application-name supplied") } -func TestResolveCertificateNameReferences_MultipleProfiles(t *testing.T) { +func TestResolveCertificateNameReferences_LegacyProjectProfileWithApplication(t *testing.T) { const ( - projectSlug = "multi-project" - projectID = "proj-uuid-multi" + projectSlug = "my-project" + projectID = "project-uuid-legacy" + profileSlug = "legacy-profile" + profileID = "profile-uuid-legacy" + applicationName = "legacy-app" + applicationID = "app-uuid-legacy" ) - profiles := map[string]string{ - "profile-a": "uuid-aaaa", - "profile-b": "uuid-bbbb", - "profile-c": "uuid-cccc", + server := newLegacyServer(t, projectSlug, projectID, profileSlug, profileID, &api.PkiApplication{ + ID: applicationID, + Name: applicationName, + ProjectID: projectID, + }) + t.Cleanup(server.Close) + withMockInfisicalURL(t, server.URL) + + certs := []AgentCertificateConfig{ + {ProjectName: projectSlug, ProfileName: profileSlug, ApplicationName: applicationName}, } + require.NoError(t, resolveCertificateNameReferences(AgentConfigVersionV1, &certs, resty.New())) + assert.Equal(t, profileID, certs[0].ProfileID) + assert.Equal(t, applicationID, certs[0].ApplicationID) +} + +func TestResolveCertificateNameReferences_LegacyProjectMissingProfile(t *testing.T) { + const ( + projectSlug = "my-project" + projectID = "project-uuid-legacy" + ) + server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.URL.Path == "/v1/projects/slug/"+projectSlug { - json.NewEncoder(w).Encode(api.Project{ - ID: projectID, - Name: "Multi Project", - Slug: projectSlug, - }) + _ = json.NewEncoder(w).Encode(api.GetProjectBySlugResponse{ID: projectID, Slug: projectSlug}) return } - for slug, id := range profiles { - if r.URL.Path == "/v1/cert-manager/certificate-profiles/slug/"+slug { - json.NewEncoder(w).Encode(api.GetCertificateProfileResponse{ - CertificateProfile: api.CertificateProfile{ - ID: id, - Name: slug, - ProjectID: projectID, - }, - }) - return - } - } http.NotFound(w, r) }), ) t.Cleanup(server.Close) + withMockInfisicalURL(t, server.URL) - orig := config.INFISICAL_URL - config.INFISICAL_URL = server.URL - t.Cleanup(func() { config.INFISICAL_URL = orig }) + certs := []AgentCertificateConfig{ + {ProjectName: projectSlug, ProfileName: "missing-profile"}, + } + + err := resolveCertificateNameReferences(AgentConfigVersionV1, &certs, resty.New()) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing-profile") + assert.Contains(t, err.Error(), projectSlug) +} + +func TestResolveCertificateNameReferences_LegacyProjectNotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.NotFound(w, r) + })) + t.Cleanup(server.Close) + withMockInfisicalURL(t, server.URL) certs := []AgentCertificateConfig{ - {ProjectName: projectSlug, ProfileName: "profile-a"}, - {ProjectName: projectSlug, ProfileName: "profile-b"}, - {ProjectName: projectSlug, ProfileName: "profile-c"}, + {ProjectName: "missing-project", ProfileName: "any-profile"}, } - require.NoError(t, resolveCertificateNameReferences(&certs, resty.New())) + err := resolveCertificateNameReferences(AgentConfigVersionV1, &certs, resty.New()) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing-project") +} - for i, cert := range certs { - assert.Equal(t, profiles[cert.ProfileName], cert.ProfileID, "cert[%d] ProfileID mismatch", i) +func TestValidateCertificateSourceConfig_V1Accepted(t *testing.T) { + certs := []AgentCertificateConfig{ + {ProjectName: "proj", ProfileName: "p"}, + {ProjectName: "proj", ProfileName: "p", ApplicationName: "app"}, + {CertificateID: "00000000-0000-0000-0000-000000000000"}, } + require.NoError(t, validateCertificateSourceConfig(AgentConfigVersionV1, &certs)) } -func TestResolveCertificateNameReferences_MissingSlugs(t *testing.T) { - certs := []AgentCertificateConfig{{}} - err := resolveCertificateNameReferences(&certs, resty.New()) +func TestValidateCertificateSourceConfig_V2Accepted(t *testing.T) { + certs := []AgentCertificateConfig{ + {ApplicationName: "app", ProfileName: "p"}, + {CertificateID: "00000000-0000-0000-0000-000000000000"}, + } + require.NoError(t, validateCertificateSourceConfig(AgentConfigVersionV2, &certs)) +} + +func TestValidateCertificateSourceConfig_V1MissingProfile(t *testing.T) { + certs := []AgentCertificateConfig{{ProjectName: "proj"}} + err := validateCertificateSourceConfig(AgentConfigVersionV1, &certs) + require.Error(t, err) + assert.Contains(t, err.Error(), "v1") + assert.Contains(t, err.Error(), "profile-name") +} + +func TestValidateCertificateSourceConfig_V1MissingProject(t *testing.T) { + certs := []AgentCertificateConfig{{ApplicationName: "app", ProfileName: "p"}} + err := validateCertificateSourceConfig(AgentConfigVersionV1, &certs) + require.Error(t, err) + assert.Contains(t, err.Error(), "v1") + assert.Contains(t, err.Error(), "project-slug") +} + +func TestValidateCertificateSourceConfig_V2RejectsProjectSlug(t *testing.T) { + certs := []AgentCertificateConfig{{ProjectName: "proj", ProfileName: "p"}} + err := validateCertificateSourceConfig(AgentConfigVersionV2, &certs) require.Error(t, err) + assert.Contains(t, err.Error(), "v2") assert.Contains(t, err.Error(), "project-slug") } + +func TestValidateCertificateSourceConfig_V2MissingApplication(t *testing.T) { + certs := []AgentCertificateConfig{{ProfileName: "p"}} + err := validateCertificateSourceConfig(AgentConfigVersionV2, &certs) + require.Error(t, err) + assert.Contains(t, err.Error(), "v2") + assert.Contains(t, err.Error(), "application-name") +} + +func TestValidateCertificateSourceConfig_CertIDExclusivity(t *testing.T) { + cases := []struct { + name string + version string + cert AgentCertificateConfig + message string + }{ + { + name: "v1 with project-slug", + version: AgentConfigVersionV1, + cert: AgentCertificateConfig{CertificateID: "00000000-0000-0000-0000-000000000000", ProjectName: "proj"}, + message: "project-slug", + }, + { + name: "v2 with application-name", + version: AgentConfigVersionV2, + cert: AgentCertificateConfig{CertificateID: "00000000-0000-0000-0000-000000000000", ApplicationName: "app"}, + message: "application-name", + }, + { + name: "v1 with profile-name", + version: AgentConfigVersionV1, + cert: AgentCertificateConfig{CertificateID: "00000000-0000-0000-0000-000000000000", ProfileName: "p"}, + message: "profile-name", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + certs := []AgentCertificateConfig{tc.cert} + err := validateCertificateSourceConfig(tc.version, &certs) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.message) + }) + } +} + +func TestValidateCertificateSourceConfig_UnsupportedVersion(t *testing.T) { + certs := []AgentCertificateConfig{{ApplicationName: "app", ProfileName: "p"}} + err := validateCertificateSourceConfig("v99", &certs) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported version") +} + +func TestResolveCertificateNameReferences_ApplicationNotFound(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.NotFound(w, r) + }), + ) + t.Cleanup(server.Close) + withMockInfisicalURL(t, server.URL) + + certs := []AgentCertificateConfig{ + {ApplicationName: "missing-app", ProfileName: "any-profile"}, + } + + err := resolveCertificateNameReferences(AgentConfigVersionV2, &certs, resty.New()) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing-app") + assert.Contains(t, err.Error(), "failed to resolve application") +} + +func TestResolveCertificateNameReferences_CertificateIDSkipsResolution(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("no API call expected for cert-id-only config; got %s", r.URL.Path) + http.NotFound(w, r) + }), + ) + t.Cleanup(server.Close) + withMockInfisicalURL(t, server.URL) + + certs := []AgentCertificateConfig{ + {CertificateID: "00000000-0000-0000-0000-000000000000"}, + } + + require.NoError(t, resolveCertificateNameReferences(AgentConfigVersionV2, &certs, resty.New())) + assert.Empty(t, certs[0].ApplicationID) + assert.Empty(t, certs[0].ProfileID) +} + +func TestResolveCertificateNameReferences_MultipleCerts(t *testing.T) { + const ( + appAName = "app-a" + appAID = "app-a-uuid" + appBName = "app-b" + appBID = "app-b-uuid" + ) + + appAProfiles := []api.PkiApplicationProfile{ + {ApplicationID: appAID, ProfileID: "profile-a1", ProfileSlug: "a1", APIConfigID: strPtr("cfg-a1")}, + {ApplicationID: appAID, ProfileID: "profile-a2", ProfileSlug: "a2", APIConfigID: strPtr("cfg-a2")}, + } + appBProfiles := []api.PkiApplicationProfile{ + {ApplicationID: appBID, ProfileID: "profile-b1", ProfileSlug: "b1", APIConfigID: strPtr("cfg-b1")}, + } + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/v1/cert-manager/applications/by-name/" + appAName: + _ = json.NewEncoder(w).Encode(api.GetPkiApplicationResponse{ + Application: api.PkiApplication{ID: appAID, Name: appAName}, + }) + case "/v1/cert-manager/applications/by-name/" + appBName: + _ = json.NewEncoder(w).Encode(api.GetPkiApplicationResponse{ + Application: api.PkiApplication{ID: appBID, Name: appBName}, + }) + case "/v1/cert-manager/applications/" + appAID + "/profiles": + _ = json.NewEncoder(w).Encode(api.ListPkiApplicationProfilesResponse{Profiles: appAProfiles}) + case "/v1/cert-manager/applications/" + appBID + "/profiles": + _ = json.NewEncoder(w).Encode(api.ListPkiApplicationProfilesResponse{Profiles: appBProfiles}) + default: + http.NotFound(w, r) + } + }), + ) + t.Cleanup(server.Close) + withMockInfisicalURL(t, server.URL) + + certs := []AgentCertificateConfig{ + {ApplicationName: appAName, ProfileName: "a1"}, + {ApplicationName: appAName, ProfileName: "a2"}, + {ApplicationName: appBName, ProfileName: "b1"}, + {CertificateID: "11111111-1111-1111-1111-111111111111"}, + } + + require.NoError(t, resolveCertificateNameReferences(AgentConfigVersionV2, &certs, resty.New())) + + assert.Equal(t, appAID, certs[0].ApplicationID) + assert.Equal(t, "profile-a1", certs[0].ProfileID) + assert.Equal(t, appAID, certs[1].ApplicationID) + assert.Equal(t, "profile-a2", certs[1].ProfileID) + assert.Equal(t, appBID, certs[2].ApplicationID) + assert.Equal(t, "profile-b1", certs[2].ProfileID) + assert.Empty(t, certs[3].ApplicationID) + assert.Empty(t, certs[3].ProfileID) +} + From dda877a9911c131f7969c4701121f9e7d4a3b6b6 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Tue, 19 May 2026 13:10:16 -0300 Subject: [PATCH 2/4] Address claude comments --- packages/api/api.go | 2 +- packages/cmd/agent.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/api/api.go b/packages/api/api.go index 3b349f4d..75d7f396 100644 --- a/packages/api/api.go +++ b/packages/api/api.go @@ -331,7 +331,7 @@ func CallGetProjectBySlug(httpClient *resty.Client, slug string) (Project, error R(). SetResult(&projectResponse). SetHeader("User-Agent", USER_AGENT). - Get(fmt.Sprintf("%v/v1/projects/slug/%s", config.INFISICAL_URL, slug)) + Get(fmt.Sprintf("%v/v1/projects/slug/%s", config.INFISICAL_URL, url.PathEscape(slug))) if err != nil { return Project{}, NewGenericRequestError("CallGetProjectBySlug", err) diff --git a/packages/cmd/agent.go b/packages/cmd/agent.go index 6f4aef8a..f9299265 100644 --- a/packages/cmd/agent.go +++ b/packages/cmd/agent.go @@ -2132,6 +2132,10 @@ func resolveCertificateApplicationReferences(cert *AgentCertificateConfig, httpC } func validateCertificateSourceConfig(version string, certificates *[]AgentCertificateConfig) error { + if len(*certificates) == 0 { + return nil + } + switch version { case AgentConfigVersionV1, AgentConfigVersionV2: default: From 78844823079678d762b3b2ac86dc0de77a2c36e5 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 20 May 2026 16:33:40 -0300 Subject: [PATCH 3/4] Address PR comments --- certificate-agent-config.yaml | 2 +- packages/api/api.go | 12 +-- packages/api/model.go | 7 +- packages/cmd/agent.go | 20 +---- packages/cmd/agent_cert_resolution_test.go | 91 ++++------------------ 5 files changed, 29 insertions(+), 103 deletions(-) diff --git a/certificate-agent-config.yaml b/certificate-agent-config.yaml index 5baf8f79..7a81f4e2 100644 --- a/certificate-agent-config.yaml +++ b/certificate-agent-config.yaml @@ -1,4 +1,4 @@ -version: v2 # v2 issues from a profile attached to a PKI Application. Use v1 for the legacy project-based flow. +version: v2 # v2 issues from a profile attached to an Application. Use v1 for the legacy project-based flow. infisical: address: "https://app.infisical.com/" diff --git a/packages/api/api.go b/packages/api/api.go index 75d7f396..e2563a16 100644 --- a/packages/api/api.go +++ b/packages/api/api.go @@ -344,18 +344,14 @@ func CallGetProjectBySlug(httpClient *resty.Client, slug string) (Project, error return Project(projectResponse), nil } -func CallGetPkiApplicationByName(httpClient *resty.Client, projectId, name string) (PkiApplication, error) { +func CallGetPkiApplicationByName(httpClient *resty.Client, name string) (PkiApplication, error) { var applicationResponse GetPkiApplicationResponse - req := httpClient. + response, err := httpClient. R(). SetResult(&applicationResponse). - SetHeader("User-Agent", USER_AGENT) - - if projectId != "" { - req = req.SetQueryParam("projectId", projectId) - } + SetHeader("User-Agent", USER_AGENT). + Get(fmt.Sprintf("%v/v1/cert-manager/applications/by-name/%s", config.INFISICAL_URL, url.PathEscape(name))) - response, err := req.Get(fmt.Sprintf("%v/v1/cert-manager/applications/by-name/%s", config.INFISICAL_URL, url.PathEscape(name))) if err != nil { return PkiApplication{}, NewGenericRequestError("CallGetPkiApplicationByName", err) } diff --git a/packages/api/model.go b/packages/api/model.go index 05936e1d..00172c30 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -160,10 +160,9 @@ type GetCertificateProfileResponse struct { } type PkiApplicationProfile struct { - ApplicationID string `json:"applicationId"` - ProfileID string `json:"profileId"` - ProfileSlug string `json:"profileSlug"` - APIConfigID *string `json:"apiConfigId,omitempty"` + ApplicationID string `json:"applicationId"` + ProfileID string `json:"profileId"` + ProfileSlug string `json:"profileSlug"` } type ListPkiApplicationProfilesResponse struct { diff --git a/packages/cmd/agent.go b/packages/cmd/agent.go index f9299265..8072368d 100644 --- a/packages/cmd/agent.go +++ b/packages/cmd/agent.go @@ -2079,17 +2079,6 @@ func resolveCertificateLegacyReferences(cert *AgentCertificateConfig, httpClient } cert.ProfileID = profile.ID - if cert.ApplicationName != "" { - application, err := api.CallGetPkiApplicationByName(httpClient, project.ID, cert.ApplicationName) - if err != nil { - return fmt.Errorf("failed to resolve application '%s' in project '%s': %v", cert.ApplicationName, cert.ProjectName, err) - } - if application.ID == "" { - return fmt.Errorf("application '%s' returned an empty ID", cert.ApplicationName) - } - cert.ApplicationID = application.ID - } - return nil } @@ -2098,7 +2087,7 @@ func resolveCertificateApplicationReferences(cert *AgentCertificateConfig, httpC return fmt.Errorf("certificate configuration must specify either 'certificate-id' or both 'application-name' and 'profile-name'") } - application, err := api.CallGetPkiApplicationByName(httpClient, "", cert.ApplicationName) + application, err := api.CallGetPkiApplicationByName(httpClient, cert.ApplicationName) if err != nil { return fmt.Errorf("failed to resolve application '%s': %v", cert.ApplicationName, err) } @@ -2123,10 +2112,6 @@ func resolveCertificateApplicationReferences(cert *AgentCertificateConfig, httpC return fmt.Errorf("profile '%s' is not attached to application '%s'", cert.ProfileName, cert.ApplicationName) } - if matched.APIConfigID == nil || *matched.APIConfigID == "" { - return fmt.Errorf("profile '%s' on application '%s' does not have API enrollment configured", cert.ProfileName, cert.ApplicationName) - } - cert.ProfileID = matched.ProfileID return nil } @@ -2166,6 +2151,9 @@ func validateCertificateSourceConfig(version string, certificates *[]AgentCertif switch version { case AgentConfigVersionV1: + if cert.ApplicationName != "" { + return fmt.Errorf("certificate %d (version v1): 'application-name' is not supported; use 'project-slug' + 'profile-name', or set 'version: v2' for the application-based flow", certIndex) + } if cert.ProjectName == "" || cert.ProfileName == "" { return fmt.Errorf("certificate %d (version v1): must specify either 'certificate-id' or both 'project-slug' and 'profile-name'", certIndex) } diff --git a/packages/cmd/agent_cert_resolution_test.go b/packages/cmd/agent_cert_resolution_test.go index 22a27b93..38a9f94d 100644 --- a/packages/cmd/agent_cert_resolution_test.go +++ b/packages/cmd/agent_cert_resolution_test.go @@ -13,10 +13,6 @@ import ( "github.com/stretchr/testify/require" ) -func strPtr(s string) *string { - return &s -} - func newApplicationServer(t *testing.T, applicationName, applicationID string, profiles []api.PkiApplicationProfile) *httptest.Server { t.Helper() server := httptest.NewServer( @@ -44,7 +40,7 @@ func withMockInfisicalURL(t *testing.T, url string) { t.Cleanup(func() { config.INFISICAL_URL = orig }) } -func TestResolveCertificateNameReferences_AttachedProfileWithAPIEnrollment(t *testing.T) { +func TestResolveCertificateNameReferences_AttachedProfile(t *testing.T) { const ( applicationName = "my-app" applicationID = "app-uuid-1" @@ -57,7 +53,6 @@ func TestResolveCertificateNameReferences_AttachedProfileWithAPIEnrollment(t *te ApplicationID: applicationID, ProfileID: profileID, ProfileSlug: profileSlug, - APIConfigID: strPtr("api-cfg-1"), }, }) t.Cleanup(server.Close) @@ -72,32 +67,6 @@ func TestResolveCertificateNameReferences_AttachedProfileWithAPIEnrollment(t *te assert.Equal(t, profileID, certs[0].ProfileID) } -func TestResolveCertificateNameReferences_MissingAPIEnrollment(t *testing.T) { - const ( - applicationName = "my-app" - applicationID = "app-uuid-2" - profileSlug = "no-api-profile" - ) - - server := newApplicationServer(t, applicationName, applicationID, []api.PkiApplicationProfile{ - { - ApplicationID: applicationID, - ProfileID: "profile-uuid-2", - ProfileSlug: profileSlug, - }, - }) - t.Cleanup(server.Close) - withMockInfisicalURL(t, server.URL) - - certs := []AgentCertificateConfig{ - {ApplicationName: applicationName, ProfileName: profileSlug}, - } - - err := resolveCertificateNameReferences(AgentConfigVersionV2, &certs, resty.New()) - require.Error(t, err) - assert.Contains(t, err.Error(), "API enrollment") -} - func TestResolveCertificateNameReferences_ProfileNotAttached(t *testing.T) { const ( applicationName = "my-app" @@ -105,7 +74,7 @@ func TestResolveCertificateNameReferences_ProfileNotAttached(t *testing.T) { ) server := newApplicationServer(t, applicationName, applicationID, []api.PkiApplicationProfile{ - {ApplicationID: applicationID, ProfileID: "x", ProfileSlug: "other-profile", APIConfigID: strPtr("y")}, + {ApplicationID: applicationID, ProfileID: "x", ProfileSlug: "other-profile"}, }) t.Cleanup(server.Close) withMockInfisicalURL(t, server.URL) @@ -133,7 +102,7 @@ func TestResolveCertificateNameReferences_UnsupportedVersion(t *testing.T) { assert.Contains(t, err.Error(), "unsupported version") } -func newLegacyServer(t *testing.T, projectSlug, projectID, profileSlug, profileID string, optionalApp *api.PkiApplication) *httptest.Server { +func newLegacyServer(t *testing.T, projectSlug, projectID, profileSlug, profileID string) *httptest.Server { t.Helper() server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -150,14 +119,6 @@ func newLegacyServer(t *testing.T, projectSlug, projectID, profileSlug, profileI CertificateProfile: api.CertificateProfile{ID: profileID, Slug: profileSlug, ProjectID: projectID}, }) default: - if optionalApp != nil && r.URL.Path == "/v1/cert-manager/applications/by-name/"+optionalApp.Name { - if r.URL.Query().Get("projectId") != projectID { - http.Error(w, "wrong projectId", http.StatusBadRequest) - return - } - _ = json.NewEncoder(w).Encode(api.GetPkiApplicationResponse{Application: *optionalApp}) - return - } http.NotFound(w, r) } }), @@ -173,7 +134,7 @@ func TestResolveCertificateNameReferences_LegacyProjectAndProfile(t *testing.T) profileID = "profile-uuid-legacy" ) - server := newLegacyServer(t, projectSlug, projectID, profileSlug, profileID, nil) + server := newLegacyServer(t, projectSlug, projectID, profileSlug, profileID) t.Cleanup(server.Close) withMockInfisicalURL(t, server.URL) @@ -186,33 +147,6 @@ func TestResolveCertificateNameReferences_LegacyProjectAndProfile(t *testing.T) assert.Empty(t, certs[0].ApplicationID, "application-id should remain empty when no application-name supplied") } -func TestResolveCertificateNameReferences_LegacyProjectProfileWithApplication(t *testing.T) { - const ( - projectSlug = "my-project" - projectID = "project-uuid-legacy" - profileSlug = "legacy-profile" - profileID = "profile-uuid-legacy" - applicationName = "legacy-app" - applicationID = "app-uuid-legacy" - ) - - server := newLegacyServer(t, projectSlug, projectID, profileSlug, profileID, &api.PkiApplication{ - ID: applicationID, - Name: applicationName, - ProjectID: projectID, - }) - t.Cleanup(server.Close) - withMockInfisicalURL(t, server.URL) - - certs := []AgentCertificateConfig{ - {ProjectName: projectSlug, ProfileName: profileSlug, ApplicationName: applicationName}, - } - - require.NoError(t, resolveCertificateNameReferences(AgentConfigVersionV1, &certs, resty.New())) - assert.Equal(t, profileID, certs[0].ProfileID) - assert.Equal(t, applicationID, certs[0].ApplicationID) -} - func TestResolveCertificateNameReferences_LegacyProjectMissingProfile(t *testing.T) { const ( projectSlug = "my-project" @@ -261,12 +195,21 @@ func TestResolveCertificateNameReferences_LegacyProjectNotFound(t *testing.T) { func TestValidateCertificateSourceConfig_V1Accepted(t *testing.T) { certs := []AgentCertificateConfig{ {ProjectName: "proj", ProfileName: "p"}, - {ProjectName: "proj", ProfileName: "p", ApplicationName: "app"}, {CertificateID: "00000000-0000-0000-0000-000000000000"}, } require.NoError(t, validateCertificateSourceConfig(AgentConfigVersionV1, &certs)) } +func TestValidateCertificateSourceConfig_V1RejectsApplicationName(t *testing.T) { + certs := []AgentCertificateConfig{ + {ProjectName: "proj", ProfileName: "p", ApplicationName: "app"}, + } + err := validateCertificateSourceConfig(AgentConfigVersionV1, &certs) + require.Error(t, err) + assert.Contains(t, err.Error(), "v1") + assert.Contains(t, err.Error(), "application-name") +} + func TestValidateCertificateSourceConfig_V2Accepted(t *testing.T) { certs := []AgentCertificateConfig{ {ApplicationName: "app", ProfileName: "p"}, @@ -397,11 +340,11 @@ func TestResolveCertificateNameReferences_MultipleCerts(t *testing.T) { ) appAProfiles := []api.PkiApplicationProfile{ - {ApplicationID: appAID, ProfileID: "profile-a1", ProfileSlug: "a1", APIConfigID: strPtr("cfg-a1")}, - {ApplicationID: appAID, ProfileID: "profile-a2", ProfileSlug: "a2", APIConfigID: strPtr("cfg-a2")}, + {ApplicationID: appAID, ProfileID: "profile-a1", ProfileSlug: "a1"}, + {ApplicationID: appAID, ProfileID: "profile-a2", ProfileSlug: "a2"}, } appBProfiles := []api.PkiApplicationProfile{ - {ApplicationID: appBID, ProfileID: "profile-b1", ProfileSlug: "b1", APIConfigID: strPtr("cfg-b1")}, + {ApplicationID: appBID, ProfileID: "profile-b1", ProfileSlug: "b1"}, } server := httptest.NewServer( From 97b67953298a81b1c105a1729bd47802a4016e77 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 20 May 2026 17:31:53 -0300 Subject: [PATCH 4/4] Fix agent e2e tests --- e2e/agent/certificate_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/agent/certificate_test.go b/e2e/agent/certificate_test.go index 41a6fcdb..92421eed 100644 --- a/e2e/agent/certificate_test.go +++ b/e2e/agent/certificate_test.go @@ -1935,7 +1935,6 @@ func certAgent_V1LegacyIssuance(t *testing.T) { { ProjectSlug: helper.ProjectSlug, ProfileSlug: helper.ProfileSlug, - ApplicationName: helper.ApplicationName, CommonName: "v1-legacy.example.com", TTL: "1h", RenewBeforeExpiry: "10m",