mirror of
https://github.com/gravitational/teleport
synced 2024-10-20 17:23:22 +00:00
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:
parent
5ce5e1c525
commit
86908cc2f3
|
@ -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
|
@ -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
45
api/client/proto/proto.go
Normal 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
150
api/client/sessions.go
Normal 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"
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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" ];
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
52
lib/cache/cache.go
vendored
|
@ -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)
|
||||
}
|
||||
|
|
18
lib/cache/cache_test.go
vendored
18
lib/cache/cache_test.go
vendored
|
@ -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{
|
||||
|
|
225
lib/cache/collections.go
vendored
225
lib/cache/collections.go
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 == "" {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 = ©
|
||||
}
|
||||
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"}
|
||||
}
|
||||
}`
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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¶ms=<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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue