diff --git a/lib/service/connect.go b/lib/service/connect.go index 0aea4219df3f3..74bb4fb5c2696 100644 --- a/lib/service/connect.go +++ b/lib/service/connect.go @@ -55,8 +55,10 @@ func (process *TeleportProcess) reconnectToAuthService(role teleport.Role) (*Con // if connected and client is present, make sure the connector's // client works, by using call that should succeed at all times if connector.Client != nil { - _, err = connector.Client.GetNamespace(defaults.Namespace) + pingResponse, err := connector.Client.Ping(process.ExitContext()) if err == nil { + process.setClusterFeatures(pingResponse.GetServerFeatures()) + process.log.Infof("%v: features loaded from auth server: %+v", role, pingResponse.GetServerFeatures()) return connector, nil } process.log.Debugf("Connected client %v failed to execute test call: %v. Node or proxy credentials are out of sync.", role, err) diff --git a/lib/service/service.go b/lib/service/service.go index 4f2f1bd2eb599..f8773be2faa4c 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -47,6 +47,7 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/client/webclient" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth" @@ -294,6 +295,9 @@ type TeleportProcess struct { // appDependCh is used by application service in single process mode to block // until auth and reverse tunnel servers are ready. appDependCh chan Event + + // clusterFeatures contain flags for supported and unsupported features. + clusterFeatures proto.Features } type keyPairKey struct { @@ -356,6 +360,22 @@ func (process *TeleportProcess) addConnector(connector *Connector) { process.connectors[connector.ClientIdentity.ID.Role] = connector } +func (process *TeleportProcess) setClusterFeatures(features *proto.Features) { + process.Lock() + defer process.Unlock() + + if features != nil { + process.clusterFeatures = *features + } +} + +func (process *TeleportProcess) getClusterFeatures() proto.Features { + process.Lock() + defer process.Unlock() + + return process.clusterFeatures +} + // GetIdentity returns the process identity (credentials to the auth server) for a given // teleport Role. A teleport process can have any combination of 3 roles: auth, node, proxy // and they have their own identities @@ -2527,23 +2547,25 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { return trace.Wrap(err) } } + webHandler, err = web.NewHandler( web.Config{ - Proxy: tsrv, - AuthServers: cfg.AuthServers[0], - DomainName: cfg.Hostname, - ProxyClient: conn.Client, - ProxySSHAddr: proxySSHAddr, - ProxyWebAddr: cfg.Proxy.WebAddr, - ProxySettings: proxySettings, - CipherSuites: cfg.CipherSuites, - FIPS: cfg.FIPS, - AccessPoint: accessPoint, - Emitter: streamEmitter, - PluginRegistry: process.PluginRegistry, - HostUUID: process.Config.HostUUID, - Context: process.ExitContext(), - StaticFS: fs, + Proxy: tsrv, + AuthServers: cfg.AuthServers[0], + DomainName: cfg.Hostname, + ProxyClient: conn.Client, + ProxySSHAddr: proxySSHAddr, + ProxyWebAddr: cfg.Proxy.WebAddr, + ProxySettings: proxySettings, + CipherSuites: cfg.CipherSuites, + FIPS: cfg.FIPS, + AccessPoint: accessPoint, + Emitter: streamEmitter, + PluginRegistry: process.PluginRegistry, + HostUUID: process.Config.HostUUID, + Context: process.ExitContext(), + StaticFS: fs, + ClusterFeatures: process.getClusterFeatures(), }) if err != nil { return trace.Wrap(err) diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index c5e57a7d14970..d8906cd66d2b2 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -36,6 +36,7 @@ import ( "time" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/client/webclient" "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" @@ -84,6 +85,9 @@ type Handler struct { // sshPort specifies the SSH proxy port extracted // from configuration sshPort string + + // clusterFeatures contain flags for supported and unsupported features. + clusterFeatures proto.Features } // HandlerOption is a functional argument - an option that can be passed @@ -157,6 +161,9 @@ type Config struct { // in the cache before getting purged after it has expired. // Defaults to cachedSessionLingeringThreshold if unspecified. cachedSessionLingeringThreshold *time.Duration + + // ClusterFeatures contains flags for supported/unsupported features. + ClusterFeatures proto.Features } type RewritingHandler struct { @@ -197,9 +204,10 @@ func (h *RewritingHandler) Close() error { func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) { const apiPrefix = "/" + teleport.WebAPIVersion h := &Handler{ - cfg: cfg, - log: newPackageLogger(), - clock: clockwork.NewRealClock(), + cfg: cfg, + log: newPackageLogger(), + clock: clockwork.NewRealClock(), + clusterFeatures: cfg.ClusterFeatures, } for _, o := range opts { @@ -284,7 +292,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) { h.GET("/webapi/sites/:site/namespaces/:namespace/nodes", h.WithClusterAuth(h.siteNodesGet)) // Get applications. - h.GET("/webapi/sites/:site/apps", h.WithClusterAuth(h.siteAppsGet)) + h.GET("/webapi/sites/:site/apps", h.WithClusterAuth(h.clusterAppsGet)) // active sessions handlers h.GET("/webapi/sites/:site/namespaces/:namespace/connect", h.WithClusterAuth(h.siteNodeConnect)) // connect to an active session (via websocket) @@ -305,6 +313,12 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) { // web context h.GET("/webapi/sites/:site/context", h.WithClusterAuth(h.getUserContext)) + // Database access handlers. + h.GET("/webapi/sites/:site/databases", h.WithClusterAuth(h.clusterDatabasesGet)) + + // Kube access handlers. + h.GET("/webapi/sites/:site/kubernetes", h.WithClusterAuth(h.clusterKubesGet)) + // OIDC related callback handlers h.GET("/webapi/oidc/login/web", h.WithRedirect(h.oidcLoginWeb)) h.GET("/webapi/oidc/callback", h.WithRedirect(h.oidcCallback)) @@ -501,7 +515,7 @@ func (h *Handler) getUserContext(w http.ResponseWriter, r *http.Request, p httpr return nil, trace.Wrap(err) } - userContext, err := ui.NewUserContext(user, roleset) + userContext, err := ui.NewUserContext(user, roleset, h.clusterFeatures) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 8e8557f61d66e..145e2278ecaf0 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -1806,6 +1806,89 @@ func (m *testModules) Features() modules.Features { } } +func TestClusterDatabasesGet(t *testing.T) { + env := newWebPack(t, 1) + + proxy := env.proxies[0] + pack := proxy.authPack(t, "test-user@example.com") + + endpoint := pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "databases") + re, err := pack.clt.Get(context.Background(), endpoint, url.Values{}) + require.NoError(t, err) + + // No db registered. + dbs := []ui.Database{} + require.NoError(t, json.Unmarshal(re.Bytes(), &dbs)) + require.Len(t, dbs, 0) + + // Register a database. + db := types.NewDatabaseServerV3("test-db-name", map[string]string{"test-field": "test-value"}, types.DatabaseServerSpecV3{ + Description: "test-description", + Protocol: "test-protocol", + URI: "test-uri", + Hostname: "test-hostname", + HostID: "test-hostID", + }) + + _, err = env.server.Auth().UpsertDatabaseServer(context.Background(), db) + require.NoError(t, err) + + re, err = pack.clt.Get(context.Background(), endpoint, url.Values{}) + require.NoError(t, err) + + dbs = []ui.Database{} + require.NoError(t, json.Unmarshal(re.Bytes(), &dbs)) + require.Len(t, dbs, 1) + require.EqualValues(t, ui.Database{ + Name: "test-db-name", + Desc: "test-description", + Protocol: "test-protocol", + Type: types.DatabaseTypeSelfHosted, + Labels: []ui.Label{{Name: "test-field", Value: "test-value"}}, + }, dbs[0]) +} + +func TestClusterKubesGet(t *testing.T) { + env := newWebPack(t, 1) + + proxy := env.proxies[0] + pack := proxy.authPack(t, "test-user@example.com") + + endpoint := pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "kubernetes") + re, err := pack.clt.Get(context.Background(), endpoint, url.Values{}) + require.NoError(t, err) + + // No kube registered. + kbs := []ui.Kube{} + require.NoError(t, json.Unmarshal(re.Bytes(), &kbs)) + require.Len(t, kbs, 0) + + // Register a kube service. + err = env.server.Auth().UpsertKubeService(context.Background(), &services.ServerV2{ + Metadata: services.Metadata{Name: "test-kube"}, + Kind: services.KindKubeService, + Version: services.V2, + Spec: services.ServerSpecV2{ + KubernetesClusters: []*services.KubernetesCluster{{ + Name: "test-kube-name", + StaticLabels: map[string]string{"test-field": "test-value"}, + }}, + }, + }) + require.NoError(t, err) + + re, err = pack.clt.Get(context.Background(), endpoint, url.Values{}) + require.NoError(t, err) + + kbs = []ui.Kube{} + require.NoError(t, json.Unmarshal(re.Bytes(), &kbs)) + require.Len(t, kbs, 1) + require.EqualValues(t, ui.Kube{ + Name: "test-kube-name", + Labels: []ui.Label{{Name: "test-field", Value: "test-value"}}, + }, kbs[0]) +} + // TestApplicationAccessDisabled makes sure application access can be disabled // via modules. func TestApplicationAccessDisabled(t *testing.T) { diff --git a/lib/web/apps.go b/lib/web/apps.go index 325f40717d4ea..33f1bad9ca62c 100644 --- a/lib/web/apps.go +++ b/lib/web/apps.go @@ -38,8 +38,8 @@ import ( "github.com/julienschmidt/httprouter" ) -// siteAppsGet returns a list of applications in a form the UI can present. -func (h *Handler) siteAppsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) { +// clusterAppsGet returns a list of applications in a form the UI can present. +func (h *Handler) clusterAppsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) { appClusterName := p.ByName("site") // Get a list of application servers. diff --git a/lib/web/servers.go b/lib/web/servers.go new file mode 100644 index 0000000000000..aebc026bb2b36 --- /dev/null +++ b/lib/web/servers.go @@ -0,0 +1,59 @@ +/** + * 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 ( + "net/http" + + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/reversetunnel" + "github.com/gravitational/teleport/lib/web/ui" + "github.com/gravitational/trace" + "github.com/julienschmidt/httprouter" +) + +// clusterKubesGet returns a list of kube clusters in a form the UI can present. +func (h *Handler) clusterKubesGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) { + clt, err := ctx.GetUserClient(site) + if err != nil { + return nil, trace.Wrap(err) + } + + // Get a list of kube servers. + kubeServers, err := clt.GetKubeServices(r.Context()) + if err != nil { + return nil, trace.Wrap(err) + } + + return ui.MakeKubes(h.auth.clusterName, kubeServers), nil +} + +// clusterDatabasesGet returns a list of db servers in a form the UI can present. +func (h *Handler) clusterDatabasesGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) { + clt, err := ctx.GetUserClient(site) + if err != nil { + return nil, trace.Wrap(err) + } + + // Get a list of database servers. + dbServers, err := clt.GetDatabaseServers(r.Context(), defaults.Namespace) + if err != nil { + return nil, trace.Wrap(err) + } + + return ui.MakeDatabases(h.auth.clusterName, dbServers), nil +} diff --git a/lib/web/ui/app.go b/lib/web/ui/app.go index 3d8439725c6fe..4b4d9a1b040ba 100644 --- a/lib/web/ui/app.go +++ b/lib/web/ui/app.go @@ -18,18 +18,11 @@ package ui import ( "fmt" + "sort" "github.com/gravitational/teleport/lib/services" ) -// AppLabel describes an application label -type AppLabel struct { - // Name is this label name - Name string `json:"name"` - // Value is this label value - Value string `json:"value"` -} - // App describes an application type App struct { // Name is the name of the application. @@ -43,7 +36,7 @@ type App struct { // ClusterID is this app cluster ID ClusterID string `json:"clusterId"` // Labels is a map of static labels associated with an application. - Labels []AppLabel `json:"labels"` + Labels []Label `json:"labels"` } // MakeApps creates server application objects @@ -53,13 +46,16 @@ func MakeApps(localClusterName string, localProxyDNSName string, appClusterName teleApps := server.GetApps() for _, teleApp := range teleApps { fqdn := AssembleAppFQDN(localClusterName, localProxyDNSName, appClusterName, teleApp) - labels := []AppLabel{} + labels := []Label{} for name, value := range teleApp.StaticLabels { - labels = append(labels, AppLabel{ + labels = append(labels, Label{ Name: name, Value: value, }) } + + sort.Sort(sortedLabels(labels)) + result = append(result, App{ Name: teleApp.Name, URI: teleApp.URI, diff --git a/lib/web/ui/server.go b/lib/web/ui/server.go index c8dcf90ab1392..6ff14e6356859 100644 --- a/lib/web/ui/server.go +++ b/lib/web/ui/server.go @@ -19,6 +19,7 @@ package ui import ( "sort" + "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/services" ) @@ -96,3 +97,93 @@ func MakeServers(clusterName string, servers []services.Server) []Server { return uiServers } + +// Kube describes a kube cluster. +type Kube struct { + // Name is the name of the kube cluster. + Name string `json:"name"` + // Labels is a map of static and dynamic labels associated with an kube cluster. + Labels []Label `json:"labels"` +} + +// MakeKubes creates ui kube objects and returns a list.. +func MakeKubes(clusterName string, servers []types.Server) []Kube { + uiKubeClusters := []Kube{} + for _, server := range servers { + // Process each kube cluster. + for _, cluster := range server.GetKubernetesClusters() { + uiLabels := []Label{} + + for name, value := range cluster.StaticLabels { + uiLabels = append(uiLabels, Label{ + Name: name, + Value: value, + }) + } + + for name, cmd := range cluster.DynamicLabels { + uiLabels = append(uiLabels, Label{ + Name: name, + Value: cmd.GetResult(), + }) + } + + sort.Sort(sortedLabels(uiLabels)) + + uiKubeClusters = append(uiKubeClusters, Kube{ + Name: cluster.Name, + Labels: uiLabels, + }) + } + } + + return uiKubeClusters +} + +// Database describes a database server. +type Database struct { + // Name is the name of the database. + Name string `json:"name"` + // Desc is the database description. + Desc string `json:"desc"` + // Protocol is the database description. + Protocol string `json:"protocol"` + // Type is the database type, self-hosted or cloud-hosted. + Type string `json:"type"` + // Labels is a map of static and dynamic labels associated with an database. + Labels []Label `json:"labels"` +} + +// MakeDatabases creates server database objects. +func MakeDatabases(clusterName string, servers []types.DatabaseServer) []Database { + uiServers := make([]Database, 0, len(servers)) + for _, server := range servers { + uiLabels := []Label{} + + for name, value := range server.GetStaticLabels() { + uiLabels = append(uiLabels, Label{ + Name: name, + Value: value, + }) + } + + for name, cmd := range server.GetDynamicLabels() { + uiLabels = append(uiLabels, Label{ + Name: name, + Value: cmd.GetResult(), + }) + } + + sort.Sort(sortedLabels(uiLabels)) + + uiServers = append(uiServers, Database{ + Name: server.GetName(), + Desc: server.GetDescription(), + Protocol: server.GetProtocol(), + Type: server.GetType(), + Labels: uiLabels, + }) + } + + return uiServers +} diff --git a/lib/web/ui/usercontext.go b/lib/web/ui/usercontext.go index c4a23365306e4..6219b98590f3b 100644 --- a/lib/web/ui/usercontext.go +++ b/lib/web/ui/usercontext.go @@ -17,6 +17,8 @@ limitations under the License. package ui import ( + "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" @@ -66,6 +68,10 @@ type userACL struct { Nodes access `json:"nodes"` // AppServers defines access to application servers AppServers access `json:"appServers"` + // DBServers defines access to database servers. + DBServers access `json:"dbServers"` + // KubeServers defines access to kubernetes servers. + KubeServers access `json:"kubeServers"` // SSH defines access to servers SSHLogins []string `json:"sshLogins"` // AccessRequests defines access to access requests @@ -166,7 +172,7 @@ func getAccessStrategy(roleset services.RoleSet) accessStrategy { } // NewUserContext returns user context -func NewUserContext(user services.User, userRoles services.RoleSet) (*UserContext, error) { +func NewUserContext(user services.User, userRoles services.RoleSet, features proto.Features) (*UserContext, error) { ctx := &services.Context{User: user} sessionAccess := newAccess(userRoles, ctx, services.KindSession) roleAccess := newAccess(userRoles, ctx, services.KindRole) @@ -177,8 +183,14 @@ func NewUserContext(user services.User, userRoles services.RoleSet) (*UserContex tokenAccess := newAccess(userRoles, ctx, services.KindToken) nodeAccess := newAccess(userRoles, ctx, services.KindNode) appServerAccess := newAccess(userRoles, ctx, services.KindAppServer) + dbServerAccess := newAccess(userRoles, ctx, types.KindDatabaseServer) + kubeServerAccess := newAccess(userRoles, ctx, types.KindKubeService) requestAccess := newAccess(userRoles, ctx, services.KindAccessRequest) - billingAccess := newAccess(userRoles, ctx, services.KindBilling) + + var billingAccess access + if features.Cloud { + billingAccess = newAccess(userRoles, ctx, services.KindBilling) + } logins := getLogins(userRoles) accessStrategy := getAccessStrategy(userRoles) @@ -186,6 +198,8 @@ func NewUserContext(user services.User, userRoles services.RoleSet) (*UserContex acl := userACL{ AccessRequests: requestAccess, AppServers: appServerAccess, + DBServers: dbServerAccess, + KubeServers: kubeServerAccess, AuthConnectors: authConnectors, TrustedClusters: trustedClusterAccess, Sessions: sessionAccess, diff --git a/lib/web/ui/usercontext_test.go b/lib/web/ui/usercontext_test.go index f3515179a2eb7..8743635cbbbe3 100644 --- a/lib/web/ui/usercontext_test.go +++ b/lib/web/ui/usercontext_test.go @@ -3,6 +3,7 @@ package ui import ( "testing" + "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/services" "gopkg.in/check.v1" @@ -46,6 +47,10 @@ func (s *UserContextSuite) TestNewUserContext(c *check.C) { Resources: []string{services.KindTrustedCluster}, Verbs: services.RW(), }, + { + Resources: []string{services.KindBilling}, + Verbs: services.RO(), + }, }) // set some logins @@ -54,7 +59,7 @@ func (s *UserContextSuite) TestNewUserContext(c *check.C) { role2.SetLogins(services.Allow, []string{"d"}) roleSet := []services.Role{role1, role2} - userContext, err := NewUserContext(user, roleSet) + userContext, err := NewUserContext(user, roleSet, proto.Features{}) c.Assert(err, check.IsNil) allowed := access{true, true, true, true, true} @@ -65,6 +70,8 @@ func (s *UserContextSuite) TestNewUserContext(c *check.C) { c.Assert(userContext.ACL.AuthConnectors, check.DeepEquals, allowed) c.Assert(userContext.ACL.TrustedClusters, check.DeepEquals, allowed) c.Assert(userContext.ACL.AppServers, check.DeepEquals, denied) + c.Assert(userContext.ACL.DBServers, check.DeepEquals, denied) + c.Assert(userContext.ACL.KubeServers, check.DeepEquals, denied) c.Assert(userContext.ACL.Events, check.DeepEquals, denied) c.Assert(userContext.ACL.Sessions, check.DeepEquals, denied) c.Assert(userContext.ACL.Roles, check.DeepEquals, denied) @@ -77,13 +84,18 @@ func (s *UserContextSuite) TestNewUserContext(c *check.C) { Type: services.RequestStrategyOptional, Prompt: "", }) + c.Assert(userContext.ACL.Billing, check.DeepEquals, denied) // test local auth type c.Assert(userContext.AuthType, check.Equals, authLocal) // test sso auth type user.Spec.GithubIdentities = []services.ExternalIdentity{{ConnectorID: "foo", Username: "bar"}} - userContext, err = NewUserContext(user, roleSet) + userContext, err = NewUserContext(user, roleSet, proto.Features{}) c.Assert(err, check.IsNil) c.Assert(userContext.AuthType, check.Equals, authSSO) + + userContext, err = NewUserContext(user, roleSet, proto.Features{Cloud: true}) + c.Assert(err, check.IsNil) + c.Assert(userContext.ACL.Billing, check.DeepEquals, access{true, true, false, false, false}) }