Skip to content

Commit

Permalink
Merge pull request #39 from aspectcapital/certificates
Browse files Browse the repository at this point in the history
Add artifactory_certificate resource type
  • Loading branch information
jamestoyer authored Oct 15, 2019
2 parents a07fed3 + 8bee20a commit edfaff3
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 0 deletions.
1 change: 1 addition & 0 deletions pkg/artifactory/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func Provider() terraform.ResourceProvider {
"artifactory_permission_target": resourceArtifactoryPermissionTarget(),
"artifactory_replication_config": resourceArtifactoryReplicationConfig(),
"artifactory_single_replication_config": resourceArtifactorySingleReplicationConfig(),
"artifactory_certificate": resourceArtifactoryCertificate(),
// Deprecated. Remove in V3
"artifactory_permission_targets": resourceArtifactoryPermissionTargets(),
},
Expand Down
205 changes: 205 additions & 0 deletions pkg/artifactory/resource_artifactory_certificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package artifactory

import (
"context"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"strings"

"github.com/atlassian/go-artifactory/v2/artifactory"
v1 "github.com/atlassian/go-artifactory/v2/artifactory/v1"
"github.com/hashicorp/terraform/helper/schema"
)

func resourceArtifactoryCertificate() *schema.Resource {
return &schema.Resource{
Create: resourceCertificateCreate,
Read: resourceCertificateRead,
Update: resourceCertificateUpdate,
Delete: resourceCertificateDelete,
Exists: resourceCertificateExists,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"alias": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"content": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
},
"fingerprint": {
Type: schema.TypeString,
Computed: true,
},
"issued_by": {
Type: schema.TypeString,
Computed: true,
},
"issued_on": {
Type: schema.TypeString,
Computed: true,
},
"issued_to": {
Type: schema.TypeString,
Computed: true,
},
"valid_until": {
Type: schema.TypeString,
Computed: true,
},
},

CustomizeDiff: func(d *schema.ResourceDiff, v interface{}) error {
fingerprint, err := calculateFingerPrint(d.Get("content").(string))
if err != nil {
return err
}
if d.Get("fingerprint").(string) != fingerprint {
if err := d.SetNewComputed("fingerprint"); err != nil {
fmt.Println(err)
return err
}
}
return nil
},
}
}

func formatFingerPrint(f []byte) string {
buf := make([]byte, 0, 3*len(f))
x := buf[1*len(f) : 3*len(f)]
hex.Encode(x, f)
for i := 0; i < len(x); i += 2 {
buf = append(buf, x[i], x[i+1], ':')
}
return strings.ToUpper(string(buf[:len(buf)-1]))
}

func extractCertificate(pemData string) (*x509.Certificate, error) {
block, rest := pem.Decode([]byte(pemData))
for block != nil {
if block.Type != "CERTIFICATE" {
block, rest = pem.Decode(rest)
continue
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}

return cert, nil
}

return nil, fmt.Errorf("no certificate in PEM data")
}

func calculateFingerPrint(pemData string) (string, error) {
cert, err := extractCertificate(pemData)
if err != nil {
return "", err
}

fingerprint := sha256.Sum256(cert.Raw)

return formatFingerPrint(fingerprint[:]), nil
}

func findCertificate(d *schema.ResourceData, m interface{}) (*v1.CertificateDetails, error) {
c := m.(*artifactory.Artifactory)

certs, _, err := c.V1.Security.GetCertificates(context.Background())
if err != nil {
return nil, err
}

// No way other than to loop through each certificate
for _, cert := range *certs {
if *cert.CertificateAlias == d.Id() {
return &cert, nil
}
}

return nil, nil
}

func resourceCertificateCreate(d *schema.ResourceData, m interface{}) error {
d.SetId(d.Get("alias").(string))
return resourceCertificateUpdate(d, m)
}

func resourceCertificateRead(d *schema.ResourceData, m interface{}) error {
cert, err := findCertificate(d, m)
if err != nil {
return err
}

if cert != nil {
hasErr := false
logErr := cascadingErr(&hasErr)

logErr(d.Set("alias", *cert.CertificateAlias))
logErr(d.Set("fingerprint", *cert.FingerPrint))
logErr(d.Set("issued_by", *cert.IssuedBy))
logErr(d.Set("issued_on", *cert.IssuedOn))
logErr(d.Set("issued_to", *cert.IssuedTo))
logErr(d.Set("valid_until", *cert.ValidUntil))

if hasErr {
return fmt.Errorf("failed to pack certificate")
}

return nil
}

d.SetId("")

return nil
}

func resourceCertificateUpdate(d *schema.ResourceData, m interface{}) error {
c := m.(*artifactory.Artifactory)

_, _, err := c.V1.Security.AddCertificate(context.Background(), d.Id(), strings.NewReader(d.Get("content").(string)))
if err != nil {
return err
}

return resourceCertificateRead(d, m)
}

func resourceCertificateDelete(d *schema.ResourceData, m interface{}) error {
c := m.(*artifactory.Artifactory)

_, _, err := c.V1.Security.DeleteCertificate(context.Background(), d.Id())
if err != nil {
return err
}

d.SetId("")

return nil
}

func resourceCertificateExists(d *schema.ResourceData, m interface{}) (bool, error) {
cert, err := findCertificate(d, m)
if err != nil {
return false, err
}

if cert != nil {
return true, nil
}

return false, nil
}
94 changes: 94 additions & 0 deletions pkg/artifactory/resource_artifactory_certificate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package artifactory

import (
"context"
"fmt"
"testing"

"github.com/atlassian/go-artifactory/v2/artifactory"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

const certificateFull = `
resource "artifactory_certificate" "foobar" {
alias = "foobar"
content = <<EOF
-----BEGIN CERTIFICATE-----
MIICUjCCAbugAwIBAgIJALRDng3rGeQvMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV
BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg
Q29tcGFueSBMdGQwHhcNMTkwNTE3MTAwMzI2WhcNMjkwNTE0MTAwMzI2WjBCMQsw
CQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZh
dWx0IENvbXBhbnkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVBRt7
Ua3j7K2htVRu1tw629ZZZQI35RGm/53ffF/QUUFXk35at+IiwYZGGQbOGuN1pdji
gki9/Qit/WO/3uadSkGelKOUYD0DIemlhcZt6iPMQq8mYlUkMPZz5Qlj0ldKI3g+
Q8Tc/6vEeBv/9jrm9Efg/uwc0DjD8B4Ny6xMHQIDAQABo1AwTjAdBgNVHQ4EFgQU
VrBaHnYLayO2lKIUde8etG0H6owwHwYDVR0jBBgwFoAUVrBaHnYLayO2lKIUde8e
tG0H6owwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQA4VBFCrbuOsKtY
uNlSQCBkTXg907iXihZ+Of/2rerS2gfDCUHdz0xbYdlttNjoGVCA+0alt7ugfYpl
fy5aAfCHLXEgYrlhe6oDtCMSskbkKFTEI/bRqwGMDb+9NO/yh2KLbNueKJz9Vs5V
GV9pUrgW6c7kLrC9vpHP+47iyQEbnw==
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANUFG3tRrePsraG1
VG7W3Drb1lllAjflEab/nd98X9BRQVeTflq34iLBhkYZBs4a43Wl2OKCSL39CK39
Y7/e5p1KQZ6Uo5RgPQMh6aWFxm3qI8xCryZiVSQw9nPlCWPSV0ojeD5DxNz/q8R4
G//2Oub0R+D+7BzQOMPwHg3LrEwdAgMBAAECgYAxWA6GoWQDcRbDZ6qYRkMbi0L6
0DAUXIabRYj/dOMI8VmOfMb/IqtKW8PLxw5Rfd8EqJc12PIauFtjWlfZ4TtP9erQ
1imw2SpVMAWt4HLUw7oONKgNMnBtVQBCoXLuXcnJbCxeRiV1oJtvrddUJPOtUc+y
t5gGTyx/zUAXzPzT7QJBAOvu4CH0Xc+1GdXFUFLzF8B3SFwnOFRERJxFq43dw4t3
tXcON/UyegYcQz2JqKcofwRhM4+uXGnWE+9oOOnxL8sCQQDnI1QtMv+tZcqIcmk6
1ykyNa530eCfoqAvVTRwPIsAD/DZLC4HJNSQauPXC4Unt1tqmOmUoZmgzYQlVsGO
ISa3AkB2xWpPrZUMWz8GPq6RE4+BdIsY2SWiRjvD787NPDaUn07bAG1rIl4LdW7k
K8ibXeeTbNtoGX6sSPkALJd6LdDBAkEA5FAhdgRKSh2iUeWxzE18g/xCuli2aPlb
AWZIxhUHuKgGYH8jeCsJTR5IsMLQZMrZohIpqId4GT7oqXlo99wHQQJBAOvX+5z6
iCooatRyMnwUV6sJ225ZawuJ4sXFt6CA7aOZQ+G5zvG694ONxG9qeF2YnySQp1HH
V87CqqFaUigTzmI=
-----END PRIVATE KEY-----
EOF
}`

func TestAccCertificate_full(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
CheckDestroy: testAccCheckUserDestroy("artifactory_certificate.foobar"),
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: certificateFull,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("artifactory_certificate.foobar", "alias", "foobar"),
resource.TestCheckResourceAttr("artifactory_certificate.foobar", "fingerprint", "ED:67:0B:D2:84:C2:93:6D:56:6F:A7:4D:5A:CC:B7:AF:8A:C0:1D:2A:7C:F3:4A:57:31:83:22:30:44:5F:63:9D"),
resource.TestCheckResourceAttr("artifactory_certificate.foobar", "issued_by", "Unknown"),
resource.TestCheckResourceAttr("artifactory_certificate.foobar", "issued_on", "2019-05-17T11:03:26.000+01:00"),
resource.TestCheckResourceAttr("artifactory_certificate.foobar", "issued_to", "Unknown"),
resource.TestCheckResourceAttr("artifactory_certificate.foobar", "valid_until", "2029-05-14T11:03:26.000+01:00"),
),
},
},
})
}

func testAccCheckCertificateDestroy(id string) func(*terraform.State) error {
return func(s *terraform.State) error {
client := testAccProvider.Meta().(*artifactory.Artifactory)
rs, ok := s.RootModule().Resources[id]

if !ok {
return fmt.Errorf("err: Resource id[%s] not found", id)
}

certs, _, err := client.V1.Security.GetCertificates(context.Background())
if err != nil {
return err
}

for _, cert := range *certs {
if *cert.CertificateAlias == rs.Primary.ID {
return fmt.Errorf("error: Certificate %s still exists", rs.Primary.ID)
}
}

return nil
}
}
1 change: 1 addition & 0 deletions website/docs/index.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ with the proper credentials before it can be used.
* [Replication Configurations](./r/artifactory_replication_config.html.markdown)
* [Single Replication Configurations](./r/artifactory_single_replication_config.html.markdown)
* [Virtual Repositories](./r/artifactory_virtual_repository.html.markdown)
* [Certificates](./r/artifactory_certificate.html.markdown)

- Deprecated Resources
* [Permission Targets (V1 API)](./r/artifactory_permission_target_v1.html.markdown)
Expand Down
53 changes: 53 additions & 0 deletions website/docs/r/artifactory_certificate.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
layout: "artifactory"
page_title: "Artifactory: artifactory_certificate"
sidebar_current: "docs-artifactory-resource-certificate"
description: |-
Provides a certificate resource.
---

# artifactory_certificate

Provides an Artifactory certificate resource. This can be used to create and manage Artifactory certificates which can be used as client authentication against remote repositories.

## Example Usage

```hcl
# Create a new Artifactory certificate called my-cert
resource "artifactory_certificate" "my-cert" {
alias = "my-cert"
content = "${file("/path/to/bundle.pem")}"
}
# This can then be used by a remote repository
resource "artifactory_remote_repository" "my-remote" {
...
client_tls_certificate = "${artifactory_certificate.my-cert.alias}"
...
}
```

## Argument Reference

The following arguments are supported:

* `alias` - (Required) Name of certificate.
* `content` - (Required) PEM-encoded client certificate and private key.

## Attribute Reference

In addition to all arguments above, the following attributes are exported:

* `fingerprint` - SHA256 fingerprint of the certificate.
* `issued_by` - Name of the certificate authority that issued the certificate.
* `issued_on` - The time & date when the certificate is valid from.
* `issued_to` - Name of whom the certificate has been issued to.
* `valid_until` - The time & date when the certificate expires.

## Import

Certificates can be imported using their alias, e.g.

```
$ terraform import artifactory_certificate.my-cert my-cert
```

0 comments on commit edfaff3

Please sign in to comment.