smallstep-certificates/acme/api/account.go

255 lines
6.9 KiB
Go
Raw Normal View History

2019-05-27 00:41:10 +00:00
package api
import (
"context"
2019-05-27 00:41:10 +00:00
"encoding/json"
"errors"
2019-05-27 00:41:10 +00:00
"net/http"
"github.com/go-chi/chi/v5"
2019-05-27 00:41:10 +00:00
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api/render"
2019-05-27 00:41:10 +00:00
"github.com/smallstep/certificates/logging"
)
// NewAccountRequest represents the payload for a new account request.
type NewAccountRequest struct {
Contact []string `json:"contact"`
OnlyReturnExisting bool `json:"onlyReturnExisting"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
ExternalAccountBinding *ExternalAccountBinding `json:"externalAccountBinding,omitempty"`
2019-05-27 00:41:10 +00:00
}
func validateContacts(cs []string) error {
for _, c := range cs {
if c == "" {
2021-03-03 23:16:25 +00:00
return acme.NewError(acme.ErrorMalformedType, "contact cannot be empty string")
2019-05-27 00:41:10 +00:00
}
}
return nil
}
// Validate validates a new-account request body.
func (n *NewAccountRequest) Validate() error {
if n.OnlyReturnExisting && len(n.Contact) > 0 {
2021-03-03 23:16:25 +00:00
return acme.NewError(acme.ErrorMalformedType, "incompatible input; onlyReturnExisting must be alone")
2019-05-27 00:41:10 +00:00
}
return validateContacts(n.Contact)
}
// UpdateAccountRequest represents an update-account request.
type UpdateAccountRequest struct {
2021-03-05 07:10:46 +00:00
Contact []string `json:"contact"`
Status acme.Status `json:"status"`
2019-05-27 00:41:10 +00:00
}
// Validate validates a update-account request body.
func (u *UpdateAccountRequest) Validate() error {
switch {
case len(u.Status) > 0 && len(u.Contact) > 0:
2021-03-03 23:16:25 +00:00
return acme.NewError(acme.ErrorMalformedType, "incompatible input; contact and "+
"status updates are mutually exclusive")
2019-05-27 00:41:10 +00:00
case len(u.Contact) > 0:
if err := validateContacts(u.Contact); err != nil {
return err
}
return nil
case len(u.Status) > 0:
2021-03-05 07:10:46 +00:00
if u.Status != acme.StatusDeactivated {
2021-03-03 23:16:25 +00:00
return acme.NewError(acme.ErrorMalformedType, "cannot update account "+
"status to %s, only deactivated", u.Status)
2019-05-27 00:41:10 +00:00
}
return nil
default:
// According to the ACME spec (https://tools.ietf.org/html/rfc8555#section-7.3.2)
// accountUpdate should ignore any fields not recognized by the server.
return nil
2019-05-27 00:41:10 +00:00
}
}
// getAccountLocationPath returns the current account URL location.
// Returned location will be of the form: https://<ca-url>/acme/<provisioner>/account/<accID>
func getAccountLocationPath(ctx context.Context, linker acme.Linker, accID string) string {
return linker.GetLink(ctx, acme.AccountLinkType, accID)
}
2019-05-27 00:41:10 +00:00
// NewAccount is the handler resource for creating new ACME accounts.
2022-04-27 22:42:26 +00:00
func NewAccount(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:14:56 +00:00
ctx := r.Context()
2022-04-29 02:15:18 +00:00
db := acme.MustDatabaseFromContext(ctx)
linker := acme.MustLinkerFromContext(ctx)
2021-03-05 07:14:56 +00:00
payload, err := payloadFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, err)
2019-05-27 00:41:10 +00:00
return
}
var nar NewAccountRequest
if err := json.Unmarshal(payload.value, &nar); err != nil {
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
2021-03-03 23:16:25 +00:00
"failed to unmarshal new-account request payload"))
2019-05-27 00:41:10 +00:00
return
}
if err := nar.Validate(); err != nil {
render.Error(w, err)
2019-05-27 00:41:10 +00:00
return
}
prov, err := acmeProvisionerFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
2019-05-27 00:41:10 +00:00
httpStatus := http.StatusCreated
acc, err := accountFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
var acmeErr *acme.Error
if !errors.As(err, &acmeErr) || acmeErr.Status != http.StatusBadRequest {
2019-05-27 00:41:10 +00:00
// Something went wrong ...
render.Error(w, err)
2019-05-27 00:41:10 +00:00
return
}
// Account does not exist //
if nar.OnlyReturnExisting {
render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType,
2021-03-03 23:16:25 +00:00
"account does not exist"))
2019-05-27 00:41:10 +00:00
return
}
2021-03-05 07:14:56 +00:00
jwk, err := jwkFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, err)
2019-05-27 00:41:10 +00:00
return
}
2022-04-27 22:42:26 +00:00
eak, err := validateExternalAccountBinding(ctx, &nar)
if err != nil {
render.Error(w, err)
return
}
acc = &acme.Account{
Key: jwk,
Contact: nar.Contact,
Status: acme.StatusValid,
LocationPrefix: getAccountLocationPath(ctx, linker, ""),
ProvisionerName: prov.GetName(),
2021-03-05 07:10:46 +00:00
}
2022-04-27 22:42:26 +00:00
if err := db.CreateAccount(ctx, acc); err != nil {
render.Error(w, acme.WrapErrorISE(err, "error creating account"))
2019-05-27 00:41:10 +00:00
return
}
2021-07-22 21:48:41 +00:00
if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
2022-04-07 12:11:53 +00:00
if err := eak.BindTo(acc); err != nil {
render.Error(w, err)
return
}
2022-04-27 22:42:26 +00:00
if err := db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil {
render.Error(w, acme.WrapErrorISE(err, "error updating external account binding key"))
return
}
acc.ExternalAccountBinding = nar.ExternalAccountBinding
}
2019-05-27 00:41:10 +00:00
} else {
2021-07-22 21:48:41 +00:00
// Account exists
2019-05-27 00:41:10 +00:00
httpStatus = http.StatusOK
}
2022-04-29 02:15:18 +00:00
linker.LinkAccount(ctx, acc)
2021-03-05 07:10:46 +00:00
w.Header().Set("Location", getAccountLocationPath(ctx, linker, acc.ID))
render.JSONStatus(w, acc, httpStatus)
2019-05-27 00:41:10 +00:00
}
// GetOrUpdateAccount is the api for updating an ACME account.
2022-04-27 22:42:26 +00:00
func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:14:56 +00:00
ctx := r.Context()
2022-04-29 02:15:18 +00:00
db := acme.MustDatabaseFromContext(ctx)
linker := acme.MustLinkerFromContext(ctx)
2021-03-05 07:14:56 +00:00
acc, err := accountFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, err)
2019-05-27 00:41:10 +00:00
return
}
2021-03-05 07:14:56 +00:00
payload, err := payloadFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, err)
2019-05-27 00:41:10 +00:00
return
}
// If PostAsGet just respond with the account, otherwise process like a
// normal Post request.
2019-05-27 00:41:10 +00:00
if !payload.isPostAsGet {
var uar UpdateAccountRequest
if err := json.Unmarshal(payload.value, &uar); err != nil {
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
2021-03-03 23:16:25 +00:00
"failed to unmarshal new-account request payload"))
2019-05-27 00:41:10 +00:00
return
}
if err := uar.Validate(); err != nil {
render.Error(w, err)
2019-05-27 00:41:10 +00:00
return
}
2021-03-12 08:16:48 +00:00
if len(uar.Status) > 0 || len(uar.Contact) > 0 {
if len(uar.Status) > 0 {
acc.Status = uar.Status
} else if len(uar.Contact) > 0 {
acc.Contact = uar.Contact
}
2022-04-27 22:42:26 +00:00
if err := db.UpdateAccount(ctx, acc); err != nil {
render.Error(w, acme.WrapErrorISE(err, "error updating account"))
2021-03-12 08:16:48 +00:00
return
}
2019-05-27 00:41:10 +00:00
}
}
2021-03-05 07:10:46 +00:00
2022-04-29 02:15:18 +00:00
linker.LinkAccount(ctx, acc)
2021-03-05 07:10:46 +00:00
2022-04-29 02:15:18 +00:00
w.Header().Set("Location", linker.GetLink(ctx, acme.AccountLinkType, acc.ID))
render.JSON(w, acc)
2019-05-27 00:41:10 +00:00
}
func logOrdersByAccount(w http.ResponseWriter, oids []string) {
if rl, ok := w.(logging.ResponseLogger); ok {
m := map[string]interface{}{
"orders": oids,
}
rl.WithFields(m)
}
}
// GetOrdersByAccountID ACME api for retrieving the list of order urls belonging to an account.
2022-04-27 22:42:26 +00:00
func GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) {
2021-03-06 21:06:43 +00:00
ctx := r.Context()
2022-04-29 02:15:18 +00:00
db := acme.MustDatabaseFromContext(ctx)
linker := acme.MustLinkerFromContext(ctx)
2021-03-06 21:06:43 +00:00
acc, err := accountFromContext(ctx)
if err != nil {
render.Error(w, err)
2021-03-06 21:06:43 +00:00
return
}
accID := chi.URLParam(r, "accID")
if acc.ID != accID {
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account ID '%s' does not match url param '%s'", acc.ID, accID))
2021-03-06 21:06:43 +00:00
return
}
2022-04-27 22:42:26 +00:00
orders, err := db.GetOrdersByAccountID(ctx, acc.ID)
2021-03-06 21:06:43 +00:00
if err != nil {
render.Error(w, err)
2021-03-06 21:06:43 +00:00
return
}
2022-04-29 02:15:18 +00:00
linker.LinkOrdersByAccountID(ctx, orders)
2021-03-06 21:06:43 +00:00
render.JSON(w, orders)
2021-03-06 21:06:43 +00:00
logOrdersByAccount(w, orders)
2019-05-27 00:41:10 +00:00
}