This is an automated email from the ASF dual-hosted git repository.

ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 533994f  Le account api (#5371)
533994f is described below

commit 533994fe83c332f4dd5be3266e1f9d1beca14550
Author: mattjackson220 <[email protected]>
AuthorDate: Thu Dec 17 16:49:17 2020 -0700

    Le account api (#5371)
    
    * API endpoints for ACME Accounts
    
    * updated changelog
    
    * fixed bad rebase in changelog
    
    * updates per comments
    
    * more updates per comments
    
    * updated docs to be better
    
    * updated other doc
    
    * moved all queries to top
---
 CHANGELOG.md                                       |   1 +
 docs/source/api/v3/acme_accounts.rst               | 197 ++++++++++++++++++++
 .../source/api/v3/acme_accounts_provider_email.rst |  65 +++++++
 docs/source/glossary.rst                           |   9 +-
 lib/go-tc/acme_account.go                          |  49 +++++
 .../2020121500000000_move_lets_encrypt_to_acme.sql |  60 ++++++
 .../traffic_ops_golang/acme/acme_account.go        | 205 +++++++++++++++++++++
 .../deliveryservice/letsencryptcert.go             |  16 +-
 traffic_ops/traffic_ops_golang/routing/routes.go   |   8 +
 9 files changed, 599 insertions(+), 11 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 859c23b..95ee32d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 - Traffic Router: log warnings when requests to Traffic Monitor return a 503 
status code
 - #5344 - Add a page that addresses migrating from Traffic Ops API v1 for each 
endpoint
 - [#5296](https://github.com/apache/trafficcontrol/issues/5296) - Fixed a bug 
where users couldn't update any regex in Traffic Ops/ Traffic Portal
+- Added API endpoints for ACME accounts
 
 ### Fixed
 - [#5195](https://github.com/apache/trafficcontrol/issues/5195) - Correctly 
show CDN ID in Changelog during Snap
diff --git a/docs/source/api/v3/acme_accounts.rst 
b/docs/source/api/v3/acme_accounts.rst
new file mode 100644
index 0000000..8a39846
--- /dev/null
+++ b/docs/source/api/v3/acme_accounts.rst
@@ -0,0 +1,197 @@
+..
+..
+.. Licensed under the Apache License, Version 2.0 (the "License");
+.. you may not use this file except in compliance with the License.
+.. You may obtain a copy of the License at
+..
+..     http://www.apache.org/licenses/LICENSE-2.0
+..
+.. Unless required by applicable law or agreed to in writing, software
+.. distributed under the License is distributed on an "AS IS" BASIS,
+.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+.. See the License for the specific language governing permissions and
+.. limitations under the License.
+..
+
+.. _to-api-acme-accounts:
+
+*****************
+``acme_accounts``
+*****************
+
+``GET``
+=======
+Gets information for all :term:`ACME Account` s.
+
+:Auth. Required: Yes
+:Roles Required: "admin"
+:Response Type:  Array
+
+Request Structure
+-----------------
+No parameters available
+
+
+Response Structure
+------------------
+:email:       The email connected to the :term:`ACME Account`.
+:privateKey:  The private key connected to the :term:`ACME Account`.
+:uri:         The URI for the :term:`ACME Account`. Differs per provider.
+:provider:    The :abbr:`ACME (Automatic Certificate Management Environment)` 
provider.
+
+.. code-block:: http
+       :caption: Response Example
+
+       HTTP/1.1 200 OK
+       Content-Type: application/json
+
+       { "response": [
+               {
+                       "email": "[email protected]",
+                       "privateKey": "-----BEGIN RSA PRIVATE 
KEY-----\nSampleKey\n-----END RSA PRIVATE KEY-----\n",
+                       "uri": "https://acme.example.com/acct/1";,
+                       "provider": "Lets Encrypt"
+               }
+       ]}
+
+
+``POST``
+========
+Creates a new :term:`ACME Account`.
+
+:Auth. Required: Yes
+:Roles Required: "admin"
+:Response Type:  Object
+
+Request Structure
+-----------------
+The request body must be a single :term:`ACME Account` object with the 
following keys:
+
+:email:       The email connected to the :term:`ACME Account`.
+:privateKey:  The private key connected to the :term:`ACME Account`.
+:uri:         The URI for the :term:`ACME Account`. Differs per provider.
+:provider:    The :abbr:`ACME (Automatic Certificate Management Environment)` 
provider.
+
+.. code-block:: http
+       :caption: Request Example
+
+       POST /api/3.0/acme/accounts HTTP/1.1
+       Host: trafficops.infra.ciab.test
+       User-Agent: curl/7.47.0
+       Accept: */*
+       Cookie: mojolicious=...
+       Content-Length: 181
+       Content-Type: application/json
+
+       {
+               "email": "[email protected]",
+               "privateKey": "-----BEGIN RSA PRIVATE 
KEY-----\nSampleKey\n-----END RSA PRIVATE KEY-----\n",
+               "uri": "https://acme.example.com/acct/1";,
+               "provider": "Lets Encrypt"
+       }
+
+Response Structure
+------------------
+:email:       The email connected to the :term:`ACME Account`.
+:privateKey:  The private key connected to the :term:`ACME Account`.
+:uri:         The URI for the :term:`ACME Account`. Differs per provider.
+:provider:    The :abbr:`ACME (Automatic Certificate Management Environment)` 
provider.
+
+.. code-block:: http
+       :caption: Response Example
+
+       HTTP/1.1 200 OK
+       Access-Control-Allow-Credentials: true
+       Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, 
Accept, Set-Cookie, Cookie
+       Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
+       Access-Control-Allow-Origin: *
+       Content-Type: application/json
+       Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 10 Dec 2020 17:40:54 
GMT; Max-Age=3600; HttpOnly
+       Whole-Content-Sha512: 
eQrl48zWids0kDpfCYmmtYMpegjnFxfOVvlBYxxLSfp7P7p6oWX4uiC+/Cfh2X9i3G+MQ36eH95gukJqOBOGbQ==
+       X-Server-Name: traffic_ops_golang/
+       Date: Wed, 05 Dec 2018 19:18:21 GMT
+       Content-Length: 253
+
+       { "alerts": [
+               {
+                       "text": "Acme account created",
+                       "level":"success"
+               }
+       ],
+       "response": {
+               "email": "[email protected]",
+               "privateKey": "-----BEGIN RSA PRIVATE 
KEY-----\nSampleKey\n-----END RSA PRIVATE KEY-----\n",
+               "uri": "https://acme.example.com/acct/1";,
+               "provider": "Lets Encrypt"
+       }}
+
+
+``PUT``
+=======
+Updates an existing :term:`ACME Account`.
+
+:Auth. Required: Yes
+:Roles Required: "admin"
+:Response Type:  Object
+
+Request Structure
+-----------------
+The request body must be a single :term:`ACME Account` object with the 
following keys:
+
+:email:       The email connected to the :term:`ACME Account`.
+:privateKey:  The private key connected to the :term:`ACME Account`.
+:uri:         The URI for the :term:`ACME Account`. Differs per provider.
+:provider:    The :abbr:`ACME (Automatic Certificate Management Environment)` 
provider.
+
+.. code-block:: http
+       :caption: Request Example
+
+       PUT /api/3.0/acme/accounts HTTP/1.1
+       Host: trafficops.infra.ciab.test
+       User-Agent: curl/7.47.0
+       Accept: */*
+       Cookie: mojolicious=...
+       Content-Length: 181
+       Content-Type: application/json
+
+       {
+               "email": "[email protected]",
+               "privateKey": "-----BEGIN RSA PRIVATE 
KEY-----\nSampleKey\n-----END RSA PRIVATE KEY-----\n",
+               "uri": "https://acme.example.com/acct/1";,
+               "provider": "Lets Encrypt"
+       }
+
+Response Structure
+------------------
+:email:       The email connected to the :term:`ACME Account`.
+:privateKey:  The private key connected to the :term:`ACME Account`.
+:uri:         The URI for the :term:`ACME Account`. Differs per provider.
+:provider:    The :abbr:`ACME (Automatic Certificate Management Environment)` 
provider.
+
+.. code-block:: http
+       :caption: Response Example
+
+       HTTP/1.1 200 OK
+       Access-Control-Allow-Credentials: true
+       Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, 
Accept, Set-Cookie, Cookie
+       Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
+       Access-Control-Allow-Origin: *
+       Content-Type: application/json
+       Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 10 Dec 2020 17:40:54 
GMT; Max-Age=3600; HttpOnly
+       Whole-Content-Sha512: 
eQrl48zWids0kDpfCYmmtYMpegjnFxfOVvlBYxxLSfp7P7p6oWX4uiC+/Cfh2X9i3G+MQ36eH95gukJqOBOGbQ==
+       X-Server-Name: traffic_ops_golang/
+       Date: Wed, 05 Dec 2018 19:18:21 GMT
+       Content-Length: 253
+
+       { "alerts": [
+               {
+                       "text": "Acme account updated",
+                       "level":"success"
+               }
+       ],
+       "response": {
+               "email": "[email protected]",
+               "privateKey": "-----BEGIN RSA PRIVATE 
KEY-----\nSampleKey\n-----END RSA PRIVATE KEY-----\n",
+               "uri": "https://acme.example.com/acct/1";,
+               "provider": "Lets Encrypt"
+       }}
diff --git a/docs/source/api/v3/acme_accounts_provider_email.rst 
b/docs/source/api/v3/acme_accounts_provider_email.rst
new file mode 100644
index 0000000..4a8b1e1
--- /dev/null
+++ b/docs/source/api/v3/acme_accounts_provider_email.rst
@@ -0,0 +1,65 @@
+..
+..
+.. Licensed under the Apache License, Version 2.0 (the "License");
+.. you may not use this file except in compliance with the License.
+.. You may obtain a copy of the License at
+..
+..     http://www.apache.org/licenses/LICENSE-2.0
+..
+.. Unless required by applicable law or agreed to in writing, software
+.. distributed under the License is distributed on an "AS IS" BASIS,
+.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+.. See the License for the specific language governing permissions and
+.. limitations under the License.
+..
+
+.. _to-api-acme-accounts-provider-email:
+
+****************************************
+``acme_accounts/{{provider}}/{{email}}``
+****************************************
+
+
+``DELETE``
+==========
+Delete :term:`ACME Account` information.
+
+:Auth. Required: Yes
+:Roles Required: "admin"
+:Response Type:  Object
+
+Request Structure
+-----------------
+.. table:: Request Path Parameters
+
+       
+----------+-----------------------------------------------------------------------------------------------------------------+
+       | Name     |                       Description                          
                                                     |
+       
+==========+=================================================================================================================+
+       | provider | The :abbr:`ACME (Automatic Certificate Management 
Environment)` provider for the account to be deleted          |
+       
+----------+-----------------------------------------------------------------------------------------------------------------+
+       | email    | The email used in the :term:`ACME Account` to be deleted   
                                                     |
+       
+----------+-----------------------------------------------------------------------------------------------------------------+
+
+Response Structure
+------------------
+.. code-block:: http
+       :caption: Response Example
+
+       HTTP/1.1 200 OK
+       Access-Control-Allow-Credentials: true
+       Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, 
Accept, Set-Cookie, Cookie
+       Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
+       Access-Control-Allow-Origin: *
+       Content-Type: application/json
+       Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 10 Dec 2020 17:40:54 
GMT; Max-Age=3600; HttpOnly
+       Whole-Content-Sha512: 
rGD2sOMHYF0sga1zuTytyLHCUkkc3ZwQRKvZ/HuPzObOP4WztKTOVXB4uhs3iJqBg9zRB2TucMxONHN+3/yShQ==
+       X-Server-Name: traffic_ops_golang/
+       Date: Thu, 10 Dec 2020 14:24:34 GMT
+       Content-Length: 60
+
+       {"alerts": [
+               {
+                       "text": "Acme account deleted",
+                       "level":"success"
+               }
+       ]}
diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst
index 30ac7fb..aea7f42 100644
--- a/docs/source/glossary.rst
+++ b/docs/source/glossary.rst
@@ -25,6 +25,9 @@ Glossary
        302 content routing
                :ref:`http-cr`.
 
+       ACME Account
+               An account previously created with an :abbr:`ACME (Automatic 
Certificate Management Environment)` provider.
+
        astats (stats_over_http)
                An :abbr:`ATS (Apache Traffic Server)` plugin that allows you 
to monitor vitals of the :abbr:`ATS (Apache Traffic Server)` server. See 
:ref:`astats`.
 
@@ -403,9 +406,9 @@ Glossary
        Server Capabilities
                A :dfn:`Server Capability` (not to be confused with a 
"Capability") expresses the capacity of a :term:`cache server` to serve a 
particular kind of traffic. For example, a :dfn:`Server Capability` could be 
created named "RAM" to be assigned to :term:`cache servers` that have RAM-disks 
allocated for content caching. :dfn:`Server Capabilities` can also be required 
by :term:`Delivery Services`, which will prevent :term:`cache servers` without 
that :dfn:`Server Capability` from being assign [...]
 
-    Service Category
-    Service Categories
-        A :dfn:`Service Category` defines the type of content being delivered 
by a :dfn:`Delivery Service`. For example, a :dfn:`Service Category` could be 
created named "linear" and assigned to a :dfn:`Delivery Service` that delivers 
linear content. 
+       Service Category
+       Service Categories
+               A :dfn:`Service Category` defines the type of content being 
delivered by a :dfn:`Delivery Service`. For example, a :dfn:`Service Category` 
could be created named "linear" and assigned to a :dfn:`Delivery Service` that 
delivers linear content.
 
        Snapshot
        Snapshots
diff --git a/lib/go-tc/acme_account.go b/lib/go-tc/acme_account.go
new file mode 100644
index 0000000..1a2b96d
--- /dev/null
+++ b/lib/go-tc/acme_account.go
@@ -0,0 +1,49 @@
+package tc
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+       "database/sql"
+
+       "github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
+       "github.com/apache/trafficcontrol/lib/go-util"
+
+       "github.com/go-ozzo/ozzo-validation"
+)
+
+// AcmeAccount is the information needed to access an account with an ACME 
provider.
+type AcmeAccount struct {
+       Email      *string `json:"email" db:"email"`
+       PrivateKey *string `json:"privateKey" db:"private_key"`
+       Uri        *string `json:"uri" db:"uri"`
+       Provider   *string `json:"provider" db:"provider"`
+}
+
+// Validate validates the AcmeAccount request is valid for creation or update.
+func (aa *AcmeAccount) Validate(tx *sql.Tx) error {
+       errs := validation.Errors{
+               "email":       validation.Validate(aa.Email, 
validation.Required),
+               "private_key": validation.Validate(aa.PrivateKey, 
validation.Required),
+               "uri":         validation.Validate(aa.Uri, validation.Required),
+               "provider":    validation.Validate(aa.Provider, 
validation.Required),
+       }
+
+       return util.JoinErrs(tovalidate.ToErrors(errs))
+}
diff --git 
a/traffic_ops/app/db/migrations/2020121500000000_move_lets_encrypt_to_acme.sql 
b/traffic_ops/app/db/migrations/2020121500000000_move_lets_encrypt_to_acme.sql
new file mode 100644
index 0000000..b28ed46
--- /dev/null
+++ 
b/traffic_ops/app/db/migrations/2020121500000000_move_lets_encrypt_to_acme.sql
@@ -0,0 +1,60 @@
+/*
+       Licensed under the Apache License, Version 2.0 (the "License");
+       you may not use this file except in compliance with the License.
+       You may obtain a copy of the License at
+               http://www.apache.org/licenses/LICENSE-2.0
+       Unless required by applicable law or agreed to in writing, software
+       distributed under the License is distributed on an "AS IS" BASIS,
+       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+       See the License for the specific language governing permissions and
+       limitations under the License.
+*/
+
+-- +goose Up
+-- SQL in section 'Up' is executed when this migration is applied
+
+CREATE TABLE IF NOT EXISTS acme_account (
+  email text NOT NULL,
+  private_key text NOT NULL,
+  provider text NOT NULL,
+  uri text NOT NULL,
+  PRIMARY KEY (email, provider)
+);
+
+INSERT INTO acme_account(
+       email,
+       private_key,
+       provider,
+       uri
+)
+SELECT
+       lets_encrypt_account.email,
+       lets_encrypt_account.private_key,
+       'Lets Encrypt',
+       lets_encrypt_account.uri
+FROM lets_encrypt_account;
+
+DROP TABLE IF EXISTS lets_encrypt_account;
+
+-- +goose Down
+-- SQL section 'Down' is executed when this migration is rolled back
+
+CREATE TABLE IF NOT EXISTS lets_encrypt_account (
+  email text NOT NULL,
+  private_key text NOT NULL,
+  uri text NOT NULL,
+  PRIMARY KEY (email)
+);
+
+INSERT INTO lets_encrypt_account(
+       email,
+       private_key,
+       uri
+)
+SELECT
+       acme_account.email,
+       acme_account.private_key,
+       acme_account.uri
+FROM acme_account WHERE acme_account.provider = 'Lets Encrypt';
+
+DROP TABLE IF EXISTS acme_account;
diff --git a/traffic_ops/traffic_ops_golang/acme/acme_account.go 
b/traffic_ops/traffic_ops_golang/acme/acme_account.go
new file mode 100644
index 0000000..2f77d46
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/acme/acme_account.go
@@ -0,0 +1,205 @@
+package acme
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+       "database/sql"
+       "errors"
+       "fmt"
+       "net/http"
+
+       "github.com/apache/trafficcontrol/lib/go-tc"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const readQuery = `SELECT email, private_key, uri, provider FROM acme_account`
+const createQuery = `INSERT INTO acme_account (email, private_key, uri, 
provider) VALUES (:email, :private_key, :uri, :provider) RETURNING email, 
provider`
+const updateQuery = `UPDATE acme_account SET private_key=:private_key, 
uri=:uri WHERE email=:email and provider=:provider RETURNING email, provider`
+const deleteQuery = `DELETE FROM acme_account WHERE email=$1 and provider=$2`
+const selectByProviderAndEmailQuery = `SELECT email, private_key, uri, 
provider from acme_account where email = $1 and provider = $2`
+const selectLimitedQuery = `SELECT email, provider from acme_account where 
email = $1 and provider = $2`
+
+func Read(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       tx := inf.Tx.Tx
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       acmeAccounts := []tc.AcmeAccount{}
+       rows, err := tx.Query(readQuery)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("querying acme accounts: "+err.Error()))
+               return
+       }
+       defer rows.Close()
+
+       for rows.Next() {
+               var acct tc.AcmeAccount
+               if err = rows.Scan(&acct.Email, &acct.PrivateKey, &acct.Uri, 
&acct.Provider); err != nil {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
nil, errors.New("scanning acme accounts: "+err.Error()))
+                       return
+               }
+               acmeAccounts = append(acmeAccounts, acct)
+       }
+
+       api.WriteResp(w, r, acmeAccounts)
+}
+
+func Create(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       tx := inf.Tx.Tx
+
+       var acmeAccount tc.AcmeAccount
+       if err := api.Parse(r.Body, tx, &acmeAccount); err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+               return
+       }
+
+       var prevEmail string
+       var prevProvider string
+       err := tx.QueryRow(selectLimitedQuery, acmeAccount.Email, 
acmeAccount.Provider).Scan(&prevEmail, &prevProvider)
+       if err != nil && err != sql.ErrNoRows {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New(fmt.Sprintf("checking if acme account with email %s and provider %s 
exists: %v", *acmeAccount.Email, *acmeAccount.Provider, err.Error())))
+               return
+       }
+
+       if prevEmail != "" && prevProvider != "" {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("acme 
account already exists"), nil)
+               return
+       }
+
+       resultRows, err := inf.Tx.NamedQuery(createQuery, acmeAccount)
+       if err != nil {
+               userErr, sysErr, errCode := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer resultRows.Close()
+
+       rowsAffected := 0
+       for resultRows.Next() {
+               rowsAffected++
+       }
+       if rowsAffected == 0 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("acme account create: no account was inserted"))
+               return
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "Acme account created")
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, acmeAccount)
+
+       changeLogMsg := fmt.Sprintf("ACME ACCOUNT: %s %s, ACTION: created", 
*acmeAccount.Email, *acmeAccount.Provider)
+       api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func Update(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       tx := inf.Tx.Tx
+
+       var acmeAccount tc.AcmeAccount
+       if err := api.Parse(r.Body, tx, &acmeAccount); err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+               return
+       }
+
+       var prevAccount tc.AcmeAccount
+       err := tx.QueryRow(selectByProviderAndEmailQuery, acmeAccount.Email, 
acmeAccount.Provider).Scan(&prevAccount.Email, &prevAccount.PrivateKey, 
&prevAccount.Uri, &prevAccount.Provider)
+       if err == sql.ErrNoRows {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, 
errors.New(fmt.Sprintf("acme account not found")), nil)
+               return
+       }
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New(fmt.Sprintf("checking if acme account with email %s and provider %s 
exists: %v", *acmeAccount.Email, *acmeAccount.Provider, err.Error())))
+               return
+       }
+
+       resultRows, err := inf.Tx.NamedQuery(updateQuery, acmeAccount)
+       if err != nil {
+               userErr, sysErr, errCode := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer resultRows.Close()
+
+       rowsAffected := 0
+       for resultRows.Next() {
+               rowsAffected++
+       }
+       if rowsAffected == 0 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("acme account update: no account was updated"))
+               return
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "Acme account updated")
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, acmeAccount)
+
+       changeLogMsg := fmt.Sprintf("ACME ACCOUNT: %s %s, ACTION: updated", 
*acmeAccount.Email, *acmeAccount.Provider)
+       api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func Delete(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"provider", 
"email"}, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       provider := inf.Params["provider"]
+       email := inf.Params["email"]
+
+       tx := inf.Tx.Tx
+
+       var prevAccount tc.AcmeAccount
+       err := tx.QueryRow(selectByProviderAndEmailQuery, email, 
provider).Scan(&prevAccount.Email, &prevAccount.PrivateKey, &prevAccount.Uri, 
&prevAccount.Provider)
+       if err == sql.ErrNoRows {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, 
errors.New(fmt.Sprintf("acme account not found")), nil)
+               return
+       }
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New(fmt.Sprintf("checking if acme account with email %s and provider %s 
exists: %v", email, provider, err.Error())))
+               return
+       }
+
+       if _, err := tx.Exec(deleteQuery, email, provider); err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New(fmt.Sprintf("deleting acme account with email %s and provider %s: 
%v", email, provider, err.Error())))
+               return
+       }
+
+       api.WriteRespAlert(w, r, tc.SuccessLevel, "Acme account deleted")
+
+       changeLogMsg := fmt.Sprintf("ACME ACCOUNT: %s %s, ACTION: deleted", 
email, provider)
+       api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go 
b/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go
index d7cefb9..9f34f37 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go
@@ -228,7 +228,7 @@ func GetLetsEncryptCertificates(cfg *config.Config, req 
tc.DeliveryServiceLetsEn
        }
        tx.Commit()
 
-       storedLEInfo, err := getStoredLetsEncryptInfo(userTx, 
cfg.ConfigLetsEncrypt.Email)
+       storedLEInfo, err := getStoredAcmeAccountInfo(userTx, 
cfg.ConfigLetsEncrypt.Email, tc.LetsEncryptAuthType)
        if err != nil {
                log.Errorf(deliveryService+": Error finding stored LE 
information: %s", err.Error())
                api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
@@ -395,7 +395,7 @@ func GetLetsEncryptCertificates(cfg *config.Config, req 
tc.DeliveryServiceLetsEn
                return errors.New("pem-encoding private key: " + err.Error())
        }
        userKeyPem := userKeyBuf.Bytes()
-       err = storeLEAccountInfo(userTx, myUser.Email, string(userKeyPem), 
myUser.Registration.URI)
+       err = storeAcmeAccountInfo(userTx, myUser.Email, string(userKeyPem), 
myUser.Registration.URI, tc.LetsEncryptAuthType)
        if err != nil {
                log.Errorf("storing user account info: " + err.Error())
                api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
@@ -407,10 +407,10 @@ func GetLetsEncryptCertificates(cfg *config.Config, req 
tc.DeliveryServiceLetsEn
        return nil
 }
 
-func getStoredLetsEncryptInfo(tx *sql.Tx, email string) (*LEInfo, error) {
+func getStoredAcmeAccountInfo(tx *sql.Tx, email string, provider string) 
(*LEInfo, error) {
        leInfo := LEInfo{}
-       selectQuery := `SELECT email, private_key, uri FROM 
lets_encrypt_account WHERE email = $1 LIMIT 1`
-       if err := tx.QueryRow(selectQuery, email).Scan(&leInfo.Email, 
&leInfo.Key, &leInfo.URI); err != nil {
+       selectQuery := `SELECT email, private_key, uri FROM acme_account WHERE 
email = $1 AND provider = $2 LIMIT 1`
+       if err := tx.QueryRow(selectQuery, email, provider).Scan(&leInfo.Email, 
&leInfo.Key, &leInfo.URI); err != nil {
                if err == sql.ErrNoRows {
                        return nil, nil
                }
@@ -427,9 +427,9 @@ func getStoredLetsEncryptInfo(tx *sql.Tx, email string) 
(*LEInfo, error) {
        return &leInfo, nil
 }
 
-func storeLEAccountInfo(tx *sql.Tx, email string, privateKey string, uri 
string) error {
-       q := `INSERT INTO lets_encrypt_account (email, private_key, uri) VALUES 
($1, $2, $3)`
-       response, err := tx.Exec(q, email, privateKey, uri)
+func storeAcmeAccountInfo(tx *sql.Tx, email string, privateKey string, uri 
string, provider string) error {
+       q := `INSERT INTO acme_account (email, private_key, uri, provider) 
VALUES ($1, $2, $3, $4)`
+       response, err := tx.Exec(q, email, privateKey, uri, provider)
        if err != nil {
                return err
        }
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go 
b/traffic_ops/traffic_ops_golang/routing/routes.go
index 7f4cb49..def9be5 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -36,6 +36,7 @@ import (
        "github.com/apache/trafficcontrol/lib/go-tc"
        "github.com/apache/trafficcontrol/lib/go-util"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/about"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/acme"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/apicapability"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/apitenant"
@@ -144,6 +145,13 @@ func Routes(d ServerData) ([]Route, []RawRoute, 
http.Handler, error) {
                /**
                 * 3.x API
                 */
+
+               // Acme account information
+               {api.Version{3, 1}, http.MethodGet, `acme_accounts/?$`, 
acme.Read, auth.PrivLevelAdmin, Authenticated, nil, 2034390561, noPerlBypass},
+               {api.Version{3, 1}, http.MethodPost, `acme_accounts/?$`, 
acme.Create, auth.PrivLevelAdmin, Authenticated, nil, 2034390562, noPerlBypass},
+               {api.Version{3, 1}, http.MethodPut, `acme_accounts/?$`, 
acme.Update, auth.PrivLevelAdmin, Authenticated, nil, 2034390563, noPerlBypass},
+               {api.Version{3, 1}, http.MethodDelete, 
`acme_accounts/{provider}/{email}?$`, acme.Delete, auth.PrivLevelAdmin, 
Authenticated, nil, 2034390564, noPerlBypass},
+
                // API Capability
                {api.Version{3, 0}, http.MethodGet, `api_capabilities/?$`, 
apicapability.GetAPICapabilitiesHandler, auth.PrivLevelReadOnly, Authenticated, 
nil, 28132065893, noPerlBypass},
 

Reply via email to