mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
Add yaml parsing and yamilfying access monitoring rule in webapi (#40936)
* Add yaml parsing and yamilfying access monitoring rule in webapi * Extract resource without CheckAndSetDefault validator
This commit is contained in:
@@ -873,6 +873,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))
|
||||
|
||||
+15
-6
@@ -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 {
|
||||
@@ -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()
|
||||
|
||||
|
||||
+114
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user