mirror of https://github.com/status-im/consul.git
read endpoint (#18268)
implement http read endpoint to expose resource grpc service read method
This commit is contained in:
parent
559c61e6b6
commit
cda884ac81
|
@ -59,6 +59,8 @@ func (h *resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
h.handleWrite(w, r, ctx)
|
h.handleWrite(w, r, ctx)
|
||||||
|
case http.MethodGet:
|
||||||
|
h.handleRead(w, r, ctx)
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
h.handleDelete(w, r, ctx)
|
h.handleDelete(w, r, ctx)
|
||||||
default:
|
default:
|
||||||
|
@ -88,17 +90,17 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tenancyInfo, resourceName, version := parseParams(r)
|
tenancyInfo, params := parseParams(r)
|
||||||
|
|
||||||
rsp, err := h.client.Write(ctx, &pbresource.WriteRequest{
|
rsp, err := h.client.Write(ctx, &pbresource.WriteRequest{
|
||||||
Resource: &pbresource.Resource{
|
Resource: &pbresource.Resource{
|
||||||
Id: &pbresource.ID{
|
Id: &pbresource.ID{
|
||||||
Type: h.reg.Type,
|
Type: h.reg.Type,
|
||||||
Tenancy: tenancyInfo,
|
Tenancy: tenancyInfo,
|
||||||
Name: resourceName,
|
Name: params["resourceName"],
|
||||||
},
|
},
|
||||||
Owner: req.Owner,
|
Owner: req.Owner,
|
||||||
Version: version,
|
Version: params["version"],
|
||||||
Metadata: req.Metadata,
|
Metadata: req.Metadata,
|
||||||
Data: anyProtoMsg,
|
Data: anyProtoMsg,
|
||||||
},
|
},
|
||||||
|
@ -117,18 +119,71 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct
|
||||||
w.Write(output)
|
w.Write(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseParams(r *http.Request) (tenancy *pbresource.Tenancy, resourceName string, version string) {
|
func (h *resourceHandler) handleRead(w http.ResponseWriter, r *http.Request, ctx context.Context) {
|
||||||
params := r.URL.Query()
|
tenancyInfo, params := parseParams(r)
|
||||||
tenancy = &pbresource.Tenancy{
|
if params["consistent"] != "" {
|
||||||
Partition: params.Get("partition"),
|
ctx = metadata.AppendToOutgoingContext(ctx, "x-consul-consistency-mode", "consistent")
|
||||||
PeerName: params.Get("peer_name"),
|
|
||||||
Namespace: params.Get("namespace"),
|
|
||||||
}
|
}
|
||||||
resourceName = path.Base(r.URL.Path)
|
|
||||||
|
rsp, err := h.client.Read(ctx, &pbresource.ReadRequest{
|
||||||
|
Id: &pbresource.ID{
|
||||||
|
Type: h.reg.Type,
|
||||||
|
Tenancy: tenancyInfo,
|
||||||
|
Name: params["resourceName"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
handleResponseError(err, w, h)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := jsonMarshal(rsp.Resource)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
h.logger.Error("Failed to unmarshal GRPC resource response", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The HTTP endpoints do not accept UID since it is quite unlikely that the user will have access to it
|
||||||
|
func (h *resourceHandler) handleDelete(w http.ResponseWriter, r *http.Request, ctx context.Context) {
|
||||||
|
tenancyInfo, params := parseParams(r)
|
||||||
|
_, err := h.client.Delete(ctx, &pbresource.DeleteRequest{
|
||||||
|
Id: &pbresource.ID{
|
||||||
|
Type: h.reg.Type,
|
||||||
|
Tenancy: tenancyInfo,
|
||||||
|
Name: params["resourceName"],
|
||||||
|
},
|
||||||
|
Version: params["version"],
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
handleResponseError(err, w, h)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
w.Write([]byte("{}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseParams(r *http.Request) (tenancy *pbresource.Tenancy, params map[string]string) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
tenancy = &pbresource.Tenancy{
|
||||||
|
Partition: query.Get("partition"),
|
||||||
|
PeerName: query.Get("peer_name"),
|
||||||
|
Namespace: query.Get("namespace"),
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceName := path.Base(r.URL.Path)
|
||||||
if resourceName == "." || resourceName == "/" {
|
if resourceName == "." || resourceName == "/" {
|
||||||
resourceName = ""
|
resourceName = ""
|
||||||
}
|
}
|
||||||
version = params.Get("version")
|
|
||||||
|
params = make(map[string]string)
|
||||||
|
params["resourceName"] = resourceName
|
||||||
|
params["version"] = query.Get("version")
|
||||||
|
if _, ok := query["consistent"]; ok {
|
||||||
|
params["consistent"] = "true"
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -173,22 +228,3 @@ func handleResponseError(err error, w http.ResponseWriter, h *resourceHandler) {
|
||||||
}
|
}
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: The HTTP endpoints do not accept UID since it is quite unlikely that the user will have access to it
|
|
||||||
func (h *resourceHandler) handleDelete(w http.ResponseWriter, r *http.Request, ctx context.Context) {
|
|
||||||
tenancyInfo, resourceName, version := parseParams(r)
|
|
||||||
_, err := h.client.Delete(ctx, &pbresource.DeleteRequest{
|
|
||||||
Id: &pbresource.ID{
|
|
||||||
Type: h.reg.Type,
|
|
||||||
Tenancy: tenancyInfo,
|
|
||||||
Name: resourceName,
|
|
||||||
},
|
|
||||||
Version: version,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
handleResponseError(err, w, h)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
w.Write([]byte("{}"))
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
|
|
||||||
const testACLTokenArtistReadPolicy = "00000000-0000-0000-0000-000000000001"
|
const testACLTokenArtistReadPolicy = "00000000-0000-0000-0000-000000000001"
|
||||||
const testACLTokenArtistWritePolicy = "00000000-0000-0000-0000-000000000002"
|
const testACLTokenArtistWritePolicy = "00000000-0000-0000-0000-000000000002"
|
||||||
|
const fakeToken = "fake-token"
|
||||||
|
|
||||||
func parseToken(req *http.Request, token *string) {
|
func parseToken(req *http.Request, token *string) {
|
||||||
*token = req.Header.Get("x-consul-token")
|
*token = req.Header.Get("x-consul-token")
|
||||||
|
@ -109,25 +110,9 @@ func TestResourceWriteHandler(t *testing.T) {
|
||||||
|
|
||||||
client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
|
client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
|
||||||
|
|
||||||
v1ArtistHandler := resourceHandler{
|
r := resource.NewRegistry()
|
||||||
resource.Registration{
|
demo.RegisterTypes(r)
|
||||||
Type: demo.TypeV1Artist,
|
handler := NewHandler(client, r, parseToken, hclog.NewNullLogger())
|
||||||
Proto: &pbdemov1.Artist{},
|
|
||||||
},
|
|
||||||
client,
|
|
||||||
parseToken,
|
|
||||||
hclog.NewNullLogger(),
|
|
||||||
}
|
|
||||||
|
|
||||||
v2ArtistHandler := resourceHandler{
|
|
||||||
resource.Registration{
|
|
||||||
Type: demo.TypeV2Artist,
|
|
||||||
Proto: &pbdemov2.Artist{},
|
|
||||||
},
|
|
||||||
client,
|
|
||||||
parseToken,
|
|
||||||
hclog.NewNullLogger(),
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("should be blocked if the token is not authorized", func(t *testing.T) {
|
t.Run("should be blocked if the token is not authorized", func(t *testing.T) {
|
||||||
rsp := httptest.NewRecorder()
|
rsp := httptest.NewRecorder()
|
||||||
|
@ -145,7 +130,7 @@ func TestResourceWriteHandler(t *testing.T) {
|
||||||
|
|
||||||
req.Header.Add("x-consul-token", testACLTokenArtistReadPolicy)
|
req.Header.Add("x-consul-token", testACLTokenArtistReadPolicy)
|
||||||
|
|
||||||
v2ArtistHandler.ServeHTTP(rsp, req)
|
handler.ServeHTTP(rsp, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusForbidden, rsp.Result().StatusCode)
|
require.Equal(t, http.StatusForbidden, rsp.Result().StatusCode)
|
||||||
})
|
})
|
||||||
|
@ -166,7 +151,7 @@ func TestResourceWriteHandler(t *testing.T) {
|
||||||
|
|
||||||
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
||||||
|
|
||||||
v2ArtistHandler.ServeHTTP(rsp, req)
|
handler.ServeHTTP(rsp, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, rsp.Result().StatusCode)
|
require.Equal(t, http.StatusOK, rsp.Result().StatusCode)
|
||||||
|
|
||||||
|
@ -206,7 +191,7 @@ func TestResourceWriteHandler(t *testing.T) {
|
||||||
|
|
||||||
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
||||||
|
|
||||||
v2ArtistHandler.ServeHTTP(rsp, req)
|
handler.ServeHTTP(rsp, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, rsp.Result().StatusCode)
|
require.Equal(t, http.StatusOK, rsp.Result().StatusCode)
|
||||||
var result map[string]any
|
var result map[string]any
|
||||||
|
@ -231,7 +216,7 @@ func TestResourceWriteHandler(t *testing.T) {
|
||||||
|
|
||||||
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
||||||
|
|
||||||
v2ArtistHandler.ServeHTTP(rsp, req)
|
handler.ServeHTTP(rsp, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusConflict, rsp.Result().StatusCode)
|
require.Equal(t, http.StatusConflict, rsp.Result().StatusCode)
|
||||||
})
|
})
|
||||||
|
@ -265,7 +250,7 @@ func TestResourceWriteHandler(t *testing.T) {
|
||||||
|
|
||||||
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
||||||
|
|
||||||
v1ArtistHandler.ServeHTTP(rsp, req)
|
handler.ServeHTTP(rsp, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, rsp.Result().StatusCode)
|
require.Equal(t, http.StatusOK, rsp.Result().StatusCode)
|
||||||
|
|
||||||
|
@ -291,7 +276,7 @@ func TestResourceWriteHandler(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func createResource(t *testing.T, artistHandler resourceHandler) {
|
func createResource(t *testing.T, artistHandler http.Handler) map[string]any {
|
||||||
rsp := httptest.NewRecorder()
|
rsp := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(`
|
req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(`
|
||||||
{
|
{
|
||||||
|
@ -309,6 +294,65 @@ func createResource(t *testing.T, artistHandler resourceHandler) {
|
||||||
|
|
||||||
artistHandler.ServeHTTP(rsp, req)
|
artistHandler.ServeHTTP(rsp, req)
|
||||||
require.Equal(t, http.StatusOK, rsp.Result().StatusCode)
|
require.Equal(t, http.StatusOK, rsp.Result().StatusCode)
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
require.NoError(t, json.NewDecoder(rsp.Body).Decode(&result))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceReadHandler(t *testing.T) {
|
||||||
|
aclResolver := &resourceSvc.MockACLResolver{}
|
||||||
|
aclResolver.On("ResolveTokenAndDefaultMeta", testACLTokenArtistReadPolicy, mock.Anything, mock.Anything).
|
||||||
|
Return(svctest.AuthorizerFrom(t, demo.ArtistV1ReadPolicy, demo.ArtistV2ReadPolicy), nil)
|
||||||
|
aclResolver.On("ResolveTokenAndDefaultMeta", testACLTokenArtistWritePolicy, mock.Anything, mock.Anything).
|
||||||
|
Return(svctest.AuthorizerFrom(t, demo.ArtistV1WritePolicy, demo.ArtistV2WritePolicy), nil)
|
||||||
|
aclResolver.On("ResolveTokenAndDefaultMeta", fakeToken, mock.Anything, mock.Anything).
|
||||||
|
Return(svctest.AuthorizerFrom(t, ""), nil)
|
||||||
|
|
||||||
|
client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
|
||||||
|
|
||||||
|
r := resource.NewRegistry()
|
||||||
|
demo.RegisterTypes(r)
|
||||||
|
handler := NewHandler(client, r, parseToken, hclog.NewNullLogger())
|
||||||
|
|
||||||
|
createdResource := createResource(t, handler)
|
||||||
|
|
||||||
|
t.Run("Read resource", func(t *testing.T) {
|
||||||
|
rsp := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&consistent", nil)
|
||||||
|
|
||||||
|
req.Header.Add("x-consul-token", testACLTokenArtistReadPolicy)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rsp, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rsp.Result().StatusCode)
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
require.NoError(t, json.NewDecoder(rsp.Body).Decode(&result))
|
||||||
|
require.Equal(t, result, createdResource)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not be found if resource not exist", func(t *testing.T) {
|
||||||
|
rsp := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/demo/v2/artist/keith-not-exist?partition=default&peer_name=local&namespace=default&consistent", nil)
|
||||||
|
|
||||||
|
req.Header.Add("x-consul-token", testACLTokenArtistReadPolicy)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rsp, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusNotFound, rsp.Result().StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should be blocked if the token is not authorized", func(t *testing.T) {
|
||||||
|
rsp := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&consistent", nil)
|
||||||
|
|
||||||
|
req.Header.Add("x-consul-token", fakeToken)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rsp, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusForbidden, rsp.Result().StatusCode)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResourceDeleteHandler(t *testing.T) {
|
func TestResourceDeleteHandler(t *testing.T) {
|
||||||
|
@ -320,38 +364,33 @@ func TestResourceDeleteHandler(t *testing.T) {
|
||||||
|
|
||||||
client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
|
client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
|
||||||
|
|
||||||
v2ArtistHandler := resourceHandler{
|
r := resource.NewRegistry()
|
||||||
resource.Registration{
|
demo.RegisterTypes(r)
|
||||||
Type: demo.TypeV2Artist,
|
|
||||||
Proto: &pbdemov2.Artist{},
|
handler := NewHandler(client, r, parseToken, hclog.NewNullLogger())
|
||||||
},
|
|
||||||
client,
|
|
||||||
parseToken,
|
|
||||||
hclog.NewNullLogger(),
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("should surface PermissionDenied error from resource service", func(t *testing.T) {
|
t.Run("should surface PermissionDenied error from resource service", func(t *testing.T) {
|
||||||
createResource(t, v2ArtistHandler)
|
createResource(t, handler)
|
||||||
|
|
||||||
deleteRsp := httptest.NewRecorder()
|
deleteRsp := httptest.NewRecorder()
|
||||||
deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(""))
|
deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(""))
|
||||||
|
|
||||||
deletReq.Header.Add("x-consul-token", testACLTokenArtistReadPolicy)
|
deletReq.Header.Add("x-consul-token", testACLTokenArtistReadPolicy)
|
||||||
|
|
||||||
v2ArtistHandler.ServeHTTP(deleteRsp, deletReq)
|
handler.ServeHTTP(deleteRsp, deletReq)
|
||||||
|
|
||||||
require.Equal(t, http.StatusForbidden, deleteRsp.Result().StatusCode)
|
require.Equal(t, http.StatusForbidden, deleteRsp.Result().StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should delete a resource without version", func(t *testing.T) {
|
t.Run("should delete a resource without version", func(t *testing.T) {
|
||||||
createResource(t, v2ArtistHandler)
|
createResource(t, handler)
|
||||||
|
|
||||||
deleteRsp := httptest.NewRecorder()
|
deleteRsp := httptest.NewRecorder()
|
||||||
deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(""))
|
deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(""))
|
||||||
|
|
||||||
deletReq.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
deletReq.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
||||||
|
|
||||||
v2ArtistHandler.ServeHTTP(deleteRsp, deletReq)
|
handler.ServeHTTP(deleteRsp, deletReq)
|
||||||
|
|
||||||
require.Equal(t, http.StatusNoContent, deleteRsp.Result().StatusCode)
|
require.Equal(t, http.StatusNoContent, deleteRsp.Result().StatusCode)
|
||||||
|
|
||||||
|
@ -370,14 +409,14 @@ func TestResourceDeleteHandler(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should delete a resource with version", func(t *testing.T) {
|
t.Run("should delete a resource with version", func(t *testing.T) {
|
||||||
createResource(t, v2ArtistHandler)
|
createResource(t, handler)
|
||||||
|
|
||||||
rsp := httptest.NewRecorder()
|
rsp := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader(""))
|
req := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader(""))
|
||||||
|
|
||||||
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
||||||
|
|
||||||
v2ArtistHandler.ServeHTTP(rsp, req)
|
handler.ServeHTTP(rsp, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusNoContent, rsp.Result().StatusCode)
|
require.Equal(t, http.StatusNoContent, rsp.Result().StatusCode)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue