Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions acme/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"crypto/x509"
"encoding/asn1"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
Expand All @@ -19,6 +20,7 @@

"github.com/smallstep/certificates/acme/wire"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/webhook"
)

type IdentifierType string
Expand Down Expand Up @@ -304,6 +306,17 @@
NotAfter: provisioner.NewTimeDuration(o.NotAfter),
}, signOps...)
if err != nil {
// Add subproblem for webhook errors, others can be added later.
var webhookErr *webhook.Error
if errors.As(err, &webhookErr) {
acmeError := NewDetailedError(ErrorUnauthorizedType, webhookErr.Error())

Check failure on line 312 in acme/order.go

View workflow job for this annotation

GitHub Actions / ci / lint / lint

printf: non-constant format string in call to github.com/smallstep/certificates/acme.NewDetailedError (govet)
acmeError.AddSubproblems(Subproblem{
Type: fmt.Sprintf("urn:smallstep:webhook:error:%s", webhookErr.Code),
Detail: webhookErr.Message,
})
return acmeError
}

return WrapErrorISE(err, "error signing certificate for order %s", o.ID)
}

Expand Down
52 changes: 52 additions & 0 deletions acme/order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/certificates/webhook"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/x509util"
)
Expand Down Expand Up @@ -590,6 +592,55 @@ func TestOrder_Finalize(t *testing.T) {
err: NewErrorISE("error signing certificate for order oID: force"),
}
},
"fail/webhook-error": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "dns", Value: "bar.internal"},
},
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
DNSNames: []string{"bar.internal"},
}

return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
ca: &mockSignAuth{
signWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return nil, errs.ForbiddenErr(&webhook.Error{Code: "theCode", Message: "The message"}, "forbidden error")
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
},
err: NewDetailedError(ErrorUnauthorizedType, "The message (theCode)").AddSubproblems(Subproblem{
Type: "urn:smallstep:webhook:error:theCode",
Detail: "The message",
}),
}
},
"fail/error-db.CreateCertificate": func(t *testing.T) test {
now := clock.Now()
o := &Order{
Expand Down Expand Up @@ -1217,6 +1268,7 @@ func TestOrder_Finalize(t *testing.T) {
assert.Equals(t, k.Status, tc.err.Status)
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
assert.Equals(t, k.Detail, tc.err.Detail)
assert.Equals(t, k.Subproblems, tc.err.Subproblems)
} else {
assert.FatalError(t, errors.New("unexpected error type"))
}
Expand Down
6 changes: 6 additions & 0 deletions authority/provisioner/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ func (wc *WebhookController) Enrich(ctx context.Context, req *webhook.RequestBod
return err
}
if !resp.Allow {
if resp.Error != nil {
return resp.Error
}
return ErrWebhookDenied
}
wc.TemplateData.SetWebhook(wh.Name, resp.Data)
Expand Down Expand Up @@ -101,6 +104,9 @@ func (wc *WebhookController) Authorize(ctx context.Context, req *webhook.Request
return err
}
if !resp.Allow {
if resp.Error != nil {
return resp.Error
}
return ErrWebhookDenied
}
}
Expand Down
50 changes: 50 additions & 0 deletions authority/provisioner/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func TestWebhookController_Enrich(t *testing.T) {
expectErr bool
expectTemplateData any
assertRequest func(t *testing.T, req *webhook.RequestBody)
assertError func(t *testing.T, err error)
}
tests := map[string]test{
"ok/no enriching webhooks": {
Expand Down Expand Up @@ -228,6 +229,28 @@ func TestWebhookController_Enrich(t *testing.T) {
responses: []*webhook.ResponseBody{{Allow: false}},
expectErr: true,
expectTemplateData: x509util.TemplateData{},
assertError: func(t *testing.T, err error) {
assert.Equal(t, ErrWebhookDenied, err)
},
},
"deny/with error": {
ctl: &WebhookController{
client: http.DefaultClient,
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
TemplateData: x509util.TemplateData{},
},
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: false, Error: &webhook.Error{
Code: "theCode", Message: "Some message",
}}},
expectErr: true,
expectTemplateData: x509util.TemplateData{},
assertError: func(t *testing.T, err error) {
assert.Equal(t, &webhook.Error{
Code: "theCode", Message: "Some message",
}, err)
},
},
"fail/with options": {
ctl: &WebhookController{
Expand Down Expand Up @@ -268,6 +291,9 @@ func TestWebhookController_Enrich(t *testing.T) {
if test.assertRequest != nil {
test.assertRequest(t, test.req)
}
if test.assertError != nil {
test.assertError(t, err)
}
})
}
}
Expand All @@ -283,6 +309,7 @@ func TestWebhookController_Authorize(t *testing.T) {
responses []*webhook.ResponseBody
expectErr bool
assertRequest func(t *testing.T, req *webhook.RequestBody)
assertError func(t *testing.T, err error)
}
tests := map[string]test{
"ok/no enriching webhooks": {
Expand Down Expand Up @@ -346,6 +373,26 @@ func TestWebhookController_Authorize(t *testing.T) {
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: false}},
expectErr: true,
assertError: func(t *testing.T, err error) {
assert.Equal(t, ErrWebhookDenied, err)
},
},
"deny/withError": {
ctl: &WebhookController{
client: http.DefaultClient,
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
},
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: false, Error: &webhook.Error{
Code: "theCode", Message: "Some message",
}}},
expectErr: true,
assertError: func(t *testing.T, err error) {
assert.Equal(t, &webhook.Error{
Code: "theCode", Message: "Some message",
}, err)
},
},
"fail/with options": {
ctl: &WebhookController{
Expand Down Expand Up @@ -383,6 +430,9 @@ func TestWebhookController_Authorize(t *testing.T) {
if test.assertRequest != nil {
test.assertRequest(t, test.req)
}
if test.assertError != nil {
test.assertError(t, err)
}
})
}
}
Expand Down
5 changes: 5 additions & 0 deletions errs/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ type ErrorResponse struct {
Message string `json:"message"`
}

// Unwrap implements the Unwrap interface and returns the original error.
func (e *Error) Unwrap() error {
return e.Err
}

// Cause implements the errors.Causer interface and returns the original error.
func (e *Error) Cause() error {
return e.Err
Expand Down
60 changes: 60 additions & 0 deletions errs/errors_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package errs

import (
"errors"
"fmt"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -67,3 +69,61 @@ func TestError_UnmarshalJSON(t *testing.T) {
})
}
}

func TestError_Unwrap(t *testing.T) {
err := errors.New("wrapped error")
tests := []struct {
name string
error error
want string
}{
{"ok New", New(http.StatusBadRequest, "some error"), "some error"},
{"ok New v-wrap", New(http.StatusBadRequest, "some error: %v", err), "some error: wrapped error"},
{"ok NewError", NewError(http.StatusBadRequest, err, "some error"), "some error: wrapped error"},
{"ok NewErr", NewErr(http.StatusBadRequest, err), "wrapped error"},
{"ok NewErr wit message", NewErr(http.StatusBadRequest, err, WithMessage("some message")), "wrapped error"},
{"ok Errorf", Errorf(http.StatusBadRequest, "some error: %w", err), "some error: wrapped error"},
{"ok Errorf v-wrap", Errorf(http.StatusBadRequest, "some error: %v", err), "some error: wrapped error"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := errors.Unwrap(tt.error)
assert.EqualError(t, got, tt.want)
})
}
}

type customError struct {
Message string
}

func (e *customError) Error() string {
return e.Message
}

func TestError_Unwrap_As(t *testing.T) {
err := &customError{Message: "wrapped error"}

tests := []struct {
name string
error error
want bool
wantErr *customError
}{
{"ok NewError", NewError(http.StatusBadRequest, err, "some error"), true, err},
{"ok NewErr", NewErr(http.StatusBadRequest, err), true, err},
{"ok NewErr wit message", NewErr(http.StatusBadRequest, err, WithMessage("some message")), true, err},
{"ok Errorf", Errorf(http.StatusBadRequest, "some error: %w", err), true, err},
{"fail New", New(http.StatusBadRequest, "some error"), false, nil},
{"fail New v-wrap", New(http.StatusBadRequest, "some error: %v", err), false, nil},
{"fail Errorf", Errorf(http.StatusBadRequest, "some error"), false, nil},
{"fail Errorf v-wrap", Errorf(http.StatusBadRequest, "some error: %v", err), false, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cerr *customError
assert.Equal(t, tt.want, errors.As(tt.error, &cerr))
assert.Equal(t, tt.wantErr, cerr)
})
}
}
16 changes: 14 additions & 2 deletions webhook/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package webhook

import (
"fmt"
"time"

"go.step.sm/crypto/sshutil"
Expand All @@ -9,8 +10,19 @@ import (

// ResponseBody is the body returned by webhook servers.
type ResponseBody struct {
Data any `json:"data"`
Allow bool `json:"allow"`
Data any `json:"data"`
Allow bool `json:"allow"`
Error *Error `json:"error,omitempty"`
}

// Error provides details explaining why the webhook was not permitted.
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
}

func (e *Error) Error() string {
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
}

// X509CertificateRequest is the certificate request sent to webhook servers for
Expand Down
Loading