mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 16:53:57 +00:00
b236f24ffa
Updates tctl edit and the web ui to use the new UpdateRole RPC which uses optimistic locking to enforce that concurrent modifications to a role are not possible. A few small improvements were also done on the github connector tests which the role tests were based on. Contributes to #30416.
531 lines
18 KiB
Go
531 lines
18 KiB
Go
/*
|
|
Copyright 2021 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/julienschmidt/httprouter"
|
|
kyaml "k8s.io/apimachinery/pkg/util/yaml"
|
|
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
kubeproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/kube/v1"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/lib/auth"
|
|
"github.com/gravitational/teleport/lib/client"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/httplib"
|
|
"github.com/gravitational/teleport/lib/reversetunnelclient"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/web/ui"
|
|
)
|
|
|
|
// checkAccessToRegisteredResource checks if calling user has access to at least one registered resource.
|
|
func (h *Handler) checkAccessToRegisteredResource(w http.ResponseWriter, r *http.Request, p httprouter.Params, c *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
|
|
// Get a client to the Auth Server with the logged in user's identity. The
|
|
// identity of the logged in user is used to fetch the list of resources.
|
|
clt, err := c.GetUserClient(r.Context(), site)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
resourceKinds := []string{types.KindNode, types.KindDatabaseServer, types.KindAppServer, types.KindKubeServer, types.KindWindowsDesktop}
|
|
for _, kind := range resourceKinds {
|
|
res, err := clt.ListResources(r.Context(), proto.ListResourcesRequest{
|
|
ResourceType: kind,
|
|
Limit: 1,
|
|
})
|
|
if err != nil {
|
|
// Access denied error is returned when user does not have permissions
|
|
// to read/list a resource kind which can be ignored as this function is not
|
|
// about checking if user has the right perms.
|
|
if trace.IsAccessDenied(err) {
|
|
continue
|
|
}
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if len(res.Resources) > 0 {
|
|
return checkAccessToRegisteredResourceResponse{
|
|
HasResource: true,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return checkAccessToRegisteredResourceResponse{
|
|
HasResource: false,
|
|
}, nil
|
|
}
|
|
|
|
func (h *Handler) getRolesHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return getRoles(clt)
|
|
}
|
|
|
|
func getRoles(clt resourcesAPIGetter) ([]ui.ResourceItem, error) {
|
|
roles, err := clt.GetRoles(context.TODO())
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return ui.NewRoles(roles)
|
|
}
|
|
|
|
func (h *Handler) deleteRole(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
roleName := params.ByName("name")
|
|
if err := clt.DeleteRole(r.Context(), roleName); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return OK(), nil
|
|
}
|
|
|
|
func (h *Handler) createRoleHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
item, err := CreateResource(r, types.KindRole, services.UnmarshalRole, clt.CreateRole)
|
|
return item, trace.Wrap(err)
|
|
}
|
|
|
|
func (h *Handler) updateRoleHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
item, err := UpdateResource(r, params, types.KindRole, services.UnmarshalRole, clt.UpdateRole)
|
|
return item, trace.Wrap(err)
|
|
}
|
|
|
|
// getPresetRoles returns a list of preset roles expected to be available on
|
|
// this server. These are hard-coded for a given Teleport version, so this
|
|
// should have the same security implications as the Teleport version exposed
|
|
// via the public ping endpoint.
|
|
func (h *Handler) getPresetRoles(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
|
presets := auth.GetPresetRoles()
|
|
return ui.NewRoles(presets)
|
|
}
|
|
|
|
func (h *Handler) getGithubConnectorsHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return getGithubConnectors(r.Context(), clt)
|
|
}
|
|
|
|
func getGithubConnectors(ctx context.Context, clt resourcesAPIGetter) ([]ui.ResourceItem, error) {
|
|
connectors, err := clt.GetGithubConnectors(ctx, true)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return ui.NewGithubConnectors(connectors)
|
|
}
|
|
|
|
func (h *Handler) deleteGithubConnector(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
connectorName := params.ByName("name")
|
|
if err := clt.DeleteGithubConnector(r.Context(), connectorName); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return OK(), nil
|
|
}
|
|
|
|
func (h *Handler) updateGithubConnectorHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
item, err := UpdateResource[types.GithubConnector](r, params, types.KindGithubConnector, services.UnmarshalGithubConnector, clt.UpdateGithubConnector)
|
|
return item, trace.Wrap(err)
|
|
}
|
|
|
|
func (h *Handler) createGithubConnectorHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
item, err := CreateResource(r, types.KindGithubConnector, services.UnmarshalGithubConnector, clt.CreateGithubConnector)
|
|
return item, trace.Wrap(err)
|
|
}
|
|
|
|
func (h *Handler) getTrustedClustersHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return getTrustedClusters(r.Context(), clt)
|
|
}
|
|
|
|
func getTrustedClusters(ctx context.Context, clt resourcesAPIGetter) ([]ui.ResourceItem, error) {
|
|
trustedClusters, err := clt.GetTrustedClusters(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return ui.NewTrustedClusters(trustedClusters)
|
|
}
|
|
|
|
func (h *Handler) deleteTrustedCluster(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
tcName := params.ByName("name")
|
|
if err := clt.DeleteTrustedCluster(r.Context(), tcName); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return OK(), nil
|
|
}
|
|
|
|
func (h *Handler) upsertTrustedClusterHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
|
clt, err := ctx.GetClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
var req ui.ResourceItem
|
|
if err := httplib.ReadJSON(r, &req); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return upsertTrustedCluster(r.Context(), clt, req.Content, r.Method, params)
|
|
}
|
|
|
|
func upsertTrustedCluster(ctx context.Context, clt resourcesAPIGetter, content, httpMethod string, params httprouter.Params) (*ui.ResourceItem, error) {
|
|
get := func(ctx context.Context, name string) (types.Resource, error) {
|
|
return clt.GetTrustedCluster(ctx, name)
|
|
}
|
|
|
|
extractedRes, err := ExtractResourceAndValidate(content)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if extractedRes.Kind != types.KindTrustedCluster {
|
|
return nil, trace.BadParameter("resource kind %q is invalid", extractedRes.Kind)
|
|
}
|
|
|
|
if err := CheckResourceUpsert(ctx, httpMethod, params, extractedRes.Metadata.Name, get); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
tc, err := services.UnmarshalTrustedCluster(extractedRes.Raw)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
_, err = clt.UpsertTrustedCluster(ctx, tc)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return ui.NewResourceItem(tc)
|
|
}
|
|
|
|
// unmarshalFunc is a type signature for an unmarshaling function.
|
|
type unmarshalFunc[T types.Resource] func([]byte, ...services.MarshalOption) (T, error)
|
|
|
|
// CreateResource is a helper function for POST requests from the UI to create a new resource. It will
|
|
// validate the request contains the appropriate items, that the resource attempting to be created is
|
|
// valid. If all validations are satisfied then the creation is attempted. If the resource already exists
|
|
// a [trace.AlreadyExists] error is returned.
|
|
func CreateResource[T types.Resource](r *http.Request, kind string, unmarshalFn unmarshalFunc[T], createFn func(ctx context.Context, r T) (T, error)) (*ui.ResourceItem, error) {
|
|
var req ui.ResourceItem
|
|
if err := httplib.ReadJSON(r, &req); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
extractedRes, err := ExtractResourceAndValidate(req.Content)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if extractedRes.Kind != kind {
|
|
return nil, trace.BadParameter("resource kind %q is invalid", extractedRes.Kind)
|
|
}
|
|
|
|
resource, err := unmarshalFn(extractedRes.Raw)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
created, err := createFn(r.Context(), resource)
|
|
if err != nil {
|
|
if trace.IsAlreadyExists(err) {
|
|
return nil, trace.AlreadyExists("resource with name %q already exists", extractedRes.Metadata.Name)
|
|
}
|
|
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
item, err := ui.NewResourceItem(created)
|
|
return item, trace.Wrap(err)
|
|
}
|
|
|
|
// UpdateResource is a helper function for PUT requests from the UI to update an existing resource. It will
|
|
// validate the request contains the appropriate items, that the resource attempting to be updated is
|
|
// valid. If all validations are satisfied then the update is attempted. If the resource does not exist
|
|
// a [trace.NotFound] error is returned.
|
|
func UpdateResource[T types.Resource](r *http.Request, params httprouter.Params, kind string, unmarshalFn unmarshalFunc[T], updateFn func(ctx context.Context, r T) (T, error)) (*ui.ResourceItem, error) {
|
|
var req ui.ResourceItem
|
|
if err := httplib.ReadJSON(r, &req); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
extractedRes, err := ExtractResourceAndValidate(req.Content)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if extractedRes.Kind != kind {
|
|
return nil, trace.BadParameter("resource kind %q is invalid", extractedRes.Kind)
|
|
}
|
|
|
|
resourceName := params.ByName("name")
|
|
if resourceName == "" {
|
|
return nil, trace.BadParameter("missing resource name")
|
|
}
|
|
|
|
// Error if the user is trying to rename the resource.
|
|
if extractedRes.Metadata.Name != resourceName {
|
|
return nil, trace.BadParameter("resource renaming is not supported, please create a different resource and then delete this one")
|
|
}
|
|
|
|
resource, err := unmarshalFn(extractedRes.Raw)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
updated, err := updateFn(r.Context(), resource)
|
|
if err != nil {
|
|
if trace.IsNotFound(err) {
|
|
return nil, trace.NotFound("resource with name %q does not exist", extractedRes.Metadata.Name)
|
|
}
|
|
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
item, err := ui.NewResourceItem(updated)
|
|
return item, trace.Wrap(err)
|
|
}
|
|
|
|
// getResource tries to retrieve a resource (by name),
|
|
// returning a NotFound error if the resource does not exist.
|
|
type getResource func(context.Context, string) (types.Resource, error)
|
|
|
|
// CheckResourceUpsert checks if the resource can be created or updated, depending on the http method.
|
|
func CheckResourceUpsert(ctx context.Context, httpMethod string, params httprouter.Params, payloadResourceName string, get getResource) error {
|
|
switch httpMethod {
|
|
case http.MethodPost:
|
|
return trace.Wrap(checkResourceCreate(ctx, payloadResourceName, get))
|
|
case http.MethodPut:
|
|
resourceName := params.ByName("name")
|
|
if resourceName == "" {
|
|
return trace.BadParameter("missing resource name")
|
|
}
|
|
return trace.Wrap(checkResourceUpdate(ctx, payloadResourceName, resourceName, get))
|
|
default:
|
|
return trace.NotImplemented("http method %q not expected. this is a bug!", httpMethod)
|
|
}
|
|
}
|
|
|
|
// checkResourceCreate checks if the resource can be created, returning nil if it can.
|
|
func checkResourceCreate(ctx context.Context, payloadResourceName string, get getResource) error {
|
|
// Try to retrieve the resource by name.
|
|
_, err := get(ctx, payloadResourceName)
|
|
|
|
// If no error, then the resource already exists and cannot be created.
|
|
if err == nil {
|
|
return trace.AlreadyExists("resource with name %q already exists", payloadResourceName)
|
|
}
|
|
|
|
// If the error is not found, then the resource does not exist and can be created.
|
|
if trace.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// checkResourceUpdate checks if the resource can be updated, returning nil if it can.
|
|
func checkResourceUpdate(ctx context.Context, payloadResourceName, resourceName string, get getResource) error {
|
|
// Error if the user is trying to rename the resource.
|
|
if payloadResourceName != resourceName {
|
|
return trace.BadParameter("resource renaming is not supported, please create a different resource and then delete this one")
|
|
}
|
|
|
|
// Try to retrieve the resource by name.
|
|
_, err := get(ctx, payloadResourceName)
|
|
|
|
// If no error, then the resource already exists and can be updated.
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// If the error is not found, then the resource does not exist and cannot be updated.
|
|
if trace.IsNotFound(err) {
|
|
return trace.NotFound("resource with name %q does not exist", payloadResourceName)
|
|
}
|
|
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
if err := unknownRes.Metadata.CheckAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &unknownRes, nil
|
|
}
|
|
|
|
func convertListResourcesRequest(r *http.Request, kind string) (*proto.ListResourcesRequest, error) {
|
|
values := r.URL.Query()
|
|
|
|
limit, err := queryLimitAsInt32(values, "limit", defaults.MaxIterationLimit)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
sortBy := types.GetSortByFromString(values.Get("sort"))
|
|
|
|
startKey := values.Get("startKey")
|
|
return &proto.ListResourcesRequest{
|
|
ResourceType: kind,
|
|
Limit: limit,
|
|
StartKey: startKey,
|
|
SortBy: sortBy,
|
|
PredicateExpression: values.Get("query"),
|
|
SearchKeywords: client.ParseSearchKeywords(values.Get("search"), ' '),
|
|
UseSearchAsRoles: values.Get("searchAsRoles") == "yes",
|
|
}, nil
|
|
}
|
|
|
|
// listKubeResources gets a list of kubernetes resources depending on the type of resource.
|
|
func listKubeResources(ctx context.Context, kubeClient kubeproto.KubeServiceClient, values url.Values, site, resourceKind string) (*kubeproto.ListKubernetesResourcesResponse, error) {
|
|
req, err := newKubeListRequest(values, site, resourceKind)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return kubeClient.ListKubernetesResources(ctx, req)
|
|
}
|
|
|
|
// newKubeListRequest parses the request parameters into a ListKubernetesResourcesRequest.
|
|
func newKubeListRequest(values url.Values, site, resourceKind string) (*kubeproto.ListKubernetesResourcesRequest, error) {
|
|
limit, err := queryLimitAsInt32(values, "limit", defaults.MaxIterationLimit)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
sortBy := types.GetSortByFromString(values.Get("sort"))
|
|
|
|
startKey := values.Get("startKey")
|
|
req := &kubeproto.ListKubernetesResourcesRequest{
|
|
ResourceType: resourceKind,
|
|
Limit: limit,
|
|
StartKey: startKey,
|
|
SortBy: &sortBy,
|
|
PredicateExpression: values.Get("query"),
|
|
SearchKeywords: client.ParseSearchKeywords(values.Get("search"), ' '),
|
|
UseSearchAsRoles: values.Get("searchAsRoles") == "yes",
|
|
TeleportCluster: site,
|
|
KubernetesCluster: values.Get("kubeCluster"),
|
|
KubernetesNamespace: values.Get("kubeNamespace"),
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
type listResourcesGetResponse struct {
|
|
// Items is a list of resources retrieved.
|
|
Items interface{} `json:"items"`
|
|
// StartKey is the position to resume search events.
|
|
StartKey string `json:"startKey"`
|
|
// TotalCount is the total count of resources available
|
|
// after filter.
|
|
TotalCount int `json:"totalCount"`
|
|
}
|
|
|
|
type checkAccessToRegisteredResourceResponse struct {
|
|
// HasResource is a flag to indicate if user has any access
|
|
// to a registered resource or not.
|
|
HasResource bool `json:"hasResource"`
|
|
}
|
|
|
|
type resourcesAPIGetter interface {
|
|
// GetRole returns role by name
|
|
GetRole(ctx context.Context, name string) (types.Role, error)
|
|
// GetRoles returns a list of roles
|
|
GetRoles(ctx context.Context) ([]types.Role, error)
|
|
// UpsertRole creates or updates role
|
|
UpsertRole(ctx context.Context, role types.Role) (types.Role, error)
|
|
// UpsertGithubConnector creates or updates a Github connector
|
|
UpsertGithubConnector(ctx context.Context, connector types.GithubConnector) error
|
|
// GetGithubConnectors returns all configured Github connectors
|
|
GetGithubConnectors(ctx context.Context, withSecrets bool) ([]types.GithubConnector, error)
|
|
// GetGithubConnector returns the specified Github connector
|
|
GetGithubConnector(ctx context.Context, id string, withSecrets bool) (types.GithubConnector, error)
|
|
// DeleteGithubConnector deletes the specified Github connector
|
|
DeleteGithubConnector(ctx context.Context, id string) error
|
|
// UpsertTrustedCluster creates or updates a TrustedCluster in the backend.
|
|
UpsertTrustedCluster(ctx context.Context, tc types.TrustedCluster) (types.TrustedCluster, error)
|
|
// GetTrustedCluster returns a single TrustedCluster by name.
|
|
GetTrustedCluster(ctx context.Context, name string) (types.TrustedCluster, error)
|
|
// GetTrustedClusters returns all TrustedClusters in the backend.
|
|
GetTrustedClusters(ctx context.Context) ([]types.TrustedCluster, error)
|
|
// DeleteTrustedCluster removes a TrustedCluster from the backend by name.
|
|
DeleteTrustedCluster(ctx context.Context, name string) error
|
|
// ListResources returns a paginated list of resources.
|
|
ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error)
|
|
}
|