Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(v9) Delete app sessions on logout #11956

Merged
6 changes: 6 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,12 @@ func (c *Client) DeleteAllAppSessions(ctx context.Context) error {
return trail.FromGRPC(err)
}

// DeleteUserAppSessions deletes all user’s application sessions.
func (c *Client) DeleteUserAppSessions(ctx context.Context, req *proto.DeleteUserAppSessionsRequest) error {
_, err := c.grpc.DeleteUserAppSessions(ctx, req, c.callOpts...)
return trail.FromGRPC(err)
}

// GenerateAppToken creates a JWT token with application access.
func (c *Client) GenerateAppToken(ctx context.Context, req types.GenerateAppTokenRequest) (string, error) {
resp, err := c.grpc.GenerateAppToken(ctx, &proto.GenerateAppTokenRequest{
Expand Down
1,623 changes: 923 additions & 700 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions api/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,10 @@ message CreateAppSessionResponse {
// DeleteAppSessionRequest contains the parameters used to remove an application web session.
message DeleteAppSessionRequest { string SessionID = 1 [ (gogoproto.jsontag) = "session_id" ]; }

// DeleteUserAppSessionsRequest contains the parameters used to remove the
// user's application web sessions.
message DeleteUserAppSessionsRequest { string Username = 1 [ (gogoproto.jsontag) = "username" ]; }

// GetWebSessionResponse contains the requested web session.
message GetWebSessionResponse {
// Session is the web session.
Expand Down Expand Up @@ -1814,6 +1818,8 @@ service AuthService {
rpc DeleteAppSession(DeleteAppSessionRequest) returns (google.protobuf.Empty);
// DeleteAllAppSessions removes all application web sessions.
rpc DeleteAllAppSessions(google.protobuf.Empty) returns (google.protobuf.Empty);
// DeleteUserAppSessions deletes all user’s application sessions.
rpc DeleteUserAppSessions(DeleteUserAppSessionsRequest) returns (google.protobuf.Empty);

// GetWebSession gets a web session.
rpc GetWebSession(types.GetWebSessionRequest) returns (GetWebSessionResponse);
Expand Down
97 changes: 77 additions & 20 deletions integration/app_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,63 @@ func TestAppServersHA(t *testing.T) {
}
}

func TestAppInvalidateAppSessionsOnLogout(t *testing.T) {
// Create cluster, user, and credentials package.
pack := setup(t)

// Create an application session.
appCookie := pack.createAppSession(t, pack.rootAppPublicAddr, pack.rootAppClusterName)

// Issue a request to the application to guarantee everything is working correctly.
status, _, err := pack.makeRequest(appCookie, http.MethodGet, "/")
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)

// Logout from Teleport.
status, _, err = pack.makeWebapiRequest(http.MethodDelete, "sessions", []byte{})
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)

// As deleting WebSessions might not happen immediately, run the next request
// in an `Eventually` block.
require.Eventually(t, func() bool {
// Issue another request to the application. Now, it should receive a
// redirect because the application sessions are gone.
status, _, err = pack.makeRequest(appCookie, http.MethodGet, "/")
require.NoError(t, err)
return status == http.StatusFound
}, time.Second, 250*time.Millisecond)
}

func TestAppInvalidateCertificatesSessionsOnLogout(t *testing.T) {
// Create cluster, user, and credentials package.
pack := setup(t)

// Generates TLS config for making app requests.
reqTLS := pack.makeTLSConfig(t, pack.rootAppPublicAddr, pack.rootAppClusterName)
require.NotNil(t, reqTLS)

// Issue a request to the application to guarantee everything is working correctly.
status, _, err := pack.makeRequestWithClientCert(reqTLS, http.MethodGet, "/")
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)

// Logout from Teleport.
status, _, err = pack.makeWebapiRequest(http.MethodDelete, "sessions", []byte{})
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)

// As deleting WebSessions might not happen immediately, run the next request
// in an `Eventually` block.
require.Eventually(t, func() bool {
// Issue another request to the application. Now, it should receive a
// redirect because the application sessions are gone.
status, _, err = pack.makeRequestWithClientCert(reqTLS, http.MethodGet, "/")
require.NoError(t, err)
return status == http.StatusFound
}, time.Second, 250*time.Millisecond)
}

// pack contains identity as well as initialized Teleport clusters and instances.
type pack struct {
username string
Expand Down Expand Up @@ -1196,38 +1253,38 @@ func (p *pack) createAppSession(t *testing.T, publicAddr, clusterName string) st
ClusterName: clusterName,
})
require.NoError(t, err)
statusCode, body, err := p.makeWebapiRequest(http.MethodPost, "sessions/app", casReq)
require.NoError(t, err)
require.Equal(t, http.StatusOK, statusCode)

var casResp *web.CreateAppSessionResponse
err = json.Unmarshal(body, &casResp)
require.NoError(t, err)

return casResp.CookieValue
}

// makeWebapiRequest makes a request to the root cluster Web API.
func (p *pack) makeWebapiRequest(method, endpoint string, payload []byte) (int, []byte, error) {
u := url.URL{
Scheme: "https",
Host: net.JoinHostPort(Loopback, p.rootCluster.GetPortWeb()),
Path: "/v1/webapi/sessions/app",
Path: fmt.Sprintf("/v1/webapi/%s", endpoint),
}

req, err := http.NewRequest(method, u.String(), bytes.NewBuffer(payload))
if err != nil {
return 0, nil, trace.Wrap(err)
}
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBuffer(casReq))
require.NoError(t, err)

req.AddCookie(&http.Cookie{
Name: web.CookieName,
Value: p.webCookie,
})
req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", p.webToken))

client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)

var casResp *web.CreateAppSessionResponse
err = json.NewDecoder(resp.Body).Decode(&casResp)
require.NoError(t, err)

return casResp.CookieValue
statusCode, body, err := p.sendRequest(req, nil)
return statusCode, []byte(body), trace.Wrap(err)
}

func (p *pack) ensureAuditEvent(t *testing.T, eventType string, checkEvent func(event apievents.AuditEvent)) {
Expand Down
41 changes: 32 additions & 9 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -1352,10 +1352,8 @@ func (*webSessionsWithRoles) Upsert(ctx context.Context, session types.WebSessio

// 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(apidefaults.Namespace, types.KindWebSession, types.VerbDelete); err != nil {
return trace.Wrap(err)
}
if err := r.c.canDeleteWebSession(req.User); err != nil {
return trace.Wrap(err)
}
return r.ws.Delete(ctx, req)
}
Expand Down Expand Up @@ -1442,6 +1440,7 @@ type webTokensWithRoles struct {
type accessChecker interface {
action(namespace, resource string, verbs ...string) error
currentUserAction(user string) error
canDeleteWebSession(username string) error
}

func (a *ServerWithRoles) GetAccessRequests(ctx context.Context, filter types.AccessRequestFilter) ([]types.AccessRequest, error) {
Expand Down Expand Up @@ -3466,11 +3465,9 @@ func (a *ServerWithRoles) DeleteAppSession(ctx context.Context, req types.Delete
if err != nil {
return trace.Wrap(err)
}
// Users can only delete their own app sessions.
if err := a.currentUserAction(session.GetUser()); err != nil {
if err := a.action(apidefaults.Namespace, types.KindWebSession, types.VerbDelete); err != nil {
return trace.Wrap(err)
}
// Check if user can delete this web session.
if err := a.canDeleteWebSession(session.GetUser()); err != nil {
return trace.Wrap(err)
}
if err := a.authServer.DeleteAppSession(ctx, req); err != nil {
return trace.Wrap(err)
Expand All @@ -3490,6 +3487,32 @@ func (a *ServerWithRoles) DeleteAllAppSessions(ctx context.Context) error {
return nil
}

// DeleteUserAppSessions deletes all user’s application sessions.
func (a *ServerWithRoles) DeleteUserAppSessions(ctx context.Context, req *proto.DeleteUserAppSessionsRequest) error {
// First, check if the current user can delete the request user sessions.
if err := a.canDeleteWebSession(req.Username); err != nil {
return trace.Wrap(err)
}

if err := a.authServer.DeleteUserAppSessions(ctx, req); err != nil {
return trace.Wrap(err)
}

return nil
}

// canDeleteWebSession checks if the current user can delete
// WebSessions from the provided `username`.
func (a *ServerWithRoles) canDeleteWebSession(username string) error {
if err := a.currentUserAction(username); err != nil {
if err := a.action(apidefaults.Namespace, types.KindWebSession, types.VerbList, types.VerbDelete); err != nil {
return trace.Wrap(err)
}
}

return nil
}

// GenerateAppToken creates a JWT token with application access.
func (a *ServerWithRoles) GenerateAppToken(ctx context.Context, req types.GenerateAppTokenRequest) (string, error) {
if err := a.action(apidefaults.Namespace, types.KindJWT, types.VerbCreate); err != nil {
Expand Down
98 changes: 98 additions & 0 deletions lib/auth/auth_with_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2313,3 +2313,101 @@ func TestListResources_KindKubernetesCluster(t *testing.T) {
require.IsDecreasing(t, names)
})
}

func TestDeleteUserAppSessions(t *testing.T) {
ctx := context.Background()

srv := newTestTLSServer(t)
t.Cleanup(func() { srv.Close() })

// Generates a new user client.
userClient := func(username string) *Client {
user, _, err := CreateUserAndRole(srv.Auth(), username, nil)
require.NoError(t, err)
identity := TestUser(user.GetName())
clt, err := srv.NewClient(identity)
require.NoError(t, err)
return clt
}

// Register users.
aliceClt := userClient("alice")
bobClt := userClient("bob")

// Register multiple applications.
applications := []struct {
name string
publicAddr string
}{
{name: "panel", publicAddr: "panel.example.com"},
{name: "admin", publicAddr: "admin.example.com"},
{name: "metrics", publicAddr: "metrics.example.com"},
}

// Register and create a session for each application.
for _, application := range applications {
// Register an application.
app, err := types.NewAppV3(types.Metadata{
Name: application.name,
}, types.AppSpecV3{
URI: "localhost",
PublicAddr: application.publicAddr,
})
require.NoError(t, err)
server, err := types.NewAppServerV3FromApp(app, "host", uuid.New().String())
require.NoError(t, err)
_, err = srv.Auth().UpsertApplicationServer(ctx, server)
require.NoError(t, err)

// Create a session for alice.
_, err = aliceClt.CreateAppSession(ctx, types.CreateAppSessionRequest{
Username: "alice",
PublicAddr: application.publicAddr,
ClusterName: "localhost",
})
require.NoError(t, err)

// Create a session for bob.
_, err = bobClt.CreateAppSession(ctx, types.CreateAppSessionRequest{
Username: "bob",
PublicAddr: application.publicAddr,
ClusterName: "localhost",
})
require.NoError(t, err)
}

// Ensure the correct number of sessions.
sessions, err := srv.Auth().GetAppSessions(ctx)
require.NoError(t, err)
require.Len(t, sessions, 6)

// Try to delete other user app sessions.
err = aliceClt.DeleteUserAppSessions(ctx, &proto.DeleteUserAppSessionsRequest{Username: "bob"})
require.Error(t, err)
require.True(t, trace.IsAccessDenied(err))

err = bobClt.DeleteUserAppSessions(ctx, &proto.DeleteUserAppSessionsRequest{Username: "alice"})
require.Error(t, err)
require.True(t, trace.IsAccessDenied(err))

// Delete alice sessions.
err = aliceClt.DeleteUserAppSessions(ctx, &proto.DeleteUserAppSessionsRequest{Username: "alice"})
require.NoError(t, err)

// Check if only bob's sessions are left.
sessions, err = srv.Auth().GetAppSessions(ctx)
require.NoError(t, err)
require.Len(t, sessions, 3)
for _, session := range sessions {
require.Equal(t, "bob", session.GetUser())
}

// Delete bob sessions.
err = bobClt.DeleteUserAppSessions(ctx, &proto.DeleteUserAppSessionsRequest{Username: "bob"})
require.NoError(t, err)

// No sessions left.
sessions, err = srv.Auth().GetAppSessions(ctx)
require.NoError(t, err)
require.Len(t, sessions, 0)
}
14 changes: 14 additions & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,20 @@ func (g *GRPCServer) DeleteAllAppSessions(ctx context.Context, _ *empty.Empty) (
return &empty.Empty{}, nil
}

// DeleteUserAppSessions removes user's all application web sessions.
func (g *GRPCServer) DeleteUserAppSessions(ctx context.Context, req *proto.DeleteUserAppSessionsRequest) (*empty.Empty, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

if err := auth.DeleteUserAppSessions(ctx, req); err != nil {
return nil, trace.Wrap(err)
}

return &empty.Empty{}, nil
}

// GenerateAppToken creates a JWT token with application access.
func (g GRPCServer) GenerateAppToken(ctx context.Context, req *proto.GenerateAppTokenRequest) (*proto.GenerateAppTokenResponse, error) {
auth, err := g.authenticate(ctx)
Expand Down
13 changes: 13 additions & 0 deletions lib/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,19 @@ func (proxy *ProxyClient) DeleteAppSession(ctx context.Context, sessionID string
return nil
}

// DeleteUserAppSessions removes user's all application web sessions.
func (proxy *ProxyClient) DeleteUserAppSessions(ctx context.Context, req *proto.DeleteUserAppSessionsRequest) error {
authClient, err := proxy.ConnectToRootCluster(ctx, true)
if err != nil {
return trace.Wrap(err)
}
err = authClient.DeleteUserAppSessions(ctx, req)
if err != nil {
return trace.Wrap(err)
}
return nil
}

// FindDatabaseServersByFilters returns registered database proxy servers that match the provided filter.
func (proxy *ProxyClient) FindDatabaseServersByFilters(ctx context.Context, req proto.ListResourcesRequest) ([]types.DatabaseServer, error) {
req.ResourceType = types.KindDatabaseServer
Expand Down
3 changes: 3 additions & 0 deletions lib/services/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"context"
"time"

"github.com/gravitational/teleport/api/client/proto"
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
wantypes "github.com/gravitational/teleport/api/types/webauthn"
Expand Down Expand Up @@ -284,6 +285,8 @@ type AppSession interface {
DeleteAppSession(context.Context, types.DeleteAppSessionRequest) error
// DeleteAllAppSessions removes all application web sessions.
DeleteAllAppSessions(context.Context) error
// DeleteUserAppSessions deletes all user’s application sessions.
DeleteUserAppSessions(ctx context.Context, req *proto.DeleteUserAppSessionsRequest) error
}

// VerifyPassword makes sure password satisfies our requirements (relaxed),
Expand Down
Loading