Web UI disconnects (#5276)

* Use fake clock consistently in units tests.
* Split web session management into two interfaces and implement them separately for clear separation
* Split session management into New/Validate to make it aparent where the sessions are created and where existing sessions are managed. Remove ttlmap in favor of a simple map and handle expirations
explicitly.
Add web session management to gRPC server for the cache.

* Reintroduce web sessions APIs under a getter interface.
* Add SubKind to WatchKind for gRPC and add conversions from/to protobuf. Fix web sessions unit tests.
* lib/web: create/insert session context in ValidateSession if the session has not yet been added to session cache.
lib/cache: add event filter for web session in auth cache.
lib/auth: propagate web session subkind in gRPC event.

* Add implicit migrations for legacy web session key path for queries.
* Integrate web token in lib/web
* Add a bearer token when upserting a web session
* Fix tests. Use fake clock wherever possible.

* Converge session cache handling in lib/web

* Clean up and add doc comments where necessary

* Use correct form of sessions/tokens controller for ServerWithRoles. Use fake time in web tests

* Converge the web sessions/tokens handling in lib/auth to match the old behavior w.r.t access checking (e.g. implicit handling of the local user identity).

* Use cached reads and waiters only when necessary. Query sessions/tokens using best-effort - first looking in the cache and falling back to a proxy client

* Properly propagate events about deletes for values with subkind.

* Update to retrofit changes after recent teleport API refactorings

* Update comment on removing legacy code to move the deadline to 7.x

* Do not close the resources on the session when it expires - this beats the purpose of this PR.
Also avoid a race between closing the cached clients and an existing reference to the session by letting the session linger for longer before removing it.

* Move web session/token request structs to the api client proto package

* Only set HTTP fs on the web handler if the UI is enabled

* Properly tear down web session test by releasing resources at the end. Fix the web UI assets configuration by removing DisableUI and instead use the presence of assets (HTTP file system) as an indicator that the web UI has been enabled.

* Decrease the expired session cache clean up threshold to 2m. Only log the expiration error message for errors other than not found

* Add test for terminal disconnect when using two proxies in HA mode
This commit is contained in:
a-palchikov 2021-02-04 16:50:18 +01:00 committed by GitHub
parent 5ce5e1c525
commit 86908cc2f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 6500 additions and 1645 deletions

View file

@ -89,11 +89,21 @@ func EventToGRPC(in types.Event) (*proto.Event, error) {
AccessRequest: r,
}
case *types.WebSessionV2:
if r.GetSubKind() != types.KindAppSession {
return nil, trace.BadParameter("only %v supported", types.KindAppSession)
switch r.GetSubKind() {
case types.KindAppSession:
out.Resource = &proto.Event_AppSession{
AppSession: r,
}
case types.KindWebSession:
out.Resource = &proto.Event_WebSession{
WebSession: r,
}
default:
return nil, trace.BadParameter("only %q supported", types.WebSessionSubKinds)
}
out.Resource = &proto.Event_AppSession{
AppSession: r,
case *types.WebTokenV3:
out.Resource = &proto.Event_WebToken{
WebToken: r,
}
case *types.RemoteClusterV3:
out.Resource = &proto.Event_RemoteCluster{
@ -176,6 +186,12 @@ func EventFromGRPC(in proto.Event) (*types.Event, error) {
} else if r := in.GetAppSession(); r != nil {
out.Resource = r
return &out, nil
} else if r := in.GetWebSession(); r != nil {
out.Resource = r
return &out, nil
} else if r := in.GetWebToken(); r != nil {
out.Resource = r
return &out, nil
} else if r := in.GetRemoteCluster(); r != nil {
out.Resource = r
return &out, nil

File diff suppressed because it is too large Load diff

View file

@ -71,6 +71,10 @@ message Event {
// DatabaseServer is a resource for database servers.
types.DatabaseServerV3 DatabaseServer = 17
[ (gogoproto.jsontag) = "database_server,omitempty" ];
// WebSession is a regular web session.
types.WebSessionV2 WebSession = 18 [ (gogoproto.jsontag) = "web_session,omitempty" ];
// WebToken is a web token.
types.WebTokenV3 WebToken = 19 [ (gogoproto.jsontag) = "web_token,omitempty" ];
}
}
@ -94,6 +98,8 @@ message WatchKind {
// Filter is an optional mapping of custom filter parameters.
// Valid values vary by resource kind.
map<string, string> Filter = 4 [ (gogoproto.jsontag) = "filter,omitempty" ];
// SubKind is a resource subkind to watch
string SubKind = 5 [ (gogoproto.jsontag) = "sub_kind,omitempty" ];
}
// Set of certificates corresponding to a single public key.
@ -384,6 +390,30 @@ message CreateAppSessionResponse {
// DeleteAppSessionRequest contains the parameters used to remove an application web session.
message DeleteAppSessionRequest { string SessionID = 1 [ (gogoproto.jsontag) = "session_id" ]; }
// GetWebSessionResponse contains the requested web session.
message GetWebSessionResponse {
// Session is the web session.
types.WebSessionV2 Session = 1 [ (gogoproto.jsontag) = "session" ];
}
// GetWebSessionsResponse contains all the requested web sessions.
message GetWebSessionsResponse {
// Sessions is a list of web sessions.
repeated types.WebSessionV2 Sessions = 1 [ (gogoproto.jsontag) = "sessions" ];
}
// GetWebTokenResponse contains the requested web token.
message GetWebTokenResponse {
// Token is the web token being requested.
types.WebTokenV3 Token = 1 [ (gogoproto.jsontag) = "token" ];
}
// GetWebTokensResponse contains all the requested web tokens.
message GetWebTokensResponse {
// Tokens is a list of web tokens.
repeated types.WebTokenV3 Tokens = 1 [ (gogoproto.jsontag) = "tokens" ];
}
// GetKubeServicesRequest are the parameters used to request kubernetes services.
message GetKubeServicesRequest {}
@ -739,6 +769,24 @@ service AuthService {
// DeleteAllAppSessions removes all application web sessions.
rpc DeleteAllAppSessions(google.protobuf.Empty) returns (google.protobuf.Empty);
// GetWebSession gets a web session.
rpc GetWebSession(types.GetWebSessionRequest) returns (GetWebSessionResponse);
// GetWebSessions gets all web sessions.
rpc GetWebSessions(google.protobuf.Empty) returns (GetWebSessionsResponse);
// DeleteWebSession deletes a web session.
rpc DeleteWebSession(types.DeleteWebSessionRequest) returns (google.protobuf.Empty);
// DeleteAllWebSessions deletes all web sessions.
rpc DeleteAllWebSessions(google.protobuf.Empty) returns (google.protobuf.Empty);
// GetWebToken gets a web token.
rpc GetWebToken(types.GetWebTokenRequest) returns (GetWebTokenResponse);
// GetWebTokens gets all web tokens.
rpc GetWebTokens(google.protobuf.Empty) returns (GetWebTokensResponse);
// DeleteWebToken deletes a web token.
rpc DeleteWebToken(types.DeleteWebTokenRequest) returns (google.protobuf.Empty);
// DeleteAllWebTokens deletes all web tokens.
rpc DeleteAllWebTokens(google.protobuf.Empty) returns (google.protobuf.Empty);
// UpdateRemoteCluster updates remote cluster
rpc UpdateRemoteCluster(types.RemoteClusterV3) returns (google.protobuf.Empty);

45
api/client/proto/proto.go Normal file
View file

@ -0,0 +1,45 @@
/*
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 proto
import (
"github.com/gravitational/teleport/api/types"
)
// FromWatchKind converts the watch kind value between internal
// and the protobuf format
func FromWatchKind(wk types.WatchKind) WatchKind {
return WatchKind{
Name: wk.Name,
Kind: wk.Kind,
SubKind: wk.SubKind,
LoadSecrets: wk.LoadSecrets,
Filter: wk.Filter,
}
}
// ToWatchKind converts the watch kind value between the protobuf
// and the internal format
func ToWatchKind(wk WatchKind) types.WatchKind {
return types.WatchKind{
Name: wk.Name,
Kind: wk.Kind,
SubKind: wk.SubKind,
LoadSecrets: wk.LoadSecrets,
Filter: wk.Filter,
}
}

150
api/client/sessions.go Normal file
View file

@ -0,0 +1,150 @@
/*
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 client
import (
"context"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/trace"
"github.com/gravitational/trace/trail"
"github.com/golang/protobuf/ptypes/empty"
)
// GetWebSession returns the web session for the specified request.
// Implements ReadAccessPoint
func (c *Client) GetWebSession(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error) {
return c.WebSessions().Get(ctx, req)
}
// WebSessions returns the web sessions controller
func (c *Client) WebSessions() types.WebSessionInterface {
return &webSessions{c: c}
}
// Get returns the web session for the specified request
func (r *webSessions) Get(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error) {
resp, err := r.c.grpc.GetWebSession(ctx, &req)
if err != nil {
return nil, trail.FromGRPC(err)
}
return resp.Session, nil
}
// List returns the list of all web sessions
func (r *webSessions) List(ctx context.Context) ([]types.WebSession, error) {
resp, err := r.c.grpc.GetWebSessions(ctx, &empty.Empty{})
if err != nil {
return nil, trail.FromGRPC(err)
}
out := make([]types.WebSession, 0, len(resp.Sessions))
for _, session := range resp.Sessions {
out = append(out, session)
}
return out, nil
}
// Upsert not implemented: can only be called locally.
func (r *webSessions) Upsert(ctx context.Context, session types.WebSession) error {
return trace.NotImplemented(notImplementedMessage)
}
// Delete deletes the web session specified with the request
func (r *webSessions) Delete(ctx context.Context, req types.DeleteWebSessionRequest) error {
_, err := r.c.grpc.DeleteWebSession(ctx, &req)
if err != nil {
return trail.FromGRPC(err)
}
return nil
}
// DeleteAll deletes all web sessions
func (r *webSessions) DeleteAll(ctx context.Context) error {
_, err := r.c.grpc.DeleteAllWebSessions(ctx, &empty.Empty{})
if err != nil {
return trail.FromGRPC(err)
}
return nil
}
type webSessions struct {
c *Client
}
// GetWebToken returns the web token for the specified request.
// Implements ReadAccessPoint
func (c *Client) GetWebToken(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) {
return c.WebTokens().Get(ctx, req)
}
// WebTokens returns the web tokens controller
func (c *Client) WebTokens() types.WebTokenInterface {
return &webTokens{c: c}
}
// Get returns the web token for the specified request
func (r *webTokens) Get(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) {
resp, err := r.c.grpc.GetWebToken(ctx, &req)
if err != nil {
return nil, trail.FromGRPC(err)
}
return resp.Token, nil
}
// List returns the list of all web tokens
func (r *webTokens) List(ctx context.Context) ([]types.WebToken, error) {
resp, err := r.c.grpc.GetWebTokens(ctx, &empty.Empty{})
if err != nil {
return nil, trail.FromGRPC(err)
}
out := make([]types.WebToken, 0, len(resp.Tokens))
for _, token := range resp.Tokens {
out = append(out, token)
}
return out, nil
}
// Upsert not implemented: can only be called locally.
func (r *webTokens) Upsert(ctx context.Context, token types.WebToken) error {
return trace.NotImplemented(notImplementedMessage)
}
// Delete deletes the web token specified with the request
func (r *webTokens) Delete(ctx context.Context, req types.DeleteWebTokenRequest) error {
_, err := r.c.grpc.DeleteWebToken(ctx, &req)
if err != nil {
return trail.FromGRPC(err)
}
return nil
}
// DeleteAll deletes all web tokens
func (r *webTokens) DeleteAll(ctx context.Context) error {
_, err := r.c.grpc.DeleteAllWebTokens(ctx, &empty.Empty{})
if err != nil {
return trail.FromGRPC(err)
}
return nil
}
type webTokens struct {
c *Client
}
const notImplementedMessage = "not implemented: can only be called by auth locally"

View file

@ -31,13 +31,8 @@ import (
func (c *Client) NewWatcher(ctx context.Context, watch types.Watch) (types.Watcher, error) {
cancelCtx, cancel := context.WithCancel(ctx)
var protoWatch proto.Watch
for _, k := range watch.Kinds {
protoWatch.Kinds = append(protoWatch.Kinds, proto.WatchKind{
Name: k.Name,
Kind: k.Kind,
LoadSecrets: k.LoadSecrets,
Filter: k.Filter,
})
for _, kind := range watch.Kinds {
protoWatch.Kinds = append(protoWatch.Kinds, proto.FromWatchKind(kind))
}
stream, err := c.grpc.WatchEvents(cancelCtx, &protoWatch)
if err != nil {

View file

@ -40,9 +40,12 @@ const (
// disconnected. The max count mirrors ClientAliveCountMax of sshd.
KeepAliveCountMax = 3
// MaxCertDuration limits maximum duration of validity of issued cert
// MaxCertDuration limits maximum duration of validity of issued certificate
MaxCertDuration = 30 * time.Hour
// CertDuration is a default certificate duration.
CertDuration = 12 * time.Hour
// KeepAliveInterval is interval at which Teleport will send keep-alive
// messages to the client. The default interval of 5 minutes (300 seconds) is
// set to help keep connections alive when using AWS NLBs (which have a default

View file

@ -84,6 +84,9 @@ const (
// KindWebSession is a web session resource
KindWebSession = "web_session"
// KindWebToken is a web token resource
KindWebToken = "web_token"
// KindAppSession represents an application specific web session.
KindAppSession = "app_session"
@ -219,6 +222,9 @@ const (
VerbRotate = "rotate"
)
// WebSessionSubKinds lists subkinds of web session resources
var WebSessionSubKinds = []string{KindAppSession, KindWebSession}
const (
// RecordAtNode is the default. Sessions are recorded at Teleport nodes.
RecordAtNode = "node"

View file

@ -18,10 +18,19 @@ package types
import (
"context"
"fmt"
"github.com/gravitational/trace"
)
// String returns text description of this event
func (r Event) String() string {
if r.Type == OpDelete {
return fmt.Sprintf("%v(%v/%v)", r.Type, r.Resource.GetKind(), r.Resource.GetSubKind())
}
return fmt.Sprintf("%v(%v)", r.Type, r.Resource)
}
// Event represents an event that happened in the backend
type Event struct {
// Type is the event type
@ -87,8 +96,12 @@ type Watch struct {
type WatchKind struct {
// Kind is a resource kind to watch
Kind string
// SubKind optionally specifies the subkind of resource to watch.
// Some resource kinds are ambigious like web sessions, subkind in this case
// specifies the type of web session
SubKind string
// Name is an optional specific resource type to watch,
// if specified only the events with a specific resource
// if specified, only the events with the given resource
// name will be sent
Name string
// LoadSecrets specifies whether to load secrets

View file

@ -17,6 +17,7 @@ limitations under the License.
package types
import (
"context"
"fmt"
"time"
@ -25,6 +26,30 @@ import (
"github.com/gravitational/trace"
)
// WebSessionsGetter provides access to web sessions
type WebSessionsGetter interface {
// WebSessions returns the web session manager
WebSessions() WebSessionInterface
}
// WebSessionInterface defines interface to regular web sessions
type WebSessionInterface interface {
// Get returns a web session state for the given request.
Get(ctx context.Context, req GetWebSessionRequest) (WebSession, error)
// List gets all regular web sessions.
List(context.Context) ([]WebSession, error)
// Upsert updates existing or inserts a new web session.
Upsert(ctx context.Context, session WebSession) error
// Delete deletes the web session described by req.
Delete(ctx context.Context, req DeleteWebSessionRequest) error
// DeleteAll removes all web sessions.
DeleteAll(context.Context) error
}
// WebSession stores key and value used to authenticate with SSH
// notes on behalf of user
type WebSession interface {
@ -51,8 +76,6 @@ type WebSession interface {
// BearerToken is a special bearer token used for additional
// bearer authentication
GetBearerToken() string
// SetBearerTokenExpiryTime sets bearer token expiry time
SetBearerTokenExpiryTime(time.Time)
// SetExpiryTime sets session expiry time
SetExpiryTime(time.Time)
// GetBearerTokenExpiryTime - absolute time when token expires
@ -78,6 +101,7 @@ func NewWebSession(name string, kind string, subkind string, spec WebSessionSpec
Metadata: Metadata{
Name: name,
Namespace: defaults.Namespace,
Expires: &spec.Expires,
},
Spec: spec,
}
@ -157,13 +181,16 @@ func (ws *WebSessionV2) CheckAndSetDefaults() error {
if err != nil {
return trace.Wrap(err)
}
if ws.Spec.User == "" {
return trace.BadParameter("missing User")
}
return nil
}
// String returns string representation of the session.
func (ws *WebSessionV2) String() string {
return fmt.Sprintf("WebSession(kind=%v,name=%v,id=%v)", ws.GetKind(), ws.GetUser(), ws.GetName())
return fmt.Sprintf("WebSession(kind=%v/%v,user=%v,id=%v,expires=%v)",
ws.GetKind(), ws.GetSubKind(), ws.GetUser(), ws.GetName(), ws.GetExpiryTime())
}
// SetUser sets user associated with this session
@ -210,11 +237,6 @@ func (ws *WebSessionV2) GetBearerToken() string {
return ws.Spec.BearerToken
}
// SetBearerTokenExpiryTime sets bearer token expiry time
func (ws *WebSessionV2) SetBearerTokenExpiryTime(tm time.Time) {
ws.Spec.BearerTokenExpires = tm
}
// SetExpiryTime sets session expiry time
func (ws *WebSessionV2) SetExpiryTime(tm time.Time) {
ws.Spec.Expires = tm
@ -281,3 +303,234 @@ func (r CreateAppSessionRequest) Check() error {
type DeleteAppSessionRequest struct {
SessionID string `json:"session_id"`
}
// NewWebToken returns a new web token with the given expiration and spec
func NewWebToken(expires time.Time, spec WebTokenSpecV3) WebToken {
return &WebTokenV3{
Kind: KindWebToken,
Version: V3,
Metadata: Metadata{
Name: spec.Token,
Namespace: defaults.Namespace,
Expires: &expires,
},
Spec: spec,
}
}
// WebTokensGetter provides access to web tokens
type WebTokensGetter interface {
// WebTokens returns the tokens manager
WebTokens() WebTokenInterface
}
// WebTokenInterface defines interface for managing web tokens
type WebTokenInterface interface {
// Get returns a token specified by the request.
Get(ctx context.Context, req GetWebTokenRequest) (WebToken, error)
// List gets all web tokens.
List(context.Context) ([]WebToken, error)
// Upsert updates existing or inserts a new web token.
Upsert(ctx context.Context, token WebToken) error
// Delete deletes the web token described by req.
Delete(ctx context.Context, req DeleteWebTokenRequest) error
// DeleteAll removes all web tokens.
DeleteAll(context.Context) error
}
// WebToken is a time-limited unique token bound to a user's session
type WebToken interface {
// Resource represents common properties for all resources.
Resource
// CheckAndSetDefaults checks and set default values for any missing fields.
CheckAndSetDefaults() error
// GetToken returns the token value
GetToken() string
// SetToken sets the token value
SetToken(token string)
// GetUser returns the user the token is bound to
GetUser() string
// SetUser sets the user the token is bound to
SetUser(user string)
// String returns the text representation of this token
String() string
}
var _ WebToken = &WebTokenV3{}
// GetMetadata returns the token metadata
func (r *WebTokenV3) GetMetadata() Metadata {
return r.Metadata
}
// GetKind returns the token resource kind
func (r *WebTokenV3) GetKind() string {
return r.Kind
}
// GetSubKind returns the token resource subkind
func (r *WebTokenV3) GetSubKind() string {
return r.SubKind
}
// SetSubKind sets the token resource subkind
func (r *WebTokenV3) SetSubKind(subKind string) {
r.SubKind = subKind
}
// GetVersion returns the token resource version
func (r *WebTokenV3) GetVersion() string {
return r.Version
}
// GetName returns the token value
func (r *WebTokenV3) GetName() string {
return r.Metadata.Name
}
// SetName sets the token value
func (r *WebTokenV3) SetName(name string) {
r.Metadata.Name = name
}
// GetResourceID returns the token resource ID
func (r *WebTokenV3) GetResourceID() int64 {
return r.Metadata.GetID()
}
// SetResourceID sets the token resource ID
func (r *WebTokenV3) SetResourceID(id int64) {
r.Metadata.SetID(id)
}
// SetTTL sets the token resource TTL (time-to-live) value
func (r *WebTokenV3) SetTTL(clock Clock, ttl time.Duration) {
r.Metadata.SetTTL(clock, ttl)
}
// GetToken returns the token value
func (r *WebTokenV3) GetToken() string {
return r.Spec.Token
}
// SetToken sets the token value
func (r *WebTokenV3) SetToken(token string) {
r.Spec.Token = token
}
// GetUser returns the user this token is bound to
func (r *WebTokenV3) GetUser() string {
return r.Spec.User
}
// SetUser sets the user this token is bound to
func (r *WebTokenV3) SetUser(user string) {
r.Spec.User = user
}
// Expiry returns the token absolute expiration time
func (r *WebTokenV3) Expiry() time.Time {
if r.Metadata.Expires == nil {
return time.Time{}
}
return *r.Metadata.Expires
}
// SetExpiry sets the token absolute expiration time
func (r *WebTokenV3) SetExpiry(t time.Time) {
r.Metadata.Expires = &t
}
// CheckAndSetDefaults validates this token value and sets defaults
func (r *WebTokenV3) CheckAndSetDefaults() error {
if err := r.Metadata.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
if r.Spec.User == "" {
return trace.BadParameter("User required")
}
if r.Spec.Token == "" {
return trace.BadParameter("Token required")
}
return nil
}
// String returns string representation of the token.
func (r *WebTokenV3) String() string {
return fmt.Sprintf("WebToken(kind=%v,user=%v,token=%v,expires=%v)",
r.GetKind(), r.GetUser(), r.GetToken(), r.Expiry())
}
// CheckAndSetDefaults validates the request and sets defaults.
func (r *NewWebSessionRequest) CheckAndSetDefaults() error {
if r.User == "" {
return trace.BadParameter("user name required")
}
if len(r.Roles) == 0 {
return trace.BadParameter("roles required")
}
if len(r.Traits) == 0 {
return trace.BadParameter("traits required")
}
if r.SessionTTL == 0 {
r.SessionTTL = defaults.CertDuration
}
return nil
}
// NewWebSessionRequest defines a request to create a new user
// web session
type NewWebSessionRequest struct {
// User specifies the user this session is bound to
User string
// Roles optionally lists additional user roles
Roles []string
// Traits optionally lists role traits
Traits map[string][]string
// SessionTTL optionally specifies the session time-to-live.
// If left unspecified, the default certificate duration is used.
SessionTTL time.Duration
}
// Check validates the request.
func (r *GetWebSessionRequest) Check() error {
if r.User == "" {
return trace.BadParameter("user name missing")
}
if r.SessionID == "" {
return trace.BadParameter("session ID missing")
}
return nil
}
// Check validates the request.
func (r *DeleteWebSessionRequest) Check() error {
if r.SessionID == "" {
return trace.BadParameter("session ID missing")
}
return nil
}
// Check validates the request.
func (r *GetWebTokenRequest) Check() error {
if r.User == "" {
return trace.BadParameter("user name missing")
}
if r.Token == "" {
return trace.BadParameter("token missing")
}
return nil
}
// Check validates the request.
func (r *DeleteWebTokenRequest) Check() error {
if r.Token == "" {
return trace.BadParameter("token missing")
}
return nil
}

File diff suppressed because it is too large Load diff

View file

@ -1444,3 +1444,63 @@ message KubernetesCluster {
map<string, CommandLabelV2> DynamicLabels = 3
[ (gogoproto.nullable) = false, (gogoproto.jsontag) = "dynamic_labels,omitempty" ];
}
// WebTokenV3 describes a web token. Web tokens are used as a transport to relay bearer tokens
// to the client.
// Initially bound to a web session, these have been factored out into a separate resource to
// enable separate lifecycle management.
message WebTokenV3 {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.stringer) = false;
// Kind is a resource kind
string Kind = 1 [ (gogoproto.jsontag) = "kind" ];
// SubKind is an optional resource sub kind
string SubKind = 2 [ (gogoproto.jsontag) = "sub_kind,omitempty" ];
// Version is the resource version
string Version = 3 [ (gogoproto.jsontag) = "version" ];
// Metadata is resource metadata
Metadata Metadata = 4 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "metadata" ];
// Spec defines the web token
WebTokenSpecV3 Spec = 5 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "spec" ];
}
// WebTokenSpecV3 is a unique time-limited token bound to a user's web session
message WebTokenSpecV3 {
// User specifies the user the token is bound to.
string User = 1 [ (gogoproto.jsontag) = "user" ];
// Token specifies the token's value.
string Token = 2 [ (gogoproto.jsontag) = "token" ];
}
// GetWebSessionRequest describes a request to query a web session
message GetWebSessionRequest {
// User specifies the user the web session is for.
string User = 1 [ (gogoproto.jsontag) = "user" ];
// SessionID specifies the web session ID.
string SessionID = 2 [ (gogoproto.jsontag) = "session_id" ];
}
// DeleteWebSessionRequest describes a request to delete a web session
message DeleteWebSessionRequest {
// User specifies the user the session is bound to
string User = 1 [ (gogoproto.jsontag) = "user" ];
// SessionID specifies the web session ID to delete.
string SessionID = 2 [ (gogoproto.jsontag) = "session_id" ];
}
// GetWebTokenRequest describes a request to query a web token
message GetWebTokenRequest {
// User specifies the user the token is for.
string User = 1 [ (gogoproto.jsontag) = "user" ];
// Token specifies the token to get.
string Token = 2 [ (gogoproto.jsontag) = "token" ];
}
// DeleteWebTokenRequest describes a request to delete a web token
message DeleteWebTokenRequest {
// User specifies the user the token is for.
string User = 1 [ (gogoproto.jsontag) = "user" ];
// Token specifies the token to delete.
string Token = 2 [ (gogoproto.jsontag) = "token" ];
}

View file

@ -163,6 +163,9 @@ const (
// ComponentAppProxy is the application handler within the web proxy service.
ComponentAppProxy = "app:web"
// ComponentWebProxy is the web handler within the web proxy service.
ComponentWebProxy = "web"
// ComponentDiagnostic is a diagnostic service
ComponentDiagnostic = "diag"

View file

@ -118,6 +118,12 @@ type ReadAccessPoint interface {
// GetAppSession gets an application web session.
GetAppSession(context.Context, services.GetAppSessionRequest) (services.WebSession, error)
// GetWebSession gets a web session for the given request
GetWebSession(context.Context, types.GetWebSessionRequest) (types.WebSession, error)
// GetWebToken gets a web token for the given request
GetWebToken(context.Context, types.GetWebTokenRequest) (types.WebToken, error)
// GetRemoteClusters returns a list of remote clusters
GetRemoteClusters(opts ...services.MarshalOption) ([]services.RemoteCluster, error)

View file

@ -31,6 +31,7 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/httplib"
@ -259,7 +260,7 @@ type HandlerWithAuthFunc func(auth ClientI, w http.ResponseWriter, r *http.Reque
func (s *APIServer) withAuth(handler HandlerWithAuthFunc) httprouter.Handle {
const accessDeniedMsg = "auth API: access denied "
return httplib.MakeHandler(func(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
// HTTPS server expects auth context to be set by the auth middleware
// HTTPS server expects auth context to be set by the auth middleware
authContext, err := s.Authorizer.Authorize(r.Context())
if err != nil {
// propagate connection problem error so we can differentiate
@ -682,17 +683,20 @@ func (s *APIServer) deleteToken(auth ClientI, w http.ResponseWriter, r *http.Req
}
func (s *APIServer) deleteWebSession(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
user, sid := p.ByName("user"), p.ByName("sid")
err := auth.DeleteWebSession(user, sid)
user, sessionID := p.ByName("user"), p.ByName("sid")
err := auth.WebSessions().Delete(r.Context(), types.DeleteWebSessionRequest{
User: user,
SessionID: sessionID,
})
if err != nil {
return nil, trace.Wrap(err)
}
return message(fmt.Sprintf("session '%v' for user '%v' deleted", sid, user)), nil
return message(fmt.Sprintf("session %q for user %q deleted", sessionID, user)), nil
}
func (s *APIServer) getWebSession(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
user, sid := p.ByName("user"), p.ByName("sid")
sess, err := auth.GetWebSessionInfo(user, sid)
sess, err := auth.GetWebSessionInfo(r.Context(), user, sid)
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -158,6 +158,18 @@ type Services struct {
events.IAuditLog
}
// GetWebSession returns existing web session described by req.
// Implements ReadAccessPoint
func (r Services) GetWebSession(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error) {
return r.Identity.WebSessions().Get(ctx, req)
}
// GetWebToken returns existing web token described by req.
// Implements ReadAccessPoint
func (r Services) GetWebToken(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) {
return r.Identity.WebTokens().Get(ctx, req)
}
var (
generateRequestsCount = prometheus.NewCounter(
prometheus.CounterOpts{
@ -799,11 +811,15 @@ func (a *Server) PreAuthenticatedSignIn(user string, identity tlsca.Identity) (s
if err != nil {
return nil, trace.Wrap(err)
}
sess, err := a.NewWebSession(user, roles, traits)
sess, err := a.NewWebSession(types.NewWebSessionRequest{
User: user,
Roles: roles,
Traits: traits,
})
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.UpsertWebSession(user, sess); err != nil {
if err := a.upsertWebSession(context.TODO(), user, sess); err != nil {
return nil, trace.Wrap(err)
}
return sess.WithoutSecrets(), nil
@ -861,10 +877,14 @@ func (a *Server) CheckU2FSignResponse(ctx context.Context, user string, response
return a.checkU2F(ctx, user, *response, a.Identity)
}
// ExtendWebSession creates a new web session for a user based on a valid previous sessionID.
// ExtendWebSession creates a new web session for a user based on a valid previous session.
// Additional roles are appended to initial roles if there is an approved access request.
// The new session expiration time will not exceed the expiration time of the old session.
func (a *Server) ExtendWebSession(user, prevSessionID, accessRequestID string, identity tlsca.Identity) (services.WebSession, error) {
prevSession, err := a.GetWebSession(user, prevSessionID)
prevSession, err := a.GetWebSession(context.TODO(), types.GetWebSessionRequest{
User: user,
SessionID: prevSessionID,
})
if err != nil {
return nil, trace.Wrap(err)
}
@ -897,15 +917,18 @@ func (a *Server) ExtendWebSession(user, prevSessionID, accessRequestID string, i
}
}
sess, err := a.NewWebSession(user, roles, traits)
sessionTTL := utils.ToTTL(a.clock, expiresAt)
sess, err := a.NewWebSession(types.NewWebSessionRequest{
User: user,
Roles: roles,
Traits: traits,
SessionTTL: sessionTTL,
})
if err != nil {
return nil, trace.Wrap(err)
}
sess.SetExpiryTime(expiresAt)
bearerTokenTTL := utils.MinTTL(utils.ToTTL(a.clock, expiresAt), BearerTokenTTL)
sess.SetBearerTokenExpiryTime(a.clock.Now().UTC().Add(bearerTokenTTL))
if err := a.UpsertWebSession(user, sess); err != nil {
if err := a.upsertWebSession(context.TODO(), user, sess); err != nil {
return nil, trace.Wrap(err)
}
@ -959,11 +982,15 @@ func (a *Server) CreateWebSession(user string) (services.WebSession, error) {
if err != nil {
return nil, trace.Wrap(err)
}
sess, err := a.NewWebSession(user, u.GetRoles(), u.GetTraits())
sess, err := a.NewWebSession(types.NewWebSessionRequest{
User: user,
Roles: u.GetRoles(),
Traits: u.GetTraits(),
})
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.UpsertWebSession(user, sess); err != nil {
if err := a.upsertWebSession(context.TODO(), user, sess); err != nil {
return nil, trace.Wrap(err)
}
return sess, nil
@ -1468,27 +1495,30 @@ func (a *Server) GetTokens(opts ...services.MarshalOption) (tokens []services.Pr
return tokens, nil
}
func (a *Server) NewWebSession(username string, roles []string, traits wrappers.Traits) (services.WebSession, error) {
user, err := a.GetUser(username, false)
// NewWebSession creates and returns a new web session for the specified request
func (a *Server) NewWebSession(req types.NewWebSessionRequest) (services.WebSession, error) {
user, err := a.GetUser(req.User, false)
if err != nil {
return nil, trace.Wrap(err)
}
checker, err := services.FetchRoles(roles, a.Access, traits)
checker, err := services.FetchRoles(req.Roles, a.Access, req.Traits)
if err != nil {
return nil, trace.Wrap(err)
}
priv, pub, err := a.GetNewKeyPairFromPool()
if err != nil {
return nil, trace.Wrap(err)
}
sessionTTL := checker.AdjustSessionTTL(defaults.CertDuration)
sessionTTL := req.SessionTTL
if sessionTTL == 0 {
sessionTTL = checker.AdjustSessionTTL(defaults.CertDuration)
}
certs, err := a.generateUserCert(certRequest{
user: user,
ttl: sessionTTL,
publicKey: pub,
checker: checker,
traits: traits,
traits: req.Traits,
})
if err != nil {
return nil, trace.Wrap(err)
@ -1503,7 +1533,7 @@ func (a *Server) NewWebSession(username string, roles []string, traits wrappers.
}
bearerTokenTTL := utils.MinTTL(sessionTTL, BearerTokenTTL)
return services.NewWebSession(token, services.KindWebSession, services.KindWebSession, services.WebSessionSpecV2{
User: user.GetName(),
User: req.User,
Priv: priv,
Pub: certs.ssh,
TLSCert: certs.tls,
@ -1513,16 +1543,11 @@ func (a *Server) NewWebSession(username string, roles []string, traits wrappers.
}), nil
}
func (a *Server) UpsertWebSession(user string, sess services.WebSession) error {
return a.Identity.UpsertWebSession(user, sess.GetName(), sess)
}
func (a *Server) GetWebSession(userName string, id string) (services.WebSession, error) {
return a.Identity.GetWebSession(userName, id)
}
func (a *Server) GetWebSessionInfo(userName string, id string) (services.WebSession, error) {
sess, err := a.Identity.GetWebSession(userName, id)
// GetWebSessionInfo returns the web session specified with sessionID for the given user.
// The session is stripped of any authentication details.
// Implements auth.WebUIService
func (a *Server) GetWebSessionInfo(ctx context.Context, user, sessionID string) (services.WebSession, error) {
sess, err := a.Identity.WebSessions().Get(ctx, types.GetWebSessionRequest{User: user, SessionID: sessionID})
if err != nil {
return nil, trace.Wrap(err)
}
@ -1543,10 +1568,6 @@ func (a *Server) DeleteNamespace(namespace string) error {
return a.Presence.DeleteNamespace(namespace)
}
func (a *Server) DeleteWebSession(user string, id string) error {
return trace.Wrap(a.Identity.DeleteWebSession(user, id))
}
// NewWatcher returns a new event watcher. In case of an auth server
// this watcher will return events as seen by the auth server's
// in memory cache, not the backend.
@ -2016,6 +2037,20 @@ func WithClock(clock clockwork.Clock) func(*Server) {
}
}
func (a *Server) upsertWebSession(ctx context.Context, user string, session services.WebSession) error {
if err := a.WebSessions().Upsert(ctx, session); err != nil {
return trace.Wrap(err)
}
token := types.NewWebToken(session.GetBearerTokenExpiryTime(), types.WebTokenSpecV3{
User: session.GetUser(),
Token: session.GetBearerToken(),
})
if err := a.WebTokens().Upsert(ctx, token); err != nil {
return trace.Wrap(err)
}
return nil
}
// authKeepAliver is a keep aliver using auth server directly
type authKeepAliver struct {
sync.RWMutex

View file

@ -28,6 +28,7 @@ import (
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/testauthority"
authority "github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/backend"
@ -147,15 +148,21 @@ func (s *AuthSuite) TestSessions(c *C) {
c.Assert(err, IsNil)
c.Assert(ws, NotNil)
out, err := s.a.GetWebSessionInfo(user, ws.GetName())
out, err := s.a.GetWebSessionInfo(context.TODO(), user, ws.GetName())
c.Assert(err, IsNil)
ws.SetPriv(nil)
fixtures.DeepCompare(c, ws, out)
err = s.a.DeleteWebSession(user, ws.GetName())
err = s.a.WebSessions().Delete(context.TODO(), types.DeleteWebSessionRequest{
User: user,
SessionID: ws.GetName(),
})
c.Assert(err, IsNil)
_, err = s.a.GetWebSession(user, ws.GetName())
_, err = s.a.GetWebSession(context.TODO(), types.GetWebSessionRequest{
User: user,
SessionID: ws.GetName(),
})
c.Assert(trace.IsNotFound(err), Equals, true, Commentf("%#v", err))
}

View file

@ -125,8 +125,8 @@ func (a *ServerWithRoles) hasLocalUserRole(checker services.AccessChecker) bool
return true
}
// AuthenticateWebUser authenticates web user, creates and returns web session
// in case if authentication is successful
// AuthenticateWebUser authenticates web user, creates and returns a web session
// in case authentication is successful
func (a *ServerWithRoles) AuthenticateWebUser(req AuthenticateUserRequest) (services.WebSession, error) {
// authentication request has it's own authentication, however this limits the requests
// types to proxies to make it harder to break
@ -489,6 +489,10 @@ func (a *ServerWithRoles) NewWatcher(ctx context.Context, watch services.Watch)
if err := a.action(defaults.Namespace, services.KindWebSession, services.VerbRead); err != nil {
return nil, trace.Wrap(err)
}
case services.KindWebToken:
if err := a.action(defaults.Namespace, services.KindWebToken, services.VerbRead); err != nil {
return nil, trace.Wrap(err)
}
case services.KindRemoteCluster:
if err := a.action(defaults.Namespace, services.KindRemoteCluster, services.VerbRead); err != nil {
return nil, trace.Wrap(err)
@ -778,6 +782,7 @@ func (a *ServerWithRoles) GetU2FSignRequest(user string, password []byte) (*u2f.
return a.authServer.U2FSignRequest(user, password)
}
// CreateWebSession creates a new web session for the specified user
func (a *ServerWithRoles) CreateWebSession(user string) (services.WebSession, error) {
if err := a.currentUserAction(user); err != nil {
return nil, trace.Wrap(err)
@ -785,6 +790,9 @@ func (a *ServerWithRoles) CreateWebSession(user string) (services.WebSession, er
return a.authServer.CreateWebSession(user)
}
// ExtendWebSession creates a new web session for a user based on a valid previous session.
// Additional roles are appended to initial roles if there is an approved access request.
// The new session expiration time will not exceed the expiration time of the old session.
func (a *ServerWithRoles) ExtendWebSession(user, prevSessionID, accessRequestID string) (services.WebSession, error) {
if err := a.currentUserAction(user); err != nil {
return nil, trace.Wrap(err)
@ -792,18 +800,148 @@ func (a *ServerWithRoles) ExtendWebSession(user, prevSessionID, accessRequestID
return a.authServer.ExtendWebSession(user, prevSessionID, accessRequestID, a.context.Identity.GetIdentity())
}
func (a *ServerWithRoles) GetWebSessionInfo(user string, sid string) (services.WebSession, error) {
// GetWebSessionInfo returns the web session for the given user specified with sid.
// The session is stripped of any authentication details.
// Implements auth.WebUIService
func (a *ServerWithRoles) GetWebSessionInfo(ctx context.Context, user, sessionID string) (services.WebSession, error) {
if err := a.currentUserAction(user); err != nil {
return nil, trace.Wrap(err)
}
return a.authServer.GetWebSessionInfo(user, sid)
return a.authServer.GetWebSessionInfo(ctx, user, sessionID)
}
func (a *ServerWithRoles) DeleteWebSession(user string, sid string) error {
if err := a.currentUserAction(user); err != nil {
// GetWebSession returns the web session specified with req.
// Implements auth.ReadAccessPoint.
func (a *ServerWithRoles) GetWebSession(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error) {
return a.WebSessions().Get(ctx, req)
}
// WebSessions returns the web session manager.
// Implements services.WebSessionsGetter.
func (a *ServerWithRoles) WebSessions() types.WebSessionInterface {
return &webSessionsWithRoles{c: a, ws: a.authServer.WebSessions()}
}
// Get returns the web session specified with req.
func (r *webSessionsWithRoles) Get(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error) {
if err := r.c.currentUserAction(req.User); err != nil {
if err := r.c.action(defaults.Namespace, services.KindWebSession, services.VerbRead); err != nil {
return nil, trace.Wrap(err)
}
}
return r.ws.Get(ctx, req)
}
// List returns the list of all web sessions.
func (r *webSessionsWithRoles) List(ctx context.Context) ([]services.WebSession, error) {
if err := r.c.action(defaults.Namespace, services.KindWebSession, services.VerbList); err != nil {
return nil, trace.Wrap(err)
}
if err := r.c.action(defaults.Namespace, services.KindWebSession, services.VerbRead); err != nil {
return nil, trace.Wrap(err)
}
return r.ws.List(ctx)
}
// Upsert creates a new or updates the existing web session from the specified session.
// TODO(dmitri): this is currently only implemented for local invocations. This needs to be
// moved into a more appropriate API
func (*webSessionsWithRoles) Upsert(ctx context.Context, session services.WebSession) error {
return trace.NotImplemented(notImplementedMessage)
}
// Delete removes the web session specified with req.
func (r *webSessionsWithRoles) Delete(ctx context.Context, req types.DeleteWebSessionRequest) error {
if err := r.c.currentUserAction(req.User); err != nil {
if err := r.c.action(defaults.Namespace, services.KindWebSession, services.VerbDelete); err != nil {
return trace.Wrap(err)
}
}
return r.ws.Delete(ctx, req)
}
// DeleteAll removes all web sessions.
func (r *webSessionsWithRoles) DeleteAll(ctx context.Context) error {
if err := r.c.action(defaults.Namespace, services.KindWebSession, services.VerbList); err != nil {
return trace.Wrap(err)
}
return a.authServer.DeleteWebSession(user, sid)
if err := r.c.action(defaults.Namespace, services.KindWebSession, services.VerbDelete); err != nil {
return trace.Wrap(err)
}
return r.ws.DeleteAll(ctx)
}
// GetWebToken returns the web token specified with req.
// Implements auth.ReadAccessPoint.
func (a *ServerWithRoles) GetWebToken(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) {
return a.WebTokens().Get(ctx, req)
}
type webSessionsWithRoles struct {
c accessChecker
ws types.WebSessionInterface
}
// WebTokens returns the web token manager.
// Implements services.WebTokensGetter.
func (a *ServerWithRoles) WebTokens() types.WebTokenInterface {
return &webTokensWithRoles{c: a, t: a.authServer.WebTokens()}
}
// Get returns the web token specified with req.
func (r *webTokensWithRoles) Get(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) {
if err := r.c.currentUserAction(req.User); err != nil {
if err := r.c.action(defaults.Namespace, services.KindWebToken, services.VerbRead); err != nil {
return nil, trace.Wrap(err)
}
}
return r.t.Get(ctx, req)
}
// List returns the list of all web tokens.
func (r *webTokensWithRoles) List(ctx context.Context) ([]types.WebToken, error) {
if err := r.c.action(defaults.Namespace, services.KindWebToken, services.VerbList); err != nil {
return nil, trace.Wrap(err)
}
return r.t.List(ctx)
}
// Upsert creates a new or updates the existing web token from the specified token.
// TODO(dmitri): this is currently only implemented for local invocations. This needs to be
// moved into a more appropriate API
func (*webTokensWithRoles) Upsert(ctx context.Context, session types.WebToken) error {
return trace.NotImplemented(notImplementedMessage)
}
// Delete removes the web token specified with req.
func (r *webTokensWithRoles) Delete(ctx context.Context, req types.DeleteWebTokenRequest) error {
if err := r.c.currentUserAction(req.User); err != nil {
if err := r.c.action(defaults.Namespace, services.KindWebToken, services.VerbDelete); err != nil {
return trace.Wrap(err)
}
}
return r.t.Delete(ctx, req)
}
// DeleteAll removes all web tokens.
func (r *webTokensWithRoles) DeleteAll(ctx context.Context) error {
if err := r.c.action(defaults.Namespace, services.KindWebToken, services.VerbList); err != nil {
return trace.Wrap(err)
}
if err := r.c.action(defaults.Namespace, services.KindWebToken, services.VerbDelete); err != nil {
return trace.Wrap(err)
}
return r.t.DeleteAll(ctx)
}
type webTokensWithRoles struct {
c accessChecker
t types.WebTokenInterface
}
type accessChecker interface {
action(namespace, resource, action string) error
currentUserAction(user string) error
}
func (a *ServerWithRoles) GetAccessRequests(ctx context.Context, filter services.AccessRequestFilter) ([]services.AccessRequest, error) {

View file

@ -1152,16 +1152,16 @@ func (c *Client) AuthenticateSSHUser(req AuthenticateSSHRequest) (*SSHLoginRespo
// GetWebSessionInfo checks if a web sesion is valid, returns session id in case if
// it is valid, or error otherwise.
func (c *Client) GetWebSessionInfo(user string, sid string) (services.WebSession, error) {
func (c *Client) GetWebSessionInfo(ctx context.Context, user, sessionID string) (services.WebSession, error) {
out, err := c.Get(
c.Endpoint("users", user, "web", "sessions", sid), url.Values{})
c.Endpoint("users", user, "web", "sessions", sessionID), url.Values{})
if err != nil {
return nil, trace.Wrap(err)
}
return services.UnmarshalWebSession(out.Bytes())
}
// DeleteWebSession deletes a web session for this user by id
// DeleteWebSession deletes the web session specified with sid for the given user
func (c *Client) DeleteWebSession(user string, sid string) error {
_, err := c.Delete(c.Endpoint("users", user, "web", "sessions", sid))
return trace.Wrap(err)
@ -2155,14 +2155,12 @@ func (c *Client) CreateAuditStream(ctx context.Context, sid session.ID) (events.
type WebService interface {
// GetWebSessionInfo checks if a web sesion is valid, returns session id in case if
// it is valid, or error otherwise.
GetWebSessionInfo(user string, sid string) (services.WebSession, error)
GetWebSessionInfo(ctx context.Context, user, sessionID string) (types.WebSession, error)
// ExtendWebSession creates a new web session for a user based on another
// valid web session
ExtendWebSession(user string, prevSessionID string, accessRequestID string) (services.WebSession, error)
ExtendWebSession(user, prevSessionID, accessRequestID string) (types.WebSession, error)
// CreateWebSession creates a new web session for a user
CreateWebSession(user string) (services.WebSession, error)
// DeleteWebSession deletes a web session for this user by id
DeleteWebSession(user string, sid string) error
CreateWebSession(user string) (types.WebSession, error)
// AppSession defines application session features.
services.AppSession
@ -2349,6 +2347,9 @@ type ClientI interface {
services.ClusterConfiguration
services.Events
types.WebSessionsGetter
types.WebTokensGetter
// NewKeepAliver returns a new instance of keep aliver
NewKeepAliver(ctx context.Context) (services.KeepAliver, error)
@ -2395,4 +2396,12 @@ type ClientI interface {
// GenerateDatabaseCert generates client certificate used by a database
// service to authenticate with the database instance.
GenerateDatabaseCert(context.Context, *proto.DatabaseCertRequest) (*proto.DatabaseCertResponse, error)
// GetWebSession queries the existing web session described with req.
// Implements ReadAccessPoint.
GetWebSession(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error)
// GetWebToken queries the existing web token described with req.
// Implements ReadAccessPoint.
GetWebToken(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error)
}

View file

@ -26,6 +26,7 @@ import (
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/modules"
@ -259,7 +260,12 @@ func (a *Server) validateGithubAuthCallback(q url.Values) (*githubAuthResponse,
// If the request is coming from a browser, create a web session.
if req.CreateWebSession {
session, err := a.createWebSession(user, params.sessionTTL)
session, err := a.createWebSession(context.TODO(), types.NewWebSessionRequest{
User: user.GetName(),
Roles: user.GetRoles(),
Traits: user.GetTraits(),
SessionTTL: params.sessionTTL,
})
if err != nil {
return nil, trace.Wrap(err)
}
@ -296,23 +302,16 @@ func (a *Server) validateGithubAuthCallback(q url.Values) (*githubAuthResponse,
return re, nil
}
func (a *Server) createWebSession(user services.User, sessionTTL time.Duration) (services.WebSession, error) {
func (a *Server) createWebSession(ctx context.Context, req types.NewWebSessionRequest) (services.WebSession, error) {
// It's safe to extract the roles and traits directly from services.User
// because this occurs during the user creation process and services.User
// is not fetched from the backend.
session, err := a.NewWebSession(user.GetName(), user.GetRoles(), user.GetTraits())
session, err := a.NewWebSession(req)
if err != nil {
return nil, trace.Wrap(err)
}
// Session expiry time is the same as the user expiry time.
session.SetExpiryTime(a.clock.Now().UTC().Add(sessionTTL))
// Bearer tokens expire quicker than the overall session time and need to be refreshed.
bearerTTL := utils.MinTTL(BearerTokenTTL, sessionTTL)
session.SetBearerTokenExpiryTime(a.clock.Now().UTC().Add(bearerTTL))
err = a.UpsertWebSession(user.GetName(), session)
err = a.upsertWebSession(ctx, req.User, session)
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -229,12 +229,7 @@ func (g *GRPCServer) WatchEvents(watch *proto.Watch, stream proto.AuthService_Wa
Name: auth.User.GetName(),
}
for _, kind := range watch.Kinds {
servicesWatch.Kinds = append(servicesWatch.Kinds, services.WatchKind{
Name: kind.Name,
Kind: kind.Kind,
LoadSecrets: kind.LoadSecrets,
Filter: kind.Filter,
})
servicesWatch.Kinds = append(servicesWatch.Kinds, proto.ToWatchKind(kind))
}
watcher, err := auth.NewWatcher(stream.Context(), servicesWatch)
if err != nil {
@ -971,6 +966,156 @@ func (g GRPCServer) GenerateAppToken(ctx context.Context, req *proto.GenerateApp
}, nil
}
// GetWebSession gets a web session.
func (g *GRPCServer) GetWebSession(ctx context.Context, req *types.GetWebSessionRequest) (*proto.GetWebSessionResponse, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
session, err := auth.WebSessions().Get(ctx, *req)
if err != nil {
return nil, trail.ToGRPC(err)
}
sess, ok := session.(*services.WebSessionV2)
if !ok {
return nil, trail.ToGRPC(trace.BadParameter("unexpected session type %T", session))
}
return &proto.GetWebSessionResponse{
Session: sess,
}, nil
}
// GetWebSessions gets all web sessions.
func (g *GRPCServer) GetWebSessions(ctx context.Context, _ *empty.Empty) (*proto.GetWebSessionsResponse, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
sessions, err := auth.WebSessions().List(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
var out []*services.WebSessionV2
for _, session := range sessions {
sess, ok := session.(*services.WebSessionV2)
if !ok {
return nil, trail.ToGRPC(trace.BadParameter("unexpected type %T", session))
}
out = append(out, sess)
}
return &proto.GetWebSessionsResponse{
Sessions: out,
}, nil
}
// DeleteWebSession removes the web session given with req.
func (g *GRPCServer) DeleteWebSession(ctx context.Context, req *types.DeleteWebSessionRequest) (*empty.Empty, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
if err := auth.WebSessions().Delete(ctx, *req); err != nil {
return nil, trail.ToGRPC(err)
}
return &empty.Empty{}, nil
}
// DeleteAllWebSessions removes all web sessions.
func (g *GRPCServer) DeleteAllWebSessions(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
if err := auth.WebSessions().DeleteAll(ctx); err != nil {
return nil, trail.ToGRPC(err)
}
return &empty.Empty{}, nil
}
// GetWebToken gets a web token.
func (g *GRPCServer) GetWebToken(ctx context.Context, req *types.GetWebTokenRequest) (*proto.GetWebTokenResponse, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
resp, err := auth.WebTokens().Get(ctx, *req)
if err != nil {
return nil, trail.ToGRPC(err)
}
token, ok := resp.(*types.WebTokenV3)
if !ok {
return nil, trail.ToGRPC(trace.BadParameter("unexpected web token type %T", resp))
}
return &proto.GetWebTokenResponse{
Token: token,
}, nil
}
// GetWebTokens gets all web tokens.
func (g *GRPCServer) GetWebTokens(ctx context.Context, _ *empty.Empty) (*proto.GetWebTokensResponse, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
tokens, err := auth.WebTokens().List(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
var out []*types.WebTokenV3
for _, t := range tokens {
token, ok := t.(*types.WebTokenV3)
if !ok {
return nil, trail.ToGRPC(trace.BadParameter("unexpected type %T", t))
}
out = append(out, token)
}
return &proto.GetWebTokensResponse{
Tokens: out,
}, nil
}
// DeleteWebToken removes the web token given with req.
func (g *GRPCServer) DeleteWebToken(ctx context.Context, req *types.DeleteWebTokenRequest) (*empty.Empty, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
if err := auth.WebTokens().Delete(ctx, *req); err != nil {
return nil, trail.ToGRPC(err)
}
return &empty.Empty{}, nil
}
// DeleteAllWebTokens removes all web tokens.
func (g *GRPCServer) DeleteAllWebTokens(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
if err := auth.WebTokens().DeleteAll(ctx); err != nil {
return nil, trail.ToGRPC(err)
}
return &empty.Empty{}, nil
}
// UpdateRemoteCluster updates remote cluster
func (g *GRPCServer) UpdateRemoteCluster(ctx context.Context, req *services.RemoteClusterV3) (*empty.Empty, error) {
auth, err := g.authenticate(ctx)
@ -1389,7 +1534,7 @@ type grpcContext struct {
// authenticate extracts authentication context and returns initialized auth server
func (g *GRPCServer) authenticate(ctx context.Context) (*grpcContext, error) {
// HTTPS server expects auth context to be set by the auth middleware
// HTTPS server expects auth context to be set by the auth middleware
authContext, err := g.Authorizer.Authorize(ctx)
if err != nil {
// propagate connection problem error so we can differentiate

View file

@ -23,6 +23,7 @@ import (
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/services"
@ -166,10 +167,9 @@ func (s *Server) authenticateUser(ctx context.Context, req AuthenticateUserReque
}
}
// AuthenticateWebUser authenticates web user, creates and returns web session
// in case if authentication is successful. In case if existing session id
// is used to authenticate, returns session associated with the existing session id
// instead of creating the new one
// AuthenticateWebUser authenticates web user, creates and returns a web session
// if authentication is successful. In case the existing session ID is used to authenticate,
// returns the existing session instead of creating a new one
func (s *Server) AuthenticateWebUser(req AuthenticateUserRequest) (services.WebSession, error) {
clusterConfig, err := s.GetClusterConfig()
if err != nil {
@ -186,7 +186,10 @@ func (s *Server) AuthenticateWebUser(req AuthenticateUserRequest) (services.WebS
}
if req.Session != nil {
session, err := s.GetWebSession(req.Username, req.Session.ID)
session, err := s.GetWebSession(context.TODO(), types.GetWebSessionRequest{
User: req.Username,
SessionID: req.Session.ID,
})
if err != nil {
return nil, trace.AccessDenied("session is invalid or has expired")
}
@ -202,7 +205,7 @@ func (s *Server) AuthenticateWebUser(req AuthenticateUserRequest) (services.WebS
return nil, trace.Wrap(err)
}
sess, err := s.createUserWebSession(user)
sess, err := s.createUserWebSession(context.TODO(), user)
if err != nil {
return nil, trace.Wrap(err)
}
@ -380,19 +383,14 @@ func (s *Server) emitNoLocalAuthEvent(username string) {
}
}
func (s *Server) createUserWebSession(user services.User) (services.WebSession, error) {
// It's safe to extract the roles and traits directly from services.User as this method
func (s *Server) createUserWebSession(ctx context.Context, user services.User) (services.WebSession, error) {
// It's safe to extract the roles and traits directly from services.User as this method
// is only used for local accounts.
sess, err := s.NewWebSession(user.GetName(), user.GetRoles(), user.GetTraits())
if err != nil {
return nil, trace.Wrap(err)
}
err = s.UpsertWebSession(user.GetName(), sess)
if err != nil {
return nil, trace.Wrap(err)
}
return sess, nil
return s.createWebSession(ctx, types.NewWebSessionRequest{
User: user.GetName(),
Roles: user.GetRoles(),
Traits: user.GetTraits(),
})
}
const noLocalAuth = "local auth disabled"

View file

@ -25,6 +25,7 @@ import (
"net/url"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/services"
@ -388,7 +389,12 @@ func (a *Server) validateOIDCAuthCallback(q url.Values) (*oidcAuthResponse, erro
// If the request is coming from a browser, create a web session.
if req.CreateWebSession {
session, err := a.createWebSession(user, params.sessionTTL)
session, err := a.createWebSession(context.TODO(), types.NewWebSessionRequest{
User: user.GetName(),
Roles: user.GetRoles(),
Traits: user.GetTraits(),
SessionTTL: params.sessionTTL,
})
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -42,7 +42,7 @@ func (s *Server) ChangePasswordWithToken(ctx context.Context, req ChangePassword
return nil, trace.Wrap(err)
}
sess, err := s.createUserWebSession(user)
sess, err := s.createUserWebSession(ctx, user)
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -280,6 +280,7 @@ func GetCheckerForBuiltinRole(clusterName string, clusterConfig services.Cluster
services.NewRule(services.KindClusterConfig, services.RO()),
services.NewRule(services.KindAppServer, services.RW()),
services.NewRule(services.KindWebSession, services.RO()),
services.NewRule(services.KindWebToken, services.RO()),
services.NewRule(services.KindJWT, services.RW()),
},
},
@ -343,6 +344,7 @@ func GetCheckerForBuiltinRole(clusterName string, clusterConfig services.Cluster
services.NewRule(services.KindSemaphore, services.RW()),
services.NewRule(services.KindAppServer, services.RO()),
services.NewRule(services.KindWebSession, services.RW()),
services.NewRule(services.KindWebToken, services.RW()),
services.NewRule(services.KindKubeService, services.RW()),
services.NewRule(types.KindDatabaseServer, services.RO()),
// this rule allows local proxy to update the remote cluster's host certificate authorities
@ -399,6 +401,7 @@ func GetCheckerForBuiltinRole(clusterName string, clusterConfig services.Cluster
services.NewRule(services.KindSemaphore, services.RW()),
services.NewRule(services.KindAppServer, services.RO()),
services.NewRule(services.KindWebSession, services.RW()),
services.NewRule(services.KindWebToken, services.RW()),
services.NewRule(services.KindKubeService, services.RW()),
services.NewRule(types.KindDatabaseServer, services.RO()),
// this rule allows local proxy to update the remote cluster's host certificate authorities
@ -429,6 +432,7 @@ func GetCheckerForBuiltinRole(clusterName string, clusterConfig services.Cluster
Namespaces: []string{services.Wildcard},
Rules: []services.Rule{
services.NewRule(services.KindWebSession, services.RW()),
services.NewRule(services.KindWebToken, services.RW()),
services.NewRule(services.KindSSHSession, services.RW()),
services.NewRule(services.KindAuthServer, services.RO()),
services.NewRule(services.KindUser, services.RO()),

View file

@ -24,6 +24,7 @@ import (
"io/ioutil"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/services"
@ -403,7 +404,12 @@ func (a *Server) validateSAMLResponse(samlResponse string) (*samlAuthResponse, e
// If the request is coming from a browser, create a web session.
if request.CreateWebSession {
session, err := a.createWebSession(user, params.sessionTTL)
session, err := a.createWebSession(context.TODO(), types.NewWebSessionRequest{
User: user.GetName(),
Roles: user.GetRoles(),
Traits: user.GetTraits(),
SessionTTL: params.sessionTTL,
})
if err != nil {
return re, trace.Wrap(err)
}

View file

@ -21,11 +21,14 @@ import (
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/wrappers"
"github.com/gravitational/teleport/lib/jwt"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/pborman/uuid"
)
@ -35,7 +38,10 @@ import (
// control is enforced.
func (s *Server) CreateAppSession(ctx context.Context, req services.CreateAppSessionRequest, user services.User, checker services.AccessChecker) (services.WebSession, error) {
// Check that a matching parent web session exists in the backend.
parentSession, err := s.GetWebSession(req.Username, req.ParentSession)
parentSession, err := s.GetWebSession(ctx, types.GetWebSessionRequest{
User: req.Username,
SessionID: req.ParentSession,
})
if err != nil {
return nil, trace.Wrap(err)
}
@ -57,7 +63,7 @@ func (s *Server) CreateAppSession(ctx context.Context, req services.CreateAppSes
// used to log into servers but SSH certificate generation code requires a
// principal be in the certificate.
traits: wrappers.Traits(map[string][]string{
teleport.TraitLogins: []string{uuid.New()},
teleport.TraitLogins: {uuid.New()},
}),
// Only allow this certificate to be used for applications.
usage: []string{teleport.UsageAppsOnly},

View file

@ -1434,7 +1434,7 @@ func (s *TLSSuite) TestWebSessionWithoutAccessRequest(c *check.C) {
web, err := s.server.NewClientFromWebSession(ws)
c.Assert(err, check.IsNil)
_, err = web.GetWebSessionInfo(user, ws.GetName())
_, err = web.GetWebSessionInfo(context.TODO(), user, ws.GetName())
c.Assert(err, check.IsNil)
new, err := web.ExtendWebSession(user, ws.GetName(), "")
@ -1448,7 +1448,7 @@ func (s *TLSSuite) TestWebSessionWithoutAccessRequest(c *check.C) {
err = clt.DeleteWebSession(user, ws.GetName())
c.Assert(err, check.IsNil)
_, err = web.GetWebSessionInfo(user, ws.GetName())
_, err = web.GetWebSessionInfo(context.TODO(), user, ws.GetName())
c.Assert(err, check.NotNil)
_, err = web.ExtendWebSession(user, ws.GetName(), "")
@ -2246,13 +2246,13 @@ func (s *TLSSuite) TestAuthenticateWebUserOTP(c *check.C) {
userClient, err := s.server.NewClientFromWebSession(ws)
c.Assert(err, check.IsNil)
_, err = userClient.GetWebSessionInfo(user, ws.GetName())
_, err = userClient.GetWebSessionInfo(context.TODO(), user, ws.GetName())
c.Assert(err, check.IsNil)
err = clt.DeleteWebSession(user, ws.GetName())
c.Assert(err, check.IsNil)
_, err = userClient.GetWebSessionInfo(user, ws.GetName())
_, err = userClient.GetWebSessionInfo(context.TODO(), user, ws.GetName())
c.Assert(err, check.NotNil)
}

52
lib/cache/cache.go vendored
View file

@ -58,7 +58,9 @@ func ForAuth(cfg Config) Config {
{Kind: services.KindTunnelConnection},
{Kind: services.KindAccessRequest},
{Kind: services.KindAppServer},
{Kind: services.KindWebSession},
{Kind: services.KindWebSession, SubKind: services.KindAppSession},
{Kind: services.KindWebSession, SubKind: services.KindWebSession},
{Kind: services.KindWebToken},
{Kind: services.KindRemoteCluster},
{Kind: services.KindKubeService},
{Kind: types.KindDatabaseServer},
@ -83,7 +85,9 @@ func ForProxy(cfg Config) Config {
{Kind: services.KindReverseTunnel},
{Kind: services.KindTunnelConnection},
{Kind: services.KindAppServer},
{Kind: services.KindWebSession},
{Kind: services.KindWebSession, SubKind: services.KindAppSession},
{Kind: services.KindWebSession, SubKind: services.KindWebSession},
{Kind: services.KindWebToken},
{Kind: services.KindRemoteCluster},
{Kind: services.KindKubeService},
{Kind: types.KindDatabaseServer},
@ -262,8 +266,8 @@ type Cache struct {
// cancel triggers exit context closure
cancel context.CancelFunc
// collections is a map of registered collections by resource Kind
collections map[string]collection
// collections is a map of registered collections by resource Kind/SubKind
collections map[resourceKind]collection
trustCache services.Trust
clusterConfigCache services.ClusterConfiguration
@ -273,6 +277,8 @@ type Cache struct {
dynamicAccessCache services.DynamicAccessExt
presenceCache services.Presence
appSessionCache services.AppSession
webSessionCache types.WebSessionInterface
webTokenCache types.WebTokenInterface
eventsFanout *services.Fanout
// closed indicates that the cache has been closed
@ -321,6 +327,8 @@ func (c *Cache) read() (readGuard, error) {
dynamicAccess: c.dynamicAccessCache,
presence: c.presenceCache,
appSession: c.appSessionCache,
webSession: c.webSessionCache,
webToken: c.webTokenCache,
release: c.rw.RUnlock,
}, nil
}
@ -334,6 +342,8 @@ func (c *Cache) read() (readGuard, error) {
dynamicAccess: c.Config.DynamicAccess,
presence: c.Config.Presence,
appSession: c.Config.AppSession,
webSession: c.Config.WebSession,
webToken: c.Config.WebToken,
release: nil,
}, nil
}
@ -351,6 +361,8 @@ type readGuard struct {
dynamicAccess services.DynamicAccess
presence services.Presence
appSession services.AppSession
webSession types.WebSessionInterface
webToken types.WebTokenInterface
release func()
released bool
}
@ -398,6 +410,10 @@ type Config struct {
Presence services.Presence
// AppSession holds application sessions.
AppSession services.AppSession
// WebSession holds regular web sessions.
WebSession types.WebSessionInterface
// WebToken holds web tokens.
WebToken types.WebTokenInterface
// Backend is a backend for local cache
Backend backend.Backend
// RetryPeriod is a period between cache retries on failures
@ -540,6 +556,8 @@ func New(config Config) (*Cache, error) {
dynamicAccessCache: local.NewDynamicAccessService(wrapper),
presenceCache: local.NewPresenceService(wrapper),
appSessionCache: local.NewIdentityService(wrapper),
webSessionCache: local.NewIdentityService(wrapper).WebSessions(),
webTokenCache: local.NewIdentityService(wrapper).WebTokens(),
eventsFanout: services.NewFanout(),
Entry: log.WithFields(log.Fields{
trace.Component: config.Component,
@ -893,9 +911,11 @@ func (c *Cache) fetch(ctx context.Context) (apply func(ctx context.Context) erro
}
func (c *Cache) processEvent(ctx context.Context, event services.Event) error {
collection, ok := c.collections[event.Resource.GetKind()]
resourceKind := resourceKindFromResource(event.Resource)
collection, ok := c.collections[resourceKind]
if !ok {
c.Warningf("Skipping unsupported event %v.", event.Resource.GetKind())
c.Warningf("Skipping unsupported event %v/%v",
event.Resource.GetKind(), event.Resource.GetSubKind())
return nil
}
if err := collection.processEvent(ctx, event); err != nil {
@ -1208,3 +1228,23 @@ func (c *Cache) GetDatabaseServers(ctx context.Context, namespace string, opts .
defer rg.Release()
return rg.presence.GetDatabaseServers(ctx, namespace, opts...)
}
// GetWebSession gets a regular web session.
func (c *Cache) GetWebSession(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error) {
rg, err := c.read()
if err != nil {
return nil, trace.Wrap(err)
}
defer rg.Release()
return rg.webSession.Get(ctx, req)
}
// GetWebToken gets a web token.
func (c *Cache) GetWebToken(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) {
rg, err := c.read()
if err != nil {
return nil, trace.Wrap(err)
}
defer rg.Release()
return rg.webToken.Get(ctx, req)
}

View file

@ -74,6 +74,8 @@ type testPack struct {
dynamicAccessS services.DynamicAccess
presenceS services.Presence
appSessionS services.AppSession
webSessionS types.WebSessionInterface
webTokenS types.WebTokenInterface
}
func (t *testPack) Close() {
@ -147,6 +149,8 @@ func newPackWithoutCache(dir string, ssetupConfig SetupConfigFn) (*testPack, err
p.accessS = local.NewAccessService(p.backend)
p.dynamicAccessS = local.NewDynamicAccessService(p.backend)
p.appSessionS = local.NewIdentityService(p.backend)
p.webSessionS = local.NewIdentityService(p.backend).WebSessions()
p.webTokenS = local.NewIdentityService(p.backend).WebTokens()
return p, nil
}
@ -170,6 +174,8 @@ func newPack(dir string, setupConfig func(c Config) Config) (*testPack, error) {
DynamicAccess: p.dynamicAccessS,
Presence: p.presenceS,
AppSession: p.appSessionS,
WebSession: p.webSessionS,
WebToken: p.webTokenS,
RetryPeriod: 200 * time.Millisecond,
EventsC: p.eventsC,
}))
@ -236,6 +242,8 @@ func (s *CacheSuite) TestOnlyRecentInit(c *check.C) {
DynamicAccess: p.dynamicAccessS,
Presence: p.presenceS,
AppSession: p.appSessionS,
WebSession: p.webSessionS,
WebToken: p.webTokenS,
RetryPeriod: 200 * time.Millisecond,
EventsC: p.eventsC,
}))
@ -449,6 +457,8 @@ func (s *CacheSuite) TestCompletenessInit(c *check.C) {
DynamicAccess: p.dynamicAccessS,
Presence: p.presenceS,
AppSession: p.appSessionS,
WebSession: p.webSessionS,
WebToken: p.webTokenS,
RetryPeriod: 200 * time.Millisecond,
EventsC: p.eventsC,
PreferRecent: PreferRecent{
@ -503,6 +513,8 @@ func (s *CacheSuite) TestCompletenessReset(c *check.C) {
DynamicAccess: p.dynamicAccessS,
Presence: p.presenceS,
AppSession: p.appSessionS,
WebSession: p.webSessionS,
WebToken: p.webTokenS,
RetryPeriod: 200 * time.Millisecond,
EventsC: p.eventsC,
PreferRecent: PreferRecent{
@ -562,6 +574,8 @@ func (s *CacheSuite) TestTombstones(c *check.C) {
DynamicAccess: p.dynamicAccessS,
Presence: p.presenceS,
AppSession: p.appSessionS,
WebSession: p.webSessionS,
WebToken: p.webTokenS,
RetryPeriod: 200 * time.Millisecond,
EventsC: p.eventsC,
PreferRecent: PreferRecent{
@ -594,6 +608,8 @@ func (s *CacheSuite) TestTombstones(c *check.C) {
DynamicAccess: p.dynamicAccessS,
Presence: p.presenceS,
AppSession: p.appSessionS,
WebSession: p.webSessionS,
WebToken: p.webTokenS,
RetryPeriod: 200 * time.Millisecond,
EventsC: p.eventsC,
PreferRecent: PreferRecent{
@ -637,6 +653,8 @@ func (s *CacheSuite) preferRecent(c *check.C) {
DynamicAccess: p.dynamicAccessS,
Presence: p.presenceS,
AppSession: p.appSessionS,
WebSession: p.webSessionS,
WebToken: p.webTokenS,
RetryPeriod: 200 * time.Millisecond,
EventsC: p.eventsC,
PreferRecent: PreferRecent{

View file

@ -45,105 +45,119 @@ type collection interface {
}
// setupCollections returns a mapping of collections
func setupCollections(c *Cache, watches []services.WatchKind) (map[string]collection, error) {
collections := make(map[string]collection, len(watches))
func setupCollections(c *Cache, watches []services.WatchKind) (map[resourceKind]collection, error) {
collections := make(map[resourceKind]collection, len(watches))
for _, watch := range watches {
resourceKind := resourceKindFromWatchKind(watch)
switch watch.Kind {
case services.KindCertAuthority:
if c.Trust == nil {
return nil, trace.BadParameter("missing parameter Trust")
}
collections[watch.Kind] = &certAuthority{watch: watch, Cache: c}
collections[resourceKind] = &certAuthority{watch: watch, Cache: c}
case services.KindStaticTokens:
if c.ClusterConfig == nil {
return nil, trace.BadParameter("missing parameter ClusterConfig")
}
collections[watch.Kind] = &staticTokens{watch: watch, Cache: c}
collections[resourceKind] = &staticTokens{watch: watch, Cache: c}
case services.KindToken:
if c.Provisioner == nil {
return nil, trace.BadParameter("missing parameter Provisioner")
}
collections[watch.Kind] = &provisionToken{watch: watch, Cache: c}
collections[resourceKind] = &provisionToken{watch: watch, Cache: c}
case services.KindClusterName:
if c.ClusterConfig == nil {
return nil, trace.BadParameter("missing parameter ClusterConfig")
}
collections[watch.Kind] = &clusterName{watch: watch, Cache: c}
collections[resourceKind] = &clusterName{watch: watch, Cache: c}
case services.KindClusterConfig:
if c.ClusterConfig == nil {
return nil, trace.BadParameter("missing parameter ClusterConfig")
}
collections[watch.Kind] = &clusterConfig{watch: watch, Cache: c}
collections[resourceKind] = &clusterConfig{watch: watch, Cache: c}
case services.KindUser:
if c.Users == nil {
return nil, trace.BadParameter("missing parameter Users")
}
collections[watch.Kind] = &user{watch: watch, Cache: c}
collections[resourceKind] = &user{watch: watch, Cache: c}
case services.KindRole:
if c.Access == nil {
return nil, trace.BadParameter("missing parameter Access")
}
collections[watch.Kind] = &role{watch: watch, Cache: c}
collections[resourceKind] = &role{watch: watch, Cache: c}
case services.KindNamespace:
if c.Presence == nil {
return nil, trace.BadParameter("missing parameter Presence")
}
collections[watch.Kind] = &namespace{watch: watch, Cache: c}
collections[resourceKind] = &namespace{watch: watch, Cache: c}
case services.KindNode:
if c.Presence == nil {
return nil, trace.BadParameter("missing parameter Presence")
}
collections[watch.Kind] = &node{watch: watch, Cache: c}
collections[resourceKind] = &node{watch: watch, Cache: c}
case services.KindProxy:
if c.Presence == nil {
return nil, trace.BadParameter("missing parameter Presence")
}
collections[watch.Kind] = &proxy{watch: watch, Cache: c}
collections[resourceKind] = &proxy{watch: watch, Cache: c}
case services.KindAuthServer:
if c.Presence == nil {
return nil, trace.BadParameter("missing parameter Presence")
}
collections[watch.Kind] = &authServer{watch: watch, Cache: c}
collections[resourceKind] = &authServer{watch: watch, Cache: c}
case services.KindReverseTunnel:
if c.Presence == nil {
return nil, trace.BadParameter("missing parameter Presence")
}
collections[watch.Kind] = &reverseTunnel{watch: watch, Cache: c}
collections[resourceKind] = &reverseTunnel{watch: watch, Cache: c}
case services.KindTunnelConnection:
if c.Presence == nil {
return nil, trace.BadParameter("missing parameter Presence")
}
collections[watch.Kind] = &tunnelConnection{watch: watch, Cache: c}
collections[resourceKind] = &tunnelConnection{watch: watch, Cache: c}
case services.KindRemoteCluster:
if c.Presence == nil {
return nil, trace.BadParameter("missing parameter Presence")
}
collections[watch.Kind] = &remoteCluster{watch: watch, Cache: c}
collections[resourceKind] = &remoteCluster{watch: watch, Cache: c}
case services.KindAccessRequest:
if c.DynamicAccess == nil {
return nil, trace.BadParameter("missing parameter DynamicAccess")
}
collections[watch.Kind] = &accessRequest{watch: watch, Cache: c}
collections[resourceKind] = &accessRequest{watch: watch, Cache: c}
case services.KindAppServer:
if c.Presence == nil {
return nil, trace.BadParameter("missing parameter Presence")
}
collections[watch.Kind] = &appServer{watch: watch, Cache: c}
collections[resourceKind] = &appServer{watch: watch, Cache: c}
case services.KindWebSession:
if c.AppSession == nil {
return nil, trace.BadParameter("missing parameter AppSession")
switch watch.SubKind {
case services.KindAppSession:
if c.AppSession == nil {
return nil, trace.BadParameter("missing parameter AppSession")
}
collections[resourceKind] = &appSession{watch: watch, Cache: c}
case services.KindWebSession:
if c.WebSession == nil {
return nil, trace.BadParameter("missing parameter WebSession")
}
collections[resourceKind] = &webSession{watch: watch, Cache: c}
}
collections[watch.Kind] = &appSession{watch: watch, Cache: c}
case services.KindWebToken:
if c.WebToken == nil {
return nil, trace.BadParameter("missing parameter WebToken")
}
collections[resourceKind] = &webToken{watch: watch, Cache: c}
case services.KindKubeService:
if c.Presence == nil {
return nil, trace.BadParameter("missing parameter Presence")
}
collections[watch.Kind] = &kubeService{watch: watch, Cache: c}
collections[resourceKind] = &kubeService{watch: watch, Cache: c}
case types.KindDatabaseServer:
if c.Presence == nil {
return nil, trace.BadParameter("missing parameter Presence")
}
collections[watch.Kind] = &databaseServer{watch: watch, Cache: c}
collections[resourceKind] = &databaseServer{watch: watch, Cache: c}
default:
return nil, trace.BadParameter("resource %q is not supported", watch.Kind)
}
@ -151,6 +165,41 @@ func setupCollections(c *Cache, watches []services.WatchKind) (map[string]collec
return collections, nil
}
func resourceKindFromWatchKind(wk services.WatchKind) resourceKind {
switch wk.Kind {
case services.KindWebSession:
// Web sessions use subkind to differentiate between
// the types of sessions
return resourceKind{
kind: wk.Kind,
subkind: wk.SubKind,
}
}
return resourceKind{
kind: wk.Kind,
}
}
func resourceKindFromResource(res services.Resource) resourceKind {
switch res.GetKind() {
case services.KindWebSession:
// Web sessions use subkind to differentiate between
// the types of sessions
return resourceKind{
kind: res.GetKind(),
subkind: res.GetSubKind(),
}
}
return resourceKind{
kind: res.GetKind(),
}
}
type resourceKind struct {
kind string
subkind string
}
type accessRequest struct {
*Cache
watch services.WatchKind
@ -1424,6 +1473,136 @@ func (a *appSession) watchKind() services.WatchKind {
return a.watch
}
type webSession struct {
*Cache
watch services.WatchKind
}
func (r *webSession) erase(ctx context.Context) error {
err := r.webSessionCache.DeleteAll(ctx)
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
return nil
}
func (r *webSession) fetch(ctx context.Context) (apply func(ctx context.Context) error, err error) {
resources, err := r.WebSession.List(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
return func(ctx context.Context) error {
if err := r.erase(ctx); err != nil {
return trace.Wrap(err)
}
for _, resource := range resources {
r.setTTL(resource)
if err := r.webSessionCache.Upsert(ctx, resource); err != nil {
return trace.Wrap(err)
}
}
return nil
}, nil
}
func (r *webSession) processEvent(ctx context.Context, event services.Event) error {
switch event.Type {
case backend.OpDelete:
err := r.webSessionCache.Delete(ctx, types.DeleteWebSessionRequest{
SessionID: event.Resource.GetName(),
})
if err != nil {
// Resource could be missing in the cache expired or not created, if the
// first consumed event is delete.
if !trace.IsNotFound(err) {
r.WithError(err).Warn("Failed to delete resource.")
return trace.Wrap(err)
}
}
case backend.OpPut:
resource, ok := event.Resource.(services.WebSession)
if !ok {
return trace.BadParameter("unexpected type %T", event.Resource)
}
r.setTTL(resource)
if err := r.webSessionCache.Upsert(ctx, resource); err != nil {
return trace.Wrap(err)
}
default:
r.WithField("event", event.Type).Warn("Skipping unsupported event type.")
}
return nil
}
func (r *webSession) watchKind() services.WatchKind {
return r.watch
}
type webToken struct {
*Cache
watch services.WatchKind
}
func (r *webToken) erase(ctx context.Context) error {
err := r.webTokenCache.DeleteAll(ctx)
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
return nil
}
func (r *webToken) fetch(ctx context.Context) (apply func(ctx context.Context) error, err error) {
resources, err := r.WebToken.List(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
return func(ctx context.Context) error {
if err := r.erase(ctx); err != nil {
return trace.Wrap(err)
}
for _, resource := range resources {
r.setTTL(resource)
if err := r.webTokenCache.Upsert(ctx, resource); err != nil {
return trace.Wrap(err)
}
}
return nil
}, nil
}
func (r *webToken) processEvent(ctx context.Context, event services.Event) error {
switch event.Type {
case backend.OpDelete:
err := r.webTokenCache.Delete(ctx, types.DeleteWebTokenRequest{
Token: event.Resource.GetName(),
})
if err != nil {
// Resource could be missing in the cache expired or not created, if the
// first consumed event is delete.
if !trace.IsNotFound(err) {
r.WithError(err).Warn("Failed to delete resource.")
return trace.Wrap(err)
}
}
case backend.OpPut:
resource, ok := event.Resource.(types.WebToken)
if !ok {
return trace.BadParameter("unexpected type %T", event.Resource)
}
r.setTTL(resource)
if err := r.webTokenCache.Upsert(ctx, resource); err != nil {
return trace.Wrap(err)
}
default:
r.WithField("event", event.Type).Warn("Skipping unsupported event type.")
}
return nil
}
func (r *webToken) watchKind() services.WatchKind {
return r.watch
}
type kubeService struct {
*Cache
watch services.WatchKind

View file

@ -422,13 +422,12 @@ const (
)
const (
// MinCertDuration specifies minimum duration of validity of issued cert
// MinCertDuration specifies minimum duration of validity of issued certificate
MinCertDuration = time.Minute
// MaxCertDuration limits maximum duration of validity of issued cert
// MaxCertDuration limits maximum duration of validity of issued certificate
MaxCertDuration = defaults.MaxCertDuration
// CertDuration is a default certificate duration
// 12 is default as it' longer than average working day (I hope so)
CertDuration = 12 * time.Hour
// CertDuration is a default certificate duration.
CertDuration = defaults.CertDuration
// RotationGracePeriod is a default rotation period for graceful
// certificate rotations, by default to set to maximum allowed user
// cert duration

View file

@ -69,7 +69,7 @@ func init() {
// server is a "reverse tunnel server". it exposes the cluster capabilities
// (like access to a cluster's auth) to remote trusted clients
// (also known as 'reverse tunnel agents'.
// (also known as 'reverse tunnel agents').
type server struct {
sync.RWMutex
Config

View file

@ -1493,6 +1493,8 @@ func (process *TeleportProcess) newAccessCache(cfg accessCacheConfig) (*cache.Ca
DynamicAccess: cfg.services,
Presence: cfg.services,
AppSession: cfg.services,
WebSession: cfg.services.WebSessions(),
WebToken: cfg.services.WebTokens(),
Component: teleport.Component(append(cfg.cacheName, process.id, teleport.ComponentCache)...),
MetricComponent: teleport.Component(append(cfg.cacheName, teleport.ComponentCache)...),
}))
@ -2458,13 +2460,19 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
if len(cfg.Proxy.Kube.PublicAddrs) > 0 {
proxySettings.Kube.PublicAddr = cfg.Proxy.Kube.PublicAddrs[0].String()
}
var fs http.FileSystem
if !process.Config.Proxy.DisableWebInterface {
fs, err = newHTTPFileSystem()
if err != nil {
return trace.Wrap(err)
}
}
webHandler, err = web.NewHandler(
web.Config{
Proxy: tsrv,
AuthServers: cfg.AuthServers[0],
DomainName: cfg.Hostname,
ProxyClient: conn.Client,
DisableUI: process.Config.Proxy.DisableWebInterface,
ProxySSHAddr: cfg.Proxy.SSHAddr,
ProxyWebAddr: cfg.Proxy.WebAddr,
ProxySettings: proxySettings,
@ -2474,6 +2482,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
Emitter: streamEmitter,
HostUUID: process.Config.HostUUID,
Context: process.ExitContext(),
StaticFS: fs,
})
if err != nil {
return trace.Wrap(err)
@ -3308,3 +3317,28 @@ func findPublicAddr(authClient auth.AccessPoint, a App) (string, error) {
}
return fmt.Sprintf("%v.%v", a.Name, cn.GetClusterName()), nil
}
// newHTTPFileSystem creates a new HTTP file system for the web handler.
// It uses external configuration to make the decision
func newHTTPFileSystem() (http.FileSystem, error) {
if !isDebugMode() {
fs, err := web.NewStaticFileSystem()
if err != nil {
return nil, trace.Wrap(err)
}
return fs, nil
}
// Use debug HTTP file system with default assets path
fs, err := web.NewDebugFileSystem("")
if err != nil {
return nil, trace.Wrap(err)
}
return fs, nil
}
// isDebugMode determines if teleport is running in a "debug" mode.
// It looks at DEBUG environment variable
func isDebugMode() bool {
v, _ := strconv.ParseBool(os.Getenv(teleport.DebugEnvVar))
return v
}

View file

@ -51,6 +51,18 @@ func (s *ServiceTestSuite) SetUpSuite(c *check.C) {
utils.InitLoggerForTests(testing.Verbose())
}
func (s *ServiceTestSuite) TestDebugModeEnv(c *check.C) {
c.Assert(isDebugMode(), check.Equals, false)
os.Setenv(teleport.DebugEnvVar, "no")
c.Assert(isDebugMode(), check.Equals, false)
os.Setenv(teleport.DebugEnvVar, "0")
c.Assert(isDebugMode(), check.Equals, false)
os.Setenv(teleport.DebugEnvVar, "1")
c.Assert(isDebugMode(), check.Equals, true)
os.Setenv(teleport.DebugEnvVar, "true")
c.Assert(isDebugMode(), check.Equals, true)
}
func (s *ServiceTestSuite) TestSelfSignedHTTPS(c *check.C) {
fileExists := func(fp string) bool {
_, err := os.Stat(fp)

View file

@ -207,16 +207,10 @@ type Identity interface {
// GetResetPasswordTokenSecrets returns token secrets
GetResetPasswordTokenSecrets(ctx context.Context, tokenID string) (ResetPasswordTokenSecrets, error)
// GetWebSession returns a web session state for a given user and session id
GetWebSession(user, sid string) (WebSession, error)
types.WebSessionsGetter
types.WebTokensGetter
// UpsertWebSession updates or inserts a web session for a user and session
UpsertWebSession(user, sid string, session WebSession) error
// DeleteWebSession deletes web session from the storage
DeleteWebSession(user, sid string) error
// AppSession defines session features.
// AppSession defines application session features.
AppSession
}

View file

@ -27,6 +27,7 @@ import (
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
)
@ -92,7 +93,16 @@ func (e *EventsService) NewWatcher(ctx context.Context, watch services.Watch) (s
case services.KindAppServer:
parser = newAppServerParser()
case services.KindWebSession:
parser = newAppSessionParser()
switch kind.SubKind {
case services.KindAppSession:
parser = newAppSessionParser()
case services.KindWebSession:
parser = newWebSessionParser()
default:
return nil, trace.BadParameter("watcher on object subkind %q is not supported", kind.SubKind)
}
case services.KindWebToken:
parser = newWebTokenParser()
case services.KindRemoteCluster:
parser = newRemoteClusterParser()
case services.KindKubeService:
@ -100,7 +110,7 @@ func (e *EventsService) NewWatcher(ctx context.Context, watch services.Watch) (s
case types.KindDatabaseServer:
parser = newDatabaseServerParser()
default:
return nil, trace.BadParameter("watcher on object kind %v is not supported", kind)
return nil, trace.BadParameter("watcher on object kind %q is not supported", kind.Kind)
}
prefixes = append(prefixes, parser.prefix())
parsers = append(parsers, parser)
@ -685,17 +695,34 @@ func (p *appServerParser) parse(event backend.Event) (services.Resource, error)
func newAppSessionParser() *webSessionParser {
return &webSessionParser{
baseParser: baseParser{matchPrefix: backend.Key(appsPrefix, sessionsPrefix)},
hdr: services.ResourceHeader{
Kind: services.KindWebSession,
SubKind: services.KindAppSession,
Version: services.V2,
},
}
}
func newWebSessionParser() *webSessionParser {
return &webSessionParser{
baseParser: baseParser{matchPrefix: backend.Key(webPrefix, sessionsPrefix)},
hdr: services.ResourceHeader{
Kind: services.KindWebSession,
SubKind: services.KindWebSession,
Version: services.V2,
},
}
}
type webSessionParser struct {
baseParser
hdr services.ResourceHeader
}
func (p *webSessionParser) parse(event backend.Event) (services.Resource, error) {
switch event.Type {
case backend.OpDelete:
return resourceHeader(event, services.KindWebSession, services.V2, 0)
return resourceHeaderWithTemplate(event, p.hdr, 0)
case backend.OpPut:
resource, err := services.UnmarshalWebSession(event.Item.Value,
services.WithResourceID(event.Item.ID),
@ -710,6 +737,34 @@ func (p *webSessionParser) parse(event backend.Event) (services.Resource, error)
}
}
func newWebTokenParser() *webTokenParser {
return &webTokenParser{
baseParser: baseParser{matchPrefix: backend.Key(webPrefix, tokensPrefix)},
}
}
type webTokenParser struct {
baseParser
}
func (p *webTokenParser) parse(event backend.Event) (services.Resource, error) {
switch event.Type {
case backend.OpDelete:
return resourceHeader(event, services.KindWebToken, services.V1, 0)
case backend.OpPut:
resource, err := services.UnmarshalWebToken(event.Item.Value,
services.WithResourceID(event.Item.ID),
services.WithExpires(event.Item.Expires),
)
if err != nil {
return nil, trace.Wrap(err)
}
return resource, nil
default:
return nil, trace.BadParameter("event %v is not supported", event.Type)
}
}
func newKubeServiceParser() *kubeServiceParser {
return &kubeServiceParser{
baseParser: baseParser{matchPrefix: backend.Key(kubeServicesPrefix)},
@ -832,6 +887,75 @@ func resourceHeader(event backend.Event, kind, version string, offset int) (serv
}, nil
}
func resourceHeaderWithTemplate(event backend.Event, hdr services.ResourceHeader, offset int) (services.Resource, error) {
name, err := base(event.Item.Key, offset)
if err != nil {
return nil, trace.Wrap(err)
}
return &services.ResourceHeader{
Kind: hdr.Kind,
SubKind: hdr.SubKind,
Version: hdr.Version,
Metadata: services.Metadata{
Name: string(name),
Namespace: defaults.Namespace,
},
}, nil
}
// WaitForEvent waits for the event matched by the specified event matcher in the given watcher.
func WaitForEvent(ctx context.Context, watcher services.Watcher, m EventMatcher, clock clockwork.Clock) (services.Resource, error) {
tick := clock.NewTicker(defaults.WebHeadersTimeout)
defer tick.Stop()
select {
case event := <-watcher.Events():
if event.Type != backend.OpInit {
return nil, trace.BadParameter("expected init event, got %v instead", event.Type)
}
case <-watcher.Done():
// Watcher closed, probably due to a network error.
return nil, trace.ConnectionProblem(watcher.Error(), "watcher is closed")
case <-tick.Chan():
return nil, trace.LimitExceeded("timed out waiting for initialize event")
}
for {
select {
case event := <-watcher.Events():
res, err := m.Match(event)
if err == nil {
return res, nil
}
if !trace.IsCompareFailed(err) {
logrus.WithError(err).Debug("Failed to match event.")
}
case <-watcher.Done():
// Watcher closed, probably due to a network error.
return nil, trace.ConnectionProblem(watcher.Error(), "watcher is closed")
case <-tick.Chan():
return nil, trace.LimitExceeded("timed out waiting for event")
}
}
}
// Match matches the specified resource event by applying itself
func (r EventMatcherFunc) Match(event services.Event) (services.Resource, error) {
return r(event)
}
// EventMatcherFunc matches the specified resource event.
// Implements EventMatcher
type EventMatcherFunc func(services.Event) (services.Resource, error)
// EventMatcher matches a specific resource event
type EventMatcher interface {
// Match matches the specified event.
// Returns the matched resource if successful.
// Returns trace.CompareFailedError for no match.
Match(services.Event) (services.Resource, error)
}
// base returns last element delimited by separator, index is
// is an index of the key part to get counting from the end
func base(key []byte, offset int) ([]byte, error) {

View file

@ -19,14 +19,16 @@ package local
import (
"context"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
)
// GetAppSession gets an application web session.
func (s *IdentityService) GetAppSession(ctx context.Context, req services.GetAppSessionRequest) (services.WebSession, error) {
func (s *IdentityService) GetAppSession(ctx context.Context, req types.GetAppSessionRequest) (types.WebSession, error) {
if err := req.Check(); err != nil {
return nil, trace.Wrap(err)
}
@ -43,14 +45,14 @@ func (s *IdentityService) GetAppSession(ctx context.Context, req services.GetApp
}
// GetAppSessions gets all application web sessions.
func (s *IdentityService) GetAppSessions(ctx context.Context) ([]services.WebSession, error) {
func (s *IdentityService) GetAppSessions(ctx context.Context) ([]types.WebSession, error) {
startKey := backend.Key(appsPrefix, sessionsPrefix)
result, err := s.GetRange(ctx, startKey, backend.RangeEnd(startKey), backend.NoLimit)
if err != nil {
return nil, trace.Wrap(err)
}
out := make([]services.WebSession, len(result.Items))
out := make([]types.WebSession, len(result.Items))
for i, item := range result.Items {
session, err := services.UnmarshalWebSession(item.Value, services.SkipValidation())
if err != nil {
@ -95,3 +97,226 @@ func (s *IdentityService) DeleteAllAppSessions(ctx context.Context) error {
}
return nil
}
// WebSessions returns the web sessions manager.
func (s *IdentityService) WebSessions() types.WebSessionInterface {
return &webSessions{backend: s.Backend, log: s.log}
}
// Get returns the web session state described with req.
func (r *webSessions) Get(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error) {
if err := req.Check(); err != nil {
return nil, trace.Wrap(err)
}
item, err := r.backend.Get(ctx, webSessionKey(req.SessionID))
if err != nil {
return nil, trace.Wrap(err)
}
session, err := services.UnmarshalWebSession(item.Value, services.SkipValidation())
if err != nil && !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
if session != nil {
return session, nil
}
// DELETE IN 7.x:
// Return web sessions from a legacy path under /web/users/<user>/sessions/<id>
return getLegacyWebSession(ctx, r.backend, req.User, req.SessionID)
}
// List gets all regular web sessions.
func (r *webSessions) List(ctx context.Context) (out []types.WebSession, err error) {
key := backend.Key(webPrefix, sessionsPrefix)
result, err := r.backend.GetRange(ctx, key, backend.RangeEnd(key), backend.NoLimit)
if err != nil {
return nil, trace.Wrap(err)
}
for _, item := range result.Items {
session, err := services.UnmarshalWebSession(item.Value, services.SkipValidation())
if err != nil {
return nil, trace.Wrap(err)
}
out = append(out, session)
}
// DELETE IN 7.x:
// Return web sessions from a legacy path under /web/users/<user>/sessions/<id>
legacySessions, err := r.listLegacySessions(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
return append(out, legacySessions...), nil
}
// Upsert updates the existing or inserts a new web session.
func (r *webSessions) Upsert(ctx context.Context, session types.WebSession) error {
value, err := services.MarshalWebSession(session)
if err != nil {
return trace.Wrap(err)
}
sessionMetadata := session.GetMetadata()
item := backend.Item{
Key: webSessionKey(session.GetName()),
Value: value,
Expires: backend.EarliestExpiry(session.GetBearerTokenExpiryTime(), sessionMetadata.Expiry()),
}
_, err = r.backend.Put(ctx, item)
if err != nil {
return trace.Wrap(err)
}
return nil
}
// Delete deletes the web session specified with req from the storage.
func (r *webSessions) Delete(ctx context.Context, req types.DeleteWebSessionRequest) error {
if err := req.Check(); err != nil {
return trace.Wrap(err)
}
return trace.Wrap(r.backend.Delete(ctx, webSessionKey(req.SessionID)))
}
// DeleteAll removes all regular web sessions.
func (r *webSessions) DeleteAll(ctx context.Context) error {
startKey := backend.Key(webPrefix, sessionsPrefix)
return trace.Wrap(r.backend.DeleteRange(ctx, startKey, backend.RangeEnd(startKey)))
}
// DELETE IN 7.x.
// listLegacySessions lists web sessions under a legacy path /web/users/<user>/sessions/<id>
func (r *webSessions) listLegacySessions(ctx context.Context) ([]types.WebSession, error) {
startKey := backend.Key(webPrefix, usersPrefix)
result, err := r.backend.GetRange(ctx, startKey, backend.RangeEnd(startKey), backend.NoLimit)
if err != nil {
return nil, trace.Wrap(err)
}
out := make([]types.WebSession, 0, len(result.Items))
for _, item := range result.Items {
suffix, _, err := baseTwoKeys(item.Key)
if err != nil && trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
if suffix != sessionsPrefix {
continue
}
session, err := services.UnmarshalWebSession(item.Value, services.SkipValidation())
if err != nil {
return nil, trace.Wrap(err)
}
out = append(out, session)
}
return out, nil
}
type webSessions struct {
backend backend.Backend
log logrus.FieldLogger
}
// WebTokens returns the web token manager.
func (s *IdentityService) WebTokens() types.WebTokenInterface {
return &webTokens{backend: s.Backend, log: s.log}
}
// Get returns the web token described with req.
func (r *webTokens) Get(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) {
if err := req.Check(); err != nil {
return nil, trace.Wrap(err)
}
item, err := r.backend.Get(ctx, webTokenKey(req.Token))
if err != nil {
return nil, trace.Wrap(err)
}
token, err := services.UnmarshalWebToken(item.Value, services.SkipValidation())
if err != nil {
return nil, trace.Wrap(err)
}
return token, nil
}
// List gets all web tokens.
func (r *webTokens) List(ctx context.Context) (out []types.WebToken, err error) {
key := backend.Key(webPrefix, tokensPrefix)
result, err := r.backend.GetRange(ctx, key, backend.RangeEnd(key), backend.NoLimit)
if err != nil {
return nil, trace.Wrap(err)
}
for _, item := range result.Items {
token, err := services.UnmarshalWebToken(item.Value, services.SkipValidation())
if err != nil {
return nil, trace.Wrap(err)
}
out = append(out, token)
}
return out, nil
}
// Upsert updates the existing or inserts a new web token.
func (r *webTokens) Upsert(ctx context.Context, token types.WebToken) error {
bytes, err := services.MarshalWebToken(token, services.WithVersion(services.V3))
if err != nil {
return trace.Wrap(err)
}
metadata := token.GetMetadata()
item := backend.Item{
Key: webTokenKey(token.GetToken()),
Value: bytes,
Expires: metadata.Expiry(),
}
_, err = r.backend.Put(ctx, item)
if err != nil {
return trace.Wrap(err)
}
return nil
}
// Delete deletes the web token specified with req from the storage.
func (r *webTokens) Delete(ctx context.Context, req types.DeleteWebTokenRequest) error {
if err := req.Check(); err != nil {
return trace.Wrap(err)
}
return trace.Wrap(r.backend.Delete(ctx, webTokenKey(req.Token)))
}
// DeleteAll removes all web tokens.
func (r *webTokens) DeleteAll(ctx context.Context) error {
startKey := backend.Key(webPrefix, tokensPrefix)
if err := r.backend.DeleteRange(ctx, startKey, backend.RangeEnd(startKey)); err != nil {
return trace.Wrap(err)
}
return nil
}
type webTokens struct {
backend backend.Backend
log logrus.FieldLogger
}
// DELETE in 7.x.
// getLegacySession returns the web session for the specified user/sessionID
// under a legacy path /web/users/<user>/sessions/<id>
func getLegacyWebSession(ctx context.Context, backend backend.Backend, user, sessionID string) (types.WebSession, error) {
item, err := backend.Get(ctx, legacyWebSessionKey(user, sessionID))
if err != nil {
return nil, trace.Wrap(err)
}
session, err := services.UnmarshalWebSession(item.Value, services.SkipValidation())
if err != nil {
return nil, trace.Wrap(err)
}
// this is for backwards compatibility to ensure we
// always have these values
session.SetUser(user)
session.SetName(sessionID)
return session, nil
}
func webSessionKey(sessionID string) (key []byte) {
return backend.Key(webPrefix, sessionsPrefix, sessionID)
}
func webTokenKey(token string) (key []byte) {
return backend.Key(webPrefix, tokensPrefix, token)
}
func legacyWebSessionKey(user, sessionID string) (key []byte) {
return backend.Key(webPrefix, usersPrefix, user, sessionsPrefix, sessionID)
}

View file

@ -34,18 +34,21 @@ import (
"github.com/gokyle/hotp"
"github.com/gravitational/trace"
"github.com/pborman/uuid"
"github.com/sirupsen/logrus"
)
// IdentityService is responsible for managing web users and currently
// user accounts as well
type IdentityService struct {
backend.Backend
log logrus.FieldLogger
}
// NewIdentityService returns a new instance of IdentityService object
func NewIdentityService(backend backend.Backend) *IdentityService {
return &IdentityService{
Backend: backend,
log: logrus.WithField(trace.Component, "identity"),
}
}
@ -450,26 +453,6 @@ func (s *IdentityService) DeleteUsedTOTPToken(user string) error {
return s.Delete(context.TODO(), backend.Key(webPrefix, usersPrefix, user, usedTOTPPrefix))
}
// UpsertWebSession updates or inserts a web session for a user and session id
// the session will be created with bearer token expiry time TTL, because
// it is expected to be extended by the client before then
func (s *IdentityService) UpsertWebSession(user, sid string, session services.WebSession) error {
session.SetUser(user)
session.SetName(sid)
value, err := services.MarshalWebSession(session)
if err != nil {
return trace.Wrap(err)
}
sessionMetadata := session.GetMetadata()
item := backend.Item{
Key: backend.Key(webPrefix, usersPrefix, user, sessionsPrefix, sid),
Value: value,
Expires: backend.EarliestExpiry(session.GetBearerTokenExpiryTime(), sessionMetadata.Expiry()),
}
_, err = s.Put(context.TODO(), item)
return trace.Wrap(err)
}
// AddUserLoginAttempt logs user login attempt
func (s *IdentityService) AddUserLoginAttempt(user string, attempt services.LoginAttempt, ttl time.Duration) error {
if err := attempt.Check(); err != nil {
@ -521,41 +504,6 @@ func (s *IdentityService) DeleteUserLoginAttempts(user string) error {
return nil
}
// GetWebSession returns a web session state for a given user and session id
func (s *IdentityService) GetWebSession(user, sid string) (services.WebSession, error) {
if user == "" {
return nil, trace.BadParameter("missing username")
}
if sid == "" {
return nil, trace.BadParameter("missing session id")
}
item, err := s.Get(context.TODO(), backend.Key(webPrefix, usersPrefix, user, sessionsPrefix, sid))
if err != nil {
return nil, trace.Wrap(err)
}
session, err := services.UnmarshalWebSession(item.Value)
if err != nil {
return nil, trace.Wrap(err)
}
// this is for backwards compatibility to ensure we
// always have these values
session.SetUser(user)
session.SetName(sid)
return session, nil
}
// DeleteWebSession deletes web session from the storage
func (s *IdentityService) DeleteWebSession(user, sid string) error {
if user == "" {
return trace.BadParameter("missing username")
}
if sid == "" {
return trace.BadParameter("missing session id")
}
err := s.Delete(context.TODO(), backend.Key(webPrefix, usersPrefix, user, sessionsPrefix, sid))
return trace.Wrap(err)
}
// UpsertPassword upserts new password hash into a backend.
func (s *IdentityService) UpsertPassword(user string, password []byte) error {
if user == "" {

View file

@ -95,7 +95,7 @@ func WithExpires(expires time.Time) MarshalOption {
func WithVersion(v string) MarshalOption {
return func(c *MarshalConfig) error {
switch v {
case V1, V2:
case V1, V2, V3:
c.Version = v
return nil
default:

View file

@ -16,6 +16,8 @@ limitations under the License.
package services
import "github.com/gravitational/teleport/api/types"
// Services collects all services
type Services interface {
UsersService
@ -27,4 +29,6 @@ type Services interface {
DynamicAccess
Presence
AppSession
types.WebSessionsGetter
types.WebTokensGetter
}

View file

@ -20,7 +20,9 @@ import (
"encoding/json"
"fmt"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
)
@ -57,7 +59,7 @@ func ExtendWebSession(ws WebSession) (WebSession, error) {
}
// UnmarshalWebSession unmarshals the WebSession resource from JSON.
func UnmarshalWebSession(bytes []byte, opts ...MarshalOption) (WebSession, error) {
func UnmarshalWebSession(bytes []byte, opts ...MarshalOption) (types.WebSession, error) {
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
@ -70,7 +72,7 @@ func UnmarshalWebSession(bytes []byte, opts ...MarshalOption) (WebSession, error
}
switch h.Version {
case V2:
var ws WebSessionV2
var ws types.WebSessionV2
if err := utils.UnmarshalWithSchema(GetWebSessionSchema(), &ws, bytes); err != nil {
return nil, trace.BadParameter(err.Error())
}
@ -94,7 +96,7 @@ func UnmarshalWebSession(bytes []byte, opts ...MarshalOption) (WebSession, error
}
// MarshalWebSession marshals the WebSession resource to JSON.
func MarshalWebSession(ws WebSession, opts ...MarshalOption) ([]byte, error) {
func MarshalWebSession(ws types.WebSession, opts ...MarshalOption) ([]byte, error) {
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
@ -114,3 +116,77 @@ func MarshalWebSession(ws WebSession, opts ...MarshalOption) ([]byte, error) {
return nil, trace.BadParameter("unrecognized web session version %T", ws)
}
}
// MarshalWebToken serializes the web token as JSON-encoded payload
func MarshalWebToken(token types.WebToken, opts ...MarshalOption) ([]byte, error) {
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
version := cfg.GetVersion()
switch version {
case V3:
value, ok := token.(*types.WebTokenV3)
if !ok {
return nil, trace.BadParameter("don't know how to marshal web token %v", token)
}
if !cfg.PreserveResourceID {
// avoid modifying the original object
// to prevent unexpected data races
copy := *value
copy.SetResourceID(0)
value = &copy
}
return utils.FastMarshal(value)
default:
return nil, trace.BadParameter("version %v is not supported", version)
}
}
// UnmarshalWebToken interprets bytes as JSON-encoded web token value
func UnmarshalWebToken(bytes []byte, opts ...MarshalOption) (types.WebToken, error) {
config, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
var hdr ResourceHeader
err = json.Unmarshal(bytes, &hdr)
if err != nil {
return nil, trace.Wrap(err)
}
switch hdr.Version {
case V3:
var token types.WebTokenV3
if err := utils.UnmarshalWithSchema(GetWebTokenSchema(), &token, bytes); err != nil {
return nil, trace.BadParameter("invalid web token: %v", err.Error())
}
if err := token.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
if config.ID != 0 {
token.SetResourceID(config.ID)
}
if !config.Expires.IsZero() {
token.Metadata.SetExpiry(config.Expires)
}
utils.UTC(token.Metadata.Expires)
return &token, nil
}
return nil, trace.BadParameter("web token resource version %v is not supported", hdr.Version)
}
// GetWebTokenSchema returns JSON schema for the web token resource
func GetWebTokenSchema() string {
return fmt.Sprintf(V2SchemaTemplate, MetadataSchema, WebTokenSpecV3Schema, "")
}
// WebTokenSpecV3Schema is JSON schema for the web token V3
const WebTokenSpecV3Schema = `{
"type": "object",
"additionalProperties": false,
"required": ["token", "user"],
"properties": {
"user": {"type": "string"},
"token": {"type": "string"}
}
}`

View file

@ -31,6 +31,7 @@ import (
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/defaults"
@ -547,39 +548,45 @@ func (s *ServicesTestSuite) PasswordHashCRUD(c *check.C) {
}
func (s *ServicesTestSuite) WebSessionCRUD(c *check.C) {
_, err := s.WebS.GetWebSession("user1", "sid1")
req := types.GetWebSessionRequest{User: "user1", SessionID: "sid1"}
_, err := s.WebS.WebSessions().Get(context.TODO(), req)
c.Assert(trace.IsNotFound(err), check.Equals, true, check.Commentf("%#v", err))
dt := s.Clock.Now().Add(1 * time.Minute)
ws := services.NewWebSession("sid1", services.KindWebSession, services.KindWebSession,
services.WebSessionSpecV2{
ws := types.NewWebSession("sid1", services.KindWebSession, services.KindWebSession,
types.WebSessionSpecV2{
User: "user1",
Pub: []byte("pub123"),
Priv: []byte("priv123"),
Expires: dt,
})
err = s.WebS.UpsertWebSession("user1", "sid1", ws)
err = s.WebS.WebSessions().Upsert(context.TODO(), ws)
c.Assert(err, check.IsNil)
out, err := s.WebS.GetWebSession("user1", "sid1")
out, err := s.WebS.WebSessions().Get(context.TODO(), req)
c.Assert(err, check.IsNil)
c.Assert(out, check.DeepEquals, ws)
ws1 := services.NewWebSession("sid1", services.KindWebSession, services.KindWebSession,
services.WebSessionSpecV2{
ws1 := types.NewWebSession("sid1", services.KindWebSession, services.KindWebSession,
types.WebSessionSpecV2{
User: "user1",
Pub: []byte("pub321"),
Priv: []byte("priv321"),
Expires: dt,
})
err = s.WebS.UpsertWebSession("user1", "sid1", ws1)
err = s.WebS.WebSessions().Upsert(context.TODO(), ws1)
c.Assert(err, check.IsNil)
out2, err := s.WebS.GetWebSession("user1", "sid1")
out2, err := s.WebS.WebSessions().Get(context.TODO(), req)
c.Assert(err, check.IsNil)
c.Assert(out2, check.DeepEquals, ws1)
c.Assert(s.WebS.DeleteWebSession("user1", "sid1"), check.IsNil)
c.Assert(s.WebS.WebSessions().Delete(context.TODO(), types.DeleteWebSessionRequest{
User: req.User,
SessionID: req.SessionID,
}), check.IsNil)
_, err = s.WebS.GetWebSession("user1", "sid1")
_, err = s.WebS.WebSessions().Get(context.TODO(), req)
fixtures.ExpectNotFound(c, err)
}

View file

@ -467,6 +467,7 @@ const (
KindSession = types.KindSession
KindSSHSession = types.KindSSHSession
KindWebSession = types.KindWebSession
KindWebToken = types.KindWebToken
KindAppSession = types.KindAppSession
KindEvent = types.KindEvent
KindAuthServer = types.KindAuthServer

View file

@ -136,7 +136,7 @@ func NewHeartbeat(cfg HeartbeatConfig) (*Heartbeat, error) {
Entry: log.WithFields(log.Fields{
trace.Component: teleport.Component(cfg.Component, "beat"),
}),
checkTicker: time.NewTicker(cfg.CheckPeriod),
checkTicker: cfg.Clock.NewTicker(cfg.CheckPeriod),
announceC: make(chan struct{}, 1),
sendC: make(chan struct{}, 1),
}
@ -237,7 +237,7 @@ type Heartbeat struct {
nextKeepAlive time.Time
// checkTicker is a ticker for state transitions
// during which different checks are performed
checkTicker *time.Ticker
checkTicker clockwork.Ticker
// keepAliver sends keep alive updates
keepAliver services.KeepAliver
// announceC is event receives an event
@ -262,7 +262,7 @@ func (h *Heartbeat) Run() error {
}
h.OnHeartbeat(err)
select {
case <-h.checkTicker.C:
case <-h.checkTicker.Chan():
case <-h.sendC:
h.Debugf("Asked check out of cycle")
case <-h.cancelCtx.Done():

View file

@ -21,7 +21,6 @@ package web
import (
"compress/gzip"
"context"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"fmt"
@ -37,6 +36,7 @@ import (
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/client"
@ -111,8 +111,6 @@ type Config struct {
DomainName string
// ProxyClient is a client that authenticated as proxy
ProxyClient auth.ClientI
// DisableUI allows to turn off serving web based UI
DisableUI bool
// ProxySSHAddr points to the SSH address of the proxy
ProxySSHAddr utils.NetAddr
// ProxyWebAddr points to the web (HTTPS) address of the proxy
@ -139,6 +137,15 @@ type Config struct {
// Context is used to signal process exit.
Context context.Context
// StaticFS optionally specifies the HTTP file system to use.
// Enables web UI if set.
StaticFS http.FileSystem
// cachedSessionLingeringThreshold specifies the time the session will linger
// in the cache before getting purged after it has expired.
// Defaults to cachedSessionLingeringThreshold if unspecified.
cachedSessionLingeringThreshold *time.Duration
}
type RewritingHandler struct {
@ -191,11 +198,18 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
}
}
auth, err := newSessionCache(&sessionCache{
proxyClient: cfg.ProxyClient,
authServers: []utils.NetAddr{cfg.AuthServers},
cipherSuites: cfg.CipherSuites,
clock: h.clock,
sessionLingeringThreshold := cachedSessionLingeringThreshold
if cfg.cachedSessionLingeringThreshold != nil {
sessionLingeringThreshold = *cfg.cachedSessionLingeringThreshold
}
auth, err := newSessionCache(sessionCacheOptions{
proxyClient: cfg.ProxyClient,
accessPoint: cfg.AccessPoint,
servers: []utils.NetAddr{cfg.AuthServers},
cipherSuites: cfg.CipherSuites,
clock: h.clock,
sessionLingeringThreshold: sessionLingeringThreshold,
})
if err != nil {
return nil, trace.Wrap(err)
@ -309,16 +323,9 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
h.POST("/webapi/host/credentials", httplib.MakeHandler(h.hostCredentials))
// if Web UI is enabled, check the assets dir:
var (
indexPage *template.Template
staticFS http.FileSystem
)
if !cfg.DisableUI {
staticFS, err = NewStaticFileSystem(isDebugMode())
if err != nil {
return nil, trace.Wrap(err)
}
index, err := staticFS.Open("/index.html")
var indexPage *template.Template
if cfg.StaticFS != nil {
index, err := cfg.StaticFS.Open("/index.html")
if err != nil {
h.log.WithError(err).Error("Failed to open index file.")
return nil, trace.Wrap(err)
@ -344,7 +351,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
}
// request is going to the web UI
if cfg.DisableUI {
if cfg.StaticFS == nil {
w.WriteHeader(http.StatusNotImplemented)
return
}
@ -358,7 +365,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
// serve Web UI:
if strings.HasPrefix(r.URL.Path, "/web/app") {
httplib.SetStaticFileHeaders(w.Header())
http.StripPrefix("/web", http.FileServer(staticFS)).ServeHTTP(w, r)
http.StripPrefix("/web", http.FileServer(cfg.StaticFS)).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/web/") || r.URL.Path == "/web" {
csrfToken, err := csrf.AddCSRFProtection(w, r)
if err != nil {
@ -374,9 +381,9 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
ctx, err := h.AuthenticateRequest(w, r, false)
if err == nil {
re, err := NewSessionResponse(ctx)
resp, err := newSessionResponse(ctx)
if err == nil {
out, err := json.Marshal(re)
out, err := json.Marshal(resp)
if err == nil {
session.Session = base64.StdEncoding.EncodeToString(out)
}
@ -946,7 +953,7 @@ func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httpr
return nil, trace.AccessDenied("access denied")
}
logger.Infof("Callback is redirecting to web browser.")
err = SetSession(w, response.Username, response.Session.GetName())
err = SetSessionCookie(w, response.Username, response.Session.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
@ -1028,7 +1035,7 @@ func (h *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprou
}
logger.Info("Callback redirecting to web browser.")
if err := SetSession(w, response.Username, response.Session.GetName()); err != nil {
if err := SetSessionCookie(w, response.Username, response.Session.GetName()); err != nil {
return nil, trace.Wrap(err)
}
return nil, httplib.SafeRedirect(w, r, response.Req.ClientRedirectURL)
@ -1163,6 +1170,12 @@ type CreateSessionReq struct {
SecondFactorToken string `json:"second_factor_token"`
}
// String returns text description of this response
func (r *CreateSessionResponse) String() string {
return fmt.Sprintf("WebSession(type=%v,token=%v,expires=%vs)",
r.Type, r.Token, r.ExpiresIn)
}
// CreateSessionResponse returns OAuth compabible data about
// access token: https://tools.ietf.org/html/rfc6749
type CreateSessionResponse struct {
@ -1174,13 +1187,13 @@ type CreateSessionResponse struct {
ExpiresIn int `json:"expires_in"`
}
func NewSessionResponse(ctx *SessionContext) (*CreateSessionResponse, error) {
func newSessionResponse(ctx *SessionContext) (*CreateSessionResponse, error) {
clt, err := ctx.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}
webSession := ctx.GetWebSession()
user, err := clt.GetUser(webSession.GetUser(), false)
token := ctx.getToken()
user, err := clt.GetUser(ctx.GetUser(), false)
if err != nil {
return nil, trace.Wrap(err)
}
@ -1196,11 +1209,10 @@ func NewSessionResponse(ctx *SessionContext) (*CreateSessionResponse, error) {
if err != nil {
return nil, trace.Wrap(err)
}
return &CreateSessionResponse{
Type: roundtrip.AuthBearer,
Token: webSession.GetBearerToken(),
ExpiresIn: int(webSession.GetBearerTokenExpiryTime().Sub(ctx.parent.clock.Now()) / time.Second),
Token: token.GetName(),
ExpiresIn: int(token.Expiry().Sub(ctx.parent.clock.Now()) / time.Second),
}, nil
}
@ -1243,17 +1255,28 @@ func (h *Handler) createWebSession(w http.ResponseWriter, r *http.Request, p htt
return nil, trace.AccessDenied("bad auth credentials")
}
if err := SetSession(w, req.User, webSession.GetName()); err != nil {
// Block and wait a few seconds for the session that was created to show up
// in the cache. If this request is not blocked here, it can get stuck in a
// racy session creation loop.
err = h.waitForWebSession(r.Context(), types.GetWebSessionRequest{
User: req.User,
SessionID: webSession.GetName(),
})
if err != nil {
return nil, trace.Wrap(err)
}
ctx, err := h.auth.ValidateSession(req.User, webSession.GetName())
if err := SetSessionCookie(w, req.User, webSession.GetName()); err != nil {
return nil, trace.Wrap(err)
}
ctx, err := h.auth.newSessionContext(req.User, webSession.GetName())
if err != nil {
h.log.WithError(err).Warnf("Access attempt denied for user %q.", req.User)
return nil, trace.AccessDenied("need auth")
}
return NewSessionResponse(ctx)
return newSessionResponse(ctx)
}
// deleteSession is called to sign out user
@ -1283,29 +1306,23 @@ func (h *Handler) logout(w http.ResponseWriter, ctx *SessionContext) error {
}
// renewSession is called in two ways:
// - Without requestId: Creates new session that is about to expire.
// - With requestId: Creates new session that includes additional roles assigned with approving access request.
//
// It issues the new session and generates new session cookie.
// It's important to understand that the old session becomes effectively invalid.
// - Without requestId: Updates the existing session about to expire.
// - With requestId: Updates existing session with additional roles assigned with approving access request.
func (h *Handler) renewSession(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
requestID := params.ByName("requestId")
newSess, err := ctx.ExtendWebSession(requestID)
newSession, err := ctx.extendWebSession(requestID)
if err != nil {
return nil, trace.Wrap(err)
}
// transfer ownership over connections that were opened in the
// sessionContext
newContext, err := ctx.parent.ValidateSession(newSess.GetUser(), newSess.GetName())
newContext, err := h.auth.newSessionContextFromSession(newSession)
if err != nil {
return nil, trace.Wrap(err)
}
newContext.AddClosers(ctx.TransferClosers()...)
if err := SetSession(w, newSess.GetUser(), newSess.GetName()); err != nil {
if err := SetSessionCookie(w, newSession.GetUser(), newSession.GetName()); err != nil {
return nil, trace.Wrap(err)
}
return NewSessionResponse(newContext)
return newSessionResponse(newContext)
}
func (h *Handler) changePasswordWithToken(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
@ -1318,27 +1335,27 @@ func (h *Handler) changePasswordWithToken(w http.ResponseWriter, r *http.Request
if err != nil {
return nil, trace.Wrap(err)
}
ctx, err := h.auth.ValidateSession(sess.GetUser(), sess.GetName())
ctx, err := h.auth.newSessionContext(sess.GetUser(), sess.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
if err := SetSession(w, sess.GetUser(), sess.GetName()); err != nil {
if err := SetSessionCookie(w, sess.GetUser(), sess.GetName()); err != nil {
return nil, trace.Wrap(err)
}
return NewSessionResponse(ctx)
return newSessionResponse(ctx)
}
// createResetPasswordToken allows a UI user to reset a user's password.
// This handler is also required for after creating new users.
func (h *Handler) createResetPasswordToken(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
clt, err := ctx.GetClient()
if err != nil {
var req auth.CreateResetPasswordTokenRequest
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
var req auth.CreateResetPasswordTokenRequest
if err := httplib.ReadJSON(r, &req); err != nil {
clt, err := ctx.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}
@ -1347,7 +1364,6 @@ func (h *Handler) createResetPasswordToken(w http.ResponseWriter, r *http.Reques
Name: req.Name,
Type: req.Type,
})
if err != nil {
return nil, trace.Wrap(err)
}
@ -1460,14 +1476,14 @@ func (h *Handler) createSessionWithU2FSignResponse(w http.ResponseWriter, r *htt
if err != nil {
return nil, trace.AccessDenied("bad auth credentials")
}
if err := SetSession(w, req.User, sess.GetName()); err != nil {
if err := SetSessionCookie(w, req.User, sess.GetName()); err != nil {
return nil, trace.Wrap(err)
}
ctx, err := h.auth.ValidateSession(req.User, sess.GetName())
ctx, err := h.auth.newSessionContext(req.User, sess.GetName())
if err != nil {
return nil, trace.AccessDenied("need auth")
}
return NewSessionResponse(ctx)
return newSessionResponse(ctx)
}
// getClusters returns a list of cluster and its data.
@ -1541,7 +1557,7 @@ func (h *Handler) siteNodesGet(w http.ResponseWriter, r *http.Request, p httprou
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
// Get a client to the Auth Server with the logged in users identity. The
// 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 nodes.
clt, err := ctx.GetUserClient(site)
if err != nil {
@ -1561,7 +1577,7 @@ func (h *Handler) siteNodesGet(w http.ResponseWriter, r *http.Request, p httprou
// GET /v1/webapi/sites/:site/namespaces/:namespace/connect?access_token=bearer_token&params=<urlencoded json-structure>
//
// Due to the nature of websocket we can't POST parameters as is, so we have
// to add query parameters. The params query parameter is a url encodeed JSON strucrture:
// to add query parameters. The params query parameter is a URL-encoded JSON structure:
//
// {"server_id": "uuid", "login": "admin", "term": {"h": 120, "w": 100}, "sid": "123"}
//
@ -1591,8 +1607,8 @@ func (h *Handler) siteNodeConnect(
return nil, trace.Wrap(err)
}
h.log.Debugf("New terminal request for ns=%s, server=%s, login=%s, sid=%s.",
req.Namespace, req.Server, req.Login, req.SessionID)
h.log.Debugf("New terminal request for ns=%s, server=%s, login=%s, sid=%s, websid=%s.",
req.Namespace, req.Server, req.Login, req.SessionID, ctx.GetSessionID())
authAccessPoint, err := site.CachingAccessPoint()
if err != nil {
@ -1623,7 +1639,7 @@ func (h *Handler) siteNodeConnect(
}
// start the websocket session with a web-based terminal:
h.log.Infof("Getting terminal to '%#v'.", req)
h.log.Infof("Getting terminal to %#v.", req)
term.Serve(w, r)
return nil, nil
@ -2231,12 +2247,12 @@ func (h *Handler) AuthenticateRequest(w http.ResponseWriter, r *http.Request, ch
}
return nil, trace.AccessDenied(missingCookieMsg)
}
d, err := DecodeCookie(cookie.Value)
decodedCookie, err := DecodeCookie(cookie.Value)
if err != nil {
logger.WithError(err).Warn("Failed to decode cookie.")
return nil, trace.AccessDenied("failed to decode cookie")
}
ctx, err := h.auth.ValidateSession(d.User, d.SID)
ctx, err := h.auth.validateSession(r.Context(), decodedCookie.User, decodedCookie.SID)
if err != nil {
logger.WithError(err).Warn("Invalid session.")
ClearSession(w)
@ -2248,9 +2264,8 @@ func (h *Handler) AuthenticateRequest(w http.ResponseWriter, r *http.Request, ch
logger.WithError(err).Warn("No auth headers.")
return nil, trace.AccessDenied("need auth")
}
if subtle.ConstantTimeCompare([]byte(creds.Password), []byte(ctx.GetWebSession().GetBearerToken())) != 1 {
logger.Warn("Request failed: bad bearer token.")
if err := ctx.validateBearerToken(r.Context(), creds.Password); err != nil {
logger.WithError(err).Warn("Request failed: bad bearer token.")
return nil, trace.AccessDenied("bad bearer token")
}
}

View file

@ -44,6 +44,7 @@ import (
"golang.org/x/text/encoding/unicode"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/mocku2f"
"github.com/gravitational/teleport/lib/auth/u2f"
@ -71,11 +72,13 @@ import (
"github.com/beevik/etree"
"github.com/gogo/protobuf/proto"
"github.com/google/go-cmp/cmp"
"github.com/jonboulle/clockwork"
lemma_secret "github.com/mailgun/lemma/secret"
"github.com/pborman/uuid"
"github.com/pquerna/otp/totp"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
. "gopkg.in/check.v1"
kyaml "k8s.io/apimachinery/pkg/util/yaml"
)
@ -123,20 +126,12 @@ func (s *WebSuite) SetUpSuite(c *C) {
os.Unsetenv(teleport.DebugEnvVar)
utils.InitLoggerForTests(testing.Verbose())
// configure tests to use static assets from webassets/teleport:
debugAssetsPath = "../../webassets/teleport"
os.Setenv(teleport.DebugEnvVar, "true")
var err error
s.mockU2F, err = mocku2f.Create()
c.Assert(err, IsNil)
c.Assert(s.mockU2F, NotNil)
}
func (s *WebSuite) TearDownSuite(c *C) {
os.Unsetenv(teleport.DebugEnvVar)
}
func (s *WebSuite) SetUpTest(c *C) {
u, err := user.Current()
c.Assert(err, IsNil)
@ -246,16 +241,22 @@ func (s *WebSuite) SetUpTest(c *C) {
)
c.Assert(err, IsNil)
// Expired sessions are purged immediately
var sessionLingeringThreshold time.Duration = 0
fs, err := NewDebugFileSystem("../../webassets/teleport")
c.Assert(err, IsNil)
handler, err := NewHandler(Config{
Proxy: revTunServer,
AuthServers: utils.FromAddr(s.server.Addr()),
DomainName: s.server.ClusterName(),
ProxyClient: s.proxyClient,
CipherSuites: utils.DefaultCipherSuites(),
AccessPoint: s.proxyClient,
Context: context.Background(),
HostUUID: proxyID,
Emitter: s.proxyClient,
Proxy: revTunServer,
AuthServers: utils.FromAddr(s.server.Addr()),
DomainName: s.server.ClusterName(),
ProxyClient: s.proxyClient,
CipherSuites: utils.DefaultCipherSuites(),
AccessPoint: s.proxyClient,
Context: context.Background(),
HostUUID: proxyID,
Emitter: s.proxyClient,
StaticFS: fs,
cachedSessionLingeringThreshold: &sessionLingeringThreshold,
}, SetSessionStreamPollPeriod(200*time.Millisecond), SetClock(s.clock))
c.Assert(err, IsNil)
@ -294,6 +295,17 @@ func (s *WebSuite) TearDownTest(c *C) {
s.proxyTunnel.Close()
}
func (r *authPack) renewSession(ctx context.Context, t *testing.T) *roundtrip.Response {
resp, err := r.clt.PostJSON(ctx, r.clt.Endpoint("webapi", "sessions", "renew"), nil)
require.NoError(t, err)
return resp
}
func (r *authPack) validateAPI(ctx context.Context, t *testing.T) {
_, err := r.clt.Get(ctx, r.clt.Endpoint("webapi", "sites"), url.Values{})
require.NoError(t, err)
}
type authPack struct {
otpSecret string
user string
@ -303,28 +315,6 @@ type authPack struct {
cookies []*http.Cookie
}
func (s *WebSuite) authPackFromResponse(c *C, re *roundtrip.Response) *authPack {
var sess *CreateSessionResponse
c.Assert(json.Unmarshal(re.Bytes(), &sess), IsNil)
jar, err := cookiejar.New(nil)
c.Assert(err, IsNil)
clt := s.client(roundtrip.BearerAuth(sess.Token), roundtrip.CookieJar(jar))
jar.SetCookies(s.url(), re.Cookies())
session, err := sess.response()
c.Assert(err, IsNil)
if session.ExpiresIn < 0 {
c.Errorf("expected expiry time to be in the future but got %v", session.ExpiresIn)
}
return &authPack{
session: session,
clt: clt,
cookies: re.Cookies(),
}
}
// authPack returns new authenticated package consisting of created valid
// user, otp token, created web session and authenticated client.
func (s *WebSuite) authPack(c *C, user string) *authPack {
@ -608,43 +598,6 @@ func (s *WebSuite) TestPasswordChange(c *C) {
c.Assert(err, IsNil)
}
func (s *WebSuite) TestWebSessionsRenew(c *C) {
pack := s.authPack(c, "foo")
// make sure we can use client to make authenticated requests
// before we issue this request, we will recover session id and bearer token
//
prevSessionCookie := *pack.cookies[0]
prevBearerToken := pack.session.Token
re, err := pack.clt.PostJSON(context.Background(), pack.clt.Endpoint("webapi", "sessions", "renew"), nil)
c.Assert(err, IsNil)
newPack := s.authPackFromResponse(c, re)
// new session is functioning
_, err = newPack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{})
c.Assert(err, IsNil)
// old session is stil valid too (until it expires)
jar, err := cookiejar.New(nil)
c.Assert(err, IsNil)
oldClt := s.client(roundtrip.BearerAuth(prevBearerToken), roundtrip.CookieJar(jar))
jar.SetCookies(s.url(), []*http.Cookie{&prevSessionCookie})
_, err = oldClt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{})
c.Assert(err, IsNil)
// now delete session
_, err = newPack.clt.Delete(
context.Background(),
pack.clt.Endpoint("webapi", "sessions"))
c.Assert(err, IsNil)
// subsequent requests trying to use this session will fail
_, err = newPack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{})
c.Assert(err, NotNil)
c.Assert(trace.IsAccessDenied(err), Equals, true)
}
func (s *WebSuite) TestWebSessionsBadInput(c *C) {
user := "bob"
pass := "abc123"
@ -981,13 +934,12 @@ func (s *WebSuite) TestTerminal(c *C) {
defer ws.Close()
termHandler := newTerminalHandler()
stream, err := termHandler.asTerminalStream(ws)
c.Assert(err, IsNil)
stream := termHandler.asTerminalStream(ws)
_, err = io.WriteString(stream, "echo vinsong\r\n")
c.Assert(err, IsNil)
err = s.waitForOutput(stream, "vinsong")
err = waitForOutput(stream, "vinsong")
c.Assert(err, IsNil)
}
@ -1039,13 +991,12 @@ func (s *WebSuite) TestWebAgentForward(c *C) {
defer ws.Close()
termHandler := newTerminalHandler()
stream, err := termHandler.asTerminalStream(ws)
c.Assert(err, IsNil)
stream := termHandler.asTerminalStream(ws)
_, err = io.WriteString(stream, "echo $SSH_AUTH_SOCK\r\n")
c.Assert(err, IsNil)
err = s.waitForOutput(stream, "/")
err = waitForOutput(stream, "/")
c.Assert(err, IsNil)
}
@ -1058,15 +1009,14 @@ func (s *WebSuite) TestActiveSessions(c *C) {
defer ws.Close()
termHandler := newTerminalHandler()
stream, err := termHandler.asTerminalStream(ws)
c.Assert(err, IsNil)
stream := termHandler.asTerminalStream(ws)
// To make sure we have a session.
_, err = io.WriteString(stream, "echo vinsong\r\n")
c.Assert(err, IsNil)
// Make sure server has replied.
err = s.waitForOutput(stream, "vinsong")
err = waitForOutput(stream, "vinsong")
c.Assert(err, IsNil)
// Make sure this session appears in the list of active sessions.
@ -1167,8 +1117,7 @@ func (s *WebSuite) TestCloseConnectionsOnLogout(c *C) {
defer ws.Close()
termHandler := newTerminalHandler()
stream, err := termHandler.asTerminalStream(ws)
c.Assert(err, IsNil)
stream := termHandler.asTerminalStream(ws)
// to make sure we have a session
_, err = io.WriteString(stream, "expr 137 + 39\r\n")
@ -1908,6 +1857,114 @@ func (s *WebSuite) TestCreateAppSession(c *C) {
}
}
// TestWebSessionsRenewDoesNotBreakExistingTerminalSession validates that the
// session renewed via one proxy does not force the terminals created by another
// proxy to disconnect
//
// See https://github.com/gravitational/teleport/issues/5265
func TestWebSessionsRenewDoesNotBreakExistingTerminalSession(t *testing.T) {
env := newWebPack(t, 2)
defer env.close(t)
proxy1, proxy2 := env.proxies[0], env.proxies[1]
// Connect to both proxies
pack1 := proxy1.authPack(t, "foo")
pack2 := proxy2.authPackFromPack(t, pack1)
ws := proxy2.makeTerminal(t, pack2, session.NewID())
defer ws.Close()
// Advance the time before renewing the session.
// This will allow the new session to have a more plausible
// expiration
const delta = 30 * time.Second
env.clock.Advance(auth.BearerTokenTTL - delta)
// Renew the session using the 1st proxy
resp := pack1.renewSession(context.TODO(), t)
// Expire the old session and make sure it has been removed.
// The bearer token is also removed after this point, so we have to
// use the new session data for future connects
env.clock.Advance(delta + 1*time.Second)
pack2 = proxy2.authPackFromResponse(t, resp)
// Verify that access via the 2nd proxy also works for the same session
pack2.validateAPI(context.TODO(), t)
// Check whether the terminal session is still active
validateTerminalStream(t, ws)
}
// TestWebSessionsRenewAllowsOldBearerTokenToLinger validates that the
// bearer token bound to the previous session is still active after the
// session renewal, if the renewal happens with a time margin.
//
// See https://github.com/gravitational/teleport/issues/5265
func TestWebSessionsRenewAllowsOldBearerTokenToLinger(t *testing.T) {
// Login to implicitly create a new web session
env := newWebPack(t, 1)
defer env.close(t)
proxy := env.proxies[0]
pack := proxy.authPack(t, "foo")
delta := 30 * time.Second
// Advance the time before renewing the session.
// This will allow the new session to have a more plausible
// expiration
env.clock.Advance(auth.BearerTokenTTL - delta)
// make sure we can use client to make authenticated requests
// before we issue this request, we will recover session id and bearer token
//
prevSessionCookie := *pack.cookies[0]
prevBearerToken := pack.session.Token
resp := pack.renewSession(context.TODO(), t)
newPack := proxy.authPackFromResponse(t, resp)
// new session is functioning
newPack.validateAPI(context.TODO(), t)
sessionCookie := *newPack.cookies[0]
bearerToken := newPack.session.Token
require.NotEmpty(t, bearerToken)
require.NotEmpty(t, cmp.Diff(bearerToken, prevBearerToken))
prevSessionID := decodeSessionCookie(t, prevSessionCookie.Value)
activeSessionID := decodeSessionCookie(t, sessionCookie.Value)
require.NotEmpty(t, cmp.Diff(prevSessionID, activeSessionID))
// old session is still valid
jar, err := cookiejar.New(nil)
require.NoError(t, err)
oldClt := proxy.newClient(t, roundtrip.BearerAuth(prevBearerToken), roundtrip.CookieJar(jar))
jar.SetCookies(&proxy.webURL, []*http.Cookie{&prevSessionCookie})
_, err = oldClt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{})
require.NoError(t, err)
// now expire the old session and make sure it has been removed
env.clock.Advance(delta)
_, err = proxy.client.GetWebSession(context.TODO(), types.GetWebSessionRequest{
User: "foo",
SessionID: prevSessionID,
})
require.Regexp(t, "^key.*not found$", err.Error())
// now delete session
_, err = newPack.clt.Delete(
context.Background(),
pack.clt.Endpoint("webapi", "sessions"))
require.NoError(t, err)
// subsequent requests to use this session will fail
_, err = newPack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{})
require.True(t, trace.IsAccessDenied(err))
}
type authProviderMock struct {
server services.ServerV2
}
@ -1971,7 +2028,7 @@ func (s *WebSuite) makeTerminal(pack *authPack, opts ...session.ID) (*websocket.
return ws, nil
}
func (s *WebSuite) waitForOutput(stream *terminalStream, substr string) error {
func waitForOutput(stream *terminalStream, substr string) error {
tickerCh := time.Tick(250 * time.Millisecond)
timeoutCh := time.After(10 * time.Second)
@ -2151,6 +2208,423 @@ func newTerminalHandler() TerminalHandler {
}
}
func decodeSessionCookie(t *testing.T, value string) (sessionID string) {
sessionBytes, err := hex.DecodeString(value)
require.NoError(t, err)
var cookie struct {
User string `json:"user"`
SessionID string `json:"sid"`
}
require.NoError(t, json.Unmarshal(sessionBytes, &cookie))
return cookie.SessionID
}
func (r CreateSessionResponse) response() (*CreateSessionResponse, error) {
return &CreateSessionResponse{Type: r.Type, Token: r.Token, ExpiresIn: r.ExpiresIn}, nil
}
func newWebPack(t *testing.T, numProxies int) *webPack {
clock := clockwork.NewFakeClock()
authServer, err := auth.NewTestAuthServer(auth.TestAuthServerConfig{
ClusterName: "localhost",
Dir: t.TempDir(),
Clock: clock,
})
require.NoError(t, err)
server, err := authServer.NewTestTLSServer()
require.NoError(t, err)
// start auth server
certs, err := server.Auth().GenerateServerKeys(auth.GenerateServerKeysRequest{
HostID: hostID,
NodeName: server.ClusterName(),
Roles: teleport.Roles{teleport.RoleNode},
})
require.NoError(t, err)
signer, err := sshutils.NewSigner(certs.Key, certs.Cert)
require.NoError(t, err)
const nodeID = "node"
nodeClient, err := server.NewClient(auth.TestIdentity{
I: auth.BuiltinRole{
Role: teleport.RoleNode,
Username: nodeID,
},
})
require.NoError(t, err)
hostSigners := []ssh.Signer{signer}
// create SSH service:
nodeDataDir := t.TempDir()
node, err := regular.New(
utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"},
server.ClusterName(),
hostSigners,
nodeClient,
nodeDataDir,
"",
utils.NetAddr{},
regular.SetUUID(nodeID),
regular.SetNamespace(defaults.Namespace),
regular.SetShell("/bin/sh"),
regular.SetSessionServer(nodeClient),
regular.SetEmitter(nodeClient),
regular.SetPAMConfig(&pam.Config{Enabled: false}),
regular.SetBPF(&bpf.NOP{}),
regular.SetClock(clock),
)
require.NoError(t, err)
require.NoError(t, node.Start())
require.NoError(t, auth.CreateUploaderDir(nodeDataDir))
var proxies []*proxy
for p := 0; p < numProxies; p++ {
proxyID := fmt.Sprintf("proxy%v", p)
proxies = append(proxies, createProxy(t, proxyID, node, server, hostSigners, clock))
}
// Wait for proxies to fully register before starting the test.
for start := time.Now(); ; {
proxies, err := proxies[0].client.GetProxies()
require.NoError(t, err)
if len(proxies) != numProxies {
break
}
if time.Since(start) > 5*time.Second {
t.Fatal("Proxy didn't register within 5s after startup.")
}
}
return &webPack{
proxies: proxies,
server: server,
node: node,
clock: clock,
}
}
func createProxy(t *testing.T, proxyID string, node *regular.Server, authServer *auth.TestTLSServer,
hostSigners []ssh.Signer, clock clockwork.FakeClock) *proxy {
// create reverse tunnel service:
client, err := authServer.NewClient(auth.TestIdentity{
I: auth.BuiltinRole{
Role: teleport.RoleProxy,
Username: proxyID,
},
})
require.NoError(t, err)
revTunListener, err := net.Listen("tcp", fmt.Sprintf("%v:0", authServer.ClusterName()))
require.NoError(t, err)
revTunServer, err := reversetunnel.NewServer(reversetunnel.Config{
ID: node.ID(),
Listener: revTunListener,
ClientTLS: client.TLSConfig(),
ClusterName: authServer.ClusterName(),
HostSigners: hostSigners,
LocalAuthClient: client,
LocalAccessPoint: client,
Emitter: client,
NewCachingAccessPoint: auth.NoCache,
DirectClusters: []reversetunnel.DirectCluster{{Name: authServer.ClusterName(), Client: client}},
DataDir: t.TempDir(),
})
require.NoError(t, err)
proxyServer, err := regular.New(
utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"},
authServer.ClusterName(),
hostSigners,
client,
t.TempDir(),
"",
utils.NetAddr{},
regular.SetUUID(proxyID),
regular.SetProxyMode(revTunServer),
regular.SetSessionServer(client),
regular.SetEmitter(client),
regular.SetNamespace(defaults.Namespace),
regular.SetBPF(&bpf.NOP{}),
regular.SetClock(clock),
)
require.NoError(t, err)
fs, err := NewDebugFileSystem("../../webassets/teleport")
require.NoError(t, err)
handler, err := NewHandler(Config{
Proxy: revTunServer,
AuthServers: utils.FromAddr(authServer.Addr()),
DomainName: authServer.ClusterName(),
ProxyClient: client,
CipherSuites: utils.DefaultCipherSuites(),
AccessPoint: client,
Context: context.Background(),
HostUUID: proxyID,
Emitter: client,
StaticFS: fs,
}, SetSessionStreamPollPeriod(200*time.Millisecond), SetClock(clock))
require.NoError(t, err)
webServer := httptest.NewUnstartedServer(handler)
webServer.StartTLS()
require.NoError(t, proxyServer.Start())
proxyAddr := utils.MustParseAddr(proxyServer.Addr())
addr := utils.MustParseAddr(webServer.Listener.Addr().String())
handler.handler.cfg.ProxyWebAddr = *addr
handler.handler.cfg.ProxySSHAddr = *proxyAddr
_, sshPort, err := net.SplitHostPort(proxyAddr.String())
require.NoError(t, err)
handler.handler.sshPort = sshPort
url, err := url.Parse("https://" + webServer.Listener.Addr().String())
require.NoError(t, err)
return &proxy{
clock: clock,
auth: authServer,
client: client,
revTun: revTunServer,
node: node,
proxy: proxyServer,
web: webServer,
handler: handler,
webURL: *url,
}
}
// webPack represents the state of a single web test.
// It replicates most of the WebSuite and serves to gradually
// transition the test suite to use the testing package
// directly.
type webPack struct {
proxies []*proxy
server *auth.TestTLSServer
node *regular.Server
clock clockwork.FakeClock
}
func (r *webPack) close(t *testing.T) {
for _, p := range r.proxies {
p.web.Close()
p.proxy.Close()
p.revTun.Close()
}
require.NoError(t, r.node.Close())
require.NoError(t, r.server.Close())
}
type proxy struct {
clock clockwork.FakeClock
client *auth.Client
auth *auth.TestTLSServer
revTun reversetunnel.Server
node *regular.Server
proxy *regular.Server
handler *RewritingHandler
web *httptest.Server
webURL url.URL
}
// authPack returns new authenticated package consisting of created valid
// user, otp token, created web session and authenticated client.
func (r *proxy) authPack(t *testing.T, user string) *authPack {
const (
loginUser = "user"
pass = "abc123"
rawSecret = "def456"
)
otpSecret := base32.StdEncoding.EncodeToString([]byte(rawSecret))
ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{
Type: teleport.Local,
SecondFactor: teleport.OTP,
})
require.NoError(t, err)
err = r.auth.Auth().SetAuthPreference(ap)
require.NoError(t, err)
r.createUser(context.TODO(), t, user, loginUser, pass, otpSecret)
// create a valid otp token
validToken, err := totp.GenerateCode(otpSecret, r.clock.Now())
require.NoError(t, err)
clt := r.newClient(t)
req := CreateSessionReq{
User: user,
Pass: pass,
SecondFactorToken: validToken,
}
csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
resp := login(t, clt, csrfToken, csrfToken, req)
var rawSession *CreateSessionResponse
require.NoError(t, json.Unmarshal(resp.Bytes(), &rawSession))
session, err := rawSession.response()
require.NoError(t, err)
jar, err := cookiejar.New(nil)
require.NoError(t, err)
clt = r.newClient(t, roundtrip.BearerAuth(session.Token), roundtrip.CookieJar(jar))
jar.SetCookies(&r.webURL, resp.Cookies())
return &authPack{
otpSecret: otpSecret,
user: user,
login: loginUser,
session: session,
clt: clt,
cookies: resp.Cookies(),
}
}
func (r *proxy) authPackFromPack(t *testing.T, pack *authPack) *authPack {
jar, err := cookiejar.New(nil)
require.NoError(t, err)
clt := r.newClient(t, roundtrip.BearerAuth(pack.session.Token), roundtrip.CookieJar(jar))
jar.SetCookies(&r.webURL, pack.cookies)
result := *pack
result.clt = clt
return &result
}
func (r *proxy) authPackFromResponse(t *testing.T, httpResp *roundtrip.Response) *authPack {
var resp *CreateSessionResponse
require.NoError(t, json.Unmarshal(httpResp.Bytes(), &resp))
jar, err := cookiejar.New(nil)
require.NoError(t, err)
clt := r.newClient(t, roundtrip.BearerAuth(resp.Token), roundtrip.CookieJar(jar))
jar.SetCookies(&r.webURL, httpResp.Cookies())
session, err := resp.response()
require.NoError(t, err)
if session.ExpiresIn < 0 {
t.Errorf("Expected expiry time to be in the future but got %v", session.ExpiresIn)
}
return &authPack{
session: session,
clt: clt,
cookies: httpResp.Cookies(),
}
}
func (r *proxy) createUser(ctx context.Context, t *testing.T, user, login, pass, otpSecret string) {
teleUser, err := services.NewUser(user)
require.NoError(t, err)
role := services.RoleForUser(teleUser)
role.SetLogins(services.Allow, []string{login})
options := role.GetOptions()
options.ForwardAgent = services.NewBool(true)
role.SetOptions(options)
err = r.auth.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
teleUser.AddRole(role.GetName())
teleUser.SetCreatedBy(services.CreatedBy{
User: services.UserRef{Name: "some-auth-user"},
})
err = r.auth.Auth().CreateUser(ctx, teleUser)
require.NoError(t, err)
err = r.auth.Auth().UpsertPassword(user, []byte(pass))
require.NoError(t, err)
if otpSecret != "" {
dev, err := services.NewTOTPDevice("otp", otpSecret, r.clock.Now())
require.NoError(t, err)
err = r.auth.Auth().UpsertMFADevice(ctx, user, dev)
require.NoError(t, err)
}
}
func (r *proxy) newClient(t *testing.T, opts ...roundtrip.ClientParam) *client.WebClient {
opts = append(opts, roundtrip.HTTPClient(client.NewInsecureWebClient()))
clt, err := client.NewWebClient(r.webURL.String(), opts...)
require.NoError(t, err)
return clt
}
func (r *proxy) makeTerminal(t *testing.T, pack *authPack, sessionID session.ID) *websocket.Conn {
u := url.URL{
Host: r.webURL.Host,
Scheme: client.WSS,
Path: fmt.Sprintf("/v1/webapi/sites/%v/connect", currentSiteShortcut),
}
data, err := json.Marshal(TerminalRequest{
Server: r.node.ID(),
Login: pack.login,
Term: session.TerminalParams{
W: 100,
H: 100,
},
SessionID: sessionID,
})
require.NoError(t, err)
q := u.Query()
q.Set("params", string(data))
q.Set(roundtrip.AccessTokenQueryParam, pack.session.Token)
u.RawQuery = q.Encode()
wscfg, err := websocket.NewConfig(u.String(), "http://localhost")
wscfg.TlsConfig = &tls.Config{
InsecureSkipVerify: true,
}
require.NoError(t, err)
for _, cookie := range pack.cookies {
wscfg.Header.Add("Cookie", cookie.String())
}
ws, err := websocket.DialConfig(wscfg)
require.NoError(t, err)
return ws
}
func login(t *testing.T, clt *client.WebClient, cookieToken, reqToken string, reqData interface{}) *roundtrip.Response {
resp, err := httplib.ConvertResponse(clt.RoundTrip(func() (*http.Response, error) {
data, err := json.Marshal(reqData)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", clt.Endpoint("webapi", "sessions"), bytes.NewBuffer(data))
if err != nil {
return nil, err
}
addCSRFCookieToReq(req, cookieToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set(csrf.HeaderName, reqToken)
return clt.HTTPClient().Do(req)
}))
require.NoError(t, err)
return resp
}
func validateTerminalStream(t *testing.T, conn *websocket.Conn) {
termHandler := newTerminalHandler()
stream := termHandler.asTerminalStream(conn)
_, err := io.WriteString(stream, "echo foo\r\n")
require.NoError(t, err)
err = waitForOutput(stream, "foo")
require.NoError(t, err)
}

View file

@ -21,7 +21,6 @@ package web
import (
"context"
"net/http"
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/backend"
@ -30,6 +29,7 @@ import (
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/web/app"
@ -115,7 +115,7 @@ func (h *Handler) createAppSession(w http.ResponseWriter, r *http.Request, p htt
// used for request routing.
ws, err := authClient.CreateAppSession(r.Context(), services.CreateAppSessionRequest{
Username: ctx.GetUser(),
ParentSession: ctx.sess.GetName(),
ParentSession: ctx.GetSessionID(),
PublicAddr: result.PublicAddr,
ClusterName: result.ClusterName,
})
@ -124,9 +124,9 @@ func (h *Handler) createAppSession(w http.ResponseWriter, r *http.Request, p htt
}
// Block and wait a few seconds for the session that was created to show up
// in the cache. If this request is not blocked here, it can get struck in a
// in the cache. If this request is not blocked here, it can get stuck in a
// racy session creation loop.
err = h.waitForSession(r.Context(), ws.GetName())
err = h.waitForAppSession(r.Context(), ws.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
@ -174,18 +174,24 @@ func (h *Handler) createAppSession(w http.ResponseWriter, r *http.Request, p htt
}, nil
}
// waitForSession will block until the requested session shows up in the
// waitForAppSession will block until the requested application session shows up in the
// cache or a timeout occurs.
func (h *Handler) waitForSession(ctx context.Context, sessionID string) error {
timeout := time.NewTimer(defaults.WebHeadersTimeout)
defer timeout.Stop()
func (h *Handler) waitForAppSession(ctx context.Context, sessionID string) error {
_, err := h.cfg.AccessPoint.GetAppSession(ctx, services.GetAppSessionRequest{SessionID: sessionID})
if err == nil {
return nil
}
logger := h.log.WithField("session", sessionID)
if !trace.IsNotFound(err) {
logger.WithError(err).Debug("Failed to query application session.")
}
// Establish a watch on application session.
watcher, err := h.cfg.AccessPoint.NewWatcher(ctx, services.Watch{
Name: teleport.ComponentAppProxy,
Kinds: []services.WatchKind{
services.WatchKind{
Kind: services.KindWebSession,
{
Kind: services.KindWebSession,
SubKind: services.KindAppSession,
},
},
MetricComponent: teleport.ComponentAppProxy,
@ -194,48 +200,20 @@ func (h *Handler) waitForSession(ctx context.Context, sessionID string) error {
return trace.Wrap(err)
}
defer watcher.Close()
select {
// Received an event, first event should always be an initialize event.
case event := <-watcher.Events():
if event.Type != backend.OpInit {
return trace.BadParameter("expected init event, got %v instead", event.Type)
matchEvent := func(event services.Event) (services.Resource, error) {
if event.Type == backend.OpPut &&
event.Resource.GetKind() == services.KindWebSession &&
event.Resource.GetSubKind() == services.KindAppSession &&
event.Resource.GetName() == sessionID {
return event.Resource, nil
}
// Watcher closed, probably due to a network error.
case <-watcher.Done():
return trace.ConnectionProblem(watcher.Error(), "watcher is closed")
// Timed out waiting for initialize event.
case <-timeout.C:
return trace.BadParameter("timed out waiting for initialize event")
return nil, trace.CompareFailed("no match")
}
// Check if the session exists in the backend.
_, err = h.cfg.AccessPoint.GetAppSession(ctx, services.GetAppSessionRequest{
SessionID: sessionID,
})
if err == nil {
return nil
}
for {
select {
// If the event is the expected one, return right away.
case event := <-watcher.Events():
if event.Resource.GetKind() != services.KindWebSession {
return trace.BadParameter("unexpected event: %v.", event.Resource.GetKind())
}
if event.Type == backend.OpPut && event.Resource.GetName() == sessionID {
return nil
}
// Watcher closed, probably due to a network error.
case <-watcher.Done():
return trace.ConnectionProblem(watcher.Error(), "watcher is closed")
// Timed out waiting for initialize event.
case <-timeout.C:
return trace.BadParameter("timed out waiting for session")
}
_, err = local.WaitForEvent(ctx, watcher, local.EventMatcherFunc(matchEvent), h.clock)
if err != nil {
logger.WithError(err).Warn("Failed to wait for application session.")
}
return trace.Wrap(err)
}
func (h *Handler) validateAppSessionRequest(ctx context.Context, req *CreateAppSessionRequest) (*validateAppSessionResult, error) {

View file

@ -49,7 +49,7 @@ func DecodeCookie(b string) (*SessionCookie, error) {
return c, nil
}
func SetSession(w http.ResponseWriter, user, sid string) error {
func SetSessionCookie(w http.ResponseWriter, user, sid string) error {
d, err := EncodeCookie(user, sid)
if err != nil {
return err

View file

@ -128,7 +128,7 @@ func (h *Handler) samlACS(w http.ResponseWriter, r *http.Request, p httprouter.P
logger.WithError(err).Warn("Unable to verify CSRF token.")
return nil, trace.AccessDenied("access denied")
}
if err := SetSession(w, response.Username, response.Session.GetName()); err != nil {
if err := SetSessionCookie(w, response.Username, response.Session.GetName()); err != nil {
return nil, trace.Wrap(err)
}
return nil, httplib.SafeRedirect(w, r, response.Req.ClientRedirectURL)

View file

@ -20,87 +20,98 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net"
"net/http"
"sync"
"time"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"github.com/gravitational/teleport"
apiclient "github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/gravitational/ttlmap"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
)
// SessionContext is a context associated with users'
// web session, it stores connected client that persists
// between requests for example to avoid connecting
// to the auth server on every page hit
// SessionContext is a context associated with a user's
// web session. An instance of the context is created for
// each web session generated for the user and provides
// a basic client cache for remote auth server connections.
type SessionContext struct {
sync.Mutex
logrus.FieldLogger
sess services.WebSession
user string
clt auth.ClientI
log logrus.FieldLogger
user string
clt auth.ClientI
parent *sessionCache
// resources is persistent resource store this context is bound to.
// The store maintains a list of resources between session renewals
resources *sessionResources
// session refers the web session created for the user.
session services.WebSession
mu sync.Mutex
remoteClt map[string]auth.ClientI
parent *sessionCache
closers []io.Closer
}
// String returns the text representation of this context
func (c *SessionContext) String() string {
return fmt.Sprintf("WebSession(user=%v,id=%v,expires=%v,bearer=%v,bearer_expires=%v)",
c.user,
c.session.GetName(),
c.session.GetExpiryTime(),
c.session.GetBearerToken(),
c.session.GetBearerTokenExpiryTime(),
)
}
// AddClosers adds the specified closers to this context
func (c *SessionContext) AddClosers(closers ...io.Closer) {
c.Lock()
defer c.Unlock()
c.closers = append(c.closers, closers...)
c.resources.addClosers(closers...)
}
// RemoveCloser removes the specified closer from this context
func (c *SessionContext) RemoveCloser(closer io.Closer) {
c.Lock()
defer c.Unlock()
for i := range c.closers {
if c.closers[i] == closer {
c.closers = append(c.closers[:i], c.closers[i+1:]...)
return
}
}
}
func (c *SessionContext) TransferClosers() []io.Closer {
c.Lock()
defer c.Unlock()
closers := c.closers
c.closers = nil
return closers
c.resources.removeCloser(closer)
}
// Invalidate invalidates this context by removing the underlying session
// and closing all underlying closers
func (c *SessionContext) Invalidate() error {
return c.parent.InvalidateSession(c)
return c.parent.invalidateSession(c)
}
func (c *SessionContext) validateBearerToken(ctx context.Context, token string) error {
_, err := c.parent.readBearerToken(ctx, types.GetWebTokenRequest{
User: c.user,
Token: token,
})
return trace.Wrap(err)
}
func (c *SessionContext) addRemoteClient(siteName string, remoteClient auth.ClientI) {
c.Lock()
defer c.Unlock()
c.mu.Lock()
defer c.mu.Unlock()
c.remoteClt[siteName] = remoteClient
}
func (c *SessionContext) getRemoteClient(siteName string) (auth.ClientI, bool) {
c.Lock()
defer c.Unlock()
c.mu.Lock()
defer c.mu.Unlock()
remoteClt, ok := c.remoteClt[siteName]
return remoteClt, ok
}
@ -116,33 +127,24 @@ func (c *SessionContext) GetClient() (auth.ClientI, error) {
// returned.
func (c *SessionContext) GetUserClient(site reversetunnel.RemoteSite) (auth.ClientI, error) {
// get the name of the current cluster
clt, err := c.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}
cn, err := clt.GetClusterName()
clusterName, err := c.clt.GetClusterName()
if err != nil {
return nil, trace.Wrap(err)
}
// if we're trying to access the local cluster, pass back the local client.
if cn.GetClusterName() == site.GetName() {
return clt, nil
if clusterName.GetClusterName() == site.GetName() {
return c.clt, nil
}
// look to see if we already have a connection to this cluster
// check if we already have a connection to this cluster
remoteClt, ok := c.getRemoteClient(site.GetName())
if !ok {
rClt, rConn, err := c.newRemoteClient(site)
rClt, err := c.newRemoteClient(site)
if err != nil {
return nil, trace.Wrap(err)
}
// add a closer for the underlying connection
if rConn != nil {
c.AddClosers(rConn)
}
// we'll save the remote client in our session context so we don't have to
// build a new connection next time. all remote clients will be closed when
// the session context is closed.
@ -156,12 +158,12 @@ func (c *SessionContext) GetUserClient(site reversetunnel.RemoteSite) (auth.Clie
// newRemoteClient returns a client to a remote cluster with the role of
// the logged in user.
func (c *SessionContext) newRemoteClient(cluster reversetunnel.RemoteSite) (auth.ClientI, net.Conn, error) {
func (c *SessionContext) newRemoteClient(cluster reversetunnel.RemoteSite) (auth.ClientI, error) {
clt, err := c.tryRemoteTLSClient(cluster)
if err != nil {
return nil, nil, trace.Wrap(err)
return nil, trace.Wrap(err)
}
return clt, nil, nil
return clt, nil
}
// clusterDialer returns DialContext function using cluster's dial function
@ -213,13 +215,14 @@ func (c *SessionContext) ClientTLSConfig(clusterName ...string) (*tls.Config, er
}
tlsConfig := utils.TLSConfig(c.parent.cipherSuites)
tlsCert, err := tls.X509KeyPair(c.sess.GetTLSCert(), c.sess.GetPriv())
tlsCert, err := tls.X509KeyPair(c.session.GetTLSCert(), c.session.GetPriv())
if err != nil {
return nil, trace.Wrap(err, "failed to parse TLS cert and key")
}
tlsConfig.Certificates = []tls.Certificate{tlsCert}
tlsConfig.RootCAs = certPool
tlsConfig.ServerName = auth.EncodeClusterName(c.parent.clusterName)
tlsConfig.Time = c.parent.clock.Now
return tlsConfig, nil
}
@ -236,25 +239,20 @@ func (c *SessionContext) GetUser() string {
return c.user
}
// GetWebSession returns a web session
func (c *SessionContext) GetWebSession() services.WebSession {
return c.sess
}
// ExtendWebSession creates a new web session for this user
// extendWebSession creates a new web session for this user
// based on the previous session
func (c *SessionContext) ExtendWebSession(accessRequestID string) (services.WebSession, error) {
sess, err := c.clt.ExtendWebSession(c.user, c.sess.GetName(), accessRequestID)
func (c *SessionContext) extendWebSession(accessRequestID string) (services.WebSession, error) {
session, err := c.clt.ExtendWebSession(c.user, c.session.GetName(), accessRequestID)
if err != nil {
return nil, trace.Wrap(err)
}
return sess, nil
return session, nil
}
// GetAgent returns agent that can be used to answer challenges
// for the web to ssh connection as well as certificate
func (c *SessionContext) GetAgent() (agent.Agent, *ssh.Certificate, error) {
pub, _, _, _, err := ssh.ParseAuthorizedKey(c.sess.GetPub())
pub, _, _, _, err := ssh.ParseAuthorizedKey(c.session.GetPub())
if err != nil {
return nil, nil, trace.Wrap(err)
}
@ -265,7 +263,7 @@ func (c *SessionContext) GetAgent() (agent.Agent, *ssh.Certificate, error) {
if len(cert.ValidPrincipals) == 0 {
return nil, nil, trace.BadParameter("expected at least valid principal in certificate")
}
privateKey, err := ssh.ParseRawPrivateKey(c.sess.GetPriv())
privateKey, err := ssh.ParseRawPrivateKey(c.session.GetPriv())
if err != nil {
return nil, nil, trace.Wrap(err, "failed to parse SSH private key")
}
@ -282,39 +280,101 @@ func (c *SessionContext) GetAgent() (agent.Agent, *ssh.Certificate, error) {
}
// GetCertificates returns the *ssh.Certificate and *x509.Certificate
// associated with this session.
// associated with this context's session.
func (c *SessionContext) GetCertificates() (*ssh.Certificate, *x509.Certificate, error) {
pub, _, _, _, err := ssh.ParseAuthorizedKey(c.sess.GetPub())
pub, _, _, _, err := ssh.ParseAuthorizedKey(c.session.GetPub())
if err != nil {
return nil, nil, trace.Wrap(err)
}
sshcert, ok := pub.(*ssh.Certificate)
sshCert, ok := pub.(*ssh.Certificate)
if !ok {
return nil, nil, trace.BadParameter("not certificate")
}
tlscert, err := tlsca.ParseCertificatePEM(c.sess.GetTLSCert())
tlsCert, err := tlsca.ParseCertificatePEM(c.session.GetTLSCert())
if err != nil {
return nil, nil, trace.Wrap(err)
}
return sshCert, tlsCert, nil
return sshcert, tlscert, nil
}
// Close cleans up connections associated with requests
// GetSessionID returns the ID of the underlying user web session.
func (c *SessionContext) GetSessionID() string {
return c.session.GetName()
}
// Close cleans up resources associated with this context and removes it
// from the user context
func (c *SessionContext) Close() error {
closers := c.TransferClosers()
for _, closer := range closers {
c.Debugf("Closing %v.", closer)
closer.Close()
c.mu.Lock()
defer c.mu.Unlock()
var errors []error
for _, clt := range c.remoteClt {
if err := clt.Close(); err != nil {
errors = append(errors, err)
}
}
if c.clt != nil {
return trace.Wrap(c.clt.Close())
if err := c.clt.Close(); err != nil {
errors = append(errors, err)
}
return nil
return trace.NewAggregate(errors...)
}
// getToken returns the bearer token associated with the underlying
// session. Note that sessions are separate from bearer tokens and this
// is only useful immediately after a session has been created to query
// the token.
func (c *SessionContext) getToken() types.WebToken {
return types.NewWebToken(c.session.GetBearerTokenExpiryTime(), types.WebTokenSpecV3{
Token: c.session.GetBearerToken(),
})
}
// expired returns whether this context has expired.
// The context is considered expired when its bearer token TTL
// is in the past (subject to lingering threshold)
func (c *SessionContext) expired(ctx context.Context) bool {
_, err := c.parent.readSession(ctx, types.GetWebSessionRequest{
User: c.user,
SessionID: c.session.GetName(),
})
if err == nil {
return false
}
expiry := c.session.GetBearerTokenExpiryTime()
if expiry.IsZero() {
return false
}
if !trace.IsNotFound(err) {
c.log.WithError(err).Debug("Failed to query web session.")
}
// Give the session some time to linger so existing users of the context
// have successfully disposed of them.
// If we remove the session immediately, a stale copy might still use the
// cached site clients.
// This is a cheaper way to avoid race without introducing object
// reference counters.
return c.parent.clock.Since(expiry) > c.parent.sessionLingeringThreshold
}
// cachedSessionLingeringThreshold specifies the maximum amount of time the session cache
// will hold onto a session before removing it. This period allows all outstanding references
// to disappear without fear of racing with the removal
const cachedSessionLingeringThreshold = 2 * time.Minute
type sessionCacheOptions struct {
proxyClient auth.ClientI
accessPoint auth.ReadAccessPoint
servers []utils.NetAddr
cipherSuites []uint16
clock clockwork.Clock
// sessionLingeringThreshold specifies the time the session will linger
// in the cache before getting purged after it has expired
sessionLingeringThreshold time.Duration
}
// newSessionCache returns new instance of the session cache
func newSessionCache(config *sessionCache) (*sessionCache, error) {
func newSessionCache(config sessionCacheOptions) (*sessionCache, error) {
clusterName, err := config.proxyClient.GetClusterName()
if err != nil {
return nil, trace.Wrap(err)
@ -322,19 +382,18 @@ func newSessionCache(config *sessionCache) (*sessionCache, error) {
if config.clock == nil {
config.clock = clockwork.NewRealClock()
}
m, err := ttlmap.New(1024, ttlmap.CallOnExpire(closeContext), ttlmap.Clock(config.clock))
if err != nil {
return nil, trace.Wrap(err)
}
cache := &sessionCache{
clusterName: clusterName.GetClusterName(),
proxyClient: config.proxyClient,
contexts: m,
authServers: config.authServers,
closer: utils.NewCloseBroadcaster(),
cipherSuites: config.cipherSuites,
log: newPackageLogger(),
clock: config.clock,
clusterName: clusterName.GetClusterName(),
proxyClient: config.proxyClient,
accessPoint: config.accessPoint,
sessions: make(map[string]*SessionContext),
resources: make(map[string]*sessionResources),
authServers: config.servers,
closer: utils.NewCloseBroadcaster(),
cipherSuites: config.cipherSuites,
log: newPackageLogger(),
clock: config.clock,
sessionLingeringThreshold: config.sessionLingeringThreshold,
}
// periodically close expired and unused sessions
go cache.expireSessions()
@ -342,19 +401,33 @@ func newSessionCache(config *sessionCache) (*sessionCache, error) {
}
// sessionCache handles web session authentication,
// and holds in memory contexts associated with each session
// and holds in-memory contexts associated with each session
type sessionCache struct {
log logrus.FieldLogger
sync.Mutex
log logrus.FieldLogger
proxyClient auth.ClientI
contexts *ttlmap.TTLMap
authServers []utils.NetAddr
accessPoint auth.ReadAccessPoint
closer *utils.CloseBroadcaster
clusterName string
clock clockwork.Clock
// sessionLingeringThreshold specifies the time the session will linger
// in the cache before getting purged after it has expired
sessionLingeringThreshold time.Duration
// cipherSuites is the list of supported TLS cipher suites.
cipherSuites []uint16
mu sync.Mutex
// sessions maps user/sessionID to an active web session value between renewals.
// This is the client-facing session handle
sessions map[string]*SessionContext
// session cache maintains a list of resources per-user as long
// as the user session is active even though individual session values
// are periodically recycled.
// Resources are disposed of when the corresponding session
// is either explicitly invalidated (e.g. during logout) or the
// resources are themselves closing
resources map[string]*sessionResources
}
// Close closes all allocated resources and stops goroutines
@ -363,46 +436,35 @@ func (s *sessionCache) Close() error {
return s.closer.Close()
}
// closeContext is called when session context expires from
// cache and will clean up connections
func closeContext(key string, val interface{}) {
go func() {
log.Infof("Closing context %v.", key)
ctx, ok := val.(*SessionContext)
if !ok {
log.Warnf("Invalid value type %T.", val)
return
}
if err := ctx.Close(); err != nil {
log.Warnf("Failed to close context: %v.", err)
}
}()
}
func (s *sessionCache) expireSessions() {
ticker := time.NewTicker(time.Second)
ticker := s.clock.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.clearExpiredSessions()
case <-ticker.Chan():
s.clearExpiredSessions(context.TODO())
case <-s.closer.C:
return
}
}
}
func (s *sessionCache) clearExpiredSessions() {
s.Lock()
defer s.Unlock()
expired := s.contexts.RemoveExpired(10)
if expired != 0 {
log.Infof("Removed %v expired sessions.", expired)
func (s *sessionCache) clearExpiredSessions(ctx context.Context) {
s.mu.Lock()
defer s.mu.Unlock()
for _, c := range s.sessions {
if !c.expired(ctx) {
continue
}
s.removeSessionContextLocked(c.session.GetUser(), c.session.GetName())
s.log.WithField("ctx", c.String()).Debug("Context expired.")
}
}
func (s *sessionCache) AuthWithOTP(user, pass string, otpToken string) (services.WebSession, error) {
// AuthWithOTP authenticates the specified user with the given password and OTP token.
// Returns a new web session if successful.
func (s *sessionCache) AuthWithOTP(user, pass, otpToken string) (services.WebSession, error) {
return s.proxyClient.AuthenticateWebUser(auth.AuthenticateUserRequest{
Username: user,
OTP: &auth.OTPCreds{
@ -412,6 +474,8 @@ func (s *sessionCache) AuthWithOTP(user, pass string, otpToken string) (services
})
}
// AuthWithoutOTP authenticates the specified user with the given password.
// Returns a new web session if successful.
func (s *sessionCache) AuthWithoutOTP(user, pass string) (services.WebSession, error) {
return s.proxyClient.AuthenticateWebUser(auth.AuthenticateUserRequest{
Username: user,
@ -434,6 +498,7 @@ func (s *sessionCache) AuthWithU2FSignResponse(user string, response *u2f.Authen
})
}
// GetCertificateWithoutOTP returns a new user certificate for the specified request.
func (s *sessionCache) GetCertificateWithoutOTP(c client.CreateSSHCertReq) (*auth.SSHLoginResponse, error) {
return s.proxyClient.AuthenticateSSHUser(auth.AuthenticateSSHRequest{
AuthenticateUserRequest: auth.AuthenticateUserRequest{
@ -450,6 +515,8 @@ func (s *sessionCache) GetCertificateWithoutOTP(c client.CreateSSHCertReq) (*aut
})
}
// GetCertificateWithOTP returns a new user certificate for the specified request.
// The request is used with the given OTP token.
func (s *sessionCache) GetCertificateWithOTP(c client.CreateSSHCertReq) (*auth.SSHLoginResponse, error) {
return s.proxyClient.AuthenticateSSHUser(auth.AuthenticateSSHRequest{
AuthenticateUserRequest: auth.AuthenticateUserRequest{
@ -465,7 +532,6 @@ func (s *sessionCache) GetCertificateWithOTP(c client.CreateSSHCertReq) (*auth.S
RouteToCluster: c.RouteToCluster,
KubernetesCluster: c.KubernetesCluster,
})
}
func (s *sessionCache) GetCertificateWithU2F(c client.CreateSSHCertWithU2FReq) (*auth.SSHLoginResponse, error) {
@ -497,70 +563,169 @@ func (s *sessionCache) ValidateTrustedCluster(validateRequest *auth.ValidateTrus
return s.proxyClient.ValidateTrustedCluster(validateRequest)
}
func (s *sessionCache) InvalidateSession(ctx *SessionContext) error {
defer ctx.Close()
if err := s.resetContext(ctx.GetUser(), ctx.GetWebSession().GetName()); err != nil {
return trace.Wrap(err)
// validateSession validates the session given with user and session ID.
// Returns a new or existing session context.
func (s *sessionCache) validateSession(ctx context.Context, user, sessionID string) (*SessionContext, error) {
sessionCtx, err := s.getContext(user, sessionID)
if err == nil {
return sessionCtx, nil
}
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
return s.newSessionContext(user, sessionID)
}
func (s *sessionCache) invalidateSession(ctx *SessionContext) error {
defer ctx.Close()
clt, err := ctx.GetClient()
if err != nil {
return trace.Wrap(err)
}
err = clt.DeleteWebSession(ctx.GetUser(), ctx.GetWebSession().GetName())
return trace.Wrap(err)
}
func (s *sessionCache) getContext(user, sid string) (*SessionContext, error) {
s.Lock()
defer s.Unlock()
val, ok := s.contexts.Get(user + sid)
if ok {
return val.(*SessionContext), nil
// Delete just the session - leave the bearer token to linger to avoid
// failing a client query still using the old token.
err = clt.WebSessions().Delete(context.TODO(), types.DeleteWebSessionRequest{
User: ctx.user,
SessionID: ctx.session.GetName(),
})
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
return nil, trace.NotFound("sessionContext not found")
}
func (s *sessionCache) insertContext(user, sid string, ctx *SessionContext, ttl time.Duration) (*SessionContext, error) {
s.Lock()
defer s.Unlock()
val, ok := s.contexts.Get(user + sid)
if ok && val != nil { // nil means that we've just invalidated the context now and set it to nil in the cache
return val.(*SessionContext), trace.AlreadyExists("exists")
}
if err := s.contexts.Set(user+sid, ctx, ttl); err != nil {
return nil, trace.Wrap(err)
}
return ctx, nil
}
func (s *sessionCache) resetContext(user, sid string) error {
s.Lock()
defer s.Unlock()
context, ok := s.contexts.Remove(user + sid)
if ok {
closeContext(user+sid, context)
if err := s.releaseResources(ctx.GetUser(), ctx.session.GetName()); err != nil {
return trace.Wrap(err)
}
return nil
}
func (s *sessionCache) ValidateSession(user, sid string) (*SessionContext, error) {
ctx, err := s.getContext(user, sid)
if err == nil {
func (s *sessionCache) getContext(user, sessionID string) (*SessionContext, error) {
s.mu.Lock()
defer s.mu.Unlock()
ctx, ok := s.sessions[user+sessionID]
if ok {
return ctx, nil
}
return nil, trace.NotFound("no context for user %v and session %v",
user, sessionID)
}
sess, err := s.proxyClient.AuthenticateWebUser(auth.AuthenticateUserRequest{
func (s *sessionCache) insertContext(user string, ctx *SessionContext) (exists bool) {
s.mu.Lock()
defer s.mu.Unlock()
id := sessionKey(user, ctx.session.GetName())
if _, exists := s.sessions[id]; exists {
return true
}
s.sessions[id] = ctx
return false
}
func (s *sessionCache) releaseResources(user, sessionID string) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.releaseResourcesLocked(user, sessionID)
}
func (s *sessionCache) removeSessionContextLocked(user, sessionID string) error {
id := sessionKey(user, sessionID)
ctx, ok := s.sessions[id]
if !ok {
return nil
}
delete(s.sessions, id)
err := ctx.Close()
if err != nil {
s.log.WithFields(logrus.Fields{
"ctx": ctx.String(),
logrus.ErrorKey: err,
}).Warn("Failed to close session context.")
return trace.Wrap(err)
}
return nil
}
func (s *sessionCache) releaseResourcesLocked(user, sessionID string) error {
var errors []error
err := s.removeSessionContextLocked(user, sessionID)
if err != nil {
errors = append(errors, err)
}
if ctx, ok := s.resources[user]; ok {
delete(s.resources, user)
if err := ctx.Close(); err != nil {
s.log.WithError(err).Warn("Failed to clean up session context.")
errors = append(errors, err)
}
}
return trace.NewAggregate(errors...)
}
func (s *sessionCache) upsertSessionContext(user string) *sessionResources {
s.mu.Lock()
defer s.mu.Unlock()
if ctx, exists := s.resources[user]; exists {
return ctx
}
ctx := &sessionResources{
log: s.log.WithFields(logrus.Fields{
trace.Component: "user-session",
"user": user,
}),
}
s.resources[user] = ctx
return ctx
}
// newSessionContext creates a new web session context for the specified user/session ID
func (s *sessionCache) newSessionContext(user, sessionID string) (*SessionContext, error) {
session, err := s.proxyClient.AuthenticateWebUser(auth.AuthenticateUserRequest{
Username: user,
Session: &auth.SessionCreds{
ID: sid,
ID: sessionID,
},
})
if err != nil {
// This will fail if the session has expired and was removed
return nil, trace.Wrap(err)
}
return s.newSessionContextFromSession(session)
}
func (s *sessionCache) newSessionContextFromSession(session services.WebSession) (*SessionContext, error) {
tlsConfig, err := s.tlsConfig(session.GetTLSCert(), session.GetPriv())
if err != nil {
return nil, trace.Wrap(err)
}
userClient, err := auth.NewTLSClient(auth.ClientConfig{
Addrs: s.authServers,
TLS: tlsConfig,
})
if err != nil {
return nil, trace.Wrap(err)
}
ctx := &SessionContext{
clt: userClient,
remoteClt: make(map[string]auth.ClientI),
user: session.GetUser(),
session: session,
parent: s,
resources: s.upsertSessionContext(session.GetUser()),
log: s.log.WithFields(logrus.Fields{
"user": session.GetUser(),
"session": session.GetShortName(),
}),
}
if err != nil {
return nil, trace.Wrap(err)
}
if exists := s.insertContext(session.GetUser(), ctx); exists {
// this means that someone has just inserted the context, so
// close our extra context and return
ctx.Close()
}
return ctx, nil
}
func (s *sessionCache) tlsConfig(cert, privKey []byte) (*tls.Config, error) {
ca, err := s.proxyClient.GetCertAuthority(services.CertAuthID{
Type: services.HostCA,
DomainName: s.clusterName,
@ -568,77 +733,133 @@ func (s *sessionCache) ValidateSession(user, sid string) (*SessionContext, error
if err != nil {
return nil, trace.Wrap(err)
}
certPool, err := services.CertPool(ca)
if err != nil {
return nil, trace.Wrap(err)
}
tlsConfig := utils.TLSConfig(s.cipherSuites)
tlsCert, err := tls.X509KeyPair(sess.GetTLSCert(), sess.GetPriv())
tlsCert, err := tls.X509KeyPair(cert, privKey)
if err != nil {
return nil, trace.Wrap(err, "failed to parse TLS cert and key")
return nil, trace.Wrap(err, "failed to parse TLS certificate and key")
}
tlsConfig.Certificates = []tls.Certificate{tlsCert}
tlsConfig.RootCAs = certPool
tlsConfig.ServerName = auth.EncodeClusterName(s.clusterName)
tlsConfig.Time = s.clock.Now
return tlsConfig, nil
}
userClient, err := auth.NewClient(apiclient.Config{
Addrs: utils.NetAddrsToStrings(s.authServers),
TLS: tlsConfig,
})
if err != nil {
return nil, trace.Wrap(err)
func (s *sessionCache) readSession(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error) {
// Read session from the cache first
session, err := s.accessPoint.GetWebSession(ctx, req)
if err == nil {
return session, nil
}
// Fallback to proxy otherwise
return s.proxyClient.GetWebSession(ctx, req)
}
c := &SessionContext{
clt: userClient,
remoteClt: make(map[string]auth.ClientI),
user: user,
sess: sess,
parent: s,
FieldLogger: log.WithFields(logrus.Fields{
"user": user,
"sess": sess.GetShortName(),
}),
func (s *sessionCache) readBearerToken(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) {
// Read token from the cache first
token, err := s.accessPoint.GetWebToken(ctx, req)
if err == nil {
return token, nil
}
// Fallback to proxy otherwise
return s.proxyClient.GetWebToken(ctx, req)
}
ttl := utils.ToTTL(s.clock, sess.GetBearerTokenExpiryTime())
out, err := s.insertContext(user, sid, c, ttl)
if err != nil {
// this means that someone has just inserted the context, so
// close our extra context and return
if trace.IsAlreadyExists(err) {
defer c.Close()
return out, nil
// Close releases all underlying resources for the user session.
func (c *sessionResources) Close() error {
closers := c.transferClosers()
var errors []error
for _, closer := range closers {
c.log.Debugf("Closing %v.", closer)
if err := closer.Close(); err != nil {
errors = append(errors, err)
}
return nil, trace.Wrap(err)
}
return out, nil
return trace.NewAggregate(errors...)
}
func (s *sessionCache) SetSession(w http.ResponseWriter, user, sid string) error {
d, err := EncodeCookie(user, sid)
if err != nil {
return err
}
c := &http.Cookie{
Name: "session",
Value: d,
Path: "/",
HttpOnly: true,
Secure: true,
}
http.SetCookie(w, c)
return nil
// sessionResources persists resources initiated by a web session
// but which might outlive the session.
type sessionResources struct {
log logrus.FieldLogger
mu sync.Mutex
closers []io.Closer
}
func (s *sessionCache) ClearSession(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
// addClosers adds the specified closers to this context
func (c *sessionResources) addClosers(closers ...io.Closer) {
c.mu.Lock()
defer c.mu.Unlock()
c.closers = append(c.closers, closers...)
}
// removeCloser removes the specified closer from this context
func (c *sessionResources) removeCloser(closer io.Closer) {
c.mu.Lock()
defer c.mu.Unlock()
for i, cls := range c.closers {
if cls == closer {
c.closers = append(c.closers[:i], c.closers[i+1:]...)
return
}
}
}
func (c *sessionResources) transferClosers() []io.Closer {
c.mu.Lock()
defer c.mu.Unlock()
closers := c.closers
c.closers = nil
return closers
}
func sessionKey(user, sessionID string) string {
return user + sessionID
}
// waitForWebSession will block until the requested web session shows up in the
// cache or a timeout occurs.
func (h *Handler) waitForWebSession(ctx context.Context, req types.GetWebSessionRequest) error {
_, err := h.cfg.AccessPoint.GetWebSession(ctx, req)
if err == nil {
return nil
}
logger := h.log.WithField("req", req)
if !trace.IsNotFound(err) {
logger.WithError(err).Debug("Failed to query web session.")
}
// Establish a watch.
watcher, err := h.cfg.AccessPoint.NewWatcher(ctx, services.Watch{
Name: teleport.ComponentWebProxy,
Kinds: []services.WatchKind{
{
Kind: services.KindWebSession,
SubKind: services.KindWebSession,
},
},
MetricComponent: teleport.ComponentWebProxy,
})
if err != nil {
return trace.Wrap(err)
}
defer watcher.Close()
matchEvent := func(event services.Event) (services.Resource, error) {
if event.Type == backend.OpPut &&
event.Resource.GetKind() == services.KindWebSession &&
event.Resource.GetSubKind() == services.KindWebSession &&
event.Resource.GetName() == req.SessionID {
return event.Resource, nil
}
return nil, trace.CompareFailed("no match")
}
_, err = local.WaitForEvent(ctx, watcher, local.EventMatcherFunc(matchEvent), h.clock)
if err != nil {
logger.WithError(err).Warn("Failed to wait for web session.")
}
return trace.Wrap(err)
}

View file

@ -24,18 +24,13 @@ import (
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/gravitational/teleport"
"github.com/gravitational/trace"
"github.com/kardianos/osext"
)
// relative path to static assets. this is useful during development.
var debugAssetsPath string
const (
webAssetsMissingError = "the teleport binary was built without web assets, try building with `make release`"
webAssetsReadError = "failure reading web assets from the binary"
@ -43,52 +38,41 @@ const (
// NewStaticFileSystem returns the initialized implementation of http.FileSystem
// interface which can be used to serve Teleport Proxy Web UI
//
// If 'debugMode' is true, it will load the web assets from the same git repo
// directory where the executable is, otherwise it will load them from the embedded
// zip archive.
//
func NewStaticFileSystem(debugMode bool) (http.FileSystem, error) {
if debugMode {
assetsToCheck := []string{"index.html", "/app"}
if debugAssetsPath == "" {
exePath, err := osext.ExecutableFolder()
if err != nil {
return nil, trace.Wrap(err)
}
_, err = os.Stat(path.Join(exePath, "../../e"))
isEnterprise := !os.IsNotExist(err)
if isEnterprise {
// enterprise web assets
debugAssetsPath = path.Join(exePath, "../../webassets/e/teleport")
} else {
// community web assets
debugAssetsPath = path.Join(exePath, "../webassets/teleport")
}
}
for _, af := range assetsToCheck {
_, err := os.Stat(filepath.Join(debugAssetsPath, af))
if err != nil {
return nil, trace.Wrap(err)
}
}
log.Infof("Using filesystem for serving web assets: %s.", debugAssetsPath)
return http.Dir(debugAssetsPath), nil
}
// otherwise, lets use the zip archive attached to the executable:
func NewStaticFileSystem() (http.FileSystem, error) {
// Use the zip archive attached to the executable:
return loadZippedExeAssets()
}
// isDebugMode determines if teleport is running in a "debug" mode.
// It looks at DEBUG environment variable
func isDebugMode() bool {
v, _ := strconv.ParseBool(os.Getenv(teleport.DebugEnvVar))
return v
// NewDebugFileSystem returns the HTTP file system implementation rooted
// at the specified assetsPath.
func NewDebugFileSystem(assetsPath string) (http.FileSystem, error) {
assetsToCheck := []string{"index.html", "/app"}
if assetsPath == "" {
exePath, err := osext.ExecutableFolder()
if err != nil {
return nil, trace.Wrap(err)
}
_, err = os.Stat(path.Join(exePath, "../../e"))
isEnterprise := !os.IsNotExist(err)
if isEnterprise {
// enterprise web assets
assetsPath = path.Join(exePath, "../../webassets/e/teleport")
} else {
// community web assets
assetsPath = path.Join(exePath, "../webassets/teleport")
}
}
for _, af := range assetsToCheck {
_, err := os.Stat(filepath.Join(assetsPath, af))
if err != nil {
return nil, trace.Wrap(err)
}
}
log.Infof("Using filesystem for serving web assets: %s.", assetsPath)
return http.Dir(assetsPath), nil
}
// LoadWebResources returns a filesystem implementation compatible

View file

@ -19,11 +19,8 @@ package web
import (
"io"
"io/ioutil"
"os"
"strings"
"github.com/gravitational/teleport"
"gopkg.in/check.v1"
)
@ -32,24 +29,8 @@ type StaticSuite struct {
var _ = check.Suite(&StaticSuite{})
func (s *StaticSuite) SetUpSuite(c *check.C) {
debugAssetsPath = "../../webassets/teleport"
}
func (s *StaticSuite) TestDebugModeEnv(c *check.C) {
c.Assert(isDebugMode(), check.Equals, false)
os.Setenv(teleport.DebugEnvVar, "no")
c.Assert(isDebugMode(), check.Equals, false)
os.Setenv(teleport.DebugEnvVar, "0")
c.Assert(isDebugMode(), check.Equals, false)
os.Setenv(teleport.DebugEnvVar, "1")
c.Assert(isDebugMode(), check.Equals, true)
os.Setenv(teleport.DebugEnvVar, "true")
c.Assert(isDebugMode(), check.Equals, true)
}
func (s *StaticSuite) TestLocalFS(c *check.C) {
fs, err := NewStaticFileSystem(true)
fs, err := NewDebugFileSystem("../../webassets/teleport")
c.Assert(err, check.IsNil)
c.Assert(fs, check.NotNil)

View file

@ -23,6 +23,7 @@ import (
"net/http"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
@ -85,7 +86,6 @@ type AuthProvider interface {
// NewTerminal creates a web-based terminal based on WebSockets and returns a
// new TerminalHandler.
func NewTerminal(req TerminalRequest, authProvider AuthProvider, ctx *SessionContext) (*TerminalHandler, error) {
// Make sure whatever session is requested is a valid session.
_, err := session.ParseID(string(req.SessionID))
if err != nil {
@ -173,6 +173,8 @@ type TerminalHandler struct {
// buffer is a buffer used to store the remaining payload data if it did not
// fit into the buffer provided by the callee to Read method
buffer []byte
closeOnce sync.Once
}
// Serve builds a connect to the remote node and then pumps back two types of
@ -197,20 +199,21 @@ func (t *TerminalHandler) Serve(w http.ResponseWriter, r *http.Request) {
// Close the websocket stream.
func (t *TerminalHandler) Close() error {
// Close the websocket connection to the client web browser.
if t.ws != nil {
t.ws.Close()
}
t.closeOnce.Do(func() {
// Close the websocket connection to the client web browser.
if t.ws != nil {
t.ws.Close()
}
// Close the SSH connection to the remote node.
if t.sshSession != nil {
t.sshSession.Close()
}
// If the terminal handler was closed (most likely due to the *SessionContext
// closing) then the stream should be closed as well.
t.terminalCancel()
// Close the SSH connection to the remote node.
if t.sshSession != nil {
t.sshSession.Close()
}
// If the terminal handler was closed (most likely due to the *SessionContext
// closing) then the stream should be closed as well.
t.terminalCancel()
})
return nil
}
@ -257,10 +260,7 @@ func (t *TerminalHandler) makeClient(ws *websocket.Conn) (*client.TeleportClient
// Create a terminal stream that wraps/unwraps the envelope used to
// communicate over the websocket.
stream, err := t.asTerminalStream(ws)
if err != nil {
return nil, trace.Wrap(err)
}
stream := t.asTerminalStream(ws)
clientConfig.ForwardAgent = true
clientConfig.HostLogin = t.params.Login
@ -595,14 +595,11 @@ func (t *TerminalHandler) read(out []byte, ws *websocket.Conn) (n int, err error
}
}
func (t *TerminalHandler) asTerminalStream(ws *websocket.Conn) (*terminalStream, error) {
if ws == nil {
return nil, trace.BadParameter("missing parameter ws")
}
func (t *TerminalHandler) asTerminalStream(ws *websocket.Conn) *terminalStream {
return &terminalStream{
ws: ws,
terminal: t,
}, nil
}
}
type terminalStream struct {