Compare commits

...

6 commits

Author SHA1 Message Date
max furman
5637e4ac65
Add authority.Capabilities implementation 2022-11-29 10:26:06 -08:00
Max
8df01c60c1
Update api/api_test.go
Co-authored-by: Herman Slatman <hslatman@users.noreply.github.com>
2022-11-29 10:18:33 -08:00
Max
2349a7a7fb
Update ca/client.go
Co-authored-by: Herman Slatman <hslatman@users.noreply.github.com>
2022-11-29 10:17:58 -08:00
Max
f19b49cefb
Update ca/client_test.go
Co-authored-by: Herman Slatman <hslatman@users.noreply.github.com>
2022-11-29 10:17:25 -08:00
Max
78d93dcdfe
Update ca/client.go
Co-authored-by: Herman Slatman <hslatman@users.noreply.github.com>
2022-11-29 10:17:17 -08:00
max furman
7c53fe32c3
Add capabilities endpoint and client integration 2022-11-26 12:16:59 -08:00
5 changed files with 154 additions and 0 deletions

View file

@ -50,6 +50,7 @@ type Authority interface {
GetRoots() ([]*x509.Certificate, error)
GetFederation() ([]*x509.Certificate, error)
Version() authority.Version
Capabilities() authority.Capabilities
GetCertificateRevocationList() ([]byte, error)
}
@ -211,6 +212,10 @@ type VersionResponse struct {
RequireClientAuthentication bool `json:"requireClientAuthentication,omitempty"`
}
// CapabilitiesResponse is the response object that returns the version of the
// server.
type CapabilitiesResponse authority.Capabilities
// HealthResponse is the response object that returns the health of the server.
type HealthResponse struct {
Status string `json:"status"`
@ -261,8 +266,10 @@ func New(auth Authority) RouterHandler {
return &caHandler{}
}
// Route defines routing for the API.
func Route(r Router) {
r.MethodFunc("GET", "/version", Version)
r.MethodFunc("GET", "/capabilities", Capabilities)
r.MethodFunc("GET", "/health", Health)
r.MethodFunc("GET", "/root/{sha}", Root)
r.MethodFunc("POST", "/sign", Sign)
@ -303,6 +310,12 @@ func Version(w http.ResponseWriter, r *http.Request) {
})
}
// Capabilities is an HTTP handler that returns the capabilities of the authority
// server.
func Capabilities(w http.ResponseWriter, r *http.Request) {
render.JSON(w, CapabilitiesResponse(mustAuthority(r.Context()).Capabilities()))
}
// Health is an HTTP handler that returns the status of the server.
func Health(w http.ResponseWriter, r *http.Request) {
render.JSON(w, HealthResponse{Status: "ok"})

View file

@ -212,6 +212,7 @@ type mockAuthority struct {
checkSSHHost func(ctx context.Context, principal, token string) (bool, error)
getSSHBastion func(ctx context.Context, user string, hostname string) (*authority.Bastion, error)
version func() authority.Version
capabilities func() authority.Capabilities
}
func (m *mockAuthority) GetCertificateRevocationList() ([]byte, error) {
@ -405,6 +406,13 @@ func (m *mockAuthority) Version() authority.Version {
return m.ret1.(authority.Version)
}
func (m *mockAuthority) Capabilities() authority.Capabilities {
if m.capabilities != nil {
return m.capabilities()
}
return m.ret1.(authority.Capabilities)
}
func TestNewCertificate(t *testing.T) {
cert := parseCertificate(rootPEM)
if !reflect.DeepEqual(Certificate{Certificate: cert}, NewCertificate(cert)) {
@ -873,6 +881,35 @@ func Test_Health(t *testing.T) {
}
}
func Test_Capabilities(t *testing.T) {
capResp := CapabilitiesResponse{
RequireClientAuthentication: false,
RemoteConfigurationManagement: true,
}
mockMustAuthority(t, &mockAuthority{ret1: authority.Capabilities(capResp)})
req := httptest.NewRequest("GET", "http://example.com/capabilities", nil)
w := httptest.NewRecorder()
Capabilities(w, req)
res := w.Result()
if res.StatusCode != 200 {
t.Errorf("caHandler.Capabilities StatusCode = %d, wants 200", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Errorf("caHandler.Capabilities unexpected error = %v", err)
}
wantBytes, err := json.Marshal(capResp)
if err != nil {
assert.FatalError(t, err)
}
if !bytes.Equal(bytes.TrimSpace(body), wantBytes) {
t.Errorf("caHandler.Capabilities Body = %s, wants %s", body, wantBytes)
}
}
func Test_Root(t *testing.T) {
tests := []struct {
name string

15
authority/capabilities.go Normal file
View file

@ -0,0 +1,15 @@
package authority
// Capabilities defines the capabilities of the authority.
type Capabilities struct {
RemoteConfigurationManagement bool `json:"remoteConfigurationManagement,omitempty"`
RequireClientAuthentication bool `json:"requireClientAuthentication,omitempty"`
}
// Capabilities returns the capabilities information of the authority.
func (a *Authority) Capabilities() Capabilities {
return Capabilities{
RemoteConfigurationManagement: a.config.AuthorityConfig.EnableAdmin,
RequireClientAuthentication: GlobalVersion.RequireClientAuthentication,
}
}

View file

@ -45,6 +45,9 @@ var DisableIdentity = false
// UserAgent will set the User-Agent header in the client requests.
var UserAgent = "step-http-client/1.0"
// ErrNotFound is a standard not-found error.
var ErrNotFound = errors.New("not found")
type uaClient struct {
Client *http.Client
}
@ -605,6 +608,34 @@ retry:
return &version, nil
}
// Capabilities performs the capabilities request to the CA and returns the
// api.CapabilitiesResponse struct.
func (c *Client) Capabilities() (*api.CapabilitiesResponse, error) {
var retried bool
u := c.endpoint.ResolveReference(&url.URL{Path: "/capabilities"})
retry:
resp, err := c.client.Get(u.String())
if err != nil {
return nil, clientError(err)
}
switch {
case resp.StatusCode == http.StatusNotFound:
return nil, ErrNotFound
case resp.StatusCode >= 400:
if !retried && c.retryOnError(resp) {
retried = true
goto retry
}
return nil, readError(resp.Body)
default:
var capabilities api.CapabilitiesResponse
if err := readJSON(resp.Body, &capabilities); err != nil {
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Capabilities; error reading %q", u)
}
return &capabilities, nil
}
}
// Health performs the health request to the CA and returns the
// api.HealthResponse struct.
func (c *Client) Health() (*api.HealthResponse, error) {

View file

@ -206,6 +206,64 @@ func TestClient_Version(t *testing.T) {
}
}
func TestClient_Capabilities(t *testing.T) {
ok := &api.CapabilitiesResponse{
RequireClientAuthentication: false,
RemoteConfigurationManagement: true,
}
tests := []struct {
name string
response interface{}
responseCode int
wantErr bool
expectedErr error
}{
{"ok", ok, 200, false, nil},
{"500", errs.InternalServer("force"), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)},
{"404", errs.NotFound("force"), 404, true, ErrNotFound},
}
srv := httptest.NewServer(nil)
defer srv.Close()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))
if err != nil {
t.Errorf("NewClient() error = %v", err)
return
}
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
render.JSONStatus(w, tt.response, tt.responseCode)
})
got, err := c.Capabilities()
if (err != nil) != tt.wantErr {
t.Errorf("Client.Capabilities() error = %v, wantErr %v", err, tt.wantErr)
return
}
switch {
case err != nil:
if got != nil {
t.Errorf("Client.Capabilities() = %v, want nil", got)
}
if tt.responseCode == http.StatusNotFound {
assert.True(t, errors.Is(err, ErrNotFound))
} else {
assert.HasPrefix(t, tt.expectedErr.Error(), err.Error())
}
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.Capabilities() = %v, want %v", got, tt.response)
}
}
})
}
}
func TestClient_Health(t *testing.T) {
ok := &api.HealthResponse{Status: "ok"}