diff --git a/cmd/vcert/cmdCloudKeystores.go b/cmd/vcert/cmdCloudKeystores.go index 0f75e0f7..dd612d2e 100644 --- a/cmd/vcert/cmdCloudKeystores.go +++ b/cmd/vcert/cmdCloudKeystores.go @@ -75,17 +75,13 @@ func doCommandProvisionCloudKeystore(c *cli.Context) error { return err } - result := ProvisioningResult{} - switch cloudKeystore.Type { - case cloud.KeystoreTypeACM: - result.ARN = metadata.GetAWSCertificateMetadata().GetARN() - case cloud.KeystoreTypeAKV: - result.AzureID = metadata.GetAzureCertificateMetadata().GetID() - result.AzureName = metadata.GetAzureCertificateMetadata().GetName() - result.AzureVersion = metadata.GetAzureCertificateMetadata().GetVersion() - case cloud.KeystoreTypeGCM: - result.GcpID = metadata.GetGCPCertificateMetadata().GetID() - result.GcpName = metadata.GetGCPCertificateMetadata().GetName() + result := ProvisioningResult{ + ARN: metadata.GetAWSCertificateMetadata().GetARN(), + AzureID: metadata.GetAzureCertificateMetadata().GetID(), + AzureName: metadata.GetAzureCertificateMetadata().GetName(), + AzureVersion: metadata.GetAzureCertificateMetadata().GetVersion(), + GcpID: metadata.GetGCPCertificateMetadata().GetID(), + GcpName: metadata.GetGCPCertificateMetadata().GetName(), } result.MachineIdentityId = metadata.GetMachineIdentityMetadata().GetID() diff --git a/pkg/domain/workflow.go b/pkg/domain/workflow.go new file mode 100644 index 00000000..2b2988db --- /dev/null +++ b/pkg/domain/workflow.go @@ -0,0 +1,24 @@ +package domain + +type WorkFlowResponseData struct { + Result interface{} `json:"result"` + WorkflowID string `json:"workflowId"` + WorkflowName string `json:"workflowName"` + WsClientID string `json:"wsClientId"` +} + +type WorkflowResponse struct { + SpecVersion string `json:"specversion"` + Id string `json:"id"` + Source string `json:"source"` + Type string `json:"type"` + Subject string `json:"subject"` + DataContentType string `json:"datacontenttype"` + Time string `json:"time"` + Data WorkFlowResponseData `json:"data"` + EventKind string `json:"eventkind"` + EventResource string `json:"eventresource"` + Recipient string `json:"recipient"` + CorrelationID string `json:"correlationid"` + Stream string `json:"stream"` +} diff --git a/pkg/endpoint/provisioning.go b/pkg/endpoint/provisioning.go index fbad7f4b..c3dc7180 100644 --- a/pkg/endpoint/provisioning.go +++ b/pkg/endpoint/provisioning.go @@ -7,13 +7,14 @@ import ( ) type ProvisioningRequest struct { - CertificateID *string - PickupID *string - KeystoreID *string - KeystoreName *string - ProviderName *string - Timeout time.Duration - Keystore *domain.CloudKeystore + MachineIdentityID *string + CertificateID *string + PickupID *string + KeystoreID *string + KeystoreName *string + ProviderName *string + Timeout time.Duration + Keystore *domain.CloudKeystore } type ProvisioningMetadata interface { diff --git a/pkg/venafi/cloud/cloudproviders.go b/pkg/venafi/cloud/cloudproviders.go index 56f82666..3ebb8586 100644 --- a/pkg/venafi/cloud/cloudproviders.go +++ b/pkg/venafi/cloud/cloudproviders.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log" "net/http" "time" @@ -216,7 +217,7 @@ func (c *Connector) getGraphqlClient() graphql.Client { func (c *Connector) getGraphqlHTTPClient() *http.Client { // We provide every type of auth here. - // The logic to decide which auth is inside struct's function: RoundTrip + // The logic to decide which auth to use is inside struct's function: RoundTrip httpclient := &http.Client{ Transport: &httputils.AuthedTransportApi{ ApiKey: c.apiKey, @@ -229,6 +230,187 @@ func (c *Connector) getGraphqlHTTPClient() *http.Client { return httpclient } +func (c *Connector) ProvisionCertificate(req *endpoint.ProvisioningRequest, options *endpoint.ProvisioningOptions) (provisioningMetadata endpoint.ProvisioningMetadata, err error) { + log.Printf("Starting Provisioning Flow") + + if req == nil { + return nil, fmt.Errorf("missing Provisioning Request") + } + + reqData := *req + + if reqData.Timeout <= 0 { + reqData.Timeout = util.DefaultTimeout * time.Second + } + + if reqData.CertificateID == nil { + if reqData.PickupID == nil { + return nil, fmt.Errorf("no Certificate ID or Pickup ID were provided for provisioning") + } + log.Printf("Certificate ID was not provided in request. Fetching it by using Pickup ID %s", *(reqData.PickupID)) + certID, err := c.getCertIDFromPickupID(*(reqData.PickupID), reqData.Timeout) + if err != nil { + return nil, err + } + reqData.CertificateID = certID + } + certificateIDString := *(reqData.CertificateID) + log.Printf("Certificate ID for provisioning: %s", certificateIDString) + + // Is certificate generated by VCP? + log.Printf("Validating if certificate is generated by VCP") + err = c.validateIfCertIsVCPGeneratedByID(*(reqData.CertificateID)) + if err != nil { + return nil, err + } + log.Println("Certificate is valid for provisioning (VCP generated)") + + // setting options for provisioning + var provisioningOptions *cloudproviders.CertificateProvisioningOptionsInput + if options != nil { + log.Println("setting provisioning options") + provisioningOptions, err = setProvisioningOptions(options) + if err != nil { + return nil, err + } + log.Println("provisioning options successfully set") + } + + ctx := context.Background() + + var keystoreIDString string + + if reqData.Keystore == nil { + if reqData.KeystoreID == nil { + if reqData.ProviderName == nil || reqData.KeystoreName == nil { + return nil, fmt.Errorf("any of keystore object, keystore ID or both Provider Name and Keystore Name must be provided for provisioning") + } + } + + // Getting Keystore to find type + keystoreIDInput := util.StringPointerToString(reqData.KeystoreID) + keystoreNameInput := util.StringPointerToString(reqData.KeystoreName) + providerNameInput := util.StringPointerToString(reqData.ProviderName) + + log.Printf("fetching keystore information for provided keystore information. KeystoreID: %s, KeystoreName: %s, ProviderName: %s", keystoreIDInput, keystoreNameInput, providerNameInput) + cloudKeystore, err := c.GetCloudKeystore(domain.GetCloudKeystoreRequest{ + CloudProviderID: nil, + CloudProviderName: req.ProviderName, + CloudKeystoreID: req.KeystoreID, + CloudKeystoreName: req.KeystoreName, + }) + if err != nil { + return nil, err + } + + keystoreIDString = cloudKeystore.ID + + log.Printf("successfully fetched keystore information for KeystoreID: %s", keystoreIDString) + } else { + log.Printf("Keystore was provided") + keystoreIDString = reqData.Keystore.ID + } + log.Printf("Keystore ID for provisioning: %s", keystoreIDString) + wsClientID := uuid.New().String() + + wsConn, err := c.notificationSvcClient.Subscribe(wsClientID) + if err != nil { + return nil, err + } + + log.Printf("Provisioning Certificate ID %s for Keystore %s", certificateIDString, keystoreIDString) + _, err = c.cloudProvidersClient.ProvisionCertificate(ctx, certificateIDString, keystoreIDString, wsClientID, provisioningOptions) + if err != nil { + return nil, err + } + + ar, err := c.notificationSvcClient.ReadResponse(wsConn) + if err != nil { + return nil, err + } + + // parsing metadata from websocket response + log.Printf("Getting Cloud Metadata of Certificate ID %s and Keystore ID: %s", certificateIDString, keystoreIDString) + cloudMetadata, err := getCloudMetadataFromWebsocketResponse(ar.Data.Result) + if err != nil { + return nil, err + } + log.Printf("Successfully got Cloud Metadata for Certificate ID %s and Keystore ID: %s", certificateIDString, keystoreIDString) + + log.Printf("Successfully finished Provisioning Flow for Certificate ID %s and Keystore ID %s", certificateIDString, keystoreIDString) + return cloudMetadata, nil +} + +func (c *Connector) ProvisionCertificateToMachineIdentity(req endpoint.ProvisioningRequest) (endpoint.ProvisioningMetadata, error) { + log.Printf("Starting Provisioning to Machine Identity Flow") + + if req.MachineIdentityID == nil { + return nil, fmt.Errorf("error trying to provision certificate to machine identity: machineIdentityID is nil") + } + + machineIdentityID := *req.MachineIdentityID + certificateID := "" + timeout := util.DefaultTimeout * time.Second + if req.Timeout != 0 { + timeout = req.Timeout + } + + if req.CertificateID == nil { + if req.PickupID == nil { + return nil, fmt.Errorf("no Certificate ID or Pickup ID were provided for provisioning") + } + + log.Printf("Certificate ID was not provided in request. Using Pickup ID %s to fetch it", *req.PickupID) + certID, err := c.getCertIDFromPickupID(*req.PickupID, timeout) + if err != nil { + return nil, err + } + certificateID = *certID + } else { + certificateID = *req.CertificateID + } + + log.Printf("certificate ID for provisioning: %s", certificateID) + + // Is certificate generated by VCP? + log.Printf("validating if certificate is generated by VCP") + err := c.validateIfCertIsVCPGeneratedByID(certificateID) + if err != nil { + return nil, err + } + log.Println("Certificate is VCP generated") + + ctx := context.Background() + wsClientID := uuid.New().String() + + wsConn, err := c.notificationSvcClient.Subscribe(wsClientID) + if err != nil { + return nil, err + } + + log.Printf("Provisioning Certificate with ID %s to Machine Identity with ID %s", certificateID, machineIdentityID) + _, err = c.cloudProvidersClient.ProvisionCertificateToMachineIdentity(ctx, &certificateID, machineIdentityID, wsClientID) + if err != nil { + return nil, err + } + + ar, err := c.notificationSvcClient.ReadResponse(wsConn) + if err != nil { + return nil, err + } + + // parsing metadata from websocket response + log.Printf("Getting Cloud Metadata of Machine Identity with ID: %s", machineIdentityID) + cloudMetadata, err := getCloudMetadataFromWebsocketResponse(ar.Data.Result) + if err != nil { + return nil, err + } + log.Printf("Successfully retrieved Cloud Metadata for Machine Identity with ID: %s", machineIdentityID) + + log.Printf("Successfully completed Provisioning Flow for Certificate ID %s and Machine Identity ID %s", certificateID, machineIdentityID) + return cloudMetadata, nil +} + func (c *Connector) GetCloudProvider(request domain.GetCloudProviderRequest) (*domain.CloudProvider, error) { cloudProvider, err := c.cloudProvidersClient.GetCloudProvider(context.Background(), request) if err != nil { @@ -278,39 +460,40 @@ func (c *Connector) DeleteMachineIdentity(id uuid.UUID) (bool, error) { return deleted, nil } -func getCloudMetadataFromWebsocketResponse(respMap interface{}, keystoreType string, keystoreId string) (*CloudProvisioningMetadata, error) { +func getCloudMetadataFromWebsocketResponse(resultMap interface{}) (*CloudProvisioningMetadata, error) { - val := CloudKeystoreProvisioningResult{} - valJs, err := json.Marshal(respMap) + result := CloudKeystoreProvisioningResult{} + resultBytes, err := json.Marshal(resultMap) if err != nil { - return nil, fmt.Errorf("unable to encode response data! Error: %s", err.Error()) + return nil, fmt.Errorf("unable to encode response data: %w", err) } - err = json.Unmarshal(valJs, &val) + err = json.Unmarshal(resultBytes, &result) if err != nil { - return nil, fmt.Errorf("unable to parse response data! Error: %s", err.Error()) + return nil, fmt.Errorf("unable to parse response data: %w", err) } - if val.Error != nil { - return nil, fmt.Errorf("unable to provision certificate! Error: %s", val.Error) + if result.Error != nil { + return nil, fmt.Errorf("unable to provision certificate: %w", result.Error) } - - if val.CloudProviderCertificateID == "" { - return nil, fmt.Errorf("provisioning is not successful, certificate ID from response is empty") + if result.CloudProviderCertificateID == "" { + return nil, fmt.Errorf("provisioning failed, certificate ID from response is empty") } - cloudMetadata := &CloudProvisioningMetadata{} - switch keystoreType { - case string(cloudproviders.CloudKeystoreTypeAcm): - cloudMetadata.awsMetadata.result = val - case string(cloudproviders.CloudKeystoreTypeAkv): - cloudMetadata.azureMetadata.result = val - case string(cloudproviders.CloudKeystoreTypeGcm): - cloudMetadata.gcpMetadata.result = val - default: - err = fmt.Errorf("unknown type %v for keystore with ID: %s", keystoreType, keystoreId) - return nil, err + cloudMetadata := &CloudProvisioningMetadata{ + machineMetadata: MachineIdentityMetadata{ + result: result, + }, } - cloudMetadata.machineMetadata.result = val + // Only ACM returns an ARN value + if result.Arn != "" { + cloudMetadata.awsMetadata.result = result + } else if result.CloudCertificateVersion != "" { + // Only Azure returns a certificate version value + cloudMetadata.azureMetadata.result = result + } else { + // No ARN and no certificate version, default to GCM + cloudMetadata.gcpMetadata.result = result + } return cloudMetadata, err } diff --git a/pkg/venafi/cloud/connector.go b/pkg/venafi/cloud/connector.go index 1beb9fdb..89c9d311 100644 --- a/pkg/venafi/cloud/connector.go +++ b/pkg/venafi/cloud/connector.go @@ -33,18 +33,15 @@ import ( "time" "github.com/go-http-utils/headers" - "github.com/google/uuid" "golang.org/x/crypto/nacl/box" - "golang.org/x/net/context" "github.com/Venafi/vcert/v5/pkg/certificate" - "github.com/Venafi/vcert/v5/pkg/domain" "github.com/Venafi/vcert/v5/pkg/endpoint" "github.com/Venafi/vcert/v5/pkg/policy" "github.com/Venafi/vcert/v5/pkg/util" "github.com/Venafi/vcert/v5/pkg/verror" "github.com/Venafi/vcert/v5/pkg/webclient/cloudproviders" - "github.com/Venafi/vcert/v5/pkg/websocket" + "github.com/Venafi/vcert/v5/pkg/webclient/notificationservice" ) type urlResource string @@ -89,16 +86,17 @@ const ( // Connector contains the base data needed to communicate with the Venafi Cloud servers type Connector struct { - baseURL string - apiKey string - accessToken string - verbose bool - user *userDetails - trust *x509.CertPool - zone cloudZone - client *http.Client - userAgent string - cloudProvidersClient *cloudproviders.CloudProvidersClient + baseURL string + apiKey string + accessToken string + verbose bool + user *userDetails + trust *x509.CertPool + zone cloudZone + client *http.Client + userAgent string + cloudProvidersClient *cloudproviders.CloudProvidersClient + notificationSvcClient *notificationservice.NotificationServiceClient } // NewConnector creates a new Venafi Cloud Connector object used to communicate with Venafi Cloud @@ -166,7 +164,10 @@ func (c *Connector) Authenticate(auth *endpoint.Authentication) error { } c.user = ud } + + // Initialize clients c.cloudProvidersClient = cloudproviders.NewCloudProvidersClient(c.getURL(urlGraphql), c.getGraphqlHTTPClient()) + c.notificationSvcClient = notificationservice.NewNotificationServiceClient(c.baseURL, c.accessToken, c.apiKey) return nil } @@ -739,120 +740,6 @@ func (c *Connector) SearchCertificate(zone string, cn string, sans *certificate. return certificate.FindNewestCertificateWithSans(certificates, sans) } -func (c *Connector) ProvisionCertificate(req *endpoint.ProvisioningRequest, options *endpoint.ProvisioningOptions) (provisioningMetadata endpoint.ProvisioningMetadata, err error) { - log.Printf("Starting Provisioning Flow") - - if req == nil { - return nil, fmt.Errorf("missing Provisioning Request") - } - - reqData := *req - - if reqData.Timeout == 0 { - reqData.Timeout = util.DefaultTimeout * time.Second - } - - if reqData.CertificateID == nil { - if reqData.PickupID == nil { - return nil, fmt.Errorf("no Certificate ID or Pickup ID were provided for provisioning") - } - log.Printf("Certificate ID was not provided in request. Fetching it by using Pickup ID %s", *(reqData.PickupID)) - certID, err := c.getCertIDFromPickupID(*(reqData.PickupID), reqData.Timeout) - if err != nil { - return nil, err - } - reqData.CertificateID = certID - } - certificateIDString := *(reqData.CertificateID) - log.Printf("Certificate ID for provisioning: %s", certificateIDString) - - // Is certificate generated by VCP? - log.Printf("Validating if certificate is generated by VCP") - err = c.validateIfCertIsVCPGeneratedByID(*(reqData.CertificateID)) - if err != nil { - return nil, err - } - log.Println("Certificate is valid for provisioning (VCP generated)") - - // setting options for provisioning - var provisioningOptions *cloudproviders.CertificateProvisioningOptionsInput - if options != nil { - log.Println("setting provisioning options") - provisioningOptions, err = setProvisioningOptions(options) - if err != nil { - return nil, err - } - log.Println("provisioning options successfully set") - } - - ctx := context.Background() - - var keystoreIDString string - var cloudKeystoreType string - - if reqData.Keystore == nil { - if reqData.KeystoreID == nil { - if reqData.ProviderName == nil || reqData.KeystoreName == nil { - return nil, fmt.Errorf("any of keystore object, keystore ID or both Provider Name and Keystore Name must be provided for provisioning") - } - } - - // Getting Keystore to find type - keystoreIDInput := util.StringPointerToString(reqData.KeystoreID) - keystoreNameInput := util.StringPointerToString(reqData.KeystoreName) - providerNameInput := util.StringPointerToString(reqData.ProviderName) - - log.Printf("fetching keystore information for provided keystore information. KeystoreID: %s, KeystoreName: %s, ProviderName: %s", keystoreIDInput, keystoreNameInput, providerNameInput) - cloudKeystore, err := c.GetCloudKeystore(domain.GetCloudKeystoreRequest{ - CloudProviderID: nil, - CloudProviderName: req.ProviderName, - CloudKeystoreID: req.KeystoreID, - CloudKeystoreName: req.KeystoreName, - }) - if err != nil { - return nil, err - } - - keystoreIDString = cloudKeystore.ID - cloudKeystoreType = cloudKeystore.Type - - log.Printf("successfully fetched keystore information for KeystoreID: %s", keystoreIDString) - } else { - log.Printf("Keystore was provided") - keystoreIDString = reqData.Keystore.ID - cloudKeystoreType = reqData.Keystore.Type - } - log.Printf("Keystore ID for provisioning: %s", keystoreIDString) - wsClientID := uuid.New().String() - - wsConn, err := websocket.Subscribe(c.apiKey, c.accessToken, c.baseURL, wsClientID) - if err != nil { - return nil, err - } - - log.Printf("Provisioning Certificate ID %s for Keystore %s", certificateIDString, keystoreIDString) - _, err = c.cloudProvidersClient.ProvisionCertificate(ctx, certificateIDString, keystoreIDString, wsClientID, provisioningOptions) - if err != nil { - return nil, err - } - - ar, err := websocket.ReadResponse(wsConn) - if err != nil { - return nil, err - } - - // parsing metadata from websocket response - log.Printf("Getting Cloud Metadata of Certificate ID %s and Keystore ID: %s", certificateIDString, keystoreIDString) - cloudMetadata, err := getCloudMetadataFromWebsocketResponse(ar.Data.Result, cloudKeystoreType, keystoreIDString) - if err != nil { - return nil, err - } - log.Printf("Successfully got Cloud Metadata for Certificate ID %s and Keystore ID: %s", certificateIDString, keystoreIDString) - - log.Printf("Successfully finished Provisioning Flow for Certificate ID %s and Keystore ID %s", certificateIDString, keystoreIDString) - return cloudMetadata, nil -} - func (c *Connector) getCertIDFromPickupID(pickupId string, timeout time.Duration) (*string, error) { if pickupId == "" { return nil, fmt.Errorf("pickupID cannot be empty in order to get certificate ID") diff --git a/pkg/webclient/cloudproviders/cloudproviders.gen.go b/pkg/webclient/cloudproviders/cloudproviders.gen.go index 38f37213..7e4ac38a 100644 --- a/pkg/webclient/cloudproviders/cloudproviders.gen.go +++ b/pkg/webclient/cloudproviders/cloudproviders.gen.go @@ -685,6 +685,33 @@ func (v *ProvisionCertificateResponse) GetProvisionToCloudKeystore() *ProvisionC return v.ProvisionToCloudKeystore } +// ProvisionCertificateToMachineIdentityProvisionToCloudMachineIdentityWorkflowResult includes the requested fields of the GraphQL type WorkflowResult. +type ProvisionCertificateToMachineIdentityProvisionToCloudMachineIdentityWorkflowResult struct { + WorkflowId string `json:"workflowId"` + WorkflowName string `json:"workflowName"` +} + +// GetWorkflowId returns ProvisionCertificateToMachineIdentityProvisionToCloudMachineIdentityWorkflowResult.WorkflowId, and is useful for accessing the field via an interface. +func (v *ProvisionCertificateToMachineIdentityProvisionToCloudMachineIdentityWorkflowResult) GetWorkflowId() string { + return v.WorkflowId +} + +// GetWorkflowName returns ProvisionCertificateToMachineIdentityProvisionToCloudMachineIdentityWorkflowResult.WorkflowName, and is useful for accessing the field via an interface. +func (v *ProvisionCertificateToMachineIdentityProvisionToCloudMachineIdentityWorkflowResult) GetWorkflowName() string { + return v.WorkflowName +} + +// ProvisionCertificateToMachineIdentityResponse is returned by ProvisionCertificateToMachineIdentity on success. +type ProvisionCertificateToMachineIdentityResponse struct { + // Provision to existing Cloud Machine Identity. If `certificateId` is not provided a re-provisioning of the existing certificate would be triggered + ProvisionToCloudMachineIdentity *ProvisionCertificateToMachineIdentityProvisionToCloudMachineIdentityWorkflowResult `json:"provisionToCloudMachineIdentity"` +} + +// GetProvisionToCloudMachineIdentity returns ProvisionCertificateToMachineIdentityResponse.ProvisionToCloudMachineIdentity, and is useful for accessing the field via an interface. +func (v *ProvisionCertificateToMachineIdentityResponse) GetProvisionToCloudMachineIdentity() *ProvisionCertificateToMachineIdentityProvisionToCloudMachineIdentityWorkflowResult { + return v.ProvisionToCloudMachineIdentity +} + // __DeleteMachineIdentitiesInput is used internally by genqlient type __DeleteMachineIdentitiesInput struct { MachineIdentityIds []string `json:"machineIdentityIds"` @@ -777,6 +804,26 @@ func (v *__ProvisionCertificateInput) GetOptions() *CertificateProvisioningOptio return v.Options } +// __ProvisionCertificateToMachineIdentityInput is used internally by genqlient +type __ProvisionCertificateToMachineIdentityInput struct { + MachineIdentityId string `json:"machineIdentityId"` + WsClientId string `json:"wsClientId"` + CertificateId *string `json:"certificateId"` +} + +// GetMachineIdentityId returns __ProvisionCertificateToMachineIdentityInput.MachineIdentityId, and is useful for accessing the field via an interface. +func (v *__ProvisionCertificateToMachineIdentityInput) GetMachineIdentityId() string { + return v.MachineIdentityId +} + +// GetWsClientId returns __ProvisionCertificateToMachineIdentityInput.WsClientId, and is useful for accessing the field via an interface. +func (v *__ProvisionCertificateToMachineIdentityInput) GetWsClientId() string { return v.WsClientId } + +// GetCertificateId returns __ProvisionCertificateToMachineIdentityInput.CertificateId, and is useful for accessing the field via an interface. +func (v *__ProvisionCertificateToMachineIdentityInput) GetCertificateId() *string { + return v.CertificateId +} + // The query or mutation executed by DeleteMachineIdentities. const DeleteMachineIdentities_Operation = ` mutation DeleteMachineIdentities ($machineIdentityIds: [UUID!]!) { @@ -1010,3 +1057,43 @@ func ProvisionCertificate( return &data_, err_ } + +// The query or mutation executed by ProvisionCertificateToMachineIdentity. +const ProvisionCertificateToMachineIdentity_Operation = ` +mutation ProvisionCertificateToMachineIdentity ($machineIdentityId: UUID!, $wsClientId: UUID!, $certificateId: UUID) { + provisionToCloudMachineIdentity(machineIdentityId: $machineIdentityId, wsClientId: $wsClientId, certificateId: $certificateId) { + workflowId + workflowName + } +} +` + +func ProvisionCertificateToMachineIdentity( + ctx_ context.Context, + client_ graphql.Client, + machineIdentityId string, + wsClientId string, + certificateId *string, +) (*ProvisionCertificateToMachineIdentityResponse, error) { + req_ := &graphql.Request{ + OpName: "ProvisionCertificateToMachineIdentity", + Query: ProvisionCertificateToMachineIdentity_Operation, + Variables: &__ProvisionCertificateToMachineIdentityInput{ + MachineIdentityId: machineIdentityId, + WsClientId: wsClientId, + CertificateId: certificateId, + }, + } + var err_ error + + var data_ ProvisionCertificateToMachineIdentityResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} diff --git a/pkg/webclient/cloudproviders/cloudproviders.go b/pkg/webclient/cloudproviders/cloudproviders.go index a938bd1b..d1c72faf 100644 --- a/pkg/webclient/cloudproviders/cloudproviders.go +++ b/pkg/webclient/cloudproviders/cloudproviders.go @@ -136,7 +136,7 @@ func (c *CloudProvidersClient) ProvisionCertificate(ctx context.Context, certifi return nil, fmt.Errorf("failed to provision certificate with certificate ID %s, keystore ID %s and websocket ID %s: %w", certificateID, cloudKeystoreID, wsClientID, err) } - if resp == nil || resp.ProvisionToCloudKeystore == nil { + if resp == nil || resp.GetProvisionToCloudKeystore() == nil { return nil, fmt.Errorf("failed to provision certificate with certificate ID %s, keystore ID %s and websocket ID %s", certificateID, cloudKeystoreID, wsClientID) } @@ -146,6 +146,34 @@ func (c *CloudProvidersClient) ProvisionCertificate(ctx context.Context, certifi }, nil } +func (c *CloudProvidersClient) ProvisionCertificateToMachineIdentity(ctx context.Context, certificateID *string, machineIdentityID string, wsClientID string) (*domain.ProvisioningResponse, error) { + if machineIdentityID == "" { + return nil, fmt.Errorf("machineIdentityID cannot be empty") + } + if wsClientID == "" { + return nil, fmt.Errorf("wsClientID cannot be empty") + } + + certID := "nil" + if certificateID != nil { + certID = *certificateID + } + + resp, err := ProvisionCertificateToMachineIdentity(ctx, c.graphqlClient, machineIdentityID, wsClientID, certificateID) + if err != nil { + return nil, fmt.Errorf("failed to provision certificate with ID %s, to machine identity with ID %s: %w", certID, machineIdentityID, err) + } + + if resp == nil || resp.GetProvisionToCloudMachineIdentity() == nil { + return nil, fmt.Errorf("failed to provision certificate with ID %s, to machine identity with ID %s", certID, machineIdentityID) + } + + return &domain.ProvisioningResponse{ + WorkflowId: resp.GetProvisionToCloudMachineIdentity().GetWorkflowId(), + WorkflowName: resp.GetProvisionToCloudMachineIdentity().GetWorkflowName(), + }, nil +} + func (v *GetMachineIdentitiesCloudMachineIdentitiesMachineIdentityConnectionNodesMachineIdentity) toDomain() (*domain.CloudMachineIdentity, error) { id, err := uuid.Parse(v.Id) if err != nil { diff --git a/pkg/webclient/cloudproviders/genqlient.graphql b/pkg/webclient/cloudproviders/genqlient.graphql index a646c2e7..d205214c 100644 --- a/pkg/webclient/cloudproviders/genqlient.graphql +++ b/pkg/webclient/cloudproviders/genqlient.graphql @@ -10,6 +10,13 @@ mutation ProvisionCertificate( } } +mutation ProvisionCertificateToMachineIdentity($machineIdentityId: UUID!, $wsClientId: UUID!, $certificateId: UUID){ + provisionToCloudMachineIdentity(machineIdentityId: $machineIdentityId, wsClientId: $wsClientId,certificateId: $certificateId ){ + workflowId + workflowName + } +} + query GetCloudProviders($status: CloudProviderStatus, $providerType: CloudProviderType, $name: String!){ cloudProviders(filter: {status: $status, type: $providerType, name: $name}){ nodes { diff --git a/pkg/webclient/notificationservice/notificationservice.go b/pkg/webclient/notificationservice/notificationservice.go new file mode 100644 index 00000000..c8d5fc50 --- /dev/null +++ b/pkg/webclient/notificationservice/notificationservice.go @@ -0,0 +1,85 @@ +package notificationservice + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "strings" + "time" + + "github.com/go-http-utils/headers" + "github.com/gorilla/websocket" + + "github.com/Venafi/vcert/v5/pkg/domain" + "github.com/Venafi/vcert/v5/pkg/util" +) + +type NotificationServiceClient struct { + baseURL string + accessToken string + apiKey string +} + +func NewNotificationServiceClient(baseURL string, accessToken string, apiKey string) *NotificationServiceClient { + return &NotificationServiceClient{ + baseURL: baseURL, + accessToken: accessToken, + apiKey: apiKey, + } +} + +func (ns *NotificationServiceClient) Subscribe(wsClientId string) (*websocket.Conn, error) { + + _, host, found := strings.Cut(ns.baseURL, "https://") + if !found { + return nil, fmt.Errorf("failed to parse baseURL") + } + + if strings.HasSuffix(host, "/") && len(host) > 0 { + host = host[:len(host)-1] + } + + notificationsUrl := url.URL{Scheme: "wss", Host: host, Path: fmt.Sprintf("ws/notificationclients/%s", wsClientId)} + httpHeader := http.Header{} + if ns.accessToken != "" { + httpHeader = http.Header{headers.Authorization: {fmt.Sprintf("%s %s", util.OauthTokenType, ns.accessToken)}} + } else if ns.apiKey != "" { + httpHeader = http.Header{util.HeaderTpplApikey: {ns.apiKey}} + } + + wsConn, resp, err := websocket.DefaultDialer.Dial(notificationsUrl.String(), httpHeader) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusSwitchingProtocols { + return nil, fmt.Errorf("failed switch protocols") + } + log.Print("successfully switched to websocket connection") + + time.Sleep(5 * time.Second) + return wsConn, nil +} + +func (ns *NotificationServiceClient) ReadResponse(wsConn *websocket.Conn) (*domain.WorkflowResponse, error) { + _, msg, err := wsConn.ReadMessage() + if err != nil { + return nil, err + } + + defer func() { + _ = wsConn.Close() + }() + log.Printf("<---- Workflow Response:\n%s", msg) + + workflowResponse := domain.WorkflowResponse{} + err = json.Unmarshal(msg, &workflowResponse) + if err != nil { + log.Printf("failed to unmarshal response %s", err.Error()) + return nil, err + } + + return &workflowResponse, nil +} diff --git a/pkg/websocket/websocket.go b/pkg/websocket/websocket.go deleted file mode 100644 index 1358bba7..00000000 --- a/pkg/websocket/websocket.go +++ /dev/null @@ -1,211 +0,0 @@ -package websocket - -import ( - "context" - "encoding/json" - "fmt" - "github.com/Venafi/vcert/v5/pkg/util" - "github.com/go-http-utils/headers" - "log" - "net/http" - "net/url" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "github.com/gorilla/websocket" -) - -// WebsocketOptions command line options group that apply to all websockets -type WebsocketOptions struct { - PingPeriod time.Duration - PongWait time.Duration - MaxMessageSize int64 - WriteWait time.Duration -} - -type WebsocketClient struct { - wsOptions *WebsocketOptions - WsConn *websocket.Conn - messages chan []byte - Ctx context.Context // corresponds to ws session, so ok in struct - Cancel context.CancelFunc // as above -} - -type WorkFlowResponseData struct { - Result interface{} `json:"result"` - WorkflowId string `json:"workflowId"` - WorkflowName string `json:"workflowName"` - WsClientId string `json:"wsClientId"` -} - -type WorkflowResponse struct { - SpecVersion string `json:"specversion"` - Id string `json:"id"` - Source string `json:"source"` - Type string `json:"type"` - Subject string `json:"subject"` - DataContentType string `json:"datacontenttype"` - Time string `json:"time"` - Data WorkFlowResponseData `json:"data"` - EventKind string `json:"eventkind"` - EventResource string `json:"eventresource"` - Recipient string `json:"recipient"` - CorrelationId string `json:"correlationid"` - Stream string `json:"stream"` -} - -func (wsc *WebsocketClient) ReadMessages() (*WorkflowResponse, error) { - defer func() { - log.Printf("ws read action defer") - wsc.Cancel() - }() - - wsc.WsConn.SetReadLimit(wsc.wsOptions.MaxMessageSize) - _ = wsc.WsConn.SetReadDeadline(time.Now().Add(wsc.wsOptions.PongWait)) - wsc.WsConn.SetPongHandler(func(string) error { - _ = wsc.WsConn.SetReadDeadline(time.Now().Add(wsc.wsOptions.PongWait)) - - log.Printf("ws read action pong") - return nil - }) - - _, msg, err := wsc.WsConn.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - log.Printf("unexpected close %v", err) - } - return nil, err - } - prettified := msg - ar := WorkflowResponse{} - err = json.Unmarshal(msg, &ar) - if err != nil { - log.Printf("failed to unmarshal response %v", err) - } else { - prettified, err = json.MarshalIndent(ar, "", " ") - if err != nil { - log.Println(err.Error()) - } - } - - log.Printf("<---- Workflow Response:\n%s", prettified) - return &ar, nil - -} - -func (wsc *WebsocketClient) writeMessages() { - interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - - ticker := time.NewTicker(wsc.wsOptions.PingPeriod) - defer func() { - signal.Stop(interrupt) - ticker.Stop() - }() - - for { - select { - case <-wsc.Ctx.Done(): - log.Print("ws write action done") - return - case <-ticker.C: - err := wsc.WsConn.SetWriteDeadline(time.Now().Add(wsc.wsOptions.WriteWait)) - if err != nil { - log.Printf("failed to set a write deadline %v", err) - return - } - - if err := wsc.WsConn.WriteMessage(websocket.PingMessage, nil); err != nil { - log.Printf("failed to write a message %v", err) - return - } - - log.Print("ws write action ping") - case <-interrupt: - log.Print("os interrupt") - - // cleanly close the connection by sending a close message and then waiting for the server to close the connection - err := wsc.WsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - if err != nil { - return - } - - select { - case <-wsc.Ctx.Done(): - case <-time.After(time.Second): - } - return - } - } -} - -func Subscribe(apiKey string, accessToken string, baseUrl string, wsClientId string) (*websocket.Conn, error) { - - _, host, found := strings.Cut(baseUrl, "https://") - if !found { - return nil, fmt.Errorf("failed to parse baseurl") - } - - if strings.HasSuffix(host, "/") && len(host) > 0 { - host = host[:len(host)-1] - } - - notificationsUrl := url.URL{Scheme: "wss", Host: host, Path: fmt.Sprintf("ws/notificationclients/%s", wsClientId)} - httpHeader := http.Header{} - if accessToken != "" { - httpHeader = http.Header{headers.Authorization: {fmt.Sprintf("%s %s", util.OauthTokenType, accessToken)}} - } else if apiKey != "" { - httpHeader = http.Header{util.HeaderTpplApikey: {apiKey}} - } - - wsConn, resp, err := websocket.DefaultDialer.Dial(notificationsUrl.String(), httpHeader) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusSwitchingProtocols { - return nil, fmt.Errorf("failed switch protocols") - } - log.Print("successfully switched to websocket connection") - - var wsc = &WebsocketClient{ - WsConn: wsConn, - } - wsc.Ctx, wsc.Cancel = context.WithCancel(context.Background()) - - time.Sleep(5 * time.Second) - return wsConn, nil -} - -func ReadResponse(wsConn *websocket.Conn) (*WorkflowResponse, error) { - - _, msg, err := wsConn.ReadMessage() - if err != nil { - return nil, err - } - - defer func() { - _ = wsConn.Close() - }() - - wfResponse := msg - ar := WorkflowResponse{} - err = json.Unmarshal(msg, &ar) - if err != nil { - log.Printf("failed to unmarshal response %v", err) - } else { - wfResponse, err = json.MarshalIndent(ar, "", " ") - if err != nil { - log.Println(err.Error()) - } - } - log.Printf("<---- Workflow Response:\n%s", wfResponse) - - if wfResponse == nil { - return nil, err - } - return &ar, nil -}