Skip to content

Commit

Permalink
[v15] Add yaml parsing and yamilfying access monitoring rule in webapi (
Browse files Browse the repository at this point in the history
#41109)

* Add yaml parsing and yamilfying access monitoring rule in webapi

* Extract resource without CheckAndSetDefault validator
  • Loading branch information
kimlisa authored May 2, 2024
1 parent 9cd1caf commit 7500631
Show file tree
Hide file tree
Showing 4 changed files with 327 additions and 6 deletions.
3 changes: 3 additions & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,9 @@ func (h *Handler) bindDefaultEndpoints() {
h.GET("/webapi/apps/:fqdnHint", h.WithAuth(h.getAppFQDN))
h.GET("/webapi/apps/:fqdnHint/:clusterName/:publicAddr", h.WithAuth(h.getAppFQDN))

h.POST("/webapi/yaml/parse/:kind", h.WithAuth(h.yamlParse))
h.POST("/webapi/yaml/stringify/:kind", h.WithAuth(h.yamlStringify))

// Desktop access endpoints.
h.GET("/webapi/sites/:site/desktops", h.WithClusterAuth(h.clusterDesktopsGet))
h.GET("/webapi/sites/:site/desktopservices", h.WithClusterAuth(h.clusterDesktopServicesGet))
Expand Down
21 changes: 15 additions & 6 deletions lib/web/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,12 +465,9 @@ func checkResourceUpdate(ctx context.Context, payloadResourceName, resourceName

// ExtractResourceAndValidate extracts resource information from given string and validates basic fields.
func ExtractResourceAndValidate(yaml string) (*services.UnknownResource, error) {
var unknownRes services.UnknownResource
reader := strings.NewReader(yaml)
decoder := kyaml.NewYAMLOrJSONDecoder(reader, 32*1024)

if err := decoder.Decode(&unknownRes); err != nil {
return nil, trace.BadParameter("not a valid resource declaration")
unknownRes, err := extractResource(yaml)
if err != nil {
return nil, trace.Wrap(err)
}

if err := unknownRes.Metadata.CheckAndSetDefaults(); err != nil {
Expand All @@ -480,6 +477,18 @@ func ExtractResourceAndValidate(yaml string) (*services.UnknownResource, error)
return &unknownRes, nil
}

func extractResource(yaml string) (services.UnknownResource, error) {
var unknownRes services.UnknownResource
reader := strings.NewReader(yaml)
decoder := kyaml.NewYAMLOrJSONDecoder(reader, 32*1024)

if err := decoder.Decode(&unknownRes); err != nil {
return services.UnknownResource{}, trace.BadParameter("not a valid resource declaration")
}

return unknownRes, nil
}

func convertListResourcesRequest(r *http.Request, kind string) (*proto.ListResourcesRequest, error) {
values := r.URL.Query()

Expand Down
114 changes: 114 additions & 0 deletions lib/web/yaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package web

import (
"net/http"

yaml "github.com/ghodss/yaml"
"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"

accessmonitoringrulesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accessmonitoringrules/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/services"
)

type yamlParseRequest struct {
YAML string `json:"yaml"`
}

type yamlParseResponse struct {
Resource interface{} `json:"resource"`
}

type yamlStringifyResponse struct {
YAML string `json:"yaml"`
}

func (h *Handler) yamlParse(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
kind := params.ByName("kind")
if len(kind) == 0 {
return nil, trace.BadParameter("query param %q is required", "kind")
}

var req yamlParseRequest
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}

switch kind {
case types.KindAccessMonitoringRule:
resource, err := ConvertYAMLToAccessMonitoringRuleResource(req.YAML)
if err != nil {
return nil, trace.Wrap(err)
}

return yamlParseResponse{Resource: resource}, nil

default:
return nil, trace.NotImplemented("parsing YAML for kind %q is not supported", kind)
}
}

func (h *Handler) yamlStringify(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
kind := params.ByName("kind")
if len(kind) == 0 {
return nil, trace.BadParameter("query param %q is required", "kind")
}

var resource interface{}

switch kind {
case types.KindAccessMonitoringRule:
var req struct {
Resource *accessmonitoringrulesv1.AccessMonitoringRule `json:"resource"`
}
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
resource = req.Resource

default:
return nil, trace.NotImplemented("YAML stringifying for kind %q is not supported", kind)
}

data, err := yaml.Marshal(resource)
if err != nil {
return nil, trace.Wrap(err)
}
return yamlStringifyResponse{YAML: string(data)}, nil
}

func ConvertYAMLToAccessMonitoringRuleResource(yaml string) (*accessmonitoringrulesv1.AccessMonitoringRule, error) {
extractedRes, err := extractResource(yaml)
if err != nil {
return nil, trace.Wrap(err)
}
if extractedRes.Kind != types.KindAccessMonitoringRule {
return nil, trace.BadParameter("resource kind %q is invalid", extractedRes.Kind)
}
resource, err := services.UnmarshalAccessMonitoringRule(extractedRes.Raw)
if err != nil {
return nil, trace.Wrap(err)
}

return resource, nil
}
195 changes: 195 additions & 0 deletions lib/web/yaml_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package web

import (
"context"
"encoding/json"
"testing"

"github.com/stretchr/testify/require"

accessmonitoringrulesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accessmonitoringrules/v1"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
"github.com/gravitational/teleport/api/types"
)

const validAccessMonitoringRuleYaml = `kind: access_monitoring_rule
metadata:
name: foo
spec:
condition: some-condition
notification:
name: mattermost
recipients:
- apple
subjects:
- access_request
version: v1
`

func getAccessMonitoringRuleResource() *accessmonitoringrulesv1.AccessMonitoringRule {
return &accessmonitoringrulesv1.AccessMonitoringRule{
Kind: types.KindAccessMonitoringRule,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: "foo",
},
Spec: &accessmonitoringrulesv1.AccessMonitoringRuleSpec{
Subjects: []string{types.KindAccessRequest},
Condition: "some-condition",
Notification: &accessmonitoringrulesv1.Notification{
Name: "mattermost",
Recipients: []string{"apple"},
},
},
}
}

func TestYAMLParse_Valid(t *testing.T) {
t.Parallel()

env := newWebPack(t, 1)
proxy := env.proxies[0]
pack := proxy.authPack(t, "[email protected]", nil)

endpoint := pack.clt.Endpoint("webapi", "yaml", "parse", types.KindAccessMonitoringRule)
re, err := pack.clt.PostJSON(context.Background(), endpoint, yamlParseRequest{
YAML: validAccessMonitoringRuleYaml,
})
require.NoError(t, err)

var endpointResp yamlParseResponse
require.NoError(t, json.Unmarshal(re.Bytes(), &endpointResp))

expectedResource := getAccessMonitoringRuleResource()

// Can't cast a unmarshaled interface{} into the expected type, so
// we are transforming the expected type to the same type as the
// one we got as a response.
b, err := json.Marshal(yamlParseResponse{Resource: expectedResource})
require.NoError(t, err)
var expectedResp yamlParseResponse
require.NoError(t, json.Unmarshal(b, &expectedResp))

require.Equal(t, expectedResp.Resource, endpointResp.Resource)
}

func TestYAMLParse_Errors(t *testing.T) {
t.Parallel()

env := newWebPack(t, 1)
proxy := env.proxies[0]
pack := proxy.authPack(t, "[email protected]", nil)

testCases := []struct {
desc string
yaml string
kind string
}{
{
desc: "unsupported kind",
yaml: validAccessMonitoringRuleYaml,
kind: "something-random",
},
{
desc: "missing kind",
yaml: validAccessMonitoringRuleYaml,
kind: "",
},
{
desc: "invalid yaml",
yaml: "// 232@#$",
kind: types.KindAccessMonitoringRule,
},
{
desc: "invalid empty yaml",
yaml: "",
kind: types.KindAccessMonitoringRule,
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
endpoint := pack.clt.Endpoint("webapi", "yaml", "parse", tc.kind)
_, err := pack.clt.PostJSON(context.Background(), endpoint, yamlParseRequest{
YAML: tc.yaml,
})

require.Error(t, err)
})
}
}

func TestYAMLStringify_Valid(t *testing.T) {
t.Parallel()

env := newWebPack(t, 1)
proxy := env.proxies[0]
pack := proxy.authPack(t, "[email protected]", nil)

req := struct {
Resource *accessmonitoringrulesv1.AccessMonitoringRule `json:"resource"`
}{
Resource: getAccessMonitoringRuleResource(),
}

endpoint := pack.clt.Endpoint("webapi", "yaml", "stringify", types.KindAccessMonitoringRule)
re, err := pack.clt.PostJSON(context.Background(), endpoint, req)
require.NoError(t, err)

var resp yamlStringifyResponse
require.NoError(t, json.Unmarshal(re.Bytes(), &resp))
require.Equal(t, validAccessMonitoringRuleYaml, resp.YAML)
}

func TestYAMLStringify_Errors(t *testing.T) {
t.Parallel()

env := newWebPack(t, 1)
proxy := env.proxies[0]
pack := proxy.authPack(t, "[email protected]", nil)

testCases := []struct {
desc string
kind string
}{
{
desc: "unsupported kind",
kind: "something-random",
},
{
desc: "missing kind",
kind: "",
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
endpoint := pack.clt.Endpoint("webapi", "yaml", "stringify", tc.kind)
_, err := pack.clt.PostJSON(context.Background(), endpoint, struct {
Resource *accessmonitoringrulesv1.AccessMonitoringRule `json:"resource"`
}{
Resource: getAccessMonitoringRuleResource(),
})

require.Error(t, err)
})
}
}

0 comments on commit 7500631

Please sign in to comment.