Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e
Submodule e updated from 071f45 to d14cd6
84 changes: 84 additions & 0 deletions lib/auth/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
type APIConfig struct {
PluginRegistry plugin.Registry
AuthServer *Server
SessionService session.Service
AuditLog events.IAuditLog
Authorizer Authorizer
Emitter apievents.Emitter
Expand Down Expand Up @@ -149,6 +150,12 @@ func NewAPIServer(config *APIConfig) (http.Handler, error) {
// Active sessions
srv.GET("/:version/namespaces/:namespace/sessions/:id/stream", srv.withAuth(srv.getSessionChunk))
srv.GET("/:version/namespaces/:namespace/sessions/:id/events", srv.withAuth(srv.getSessionEvents))
// DELETE IN 12.0.0
srv.POST("/:version/namespaces/:namespace/sessions", srv.withAuth(srv.createSession))
srv.PUT("/:version/namespaces/:namespace/sessions/:id", srv.withAuth(srv.updateSession))
srv.DELETE("/:version/namespaces/:namespace/sessions/:id", srv.withAuth(srv.deleteSession))
srv.GET("/:version/namespaces/:namespace/sessions", srv.withAuth(srv.getSessions))
srv.GET("/:version/namespaces/:namespace/sessions/:id", srv.withAuth(srv.getSession))

// Namespaces
srv.POST("/:version/namespaces", srv.withAuth(srv.upsertNamespace))
Expand Down Expand Up @@ -214,6 +221,7 @@ func (s *APIServer) withAuth(handler HandlerWithAuthFunc) httprouter.Handle {
auth := &ServerWithRoles{
authServer: s.AuthServer,
context: *authContext,
sessions: s.SessionService,
alog: s.AuthServer,
}
version := p.ByName("version")
Expand Down Expand Up @@ -784,6 +792,82 @@ func (s *APIServer) deleteCertAuthority(auth ClientI, w http.ResponseWriter, r *
return message(fmt.Sprintf("cert '%v' deleted", id)), nil
}

type createSessionReq struct {
Session session.Session `json:"session"`
}

func (s *APIServer) createSession(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
var req *createSessionReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
namespace := p.ByName("namespace")
if !types.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
req.Session.Namespace = namespace
if err := auth.CreateSession(r.Context(), req.Session); err != nil {
return nil, trace.Wrap(err)
}
return message("ok"), nil
}

type updateSessionReq struct {
Update session.UpdateRequest `json:"update"`
}

func (s *APIServer) updateSession(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
var req *updateSessionReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
namespace := p.ByName("namespace")
if !types.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
req.Update.Namespace = namespace
if err := auth.UpdateSession(r.Context(), req.Update); err != nil {
return nil, trace.Wrap(err)
}
return message("ok"), nil
}

func (s *APIServer) deleteSession(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
err := auth.DeleteSession(r.Context(), p.ByName("namespace"), session.ID(p.ByName("id")))
if err != nil {
return nil, trace.Wrap(err)
}
return message("ok"), nil
}

func (s *APIServer) getSessions(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
namespace := p.ByName("namespace")
if !types.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
sessions, err := auth.GetSessions(r.Context(), namespace)
if err != nil {
return nil, trace.Wrap(err)
}
return sessions, nil
}

func (s *APIServer) getSession(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
sid, err := session.ParseID(p.ByName("id"))
if err != nil {
return nil, trace.Wrap(err)
}
namespace := p.ByName("namespace")
if !types.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
}
se, err := auth.GetSession(r.Context(), namespace, *sid)
if err != nil {
return nil, trace.Wrap(err)
}
return se, nil
}

type validateOIDCAuthCallbackReq struct {
Query url.Values `json:"query"`
}
Expand Down
244 changes: 244 additions & 0 deletions lib/auth/apiserver_active_sessions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// 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 auth

import (
"context"
"sort"
"testing"
"time"

apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"

"github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
)

func TestAPIServer_activeSessions_whereConditions(t *testing.T) {
t.Parallel()

ctx := context.Background()
tlsServer := newTestTLSServer(t)
authServer := tlsServer.Auth()

// - "admin" has permissions to access all active sessions
// - "alpaca" has permissions to access only their own active sessions
// Each user is assigned its corresponding role, plus whatever extra
// permissions are needed to run the scenario.
const admin = "admin"
const alpaca = "alpaca"
alpacaRole := services.RoleForUser(&types.UserV2{Metadata: types.Metadata{Name: alpaca}})
alpacaRole.SetLogins(types.Allow, []string{alpaca})
alpacaRole.SetRules(types.Allow, append(alpacaRole.GetRules(types.Allow), types.Rule{
Resources: []string{"ssh_session"},
// Allow all ssh_session verbs, deny rule below takes precedence.
Verbs: []string{"*"},
}))
alpacaRole.SetRules(types.Deny, append(alpacaRole.GetRules(types.Deny), types.Rule{
Resources: []string{"ssh_session"},
Verbs: []string{"list", "read", "update", "delete"},
Where: "!contains(ssh_session.participants, user.metadata.name)",
}))
_, err := CreateUser(authServer, alpaca, alpacaRole)
require.NoError(t, err)

// Prepare clients.
adminClient, err := tlsServer.NewClient(TestAdmin())
require.NoError(t, err)
alpacaClient, err := tlsServer.NewClient(TestUser(alpaca))
require.NoError(t, err)

// Prepare one session per user.
createSession := func(clt ClientI, user string) session.ID {
id := session.NewID()
now := time.Now()

// Create initial session.
require.NoError(t, clt.CreateSession(ctx, session.Session{
ID: id,
Namespace: apidefaults.Namespace,
TerminalParams: session.TerminalParams{
W: 100,
H: 100,
},
Login: user,
Created: now,
LastActive: now,
}))

// Add parties, must be done via update.
// Usually the Node does this, in the test we are taking a shortcut and
// using admin due to its powerful permissions.
require.NoError(t, adminClient.UpdateSession(ctx, session.UpdateRequest{
ID: id,
Namespace: apidefaults.Namespace,
Parties: &[]session.Party{
{ID: session.NewID(), User: user},
},
}))
return id
}
adminSessionID := createSession(adminClient, admin)
alpacaSessionID := createSession(alpacaClient, alpaca)

t.Run("GetSessions respects role conditions", func(t *testing.T) {
tests := []struct {
name string
clt ClientI
wantIDs []session.ID
}{
{
name: admin,
clt: adminClient,
wantIDs: []session.ID{adminSessionID, alpacaSessionID},
},
{
name: alpaca,
clt: alpacaClient,
wantIDs: []session.ID{alpacaSessionID},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sessions, err := test.clt.GetSessions(ctx, apidefaults.Namespace)
require.NoError(t, err)

got := make([]session.ID, len(sessions))
for i, s := range sessions {
got[i] = s.ID
}
want := test.wantIDs
sort.Slice(got, func(i, j int) bool { return got[i] < got[j] })
sort.Slice(want, func(i, j int) bool { return want[i] < want[j] })
if diff := cmp.Diff(test.wantIDs, got); diff != "" {
t.Errorf("GetSessions() mismatch (-want +got):\n%s", diff)
}
})
}
})

// Helper functions used by test cases below.
getSession := func(clt ClientI) func(id session.ID) error {
return func(id session.ID) error {
_, err := clt.GetSession(ctx, apidefaults.Namespace, id)
return err
}
}
updateSession := func(clt ClientI) func(id session.ID) error {
return func(id session.ID) error {
return clt.UpdateSession(ctx, session.UpdateRequest{
ID: id,
Namespace: apidefaults.Namespace,
TerminalParams: &session.TerminalParams{W: 150, H: 150},
})
}
}
deleteSession := func(clt ClientI) func(id session.ID) error {
return func(id session.ID) error {
return clt.UpdateSession(ctx, session.UpdateRequest{
ID: id,
Namespace: apidefaults.Namespace,
TerminalParams: &session.TerminalParams{W: 150, H: 150},
})
}
}

t.Run("users can't interact with denied sessions", func(t *testing.T) {
clt := alpacaClient
sessionID := adminSessionID
tests := []struct {
name string
fn func(id session.ID) error
}{
{
name: "GetSession",
fn: getSession(clt),
},
{
name: "UpdateSession",
fn: updateSession(clt),
},
{
name: "DeleteSession",
fn: deleteSession(clt),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.fn(sessionID)
require.True(t, trace.IsAccessDenied(err), "unexpected err: %v (want access denied)", err)
})
}
})

t.Run("users can interact with allowed sessions", func(t *testing.T) {
tests := []struct {
name string
fn func(session.ID) error
sessionID session.ID
}{
{
name: "admin reads own session",
fn: getSession(adminClient),
sessionID: adminSessionID,
},
{
name: "admin updates own session",
fn: updateSession(adminClient),
sessionID: adminSessionID,
},
{
name: "admin deletes own session",
fn: deleteSession(adminClient),
sessionID: adminSessionID,
},
{
name: "admin reads alpaca session",
fn: getSession(adminClient),
sessionID: alpacaSessionID,
},
{
name: "admin updates alpaca session",
fn: updateSession(adminClient),
sessionID: alpacaSessionID,
},

{
name: "alpaca reads own session",
fn: getSession(alpacaClient),
sessionID: alpacaSessionID,
},
{
name: "alpaca updates own session",
fn: updateSession(alpacaClient),
sessionID: alpacaSessionID,
},
{
name: "alpaca deletes own session",
fn: deleteSession(alpacaClient),
sessionID: alpacaSessionID,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.NoError(t, test.fn(test.sessionID))
})
}
})
}
Loading