concurrent session control

Adds support for Concurrent Session Control and a new
semaphore API.  Roles now support two new configuration
options, `max_ssh_connections` and `max_ssh_sessions`
which correspond to the total number of authenticated
ssh connections per cluster, and the number of ssh sessions
within a connection respectively.  Attempting to exceed
these limits generate variants of the `session.rejected`
audit event and cause the connection/session to be
rejected.
This commit is contained in:
Forrest Marshall 2020-08-25 12:02:16 -07:00 committed by Forrest Marshall
parent 0f4e82548f
commit ae2336dfd0
34 changed files with 3312 additions and 1288 deletions

View file

@ -589,6 +589,11 @@ const (
// NodeIsAmbiguous serves as an identifying error string indicating that
// the proxy subsystem found multiple nodes matching the specified hostname.
NodeIsAmbiguous = "err-node-is-ambiguous"
// MaxLeases serves as an identifying error string indicating that the
// semaphore system is rejecting an acquisition attempt due to max
// leases having already been reached.
MaxLeases = "err-max-leases"
)
const (

View file

@ -895,6 +895,10 @@ type disconnectTestCase struct {
recordingMode string
options services.RoleOptions
disconnectTimeout time.Duration
concurrentConns int
sessCtlTimeout time.Duration
assertExpected func(*check.C, error)
postFunc func(context.Context, *check.C, *TeleInstance)
}
// TestDisconnectScenarios tests multiple scenarios with client disconnects
@ -935,6 +939,78 @@ func (s *IntSuite) TestDisconnectScenarios(c *check.C) {
},
disconnectTimeout: 4 * time.Second,
},
{ // verify that concurrent connection limits are applied when recording at node
recordingMode: services.RecordAtNode,
options: services.RoleOptions{
MaxConnections: 1,
},
disconnectTimeout: 1 * time.Second,
concurrentConns: 2,
assertExpected: func(c *check.C, err error) {
if err == nil || !strings.Contains(err.Error(), "administratively prohibited") {
c.Fatalf("Expected 'administratively prohibited', got: %v", err)
}
},
},
{ // verify that concurrent connection limits are applied when recording at proxy
recordingMode: services.RecordAtProxy,
options: services.RoleOptions{
ForwardAgent: services.NewBool(true),
MaxConnections: 1,
},
disconnectTimeout: 1 * time.Second,
concurrentConns: 2,
assertExpected: func(c *check.C, err error) {
if err == nil || !strings.Contains(err.Error(), "administratively prohibited") {
c.Fatalf("Expected 'administratively prohibited', got: %v", err)
}
},
},
{ // verify that lost connections to auth server terminate controlled conns
recordingMode: services.RecordAtNode,
options: services.RoleOptions{
MaxConnections: 1,
},
disconnectTimeout: time.Second,
sessCtlTimeout: 500 * time.Millisecond,
// use postFunc to wait for the semaphore to be acquired and a session
// to be started, then shut down the auth server.
postFunc: func(ctx context.Context, c *check.C, t *TeleInstance) {
site := t.GetSiteAPI(Site)
var sems []services.Semaphore
var err error
for i := 0; i < 6; i++ {
sems, err = site.GetSemaphores(ctx, services.SemaphoreFilter{
SemaphoreKind: services.SemaphoreKindConnection,
})
if err == nil && len(sems) > 0 {
break
}
select {
case <-time.After(time.Millisecond * 100):
case <-ctx.Done():
return
}
}
c.Assert(err, check.IsNil)
c.Assert(len(sems), check.Equals, 1)
var ss []session.Session
for i := 0; i < 6; i++ {
ss, err = site.GetSessions(defaults.Namespace)
if err == nil && len(ss) > 0 {
break
}
select {
case <-time.After(time.Millisecond * 100):
case <-ctx.Done():
return
}
}
c.Assert(err, check.IsNil)
c.Assert(len(ss), check.Equals, 1)
c.Assert(t.StopAuth(false), check.IsNil)
},
},
}
for _, tc := range testCases {
s.runDisconnectTest(c, tc)
@ -962,8 +1038,9 @@ func (s *IntSuite) runDisconnectTest(c *check.C, tc disconnectTestCase) {
t.AddUserWithRole(username, role)
clusterConfig, err := services.NewClusterConfig(services.ClusterConfigSpecV3{
SessionRecording: tc.recordingMode,
LocalAuth: services.NewBool(true),
SessionRecording: tc.recordingMode,
LocalAuth: services.NewBool(true),
SessionControlTimeout: services.Duration(tc.sessCtlTimeout),
})
c.Assert(err, check.IsNil)
@ -983,35 +1060,59 @@ func (s *IntSuite) runDisconnectTest(c *check.C, tc disconnectTestCase) {
site := t.GetSiteAPI(Site)
c.Assert(site, check.NotNil)
person := NewTerminal(250)
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
// PersonA: SSH into the server, wait one second, then type some commands on stdin:
sessionCtx, sessionCancel := context.WithCancel(context.TODO())
openSession := func() {
defer sessionCancel()
cl, err := t.NewClient(ClientConfig{Login: username, Cluster: Site, Host: Host, Port: t.GetPortSSHInt()})
c.Assert(err, check.IsNil)
cl.Stdout = person
cl.Stdin = person
err = cl.SSH(context.TODO(), []string{}, false)
if err != nil && err != io.EOF {
c.Fatalf("expected EOF or nil, got %v instead", err)
}
if tc.concurrentConns < 1 {
// test cases that don't specify concurrentConns are single-connection tests.
tc.concurrentConns = 1
}
go openSession()
for i := 0; i < tc.concurrentConns; i++ {
person := NewTerminal(250)
openSession := func() {
defer cancel()
cl, err := t.NewClient(ClientConfig{Login: username, Cluster: Site, Host: Host, Port: t.GetPortSSHInt()})
c.Assert(err, check.IsNil)
cl.Stdout = person
cl.Stdin = person
err = cl.SSH(ctx, []string{}, false)
select {
case <-ctx.Done():
// either we timed out, or a different session
// triggered closure.
return
default:
}
if tc.assertExpected != nil {
tc.assertExpected(c, err)
} else if err != nil && !trace.IsEOF(err) {
c.Fatalf("expected EOF or nil, got %v instead", err)
}
}
go openSession()
go enterInput(ctx, c, person, "echo start \r\n", ".*start.*")
}
if tc.postFunc != nil {
// test case modifies the teleport instance after session start
tc.postFunc(ctx, c, t)
}
enterInput(c, person, "echo start \r\n", ".*start.*")
select {
case <-time.After(tc.disconnectTimeout):
c.Fatalf("timeout waiting for session to exit")
case <-sessionCtx.Done():
// session closed
case <-time.After(tc.disconnectTimeout + time.Second):
c.Fatalf("timeout waiting for session to exit: %+v", tc)
case <-ctx.Done():
// session closed. a test case is successful if the first
// session to close encountered the expected error variant.
}
}
func enterInput(c *check.C, person *Terminal, command, pattern string) {
func enterInput(ctx context.Context, c *check.C, person *Terminal, command, pattern string) {
person.Type(command)
abortTime := time.Now().Add(10 * time.Second)
var matched bool
@ -1020,16 +1121,19 @@ func enterInput(c *check.C, person *Terminal, command, pattern string) {
output = replaceNewlines(person.Output(1000))
matched, _ = regexp.MatchString(pattern, output)
if matched {
break
return
}
select {
case <-time.After(time.Millisecond * 50):
case <-ctx.Done():
// cancellation means that we don't care about the input being
// confirmed anymore; not equivalent to a timeout.
return
}
time.Sleep(time.Millisecond * 200)
if time.Now().After(abortTime) {
c.Fatalf("failed to capture pattern %q in %q", pattern, output)
}
}
if !matched {
c.Fatalf("output %q does not match pattern %q", output, pattern)
}
}
// TestInvalidLogins validates that you can't login with invalid login or

View file

@ -1117,7 +1117,7 @@ func (s *KubeSuite) runKubeDisconnectTest(c *check.C, tc disconnectTestCase) {
}()
// lets type something followed by "enter" and then hang the session
enterInput(c, term, "echo boring platapus\r\n", ".*boring platapus.*")
enterInput(sessionCtx, c, term, "echo boring platapus\r\n", ".*boring platapus.*")
time.Sleep(tc.disconnectTimeout)
select {
case <-time.After(tc.disconnectTimeout):

View file

@ -103,6 +103,9 @@ type AccessPoint interface {
// Announcer adds methods used to announce presence
Announcer
// Semaphores provides semaphore operations
services.Semaphores
// UpsertTunnelConnection upserts tunnel connection
UpsertTunnelConnection(conn services.TunnelConnection) error
@ -144,56 +147,80 @@ type AuthCache interface {
}
// NewWrapper returns new access point wrapper
func NewWrapper(writer AccessPoint, cache ReadAccessPoint) AccessPoint {
func NewWrapper(base AccessPoint, cache ReadAccessPoint) AccessPoint {
return &Wrapper{
Write: writer,
NoCache: base,
ReadAccessPoint: cache,
}
}
// Wrapper wraps access point and auth cache in one client
// so that update operations are going through access point
// and read operations are going though cache
// so that reads of cached values can be intercepted.
type Wrapper struct {
ReadAccessPoint
Write AccessPoint
NoCache AccessPoint
}
// Close closes all associated resources
func (w *Wrapper) Close() error {
err := w.Write.Close()
err := w.NoCache.Close()
err2 := w.ReadAccessPoint.Close()
return trace.NewAggregate(err, err2)
}
// UpsertNode is part of auth.AccessPoint implementation
func (w *Wrapper) UpsertNode(s services.Server) (*services.KeepAlive, error) {
return w.Write.UpsertNode(s)
return w.NoCache.UpsertNode(s)
}
// UpsertAuthServer is part of auth.AccessPoint implementation
func (w *Wrapper) UpsertAuthServer(s services.Server) error {
return w.Write.UpsertAuthServer(s)
return w.NoCache.UpsertAuthServer(s)
}
// NewKeepAliver returns a new instance of keep aliver
func (w *Wrapper) NewKeepAliver(ctx context.Context) (services.KeepAliver, error) {
return w.Write.NewKeepAliver(ctx)
return w.NoCache.NewKeepAliver(ctx)
}
// UpsertProxy is part of auth.AccessPoint implementation
func (w *Wrapper) UpsertProxy(s services.Server) error {
return w.Write.UpsertProxy(s)
return w.NoCache.UpsertProxy(s)
}
// UpsertTunnelConnection is a part of auth.AccessPoint implementation
func (w *Wrapper) UpsertTunnelConnection(conn services.TunnelConnection) error {
return w.Write.UpsertTunnelConnection(conn)
return w.NoCache.UpsertTunnelConnection(conn)
}
// DeleteTunnelConnection is a part of auth.AccessPoint implementation
func (w *Wrapper) DeleteTunnelConnection(clusterName, connName string) error {
return w.Write.DeleteTunnelConnection(clusterName, connName)
return w.NoCache.DeleteTunnelConnection(clusterName, connName)
}
// AcquireSemaphore acquires lease with requested resources from semaphore
func (w *Wrapper) AcquireSemaphore(ctx context.Context, params services.AcquireSemaphoreRequest) (*services.SemaphoreLease, error) {
return w.NoCache.AcquireSemaphore(ctx, params)
}
// KeepAliveSemaphoreLease updates semaphore lease
func (w *Wrapper) KeepAliveSemaphoreLease(ctx context.Context, lease services.SemaphoreLease) error {
return w.NoCache.KeepAliveSemaphoreLease(ctx, lease)
}
// CancelSemaphoreLease cancels semaphore lease early
func (w *Wrapper) CancelSemaphoreLease(ctx context.Context, lease services.SemaphoreLease) error {
return w.NoCache.CancelSemaphoreLease(ctx, lease)
}
// GetSemaphores returns a list of semaphores matching supplied filter.
func (w *Wrapper) GetSemaphores(ctx context.Context, filter services.SemaphoreFilter) ([]services.Semaphore, error) {
return w.NoCache.GetSemaphores(ctx, filter)
}
// DeleteSemaphore deletes a semaphore matching supplied filter.
func (w *Wrapper) DeleteSemaphore(ctx context.Context, filter services.SemaphoreFilter) error {
return w.NoCache.DeleteSemaphore(ctx, filter)
}
// NewCachingAcessPoint returns new caching access point using

View file

@ -1772,6 +1772,52 @@ func (a *AuthWithRoles) DeleteAllRemoteClusters() error {
return a.authServer.DeleteAllRemoteClusters()
}
// AcquireSemaphore acquires lease with requested resources from semaphore.
func (a *AuthWithRoles) AcquireSemaphore(ctx context.Context, params services.AcquireSemaphoreRequest) (*services.SemaphoreLease, error) {
if err := a.action(defaults.Namespace, services.KindSemaphore, services.VerbCreate); err != nil {
return nil, trace.Wrap(err)
}
if err := a.action(defaults.Namespace, services.KindSemaphore, services.VerbUpdate); err != nil {
return nil, trace.Wrap(err)
}
return a.authServer.AcquireSemaphore(ctx, params)
}
// KeepAliveSemaphoreLease updates semaphore lease.
func (a *AuthWithRoles) KeepAliveSemaphoreLease(ctx context.Context, lease services.SemaphoreLease) error {
if err := a.action(defaults.Namespace, services.KindSemaphore, services.VerbUpdate); err != nil {
return trace.Wrap(err)
}
return a.authServer.KeepAliveSemaphoreLease(ctx, lease)
}
// CancelSemaphoreLease cancels semaphore lease early.
func (a *AuthWithRoles) CancelSemaphoreLease(ctx context.Context, lease services.SemaphoreLease) error {
if err := a.action(defaults.Namespace, services.KindSemaphore, services.VerbUpdate); err != nil {
return trace.Wrap(err)
}
return a.authServer.CancelSemaphoreLease(ctx, lease)
}
// GetSemaphores returns a list of all semaphores matching the supplied filter.
func (a *AuthWithRoles) GetSemaphores(ctx context.Context, filter services.SemaphoreFilter) ([]services.Semaphore, error) {
if err := a.action(defaults.Namespace, services.KindSemaphore, services.VerbReadNoSecrets); err != nil {
return nil, trace.Wrap(err)
}
if err := a.action(defaults.Namespace, services.KindSemaphore, services.VerbList); err != nil {
return nil, trace.Wrap(err)
}
return a.authServer.GetSemaphores(ctx, filter)
}
// DeleteSemaphore deletes a semaphore matching the supplied filter.
func (a *AuthWithRoles) DeleteSemaphore(ctx context.Context, filter services.SemaphoreFilter) error {
if err := a.action(defaults.Namespace, services.KindSemaphore, services.VerbDelete); err != nil {
return trace.Wrap(err)
}
return a.authServer.DeleteSemaphore(ctx, filter)
}
// ProcessKubeCSR processes CSR request against Kubernetes CA, returns
// signed certificate if successful.
func (a *AuthWithRoles) ProcessKubeCSR(req KubeCSR) (*KubeCSRResponse, error) {

View file

@ -2695,6 +2695,72 @@ func (c *Client) Ping(ctx context.Context) (proto.PingResponse, error) {
return *rsp, nil
}
// AcquireSemaphore acquires lease with requested resources from semaphore.
func (c *Client) AcquireSemaphore(ctx context.Context, params services.AcquireSemaphoreRequest) (*services.SemaphoreLease, error) {
clt, err := c.grpc()
if err != nil {
return nil, trace.Wrap(err)
}
lease, err := clt.AcquireSemaphore(ctx, &params)
if err != nil {
return nil, trail.FromGRPC(err)
}
return lease, nil
}
// KeepAliveSemaphoreLease updates semaphore lease.
func (c *Client) KeepAliveSemaphoreLease(ctx context.Context, lease services.SemaphoreLease) error {
clt, err := c.grpc()
if err != nil {
return trace.Wrap(err)
}
if _, err := clt.KeepAliveSemaphoreLease(ctx, &lease); err != nil {
return trail.FromGRPC(err)
}
return nil
}
// CancelSemaphoreLease cancels semaphore lease early.
func (c *Client) CancelSemaphoreLease(ctx context.Context, lease services.SemaphoreLease) error {
clt, err := c.grpc()
if err != nil {
return trace.Wrap(err)
}
if _, err := clt.CancelSemaphoreLease(ctx, &lease); err != nil {
return trail.FromGRPC(err)
}
return nil
}
// GetSemaphores returns a list of all semaphores matching the supplied filter.
func (c *Client) GetSemaphores(ctx context.Context, filter services.SemaphoreFilter) ([]services.Semaphore, error) {
clt, err := c.grpc()
if err != nil {
return nil, trace.Wrap(err)
}
rsp, err := clt.GetSemaphores(ctx, &filter)
if err != nil {
return nil, trail.FromGRPC(err)
}
sems := make([]services.Semaphore, 0, len(rsp.Semaphores))
for _, s := range rsp.Semaphores {
sems = append(sems, s)
}
return sems, nil
}
// DeleteSemaphore deletes a semaphore matching the supplied filter.
func (c *Client) DeleteSemaphore(ctx context.Context, filter services.SemaphoreFilter) error {
clt, err := c.grpc()
if err != nil {
return trace.Wrap(err)
}
if _, err := clt.DeleteSemaphore(ctx, &filter); err != nil {
return trail.FromGRPC(err)
}
return nil
}
// WebService implements features used by Web UI clients
type WebService interface {
// GetWebSessionInfo checks if a web sesion is valid, returns session id in case if

View file

@ -428,6 +428,76 @@ func (g *GRPCServer) DeleteUser(ctx context.Context, req *proto.DeleteUserReques
return &empty.Empty{}, nil
}
// AcquireSemaphore acquires lease with requested resources from semaphore.
func (g *GRPCServer) AcquireSemaphore(ctx context.Context, params *services.AcquireSemaphoreRequest) (*services.SemaphoreLease, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
lease, err := auth.AcquireSemaphore(ctx, *params)
return lease, trail.ToGRPC(err)
}
// KeepAliveSemaphoreLease updates semaphore lease.
func (g *GRPCServer) KeepAliveSemaphoreLease(ctx context.Context, req *services.SemaphoreLease) (*empty.Empty, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
if err := auth.KeepAliveSemaphoreLease(ctx, *req); err != nil {
return nil, trail.ToGRPC(err)
}
return &empty.Empty{}, nil
}
// CancelSemaphoreLease cancels semaphore lease early.
func (g *GRPCServer) CancelSemaphoreLease(ctx context.Context, req *services.SemaphoreLease) (*empty.Empty, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
if err := auth.CancelSemaphoreLease(ctx, *req); err != nil {
return nil, trail.ToGRPC(err)
}
return &empty.Empty{}, nil
}
// GetSemaphores returns a list of all semaphores matching the supplied filter.
func (g *GRPCServer) GetSemaphores(ctx context.Context, req *services.SemaphoreFilter) (*proto.Semaphores, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
semaphores, err := auth.GetSemaphores(ctx, *req)
if err != nil {
return nil, trail.ToGRPC(err)
}
ss := make([]*services.SemaphoreV3, 0, len(semaphores))
for _, sem := range semaphores {
s, ok := sem.(*services.SemaphoreV3)
if !ok {
return nil, trail.ToGRPC(trace.BadParameter("unexpected semaphore type: %T", sem))
}
ss = append(ss, s)
}
return &proto.Semaphores{
Semaphores: ss,
}, nil
}
// DeleteSemaphore deletes a semaphore matching the supplied filter.
func (g *GRPCServer) DeleteSemaphore(ctx context.Context, req *services.SemaphoreFilter) (*empty.Empty, error) {
auth, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
if err := auth.DeleteSemaphore(ctx, *req); err != nil {
return nil, trail.ToGRPC(err)
}
return &empty.Empty{}, nil
}
type grpcContext struct {
*AuthContext
*AuthWithRoles

View file

@ -251,6 +251,7 @@ func GetCheckerForBuiltinRole(clusterName string, clusterConfig services.Cluster
services.NewRule(services.KindReverseTunnel, services.RW()),
services.NewRule(services.KindTunnelConnection, services.RO()),
services.NewRule(services.KindClusterConfig, services.RO()),
services.NewRule(services.KindSemaphore, services.RW()),
},
},
})
@ -288,6 +289,7 @@ func GetCheckerForBuiltinRole(clusterName string, clusterConfig services.Cluster
services.NewRule(services.KindTunnelConnection, services.RW()),
services.NewRule(services.KindHostCert, services.RW()),
services.NewRule(services.KindRemoteCluster, services.RO()),
services.NewRule(services.KindSemaphore, services.RW()),
// this rule allows local proxy to update the remote cluster's host certificate authorities
// during certificates renewal
{
@ -338,6 +340,7 @@ func GetCheckerForBuiltinRole(clusterName string, clusterConfig services.Cluster
services.NewRule(services.KindStaticTokens, services.RO()),
services.NewRule(services.KindTunnelConnection, services.RW()),
services.NewRule(services.KindRemoteCluster, services.RO()),
services.NewRule(services.KindSemaphore, services.RW()),
// this rule allows local proxy to update the remote cluster's host certificate authorities
// during certificates renewal
{

View file

@ -60,7 +60,7 @@ func (x Operation) String() string {
return proto.EnumName(Operation_name, int32(x))
}
func (Operation) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{0}
return fileDescriptor_auth_df26b5d43b9135f6, []int{0}
}
// Event returns cluster event
@ -93,7 +93,7 @@ func (m *Event) Reset() { *m = Event{} }
func (m *Event) String() string { return proto.CompactTextString(m) }
func (*Event) ProtoMessage() {}
func (*Event) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{0}
return fileDescriptor_auth_df26b5d43b9135f6, []int{0}
}
func (m *Event) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -583,7 +583,7 @@ func (m *Watch) Reset() { *m = Watch{} }
func (m *Watch) String() string { return proto.CompactTextString(m) }
func (*Watch) ProtoMessage() {}
func (*Watch) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{1}
return fileDescriptor_auth_df26b5d43b9135f6, []int{1}
}
func (m *Watch) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -641,7 +641,7 @@ func (m *WatchKind) Reset() { *m = WatchKind{} }
func (m *WatchKind) String() string { return proto.CompactTextString(m) }
func (*WatchKind) ProtoMessage() {}
func (*WatchKind) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{2}
return fileDescriptor_auth_df26b5d43b9135f6, []int{2}
}
func (m *WatchKind) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -713,7 +713,7 @@ func (m *Certs) Reset() { *m = Certs{} }
func (m *Certs) String() string { return proto.CompactTextString(m) }
func (*Certs) ProtoMessage() {}
func (*Certs) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{3}
return fileDescriptor_auth_df26b5d43b9135f6, []int{3}
}
func (m *Certs) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -787,7 +787,7 @@ func (m *UserCertsRequest) Reset() { *m = UserCertsRequest{} }
func (m *UserCertsRequest) String() string { return proto.CompactTextString(m) }
func (*UserCertsRequest) ProtoMessage() {}
func (*UserCertsRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{4}
return fileDescriptor_auth_df26b5d43b9135f6, []int{4}
}
func (m *UserCertsRequest) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -873,7 +873,7 @@ func (m *GetUserRequest) Reset() { *m = GetUserRequest{} }
func (m *GetUserRequest) String() string { return proto.CompactTextString(m) }
func (*GetUserRequest) ProtoMessage() {}
func (*GetUserRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{5}
return fileDescriptor_auth_df26b5d43b9135f6, []int{5}
}
func (m *GetUserRequest) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -929,7 +929,7 @@ func (m *GetUsersRequest) Reset() { *m = GetUsersRequest{} }
func (m *GetUsersRequest) String() string { return proto.CompactTextString(m) }
func (*GetUsersRequest) ProtoMessage() {}
func (*GetUsersRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{6}
return fileDescriptor_auth_df26b5d43b9135f6, []int{6}
}
func (m *GetUsersRequest) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -977,7 +977,7 @@ func (m *AccessRequests) Reset() { *m = AccessRequests{} }
func (m *AccessRequests) String() string { return proto.CompactTextString(m) }
func (*AccessRequests) ProtoMessage() {}
func (*AccessRequests) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{7}
return fileDescriptor_auth_df26b5d43b9135f6, []int{7}
}
func (m *AccessRequests) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -1025,7 +1025,7 @@ func (m *PluginDataSeq) Reset() { *m = PluginDataSeq{} }
func (m *PluginDataSeq) String() string { return proto.CompactTextString(m) }
func (*PluginDataSeq) ProtoMessage() {}
func (*PluginDataSeq) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{8}
return fileDescriptor_auth_df26b5d43b9135f6, []int{8}
}
func (m *PluginDataSeq) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -1081,7 +1081,7 @@ func (m *RequestStateSetter) Reset() { *m = RequestStateSetter{} }
func (m *RequestStateSetter) String() string { return proto.CompactTextString(m) }
func (*RequestStateSetter) ProtoMessage() {}
func (*RequestStateSetter) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{9}
return fileDescriptor_auth_df26b5d43b9135f6, []int{9}
}
func (m *RequestStateSetter) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -1143,7 +1143,7 @@ func (m *RequestID) Reset() { *m = RequestID{} }
func (m *RequestID) String() string { return proto.CompactTextString(m) }
func (*RequestID) ProtoMessage() {}
func (*RequestID) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{10}
return fileDescriptor_auth_df26b5d43b9135f6, []int{10}
}
func (m *RequestID) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -1193,7 +1193,7 @@ func (m *RotateResetPasswordTokenSecretsRequest) Reset() {
func (m *RotateResetPasswordTokenSecretsRequest) String() string { return proto.CompactTextString(m) }
func (*RotateResetPasswordTokenSecretsRequest) ProtoMessage() {}
func (*RotateResetPasswordTokenSecretsRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{11}
return fileDescriptor_auth_df26b5d43b9135f6, []int{11}
}
func (m *RotateResetPasswordTokenSecretsRequest) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -1241,7 +1241,7 @@ func (m *GetResetPasswordTokenRequest) Reset() { *m = GetResetPasswordTo
func (m *GetResetPasswordTokenRequest) String() string { return proto.CompactTextString(m) }
func (*GetResetPasswordTokenRequest) ProtoMessage() {}
func (*GetResetPasswordTokenRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{12}
return fileDescriptor_auth_df26b5d43b9135f6, []int{12}
}
func (m *GetResetPasswordTokenRequest) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -1295,7 +1295,7 @@ func (m *CreateResetPasswordTokenRequest) Reset() { *m = CreateResetPass
func (m *CreateResetPasswordTokenRequest) String() string { return proto.CompactTextString(m) }
func (*CreateResetPasswordTokenRequest) ProtoMessage() {}
func (*CreateResetPasswordTokenRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{13}
return fileDescriptor_auth_df26b5d43b9135f6, []int{13}
}
func (m *CreateResetPasswordTokenRequest) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -1356,7 +1356,7 @@ func (m *PingRequest) Reset() { *m = PingRequest{} }
func (m *PingRequest) String() string { return proto.CompactTextString(m) }
func (*PingRequest) ProtoMessage() {}
func (*PingRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{14}
return fileDescriptor_auth_df26b5d43b9135f6, []int{14}
}
func (m *PingRequest) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -1400,7 +1400,7 @@ func (m *PingResponse) Reset() { *m = PingResponse{} }
func (m *PingResponse) String() string { return proto.CompactTextString(m) }
func (*PingResponse) ProtoMessage() {}
func (*PingResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{15}
return fileDescriptor_auth_df26b5d43b9135f6, []int{15}
}
func (m *PingResponse) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -1456,7 +1456,7 @@ func (m *DeleteUserRequest) Reset() { *m = DeleteUserRequest{} }
func (m *DeleteUserRequest) String() string { return proto.CompactTextString(m) }
func (*DeleteUserRequest) ProtoMessage() {}
func (*DeleteUserRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_cc9d777e75d42d1b, []int{16}
return fileDescriptor_auth_df26b5d43b9135f6, []int{16}
}
func (m *DeleteUserRequest) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -1492,6 +1492,54 @@ func (m *DeleteUserRequest) GetName() string {
return ""
}
// Semaphores is a sequence of Semaphore resources.
type Semaphores struct {
Semaphores []*services.SemaphoreV3 `protobuf:"bytes,1,rep,name=Semaphores" json:"semaphores"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Semaphores) Reset() { *m = Semaphores{} }
func (m *Semaphores) String() string { return proto.CompactTextString(m) }
func (*Semaphores) ProtoMessage() {}
func (*Semaphores) Descriptor() ([]byte, []int) {
return fileDescriptor_auth_df26b5d43b9135f6, []int{17}
}
func (m *Semaphores) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *Semaphores) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_Semaphores.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalTo(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (dst *Semaphores) XXX_Merge(src proto.Message) {
xxx_messageInfo_Semaphores.Merge(dst, src)
}
func (m *Semaphores) XXX_Size() int {
return m.Size()
}
func (m *Semaphores) XXX_DiscardUnknown() {
xxx_messageInfo_Semaphores.DiscardUnknown(m)
}
var xxx_messageInfo_Semaphores proto.InternalMessageInfo
func (m *Semaphores) GetSemaphores() []*services.SemaphoreV3 {
if m != nil {
return m.Semaphores
}
return nil
}
func init() {
proto.RegisterType((*Event)(nil), "proto.Event")
proto.RegisterType((*Watch)(nil), "proto.Watch")
@ -1511,6 +1559,7 @@ func init() {
proto.RegisterType((*PingRequest)(nil), "proto.PingRequest")
proto.RegisterType((*PingResponse)(nil), "proto.PingResponse")
proto.RegisterType((*DeleteUserRequest)(nil), "proto.DeleteUserRequest")
proto.RegisterType((*Semaphores)(nil), "proto.Semaphores")
proto.RegisterEnum("proto.Operation", Operation_name, Operation_value)
}
@ -1564,8 +1613,18 @@ type AuthServiceClient interface {
CreateUser(ctx context.Context, in *services.UserV2, opts ...grpc.CallOption) (*empty.Empty, error)
// UpdateUser updates an existing user in a backend.
UpdateUser(ctx context.Context, in *services.UserV2, opts ...grpc.CallOption) (*empty.Empty, error)
// DeleteUser deletes an exisitng user in a backend by username.
// DeleteUser deletes an existing user in a backend by username.
DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*empty.Empty, error)
// AcquireSemaphore acquires lease with requested resources from semaphore.
AcquireSemaphore(ctx context.Context, in *services.AcquireSemaphoreRequest, opts ...grpc.CallOption) (*services.SemaphoreLease, error)
// KeepAliveSemaphoreLease updates semaphore lease.
KeepAliveSemaphoreLease(ctx context.Context, in *services.SemaphoreLease, opts ...grpc.CallOption) (*empty.Empty, error)
// CancelSemaphoreLease cancels semaphore lease early.
CancelSemaphoreLease(ctx context.Context, in *services.SemaphoreLease, opts ...grpc.CallOption) (*empty.Empty, error)
// GetSemaphores returns a list of all semaphores matching the supplied filter.
GetSemaphores(ctx context.Context, in *services.SemaphoreFilter, opts ...grpc.CallOption) (*Semaphores, error)
// DeleteSemaphore deletes a semaphore matching the supplied filter.
DeleteSemaphore(ctx context.Context, in *services.SemaphoreFilter, opts ...grpc.CallOption) (*empty.Empty, error)
}
type authServiceClient struct {
@ -1818,6 +1877,51 @@ func (c *authServiceClient) DeleteUser(ctx context.Context, in *DeleteUserReques
return out, nil
}
func (c *authServiceClient) AcquireSemaphore(ctx context.Context, in *services.AcquireSemaphoreRequest, opts ...grpc.CallOption) (*services.SemaphoreLease, error) {
out := new(services.SemaphoreLease)
err := c.cc.Invoke(ctx, "/proto.AuthService/AcquireSemaphore", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) KeepAliveSemaphoreLease(ctx context.Context, in *services.SemaphoreLease, opts ...grpc.CallOption) (*empty.Empty, error) {
out := new(empty.Empty)
err := c.cc.Invoke(ctx, "/proto.AuthService/KeepAliveSemaphoreLease", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) CancelSemaphoreLease(ctx context.Context, in *services.SemaphoreLease, opts ...grpc.CallOption) (*empty.Empty, error) {
out := new(empty.Empty)
err := c.cc.Invoke(ctx, "/proto.AuthService/CancelSemaphoreLease", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) GetSemaphores(ctx context.Context, in *services.SemaphoreFilter, opts ...grpc.CallOption) (*Semaphores, error) {
out := new(Semaphores)
err := c.cc.Invoke(ctx, "/proto.AuthService/GetSemaphores", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) DeleteSemaphore(ctx context.Context, in *services.SemaphoreFilter, opts ...grpc.CallOption) (*empty.Empty, error) {
out := new(empty.Empty)
err := c.cc.Invoke(ctx, "/proto.AuthService/DeleteSemaphore", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for AuthService service
type AuthServiceServer interface {
@ -1860,8 +1964,18 @@ type AuthServiceServer interface {
CreateUser(context.Context, *services.UserV2) (*empty.Empty, error)
// UpdateUser updates an existing user in a backend.
UpdateUser(context.Context, *services.UserV2) (*empty.Empty, error)
// DeleteUser deletes an exisitng user in a backend by username.
// DeleteUser deletes an existing user in a backend by username.
DeleteUser(context.Context, *DeleteUserRequest) (*empty.Empty, error)
// AcquireSemaphore acquires lease with requested resources from semaphore.
AcquireSemaphore(context.Context, *services.AcquireSemaphoreRequest) (*services.SemaphoreLease, error)
// KeepAliveSemaphoreLease updates semaphore lease.
KeepAliveSemaphoreLease(context.Context, *services.SemaphoreLease) (*empty.Empty, error)
// CancelSemaphoreLease cancels semaphore lease early.
CancelSemaphoreLease(context.Context, *services.SemaphoreLease) (*empty.Empty, error)
// GetSemaphores returns a list of all semaphores matching the supplied filter.
GetSemaphores(context.Context, *services.SemaphoreFilter) (*Semaphores, error)
// DeleteSemaphore deletes a semaphore matching the supplied filter.
DeleteSemaphore(context.Context, *services.SemaphoreFilter) (*empty.Empty, error)
}
func RegisterAuthServiceServer(s *grpc.Server, srv AuthServiceServer) {
@ -2224,6 +2338,96 @@ func _AuthService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
func _AuthService_AcquireSemaphore_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(services.AcquireSemaphoreRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).AcquireSemaphore(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.AuthService/AcquireSemaphore",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).AcquireSemaphore(ctx, req.(*services.AcquireSemaphoreRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_KeepAliveSemaphoreLease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(services.SemaphoreLease)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).KeepAliveSemaphoreLease(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.AuthService/KeepAliveSemaphoreLease",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).KeepAliveSemaphoreLease(ctx, req.(*services.SemaphoreLease))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_CancelSemaphoreLease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(services.SemaphoreLease)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).CancelSemaphoreLease(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.AuthService/CancelSemaphoreLease",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).CancelSemaphoreLease(ctx, req.(*services.SemaphoreLease))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_GetSemaphores_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(services.SemaphoreFilter)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).GetSemaphores(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.AuthService/GetSemaphores",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).GetSemaphores(ctx, req.(*services.SemaphoreFilter))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_DeleteSemaphore_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(services.SemaphoreFilter)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).DeleteSemaphore(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.AuthService/DeleteSemaphore",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).DeleteSemaphore(ctx, req.(*services.SemaphoreFilter))
}
return interceptor(ctx, in, info, handler)
}
var _AuthService_serviceDesc = grpc.ServiceDesc{
ServiceName: "proto.AuthService",
HandlerType: (*AuthServiceServer)(nil),
@ -2292,6 +2496,26 @@ var _AuthService_serviceDesc = grpc.ServiceDesc{
MethodName: "DeleteUser",
Handler: _AuthService_DeleteUser_Handler,
},
{
MethodName: "AcquireSemaphore",
Handler: _AuthService_AcquireSemaphore_Handler,
},
{
MethodName: "KeepAliveSemaphoreLease",
Handler: _AuthService_KeepAliveSemaphoreLease_Handler,
},
{
MethodName: "CancelSemaphoreLease",
Handler: _AuthService_CancelSemaphoreLease_Handler,
},
{
MethodName: "GetSemaphores",
Handler: _AuthService_GetSemaphores_Handler,
},
{
MethodName: "DeleteSemaphore",
Handler: _AuthService_DeleteSemaphore_Handler,
},
},
Streams: []grpc.StreamDesc{
{
@ -3094,6 +3318,39 @@ func (m *DeleteUserRequest) MarshalTo(dAtA []byte) (int, error) {
return i, nil
}
func (m *Semaphores) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *Semaphores) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.Semaphores) > 0 {
for _, msg := range m.Semaphores {
dAtA[i] = 0xa
i++
i = encodeVarintAuth(dAtA, i, uint64(msg.Size()))
n, err := msg.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n
}
}
if m.XXX_unrecognized != nil {
i += copy(dAtA[i:], m.XXX_unrecognized)
}
return i, nil
}
func encodeVarintAuth(dAtA []byte, offset int, v uint64) int {
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
@ -3504,6 +3761,21 @@ func (m *DeleteUserRequest) Size() (n int) {
return n
}
func (m *Semaphores) Size() (n int) {
var l int
_ = l
if len(m.Semaphores) > 0 {
for _, e := range m.Semaphores {
l = e.Size()
n += 1 + l + sovAuth(uint64(l))
}
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
return n
}
func sovAuth(x uint64) (n int) {
for {
n++
@ -5744,6 +6016,88 @@ func (m *DeleteUserRequest) Unmarshal(dAtA []byte) error {
}
return nil
}
func (m *Semaphores) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAuth
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: Semaphores: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: Semaphores: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Semaphores", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAuth
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthAuth
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Semaphores = append(m.Semaphores, &services.SemaphoreV3{})
if err := m.Semaphores[len(m.Semaphores)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipAuth(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthAuth
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipAuth(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
@ -5849,118 +6203,125 @@ var (
ErrIntOverflowAuth = fmt.Errorf("proto: integer overflow")
)
func init() { proto.RegisterFile("auth.proto", fileDescriptor_auth_cc9d777e75d42d1b) }
func init() { proto.RegisterFile("auth.proto", fileDescriptor_auth_df26b5d43b9135f6) }
var fileDescriptor_auth_cc9d777e75d42d1b = []byte{
// 1758 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x57, 0xdd, 0x6e, 0xdb, 0xc8,
0x15, 0x0e, 0x2d, 0xcb, 0x96, 0x8e, 0x6c, 0x45, 0x19, 0x3b, 0x36, 0xa3, 0x38, 0xa6, 0xa1, 0x60,
0x17, 0xc6, 0x36, 0x95, 0xb6, 0x32, 0x36, 0x4d, 0x83, 0xa2, 0x41, 0x68, 0x2b, 0xb6, 0x37, 0x6e,
0xea, 0x52, 0x8a, 0x52, 0xb4, 0x05, 0x04, 0x5a, 0x9a, 0xc8, 0x84, 0x29, 0x52, 0xcb, 0x19, 0x29,
0x35, 0xd0, 0xab, 0xa2, 0x0f, 0xd0, 0xcb, 0x5e, 0xf4, 0x05, 0xfa, 0x14, 0xbd, 0xcd, 0x65, 0x9f,
0x80, 0x6d, 0xd3, 0x3b, 0x3e, 0x42, 0xd1, 0x8b, 0xc5, 0xfc, 0x50, 0xe4, 0x50, 0x56, 0x36, 0xb9,
0x12, 0xe7, 0xfc, 0x7c, 0xe7, 0xcc, 0x99, 0x33, 0xdf, 0x1c, 0x01, 0xd8, 0x13, 0x7a, 0x59, 0x1f,
0x07, 0x3e, 0xf5, 0x51, 0x9e, 0xff, 0x54, 0x9f, 0x0e, 0x1d, 0x7a, 0x39, 0xb9, 0xa8, 0xf7, 0xfd,
0x51, 0x63, 0x18, 0xd8, 0x53, 0x87, 0xda, 0xd4, 0xf1, 0x3d, 0xdb, 0x6d, 0x50, 0xec, 0xe2, 0xb1,
0x1f, 0xd0, 0x86, 0xeb, 0x5c, 0x34, 0x08, 0x0e, 0xa6, 0x4e, 0x1f, 0x93, 0x06, 0xbd, 0x1e, 0x63,
0x22, 0x20, 0xaa, 0x9b, 0x43, 0x7f, 0xe8, 0xf3, 0xcf, 0x06, 0xfb, 0x92, 0xd2, 0xfb, 0x43, 0xdf,
0x1f, 0xba, 0xb8, 0xc1, 0x57, 0x17, 0x93, 0xb7, 0x0d, 0x3c, 0x1a, 0xd3, 0x6b, 0xa9, 0x34, 0xb2,
0x4a, 0xea, 0x8c, 0x30, 0xa1, 0xf6, 0x68, 0x2c, 0x0c, 0x6a, 0xff, 0x28, 0x42, 0xbe, 0x35, 0xc5,
0x1e, 0x45, 0x4f, 0x60, 0xb9, 0x73, 0x3d, 0xc6, 0xba, 0xb6, 0xa7, 0xed, 0x97, 0x9b, 0x15, 0xa1,
0xaf, 0xff, 0x6a, 0x8c, 0x03, 0x9e, 0xa1, 0x89, 0xa2, 0xd0, 0x28, 0xb3, 0x74, 0x1e, 0xf9, 0x23,
0x87, 0xf2, 0x20, 0x16, 0xf7, 0x40, 0xbf, 0x85, 0xb2, 0x85, 0x89, 0x3f, 0x09, 0xfa, 0xf8, 0x04,
0xdb, 0x03, 0x1c, 0xe8, 0x4b, 0x7b, 0xda, 0x7e, 0xa9, 0xa9, 0xd7, 0xe3, 0x6d, 0xd4, 0x55, 0xbd,
0xb9, 0x15, 0x85, 0x06, 0x0a, 0xa4, 0x2c, 0xc1, 0x3b, 0xb9, 0x65, 0x65, 0x90, 0x50, 0x0f, 0xd6,
0x0f, 0x71, 0x40, 0x9f, 0x4f, 0xe8, 0xa5, 0x1f, 0x38, 0xf4, 0x5a, 0xcf, 0x71, 0xe8, 0x7b, 0x09,
0xb4, 0xa2, 0xee, 0x36, 0xcd, 0x9d, 0x28, 0x34, 0xf4, 0x3e, 0x0e, 0x68, 0xcf, 0x8e, 0xa5, 0x4a,
0x04, 0x15, 0x0f, 0xfd, 0x0e, 0xd6, 0xda, 0xec, 0x0c, 0xfa, 0x1d, 0xff, 0x0a, 0x7b, 0x44, 0x5f,
0xce, 0xa6, 0x9e, 0xd6, 0x76, 0x9b, 0xe6, 0xfd, 0x28, 0x34, 0xb6, 0x09, 0x97, 0xf5, 0x28, 0x17,
0x2a, 0xe8, 0x0a, 0x18, 0xea, 0x43, 0xf9, 0x3c, 0xf0, 0xa7, 0x0e, 0x71, 0x7c, 0x8f, 0x8b, 0xf4,
0x3c, 0x87, 0xaf, 0x26, 0xf0, 0xaa, 0xbe, 0xdb, 0x34, 0x1f, 0x44, 0xa1, 0x71, 0x6f, 0x1c, 0x4b,
0x45, 0x0c, 0xb5, 0x44, 0xaa, 0x0b, 0x7a, 0x03, 0xa5, 0x43, 0x77, 0x42, 0x28, 0x0e, 0x5e, 0xd9,
0x23, 0xac, 0xaf, 0xf0, 0x08, 0xdb, 0xa9, 0x02, 0x25, 0xca, 0x6e, 0xd3, 0xac, 0x46, 0xa1, 0xb1,
0xd5, 0x17, 0xa2, 0x9e, 0x67, 0x8f, 0xd4, 0xf2, 0xa7, 0x91, 0x78, 0xed, 0xc5, 0xf2, 0xd0, 0xf7,
0xde, 0x3a, 0x43, 0x7d, 0x75, 0xae, 0xf6, 0x69, 0x75, 0xf7, 0x40, 0xd6, 0x5e, 0x82, 0xf7, 0xb9,
0x34, 0x53, 0xfb, 0xb4, 0x03, 0x7a, 0x0a, 0xcb, 0xaf, 0x09, 0x0e, 0xf4, 0x02, 0xc7, 0xad, 0x24,
0xb8, 0x4c, 0xda, 0x6d, 0x8a, 0x96, 0x9b, 0x10, 0x1c, 0x28, 0x20, 0xdc, 0x87, 0xf9, 0x5a, 0xbe,
0x8b, 0xf5, 0x62, 0xd6, 0x97, 0x49, 0xbb, 0x07, 0xc2, 0x37, 0xf0, 0x5d, 0x75, 0x7f, 0xdc, 0x07,
0x9d, 0x41, 0x91, 0x6d, 0x90, 0x8c, 0xed, 0x3e, 0xd6, 0x81, 0x03, 0x6c, 0x24, 0x00, 0x33, 0x95,
0xb9, 0x1d, 0x85, 0xc6, 0x86, 0x17, 0x2f, 0x15, 0xa0, 0x04, 0x00, 0x99, 0xb0, 0xd2, 0xc6, 0xc1,
0x14, 0x07, 0x7a, 0x89, 0x43, 0xa1, 0x54, 0xef, 0x70, 0x79, 0xb7, 0x69, 0x6e, 0x46, 0xa1, 0x51,
0x21, 0x7c, 0xa5, 0xc0, 0x48, 0x4f, 0x56, 0x6a, 0x0b, 0x4f, 0x71, 0x40, 0x70, 0x67, 0xe2, 0x79,
0xd8, 0xd5, 0xd7, 0xb2, 0xa5, 0x56, 0xd4, 0x71, 0x9b, 0x07, 0x42, 0xd8, 0xa3, 0x5c, 0xaa, 0x96,
0x5a, 0x71, 0x40, 0x57, 0x50, 0x11, 0x5f, 0x87, 0xbe, 0xe7, 0xe1, 0x3e, 0xbb, 0xd1, 0xfa, 0x3a,
0x8f, 0xb1, 0x93, 0xc4, 0xc8, 0x5a, 0x74, 0x9b, 0xa6, 0x11, 0x85, 0xc6, 0x7d, 0x01, 0xcf, 0x0e,
0x54, 0x2a, 0x94, 0x48, 0x73, 0xc0, 0x6c, 0x37, 0xcf, 0xfb, 0x7d, 0x4c, 0x88, 0x85, 0xbf, 0x9b,
0x60, 0x42, 0xf5, 0x72, 0x76, 0x37, 0x8a, 0x3a, 0x6e, 0x1c, 0x9b, 0x0b, 0x7b, 0x81, 0x90, 0xaa,
0xbb, 0x51, 0x1c, 0x4c, 0x80, 0x42, 0xcc, 0x13, 0xb5, 0x13, 0xc8, 0xbf, 0xb1, 0x69, 0xff, 0x12,
0x3d, 0x83, 0xfc, 0x4b, 0xc7, 0x1b, 0x10, 0x5d, 0xdb, 0xcb, 0xf1, 0x96, 0x10, 0x0c, 0xc6, 0x95,
0x4c, 0x61, 0x6e, 0xbf, 0x0f, 0x8d, 0x5b, 0x51, 0x68, 0xdc, 0xbe, 0x62, 0x66, 0x29, 0x1a, 0x13,
0x7e, 0xb5, 0x3f, 0x2d, 0x41, 0x71, 0x66, 0x8d, 0x76, 0x60, 0x99, 0xfd, 0x72, 0x3e, 0x2c, 0x9a,
0x85, 0x28, 0x34, 0x96, 0x99, 0x9f, 0xc5, 0xa5, 0xa8, 0x09, 0xa5, 0x33, 0xdf, 0x1e, 0xb4, 0x71,
0x3f, 0xc0, 0x94, 0x70, 0xc2, 0x2b, 0x98, 0x95, 0x28, 0x34, 0xd6, 0x5c, 0xdf, 0x1e, 0xf4, 0x88,
0x90, 0x5b, 0x69, 0x23, 0x86, 0xc8, 0x6f, 0x68, 0x2e, 0x41, 0x64, 0xcd, 0x65, 0x71, 0x29, 0xfa,
0x16, 0x56, 0x5e, 0x38, 0x2e, 0xc5, 0x81, 0xbe, 0xcc, 0xf3, 0xdf, 0xc9, 0xe6, 0x5f, 0x17, 0xea,
0x96, 0x47, 0x83, 0x6b, 0xd1, 0x50, 0x6f, 0xb9, 0x20, 0xb5, 0x11, 0x89, 0x50, 0xfd, 0x19, 0x94,
0x52, 0xc6, 0xa8, 0x02, 0xb9, 0x2b, 0x7c, 0x2d, 0x76, 0x62, 0xb1, 0x4f, 0xb4, 0x09, 0xf9, 0xa9,
0xed, 0x4e, 0x30, 0x4f, 0xbc, 0x68, 0x89, 0xc5, 0xd3, 0xa5, 0x27, 0x5a, 0xed, 0xd7, 0x90, 0x67,
0x04, 0x49, 0xd0, 0x43, 0xc8, 0xb5, 0xdb, 0x27, 0xdc, 0x69, 0xcd, 0xbc, 0x13, 0x85, 0xc6, 0x3a,
0x21, 0x97, 0xa9, 0x58, 0x4c, 0xcb, 0x8c, 0x3a, 0x67, 0x6d, 0x8e, 0x22, 0x8d, 0xa8, 0x9b, 0xae,
0x2c, 0xd3, 0xd6, 0xfe, 0xbf, 0x04, 0x15, 0x76, 0x67, 0x39, 0xae, 0x3c, 0x42, 0xf4, 0x08, 0x8a,
0xe7, 0x93, 0x0b, 0xd7, 0xe9, 0xbf, 0x94, 0x99, 0xad, 0x99, 0xe5, 0x28, 0x34, 0x60, 0xcc, 0x85,
0xbd, 0x2b, 0x7c, 0x6d, 0x25, 0x06, 0x68, 0x1f, 0x0a, 0x0c, 0x81, 0x95, 0x4b, 0xa4, 0x6c, 0xae,
0x45, 0xa1, 0x51, 0x98, 0x48, 0x99, 0x35, 0xd3, 0xa2, 0x36, 0xac, 0xb6, 0xfe, 0x30, 0x76, 0x02,
0x4c, 0xe4, 0x53, 0x51, 0xad, 0x8b, 0x37, 0xb0, 0x1e, 0xbf, 0x81, 0xf5, 0x4e, 0xfc, 0x06, 0x9a,
0x0f, 0x64, 0x47, 0xdc, 0xc1, 0xc2, 0x25, 0xc9, 0xfc, 0x2f, 0xff, 0x32, 0x34, 0x2b, 0x46, 0x42,
0x8f, 0x60, 0xe5, 0x85, 0x1f, 0x8c, 0x6c, 0xca, 0x9f, 0x87, 0xa2, 0xac, 0x3e, 0x97, 0x28, 0xd5,
0xe7, 0x12, 0xf4, 0x02, 0xca, 0x96, 0x3f, 0xa1, 0xb8, 0xe3, 0x4b, 0xba, 0xe3, 0xac, 0x5f, 0x34,
0x77, 0xa3, 0xd0, 0xa8, 0x06, 0x4c, 0xd3, 0xa3, 0x7e, 0x4f, 0xd2, 0x64, 0xca, 0x3f, 0xe3, 0x85,
0x5a, 0x50, 0x56, 0xda, 0x9e, 0xe8, 0x2b, 0x7b, 0xb9, 0xfd, 0xa2, 0x78, 0x21, 0xd4, 0xcb, 0x92,
0xae, 0x79, 0xc6, 0xa9, 0xe6, 0x42, 0xf9, 0x18, 0x53, 0x56, 0xa0, 0xb8, 0xf6, 0x71, 0x23, 0x6a,
0x37, 0x36, 0xe2, 0xcf, 0xa1, 0xf4, 0xc6, 0xa1, 0x97, 0x6a, 0x6b, 0xf3, 0x67, 0xe3, 0x9d, 0x43,
0x2f, 0xe3, 0xd6, 0x4e, 0x05, 0x4c, 0x9b, 0xd7, 0x5a, 0x70, 0x5b, 0x46, 0x9b, 0x1d, 0x75, 0x53,
0x05, 0xd4, 0x92, 0xbb, 0x92, 0x06, 0x54, 0x61, 0x2e, 0xb3, 0x7b, 0x47, 0xdd, 0xb9, 0x6a, 0x88,
0x7b, 0xfe, 0x11, 0x56, 0xd9, 0x60, 0x97, 0x3d, 0x53, 0xa8, 0xb9, 0xf2, 0xfc, 0x06, 0xd6, 0xcf,
0xdd, 0xc9, 0xd0, 0xf1, 0x8e, 0x6c, 0x6a, 0xb7, 0xf1, 0x77, 0xe8, 0x18, 0x20, 0x11, 0xc8, 0x20,
0x5b, 0xa9, 0x07, 0x7b, 0xa6, 0xeb, 0x1e, 0x98, 0xb7, 0xa3, 0xd0, 0x28, 0x8d, 0xb9, 0xa4, 0x37,
0xb0, 0xa9, 0x6d, 0xa5, 0x5c, 0x6b, 0x7f, 0xd3, 0x00, 0xc9, 0x30, 0x6c, 0x2a, 0xc0, 0x6d, 0x4c,
0xd9, 0xb1, 0x6e, 0xc1, 0xd2, 0xe9, 0x91, 0xac, 0xfd, 0x4a, 0x14, 0x1a, 0x4b, 0xce, 0xc0, 0x5a,
0x3a, 0x3d, 0x42, 0x3f, 0x85, 0x3c, 0x37, 0xe3, 0x15, 0x2f, 0xa7, 0x43, 0xa6, 0x41, 0xcc, 0x62,
0x14, 0x1a, 0x79, 0x36, 0x80, 0x60, 0x4b, 0xd8, 0xa3, 0x6f, 0xa0, 0x78, 0x84, 0x5d, 0x3c, 0xb4,
0xa9, 0x1f, 0x48, 0x72, 0xe1, 0x2f, 0xd7, 0x20, 0x16, 0xa6, 0xce, 0x2a, 0xb1, 0xac, 0x3d, 0x84,
0xa2, 0x04, 0x3e, 0x3d, 0x5a, 0x94, 0x54, 0xed, 0x97, 0xf0, 0xa5, 0xe5, 0xf3, 0x60, 0x98, 0x60,
0x7a, 0x6e, 0x13, 0xf2, 0xce, 0x0f, 0x06, 0x7c, 0xf0, 0x90, 0x47, 0x15, 0x9f, 0xf2, 0x43, 0x58,
0xe5, 0xe2, 0x19, 0x0c, 0x4f, 0x94, 0x8f, 0x2f, 0x56, 0xac, 0xa9, 0x1d, 0xc2, 0xce, 0x31, 0xa6,
0xf3, 0x58, 0x9f, 0x05, 0xf2, 0x67, 0x0d, 0x8c, 0xc3, 0x00, 0xdf, 0x98, 0xd4, 0xa7, 0xb5, 0xf8,
0x8e, 0x9c, 0x75, 0x97, 0x12, 0x2d, 0x9b, 0x6c, 0xe5, 0x3c, 0xfb, 0x05, 0xe4, 0x3a, 0x9d, 0x33,
0x5e, 0xc9, 0x1c, 0xef, 0xa1, 0x1c, 0xa5, 0xee, 0xff, 0x42, 0xa3, 0x70, 0x34, 0x11, 0xb3, 0xb0,
0xc5, 0xf4, 0xb5, 0x75, 0x28, 0x9d, 0x3b, 0xde, 0x50, 0x46, 0xac, 0xfd, 0x11, 0xd6, 0xc4, 0x92,
0x8c, 0x7d, 0x8f, 0x60, 0xd6, 0xf5, 0xe9, 0xb1, 0x4c, 0x24, 0xc2, 0xbb, 0x3e, 0x3d, 0x7d, 0xa9,
0x13, 0xd7, 0x13, 0x58, 0x97, 0x23, 0x03, 0x0e, 0xd8, 0x80, 0x27, 0x13, 0xe4, 0xb3, 0x8c, 0x98,
0x1e, 0x7a, 0x53, 0xa1, 0xb1, 0x54, 0xc3, 0xda, 0x4f, 0xe0, 0x0e, 0x3b, 0x59, 0x8a, 0x3f, 0xf9,
0x9e, 0x7f, 0xf5, 0x15, 0x14, 0x67, 0xd3, 0x3d, 0x2a, 0xc0, 0xf2, 0xe9, 0xab, 0xd3, 0x4e, 0xe5,
0x16, 0x5a, 0x85, 0xdc, 0xf9, 0xeb, 0x4e, 0x45, 0x43, 0x00, 0x2b, 0x47, 0xad, 0xb3, 0x56, 0xa7,
0x55, 0x59, 0x6a, 0xfe, 0x1d, 0xa0, 0xc4, 0x66, 0xe6, 0xb6, 0x68, 0x49, 0xf4, 0x0c, 0xca, 0x6d,
0xec, 0x0d, 0x5e, 0x62, 0x3c, 0x7e, 0xee, 0x3a, 0x53, 0x4c, 0x50, 0x6a, 0x80, 0x9a, 0x49, 0xab,
0x5b, 0x73, 0xdc, 0xdb, 0x62, 0x5d, 0xb8, 0xaf, 0xa1, 0x1f, 0x41, 0x89, 0x3f, 0x6c, 0xfc, 0xbf,
0x07, 0x41, 0x6b, 0xe9, 0xc7, 0xae, 0x1a, 0xaf, 0xb8, 0xf2, 0x6b, 0x0d, 0x7d, 0x03, 0xf0, 0x7a,
0x4c, 0x70, 0x40, 0x5f, 0xf9, 0x03, 0x8c, 0x6e, 0x98, 0xaf, 0xaa, 0x37, 0x45, 0x47, 0x4f, 0xe1,
0xce, 0x31, 0xf6, 0xd8, 0x0e, 0xf1, 0xec, 0xf9, 0x41, 0xdb, 0x12, 0x3b, 0xfb, 0x20, 0xcd, 0x82,
0x0a, 0xb3, 0x26, 0xac, 0x4a, 0x1a, 0x43, 0x77, 0xa5, 0x42, 0x25, 0xd1, 0xea, 0xdc, 0xb8, 0x8a,
0x1e, 0x43, 0x21, 0xa6, 0x3e, 0xb4, 0xa5, 0x3a, 0x91, 0x85, 0x5e, 0x5f, 0x6b, 0xe8, 0x94, 0xe5,
0x49, 0x33, 0x74, 0xf7, 0x60, 0x01, 0xad, 0xc9, 0x17, 0x3e, 0x4e, 0x2a, 0xe3, 0x75, 0x02, 0x1b,
0xe2, 0x66, 0x28, 0x72, 0xb4, 0x98, 0x23, 0x17, 0x1d, 0x11, 0x7a, 0x06, 0x1b, 0xa2, 0xa1, 0x54,
0xa4, 0x78, 0xaa, 0x9a, 0x31, 0xc7, 0x42, 0x80, 0x6f, 0xe1, 0x6e, 0x3b, 0xb3, 0x2b, 0x41, 0x57,
0xf7, 0x54, 0x88, 0x14, 0x35, 0x2e, 0xc4, 0x7a, 0x0e, 0xeb, 0xc7, 0x98, 0x26, 0xd4, 0x8a, 0xaa,
0x37, 0xf1, 0xb1, 0x2c, 0xcd, 0xa6, 0xc4, 0x57, 0x59, 0xfd, 0x0c, 0x2a, 0xaf, 0xc7, 0x03, 0x9b,
0xe2, 0x14, 0xca, 0xde, 0x4d, 0x28, 0xd2, 0xca, 0x0e, 0xec, 0x11, 0x59, 0x98, 0x50, 0x03, 0x96,
0xd9, 0x65, 0x47, 0x28, 0x8e, 0x95, 0x10, 0x41, 0x75, 0x43, 0x91, 0x49, 0x36, 0x78, 0x07, 0xc6,
0x0f, 0xf0, 0x28, 0xfa, 0x71, 0x5c, 0x97, 0x4f, 0xe2, 0xdb, 0xea, 0x17, 0xca, 0xbf, 0xeb, 0x9b,
0x6d, 0xbb, 0x07, 0xe8, 0xf7, 0x70, 0xf7, 0x46, 0xc6, 0x45, 0x0f, 0x93, 0x0e, 0x5d, 0x48, 0xa3,
0xd5, 0xdd, 0x8f, 0x05, 0xe9, 0x1e, 0xa0, 0x0b, 0xd0, 0x17, 0x31, 0x31, 0xfa, 0x32, 0xbe, 0x50,
0x1f, 0xa7, 0xea, 0x1f, 0x8c, 0xf1, 0x18, 0x40, 0x40, 0xf0, 0xdb, 0x38, 0x77, 0x81, 0x16, 0x9e,
0xd1, 0x63, 0xc6, 0x1a, 0x83, 0xcf, 0xf7, 0xfb, 0x05, 0x40, 0x42, 0xa5, 0x48, 0x97, 0xbb, 0x98,
0x63, 0xd7, 0x45, 0xfe, 0xe6, 0xe6, 0xfb, 0xff, 0xec, 0x6a, 0xef, 0x3f, 0xec, 0x6a, 0xff, 0xfc,
0xb0, 0xab, 0xfd, 0xfb, 0xc3, 0xae, 0xf6, 0xd7, 0xff, 0xee, 0xde, 0xba, 0x58, 0xe1, 0x56, 0x07,
0xdf, 0x07, 0x00, 0x00, 0xff, 0xff, 0x76, 0xfd, 0xe5, 0x52, 0x14, 0x12, 0x00, 0x00,
var fileDescriptor_auth_df26b5d43b9135f6 = []byte{
// 1865 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x57, 0x4f, 0x6f, 0xdb, 0xc8,
0x15, 0x8f, 0x2c, 0xdb, 0xb1, 0x9e, 0x6c, 0x45, 0x1e, 0x3b, 0x36, 0xa3, 0x38, 0xa6, 0xab, 0x60,
0x17, 0xc6, 0x36, 0xb5, 0xb7, 0x32, 0x36, 0x4d, 0x83, 0xb6, 0x81, 0x69, 0x2b, 0xb6, 0x37, 0x6e,
0xd6, 0xa5, 0x14, 0xa5, 0x68, 0x0b, 0x08, 0xb4, 0xf4, 0x22, 0x13, 0xa6, 0x48, 0x85, 0x33, 0x72,
0x6a, 0xa0, 0xa7, 0xa2, 0x1f, 0xa0, 0xa7, 0xa2, 0x87, 0x7e, 0x97, 0x5e, 0x73, 0xec, 0x27, 0x60,
0xdb, 0xf4, 0xc6, 0x8f, 0x50, 0xf4, 0x50, 0xcc, 0x1f, 0x8a, 0x1c, 0x59, 0xca, 0x66, 0xb1, 0x27,
0x71, 0xde, 0x9f, 0xdf, 0x7b, 0xf3, 0xe6, 0xcd, 0x6f, 0x9e, 0x00, 0x9c, 0x21, 0xbb, 0xd8, 0x19,
0x84, 0x01, 0x0b, 0xc8, 0x9c, 0xf8, 0xa9, 0x3c, 0xed, 0xb9, 0xec, 0x62, 0x78, 0xbe, 0xd3, 0x09,
0xfa, 0xbb, 0xbd, 0xd0, 0xb9, 0x72, 0x99, 0xc3, 0xdc, 0xc0, 0x77, 0xbc, 0x5d, 0x86, 0x1e, 0x0e,
0x82, 0x90, 0xed, 0x7a, 0xee, 0xf9, 0x2e, 0xc5, 0xf0, 0xca, 0xed, 0x20, 0xdd, 0x65, 0xd7, 0x03,
0xa4, 0x12, 0xa2, 0xb2, 0xda, 0x0b, 0x7a, 0x81, 0xf8, 0xdc, 0xe5, 0x5f, 0x4a, 0x7a, 0xbf, 0x17,
0x04, 0x3d, 0x0f, 0x77, 0xc5, 0xea, 0x7c, 0xf8, 0x66, 0x17, 0xfb, 0x03, 0x76, 0xad, 0x94, 0xe6,
0xb8, 0x92, 0xb9, 0x7d, 0xa4, 0xcc, 0xe9, 0x0f, 0xa4, 0x41, 0xf5, 0xef, 0x05, 0x98, 0xab, 0x5f,
0xa1, 0xcf, 0xc8, 0x13, 0x98, 0x6d, 0x5e, 0x0f, 0xd0, 0xc8, 0x6d, 0xe5, 0xb6, 0x4b, 0xb5, 0xb2,
0xd4, 0xef, 0x7c, 0x33, 0xc0, 0x50, 0x64, 0x68, 0x91, 0x38, 0x32, 0x4b, 0x3c, 0x9d, 0x47, 0x41,
0xdf, 0x65, 0x22, 0x88, 0x2d, 0x3c, 0xc8, 0x6f, 0xa0, 0x64, 0x23, 0x0d, 0x86, 0x61, 0x07, 0x8f,
0xd1, 0xe9, 0x62, 0x68, 0xcc, 0x6c, 0xe5, 0xb6, 0x8b, 0x35, 0x63, 0x27, 0xd9, 0xc6, 0x8e, 0xae,
0xb7, 0xd6, 0xe2, 0xc8, 0x24, 0xa1, 0x92, 0xa5, 0x78, 0xc7, 0xb7, 0xec, 0x31, 0x24, 0xd2, 0x86,
0xa5, 0x03, 0x0c, 0xd9, 0xfe, 0x90, 0x5d, 0x04, 0xa1, 0xcb, 0xae, 0x8d, 0xbc, 0x80, 0xbe, 0x97,
0x42, 0x6b, 0xea, 0x56, 0xcd, 0xda, 0x88, 0x23, 0xd3, 0xe8, 0x60, 0xc8, 0xda, 0x4e, 0x22, 0xd5,
0x22, 0xe8, 0x78, 0xe4, 0xb7, 0xb0, 0xd8, 0xe0, 0x67, 0xd0, 0x69, 0x06, 0x97, 0xe8, 0x53, 0x63,
0x76, 0x3c, 0xf5, 0xac, 0xb6, 0x55, 0xb3, 0xee, 0xc7, 0x91, 0xb9, 0x4e, 0x85, 0xac, 0xcd, 0x84,
0x50, 0x43, 0xd7, 0xc0, 0x48, 0x07, 0x4a, 0x67, 0x61, 0x70, 0xe5, 0x52, 0x37, 0xf0, 0x85, 0xc8,
0x98, 0x13, 0xf0, 0x95, 0x14, 0x5e, 0xd7, 0xb7, 0x6a, 0xd6, 0x83, 0x38, 0x32, 0xef, 0x0d, 0x12,
0xa9, 0x8c, 0xa1, 0x97, 0x48, 0x77, 0x21, 0xaf, 0xa1, 0x78, 0xe0, 0x0d, 0x29, 0xc3, 0xf0, 0xa5,
0xd3, 0x47, 0x63, 0x5e, 0x44, 0x58, 0xcf, 0x14, 0x28, 0x55, 0xb6, 0x6a, 0x56, 0x25, 0x8e, 0xcc,
0xb5, 0x8e, 0x14, 0xb5, 0x7d, 0xa7, 0xaf, 0x97, 0x3f, 0x8b, 0x24, 0x6a, 0x2f, 0x97, 0x07, 0x81,
0xff, 0xc6, 0xed, 0x19, 0xb7, 0x6f, 0xd4, 0x3e, 0xab, 0x6e, 0xed, 0xa9, 0xda, 0x2b, 0xf0, 0x8e,
0x90, 0x8e, 0xd5, 0x3e, 0xeb, 0x40, 0x9e, 0xc2, 0xec, 0x2b, 0x8a, 0xa1, 0xb1, 0x20, 0x70, 0xcb,
0x29, 0x2e, 0x97, 0xb6, 0x6a, 0xb2, 0xe5, 0x86, 0x14, 0x43, 0x0d, 0x44, 0xf8, 0x70, 0x5f, 0x3b,
0xf0, 0xd0, 0x28, 0x8c, 0xfb, 0x72, 0x69, 0x6b, 0x4f, 0xfa, 0x86, 0x81, 0xa7, 0xef, 0x4f, 0xf8,
0x90, 0x53, 0x28, 0xf0, 0x0d, 0xd2, 0x81, 0xd3, 0x41, 0x03, 0x04, 0xc0, 0x4a, 0x0a, 0x30, 0x52,
0x59, 0xeb, 0x71, 0x64, 0xae, 0xf8, 0xc9, 0x52, 0x03, 0x4a, 0x01, 0x88, 0x05, 0xf3, 0x0d, 0x0c,
0xaf, 0x30, 0x34, 0x8a, 0x02, 0x8a, 0x64, 0x7a, 0x47, 0xc8, 0x5b, 0x35, 0x6b, 0x35, 0x8e, 0xcc,
0x32, 0x15, 0x2b, 0x0d, 0x46, 0x79, 0xf2, 0x52, 0xdb, 0x78, 0x85, 0x21, 0xc5, 0xe6, 0xd0, 0xf7,
0xd1, 0x33, 0x16, 0xc7, 0x4b, 0xad, 0xa9, 0x93, 0x36, 0x0f, 0xa5, 0xb0, 0xcd, 0x84, 0x54, 0x2f,
0xb5, 0xe6, 0x40, 0x2e, 0xa1, 0x2c, 0xbf, 0x0e, 0x02, 0xdf, 0xc7, 0x0e, 0xbf, 0xd1, 0xc6, 0x92,
0x88, 0xb1, 0x91, 0xc6, 0x18, 0xb7, 0x68, 0xd5, 0x2c, 0x33, 0x8e, 0xcc, 0xfb, 0x12, 0x9e, 0x1f,
0xa8, 0x52, 0x68, 0x91, 0x6e, 0x00, 0xf3, 0xdd, 0xec, 0x77, 0x3a, 0x48, 0xa9, 0x8d, 0x6f, 0x87,
0x48, 0x99, 0x51, 0x1a, 0xdf, 0x8d, 0xa6, 0x4e, 0x1a, 0xc7, 0x11, 0xc2, 0x76, 0x28, 0xa5, 0xfa,
0x6e, 0x34, 0x07, 0x0b, 0x60, 0x21, 0xe1, 0x89, 0xea, 0x31, 0xcc, 0xbd, 0x76, 0x58, 0xe7, 0x82,
0x3c, 0x83, 0xb9, 0x17, 0xae, 0xdf, 0xa5, 0x46, 0x6e, 0x2b, 0x2f, 0x5a, 0x42, 0x32, 0x98, 0x50,
0x72, 0x85, 0xb5, 0xfe, 0x3e, 0x32, 0x6f, 0xc5, 0x91, 0x79, 0xe7, 0x92, 0x9b, 0x65, 0x68, 0x4c,
0xfa, 0x55, 0xff, 0x38, 0x03, 0x85, 0x91, 0x35, 0xd9, 0x80, 0x59, 0xfe, 0x2b, 0xf8, 0xb0, 0x60,
0x2d, 0xc4, 0x91, 0x39, 0xcb, 0xfd, 0x6c, 0x21, 0x25, 0x35, 0x28, 0x9e, 0x06, 0x4e, 0xb7, 0x81,
0x9d, 0x10, 0x19, 0x15, 0x84, 0xb7, 0x60, 0x95, 0xe3, 0xc8, 0x5c, 0xf4, 0x02, 0xa7, 0xdb, 0xa6,
0x52, 0x6e, 0x67, 0x8d, 0x38, 0xa2, 0xb8, 0xa1, 0xf9, 0x14, 0x91, 0x37, 0x97, 0x2d, 0xa4, 0xe4,
0x6b, 0x98, 0x7f, 0xee, 0x7a, 0x0c, 0x43, 0x63, 0x56, 0xe4, 0xbf, 0x31, 0x9e, 0xff, 0x8e, 0x54,
0xd7, 0x7d, 0x16, 0x5e, 0xcb, 0x86, 0x7a, 0x23, 0x04, 0x99, 0x8d, 0x28, 0x84, 0xca, 0x4f, 0xa1,
0x98, 0x31, 0x26, 0x65, 0xc8, 0x5f, 0xe2, 0xb5, 0xdc, 0x89, 0xcd, 0x3f, 0xc9, 0x2a, 0xcc, 0x5d,
0x39, 0xde, 0x10, 0x45, 0xe2, 0x05, 0x5b, 0x2e, 0x9e, 0xce, 0x3c, 0xc9, 0x55, 0x7f, 0x05, 0x73,
0x9c, 0x20, 0x29, 0x79, 0x08, 0xf9, 0x46, 0xe3, 0x58, 0x38, 0x2d, 0x5a, 0xcb, 0x71, 0x64, 0x2e,
0x51, 0x7a, 0x91, 0x89, 0xc5, 0xb5, 0xdc, 0xa8, 0x79, 0xda, 0x10, 0x28, 0xca, 0x88, 0x79, 0xd9,
0xca, 0x72, 0x6d, 0xf5, 0x7f, 0x33, 0x50, 0xe6, 0x77, 0x56, 0xe0, 0xaa, 0x23, 0x24, 0x8f, 0xa0,
0x70, 0x36, 0x3c, 0xf7, 0xdc, 0xce, 0x0b, 0x95, 0xd9, 0xa2, 0x55, 0x8a, 0x23, 0x13, 0x06, 0x42,
0xd8, 0xbe, 0xc4, 0x6b, 0x3b, 0x35, 0x20, 0xdb, 0xb0, 0xc0, 0x11, 0x78, 0xb9, 0x64, 0xca, 0xd6,
0x62, 0x1c, 0x99, 0x0b, 0x43, 0x25, 0xb3, 0x47, 0x5a, 0xd2, 0x80, 0xdb, 0xf5, 0xdf, 0x0f, 0xdc,
0x10, 0xa9, 0x7a, 0x2a, 0x2a, 0x3b, 0xf2, 0x0d, 0xdc, 0x49, 0xde, 0xc0, 0x9d, 0x66, 0xf2, 0x06,
0x5a, 0x0f, 0x54, 0x47, 0x2c, 0xa3, 0x74, 0x49, 0x33, 0xff, 0xf3, 0x3f, 0xcd, 0x9c, 0x9d, 0x20,
0x91, 0x47, 0x30, 0xff, 0x3c, 0x08, 0xfb, 0x0e, 0x13, 0xcf, 0x43, 0x41, 0x55, 0x5f, 0x48, 0xb4,
0xea, 0x0b, 0x09, 0x79, 0x0e, 0x25, 0x3b, 0x18, 0x32, 0x6c, 0x06, 0x8a, 0xee, 0x04, 0xeb, 0x17,
0xac, 0xcd, 0x38, 0x32, 0x2b, 0x21, 0xd7, 0xb4, 0x59, 0xd0, 0x56, 0x34, 0x99, 0xf1, 0x1f, 0xf3,
0x22, 0x75, 0x28, 0x69, 0x6d, 0x4f, 0x8d, 0xf9, 0xad, 0xfc, 0x76, 0x41, 0xbe, 0x10, 0xfa, 0x65,
0xc9, 0xd6, 0x7c, 0xcc, 0xa9, 0xea, 0x41, 0xe9, 0x08, 0x19, 0x2f, 0x50, 0x52, 0xfb, 0xa4, 0x11,
0x73, 0x13, 0x1b, 0xf1, 0x67, 0x50, 0x7c, 0xed, 0xb2, 0x0b, 0xbd, 0xb5, 0xc5, 0xb3, 0xf1, 0xce,
0x65, 0x17, 0x49, 0x6b, 0x67, 0x02, 0x66, 0xcd, 0xab, 0x75, 0xb8, 0xa3, 0xa2, 0x8d, 0x8e, 0xba,
0xa6, 0x03, 0xe6, 0xd2, 0xbb, 0x92, 0x05, 0xd4, 0x61, 0x2e, 0xc6, 0xf7, 0x4e, 0x5a, 0x37, 0xaa,
0x21, 0xef, 0xf9, 0x47, 0x58, 0x65, 0x85, 0x5f, 0xf6, 0xb1, 0x42, 0xdd, 0x28, 0xcf, 0xaf, 0x61,
0xe9, 0xcc, 0x1b, 0xf6, 0x5c, 0xff, 0xd0, 0x61, 0x4e, 0x03, 0xdf, 0x92, 0x23, 0x80, 0x54, 0xa0,
0x82, 0xac, 0x65, 0x1e, 0xec, 0x91, 0xae, 0xb5, 0x67, 0xdd, 0x89, 0x23, 0xb3, 0x38, 0x10, 0x92,
0x76, 0xd7, 0x61, 0x8e, 0x9d, 0x71, 0xad, 0xfe, 0x2d, 0x07, 0x44, 0x85, 0xe1, 0x53, 0x01, 0x36,
0x90, 0xf1, 0x63, 0x5d, 0x83, 0x99, 0x93, 0x43, 0x55, 0xfb, 0xf9, 0x38, 0x32, 0x67, 0xdc, 0xae,
0x3d, 0x73, 0x72, 0x48, 0x7e, 0x02, 0x73, 0xc2, 0x4c, 0x54, 0xbc, 0x94, 0x0d, 0x99, 0x05, 0xb1,
0x0a, 0x71, 0x64, 0xce, 0xf1, 0x01, 0x04, 0x6d, 0x69, 0x4f, 0xbe, 0x82, 0xc2, 0x21, 0x7a, 0xd8,
0x73, 0x58, 0x10, 0x2a, 0x72, 0x11, 0x2f, 0x57, 0x37, 0x11, 0x66, 0xce, 0x2a, 0xb5, 0xac, 0x3e,
0x84, 0x82, 0x02, 0x3e, 0x39, 0x9c, 0x96, 0x54, 0xf5, 0x97, 0xf0, 0xb9, 0x1d, 0x88, 0x60, 0x48,
0x91, 0x9d, 0x39, 0x94, 0xbe, 0x0b, 0xc2, 0xae, 0x18, 0x3c, 0xd4, 0x51, 0x25, 0xa7, 0xfc, 0x10,
0x6e, 0x0b, 0xf1, 0x08, 0x46, 0x24, 0x2a, 0xc6, 0x17, 0x3b, 0xd1, 0x54, 0x0f, 0x60, 0xe3, 0x08,
0xd9, 0x4d, 0xac, 0xef, 0x04, 0xf2, 0xa7, 0x1c, 0x98, 0x07, 0x21, 0x4e, 0x4c, 0xea, 0xd3, 0x5a,
0x7c, 0x43, 0xcd, 0xba, 0x33, 0xa9, 0x96, 0x4f, 0xb6, 0x6a, 0x9e, 0xfd, 0x0c, 0xf2, 0xcd, 0xe6,
0xa9, 0xa8, 0x64, 0x5e, 0xf4, 0x50, 0x9e, 0x31, 0xef, 0xbf, 0x91, 0xb9, 0x70, 0x38, 0x94, 0xb3,
0xb0, 0xcd, 0xf5, 0xd5, 0x25, 0x28, 0x9e, 0xb9, 0x7e, 0x4f, 0x45, 0xac, 0xfe, 0x01, 0x16, 0xe5,
0x92, 0x0e, 0x02, 0x9f, 0x22, 0xef, 0xfa, 0xec, 0x58, 0x26, 0x13, 0x11, 0x5d, 0x9f, 0x9d, 0xbe,
0xf4, 0x89, 0xeb, 0x09, 0x2c, 0xa9, 0x91, 0x01, 0x43, 0x3e, 0xe0, 0xa9, 0x04, 0xc5, 0x2c, 0x23,
0xa7, 0x87, 0xf6, 0x95, 0xd4, 0xd8, 0xba, 0x61, 0xf5, 0xc7, 0xb0, 0xcc, 0x4f, 0x96, 0xe1, 0x27,
0xdf, 0xf3, 0x6a, 0x03, 0xa0, 0x81, 0x7d, 0x67, 0x70, 0x11, 0x70, 0x8a, 0xab, 0x67, 0x57, 0xaa,
0xeb, 0xef, 0x66, 0x27, 0x19, 0xa5, 0x6b, 0xed, 0x49, 0x9e, 0xa6, 0x23, 0x63, 0x3b, 0xe3, 0xf8,
0xc5, 0x17, 0x50, 0x18, 0xfd, 0x65, 0x20, 0x0b, 0x30, 0x7b, 0xf2, 0xf2, 0xa4, 0x59, 0xbe, 0x45,
0x6e, 0x43, 0xfe, 0xec, 0x55, 0xb3, 0x9c, 0x23, 0x00, 0xf3, 0x87, 0xf5, 0xd3, 0x7a, 0xb3, 0x5e,
0x9e, 0xa9, 0xfd, 0x65, 0x09, 0x8a, 0x7c, 0x10, 0x6f, 0xc8, 0x20, 0xe4, 0x19, 0x94, 0x1a, 0xe8,
0x77, 0x5f, 0x20, 0x0e, 0xf6, 0x3d, 0xf7, 0x0a, 0x29, 0xc9, 0x4c, 0x65, 0x23, 0x69, 0x65, 0xed,
0x06, 0xa1, 0xd7, 0x79, 0x6b, 0x6f, 0xe7, 0xc8, 0x0f, 0xa1, 0x28, 0x5e, 0x4b, 0xf1, 0x87, 0x86,
0x92, 0xc5, 0xec, 0x0b, 0x5a, 0x49, 0x56, 0x42, 0xf9, 0x65, 0x8e, 0x7c, 0x05, 0xf0, 0x6a, 0x40,
0x31, 0x64, 0x2f, 0x83, 0x2e, 0x92, 0x09, 0x43, 0x5b, 0x65, 0x52, 0x74, 0xf2, 0x14, 0x96, 0x8f,
0xd0, 0xe7, 0x3b, 0xc4, 0xd1, 0x9b, 0x46, 0xd6, 0x15, 0xf6, 0xf8, 0x2b, 0x37, 0x0a, 0x2a, 0xcd,
0x6a, 0x70, 0x5b, 0x71, 0x23, 0xb9, 0xab, 0x14, 0x3a, 0x33, 0x57, 0x6e, 0xcc, 0xc0, 0xe4, 0x31,
0x2c, 0x24, 0x7c, 0x4a, 0xd6, 0x74, 0x27, 0x3a, 0xd5, 0xeb, 0xcb, 0x1c, 0x39, 0xe1, 0x79, 0xb2,
0x31, 0x0e, 0x7d, 0x30, 0x85, 0x2b, 0xd5, 0xd8, 0x90, 0x24, 0x35, 0xe6, 0x75, 0x0c, 0x2b, 0xf2,
0xba, 0x69, 0x72, 0x32, 0x9d, 0x78, 0xa7, 0x1d, 0x11, 0x79, 0x06, 0x2b, 0xb2, 0x4b, 0x75, 0xa4,
0x64, 0x54, 0x1b, 0xd1, 0xd1, 0x54, 0x80, 0xaf, 0xe1, 0x6e, 0x63, 0x6c, 0x57, 0x92, 0x03, 0xef,
0xe9, 0x10, 0x19, 0xbe, 0x9d, 0x8a, 0xb5, 0x0f, 0x4b, 0x47, 0xc8, 0x52, 0xbe, 0x26, 0x95, 0x49,
0x24, 0xaf, 0x4a, 0xb3, 0xaa, 0xf0, 0xf5, 0xa7, 0xe2, 0x14, 0xca, 0xaf, 0x06, 0x5d, 0x87, 0x61,
0x06, 0x65, 0x6b, 0x12, 0x8a, 0xb2, 0x72, 0x42, 0xa7, 0x4f, 0xa7, 0x26, 0xb4, 0x0b, 0xb3, 0x9c,
0x41, 0x08, 0x49, 0x62, 0xa5, 0xec, 0x52, 0x59, 0xd1, 0x64, 0x8a, 0x62, 0xde, 0x81, 0xf9, 0x2d,
0xe4, 0x4c, 0x7e, 0x94, 0xd4, 0xe5, 0x93, 0x48, 0xbc, 0xf2, 0x99, 0xf6, 0x97, 0x7d, 0xb2, 0x6d,
0x6b, 0x8f, 0xfc, 0x0e, 0xee, 0x4e, 0xa4, 0x71, 0xf2, 0x30, 0xed, 0xd0, 0xa9, 0xdc, 0x5c, 0xd9,
0xfc, 0x58, 0x90, 0xd6, 0x1e, 0x39, 0x07, 0x63, 0x1a, 0xbd, 0x93, 0xcf, 0x93, 0x0b, 0xf5, 0x71,
0xfe, 0xff, 0xd6, 0x18, 0x8f, 0x01, 0x24, 0x84, 0xb8, 0x8d, 0x37, 0x2e, 0xd0, 0xd4, 0x33, 0x7a,
0xcc, 0x59, 0xa3, 0xfb, 0xdd, 0xfd, 0x7e, 0x01, 0x90, 0xf2, 0x33, 0x31, 0xd4, 0x2e, 0x6e, 0x50,
0xf6, 0x54, 0xff, 0x6f, 0xa0, 0xbc, 0xdf, 0x79, 0x3b, 0x74, 0x43, 0x1c, 0x91, 0x2d, 0xf9, 0x41,
0xf6, 0x02, 0xea, 0xba, 0x04, 0xce, 0x98, 0xc0, 0xe0, 0xa7, 0xe8, 0x50, 0x24, 0x2f, 0x60, 0x7d,
0x44, 0x6a, 0x63, 0xaa, 0xa9, 0x4e, 0x53, 0xb3, 0x3b, 0x86, 0xd5, 0x03, 0xc7, 0xef, 0xa0, 0xf7,
0xbd, 0x91, 0x7e, 0x2e, 0x2e, 0x65, 0xe6, 0x5d, 0xba, 0x37, 0x01, 0x42, 0xdd, 0xc9, 0x65, 0x55,
0xc5, 0x8c, 0xf5, 0x21, 0xdc, 0x91, 0x35, 0x4d, 0xab, 0xf4, 0x11, 0x80, 0x29, 0x49, 0x58, 0xab,
0xef, 0xff, 0xbd, 0x99, 0x7b, 0xff, 0x61, 0x33, 0xf7, 0x8f, 0x0f, 0x9b, 0xb9, 0x7f, 0x7d, 0xd8,
0xcc, 0xfd, 0xf5, 0x3f, 0x9b, 0xb7, 0xce, 0xe7, 0x85, 0xd5, 0xde, 0xff, 0x03, 0x00, 0x00, 0xff,
0xff, 0x55, 0x42, 0x2d, 0x6e, 0xd6, 0x13, 0x00, 0x00,
}

View file

@ -203,6 +203,11 @@ message DeleteUserRequest {
string Name = 1 [ (gogoproto.jsontag) = "name" ];
}
// Semaphores is a sequence of Semaphore resources.
message Semaphores {
repeated services.SemaphoreV3 Semaphores = 1 [ (gogoproto.jsontag) = "semaphores" ];
}
// AuthService is authentication/authorization service implementation
service AuthService {
// SendKeepAlives allows node to send a stream of keep alive requests
@ -246,6 +251,16 @@ service AuthService {
rpc CreateUser(services.UserV2) returns (google.protobuf.Empty);
// UpdateUser updates an existing user in a backend.
rpc UpdateUser(services.UserV2) returns (google.protobuf.Empty);
// DeleteUser deletes an exisitng user in a backend by username.
// DeleteUser deletes an existing user in a backend by username.
rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty);
// AcquireSemaphore acquires lease with requested resources from semaphore.
rpc AcquireSemaphore(services.AcquireSemaphoreRequest) returns (services.SemaphoreLease);
// KeepAliveSemaphoreLease updates semaphore lease.
rpc KeepAliveSemaphoreLease(services.SemaphoreLease) returns (google.protobuf.Empty);
// CancelSemaphoreLease cancels semaphore lease early.
rpc CancelSemaphoreLease(services.SemaphoreLease) returns (google.protobuf.Empty);
// GetSemaphores returns a list of all semaphores matching the supplied filter.
rpc GetSemaphores(services.SemaphoreFilter) returns (Semaphores);
// DeleteSemaphore deletes a semaphore matching the supplied filter.
rpc DeleteSemaphore(services.SemaphoreFilter) returns (google.protobuf.Empty);
}

View file

@ -440,6 +440,7 @@ func applyAuthConfig(fc *FileConfig, cfg *service.Config) error {
KeepAliveInterval: fc.Auth.KeepAliveInterval,
KeepAliveCountMax: fc.Auth.KeepAliveCountMax,
LocalAuth: localAuth,
SessionControlTimeout: fc.Auth.SessionControlTimeout,
})
if err != nil {
return trace.Wrap(err)

View file

@ -151,6 +151,7 @@ var (
"pam": true,
"service_name": false,
"client_idle_timeout": false,
"session_control_timeout": false,
"disconnect_expired_cert": false,
"ciphersuites": false,
"ca_pin": false,
@ -572,6 +573,10 @@ type Auth struct {
// if true, connections with expired client certificates will get disconnected
DisconnectExpiredCert services.Bool `yaml:"disconnect_expired_cert,omitempty"`
// SessionControlTimeout specifies the maximum amount of time a node can be out
// of contact with the auth server before it starts terminating controlled sessions.
SessionControlTimeout services.Duration `yaml:"session_control_timeout,omitempty"`
// KubeconfigFile is an optional path to kubeconfig file,
// if specified, teleport will use API server address and
// trusted certificate authority information from it

View file

@ -87,16 +87,6 @@ const (
// connection attempts
DefaultDialTimeout = 30 * time.Second
// SemaphoreTimeout is a timeout for acquiring semaphore
SemaphoreTimeout = DefaultDialTimeout / 2
// SemaphoreRetryPeriod is a period to retry semaphore
SemaphoreRetryPeriod = 500 * time.Millisecond
// SemaphoreRetries is a default amount of semaphore retries
// on contention errors
SemaphoreRetries = 3
// HTTPMaxIdleConns is the max idle connections across all hosts.
HTTPMaxIdleConns = 2000
@ -360,6 +350,11 @@ var (
// CASignatureAlgorithm is the default signing algorithm to use when
// creating new SSH CAs.
CASignatureAlgorithm = ssh.SigAlgoRSASHA2512
// SessionControlTimeout is the maximum amount of time a controlled session
// may persist after contact with the auth server is lost (sessctl semaphore
// leases are refreshed at a rate of ~1/2 this duration).
SessionControlTimeout = time.Minute * 2
)
// Default connection limits, they can be applied separately on any of the Teleport

View file

@ -326,6 +326,21 @@ const (
SAMLConnectorCreatedEvent = "saml.created"
// SAMLConnectorDeletedEvent fires when SAML connector is deleted.
SAMLConnectorDeletedEvent = "saml.deleted"
// SessionRejected fires when a user's attempt to create an authenticated
// session has been rejected due to exceeding a session control limit.
SessionRejectedEvent = "session.rejected"
// SessionRejectedReasonMaxConnections indicates that a session.rejected event
// corresponds to enforcement of the max_connections control.
SessionRejectedReasonMaxConnections = "max_connections limit reached"
// SessionRejectedReasonMaxSessions indicates that a session.rejected event
// corresponds to enforcement of the max_sessions control.
SessionRejectedReasonMaxSessions = "max_sessions limit reached"
// Maximum is an event field specifying a maximal value (e.g. the value
// of `max_connections` for a `session.rejected` event).
Maximum = "max"
)
const (

View file

@ -257,6 +257,11 @@ var (
Name: SAMLConnectorDeletedEvent,
Code: SAMLConnectorDeletedCode,
}
// SessionRejected is emitted when a user hits `max_connections`.
SessionRejected = Event{
Name: SessionRejectedEvent,
Code: SessionRejectedCode,
}
)
// There is no strict algorithm for picking an event code, however existing
@ -286,6 +291,10 @@ const (
// UserPasswordChangeCode is an event code for when user changes their own password.
UserPasswordChangeCode = "T1005I"
// SessionRejectedCode is an event code for when a user's attempt to create an
// session/connection has been rejected.
SessionRejectedCode = "T1006W"
// SessionStartCode is the session start event code.
SessionStartCode = "T2000I"
// SessionJoinCode is the session join event code.

View file

@ -67,6 +67,12 @@ type ClusterConfig interface {
// SetClientIdleTimeout sets client idle timeout setting
SetClientIdleTimeout(t time.Duration)
// GetSessionControlTimeout gets the session control timeout.
GetSessionControlTimeout() time.Duration
// SetSessionControlTimeout sets the session control timeout.
SetSessionControlTimeout(t time.Duration)
// GetDisconnectExpiredCert returns disconnect expired certificate setting
GetDisconnectExpiredCert() bool
@ -287,6 +293,16 @@ func (c *ClusterConfigV3) SetClientIdleTimeout(d time.Duration) {
c.Spec.ClientIdleTimeout = Duration(d)
}
// GetSessionControlTimeout gets the session control timeout.
func (c *ClusterConfigV3) GetSessionControlTimeout() time.Duration {
return c.Spec.SessionControlTimeout.Duration()
}
// SetSessionControlTimeout sets the session control timeout.
func (c *ClusterConfigV3) SetSessionControlTimeout(d time.Duration) {
c.Spec.SessionControlTimeout = Duration(d)
}
// GetDisconnectExpiredCert returns disconnect expired certificate setting
func (c *ClusterConfigV3) GetDisconnectExpiredCert() bool {
return c.Spec.DisconnectExpiredCert.Value()
@ -399,6 +415,9 @@ const ClusterConfigSpecSchemaTemplate = `{
"client_idle_timeout": {
"type": "string"
},
"session_control_timeout": {
"type": "string"
},
"disconnect_expired_cert": {
"anyOf": [{"type": "string"}, { "type": "boolean"}]
},

View file

@ -1,5 +1,5 @@
/*
Copyright 2015-2019 Gravitational, Inc.
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -25,15 +25,18 @@ import (
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/pborman/uuid"
"github.com/sirupsen/logrus"
)
// PresenceService records and reports the presence of all components
// of the cluster - Nodes, Proxies and SSH nodes
type PresenceService struct {
log *logrus.Entry
log *logrus.Entry
jitter utils.Jitter
backend.Backend
}
@ -41,6 +44,7 @@ type PresenceService struct {
func NewPresenceService(b backend.Backend) *PresenceService {
return &PresenceService{
log: logrus.WithFields(logrus.Fields{trace.Component: "Presence"}),
jitter: utils.NewJitter(),
Backend: b,
}
}
@ -674,98 +678,127 @@ func (s *PresenceService) DeleteAllRemoteClusters() error {
return trace.Wrap(err)
}
// TryAcquireSemaphore tries to acquire semaphore resources using optimistic concurrency
// (hoping that compare and swap will succeed).
//
// In case of unsuccessfull concurrent operation returns CompareFailed or AlreadyExists,
// what indicates to the client that it can retry the operation with the existing values.
//
// In case if the operation returns trace.LimitExceeded, it means that semaphore resources
// acquired by other clients and it's up to client whether to keep trying or reject
// the connection.
//
// In all other cases the error indicates the networking, backend or parameter validation problem
//
func (s *PresenceService) TryAcquireSemaphore(ctx context.Context, sem services.Semaphore, l services.SemaphoreLease) (*services.SemaphoreLease, error) {
if err := sem.CheckAndSetDefaults(); err != nil {
// AcquireSemaphore attempts to acquire the specified semaphore. AcquireSemaphore will automatically handle
// retry on contention. If the semaphore has already reached MaxLeases, or there is too much contention,
// a LimitExceeded error is returned (contention in this context means concurrent attempts to update the
// *same* semaphore, separate semaphores can be modified concurrently without issue). Note that this function
// is the only semaphore method that handles retries internally. This is because this method both blocks
// user-facing operations, and contains multiple different potential contention points.
func (s *PresenceService) AcquireSemaphore(ctx context.Context, req services.AcquireSemaphoreRequest) (*services.SemaphoreLease, error) {
// this combination of backoff parameters leads to worst-case total time spent
// in backoff between 1200ms and 2400ms depending on jitter. tests are in
// place to verify that this is sufficient to resolve a 20-lease contention
// event, which is worse than should ever occur in practice.
const baseBackoff = time.Millisecond * 300
const acquireAttempts int64 = 6
if err := req.Check(); err != nil {
return nil, trace.Wrap(err)
}
l.SemaphoreName = sem.GetName()
l.SemaphoreSubKind = sem.GetSubKind()
if err := l.CheckAndSetDefaults(); err != nil {
if req.Expires.Before(s.Clock().Now().UTC()) {
return nil, trace.BadParameter("cannot acquire expired semaphore lease")
}
leaseID := uuid.New()
// key is not modified, so allocate it once
key := backend.Key(semaphoresPrefix, req.SemaphoreKind, req.SemaphoreName)
Acquire:
for i := int64(0); i < acquireAttempts; i++ {
if i > 0 {
// Not our first attempt, apply backoff. If we knew that we were only in
// contention with one other acquire attempt we could retry immediately
// since we got here because some other attempt *succeeded*. It is safer,
// however, to assume that we are under high contention and attempt to
// spread out retries via random backoff.
select {
case <-time.After(s.jitter(baseBackoff * time.Duration(i))):
case <-ctx.Done():
return nil, trace.Wrap(ctx.Err())
}
}
// attempt to acquire an existing semaphore
lease, err := s.acquireSemaphore(ctx, key, leaseID, req)
switch {
case err == nil:
// acquire was successful, return the lease.
return lease, nil
case trace.IsNotFound(err):
// semaphore does not exist, attempt to perform a
// simultaneous init+acquire.
lease, err = s.initSemaphore(ctx, key, leaseID, req)
if err != nil {
if trace.IsAlreadyExists(err) {
// semaphore was concurrently created
continue Acquire
}
return nil, trace.Wrap(err)
}
return lease, nil
case trace.IsCompareFailed(err):
// semaphore was concurrently updated
continue Acquire
default:
// If we get here then we encountered an error other than NotFound or CompareFailed,
// meaning that contention isn't the issue. No point in re-attempting.
return nil, trace.Wrap(err)
}
}
return nil, trace.LimitExceeded("too much contention on semaphore %s/%s", req.SemaphoreKind, req.SemaphoreName)
}
// initSemaphore attempts to initialize/acquire a semaphore which does not yet exist.
// Returns AlreadyExistsError if the semaphore is concurrently created.
func (s *PresenceService) initSemaphore(ctx context.Context, key []byte, leaseID string, req services.AcquireSemaphoreRequest) (*services.SemaphoreLease, error) {
// create a new empty semaphore resource configured to specifically match
// this acquire request.
sem, err := req.ConfigureSemaphore()
if err != nil {
return nil, trace.Wrap(err)
}
lease, err := sem.Acquire(leaseID, req)
if err != nil {
return nil, trace.Wrap(err)
}
value, err := services.GetSemaphoreMarshaler().Marshal(sem)
if err != nil {
return nil, trace.Wrap(err)
}
item := backend.Item{
Key: key,
Value: value,
Expires: sem.Expiry(),
}
_, err = s.Create(ctx, item)
if err != nil {
return nil, trace.Wrap(err)
}
return lease, nil
}
// acquireSemaphore attempts to acquire an existing semaphore. Returns NotFoundError if no semaphore exists,
// and CompareFailed if the semaphore was concurrently updated.
func (s *PresenceService) acquireSemaphore(ctx context.Context, key []byte, leaseID string, req services.AcquireSemaphoreRequest) (*services.SemaphoreLease, error) {
item, err := s.Get(ctx, key)
if err != nil {
return nil, trace.Wrap(err)
}
sem, err := services.GetSemaphoreMarshaler().Unmarshal(item.Value)
if err != nil {
return nil, trace.Wrap(err)
}
sem.RemoveExpiredLeases(s.Clock().Now().UTC())
if l.Resources > sem.GetMaxResources()-sem.AcquiredResources() {
return nil, trace.LimitExceeded(
"lease %v can't acquire %v resources, semaphore %v has %v available",
l.ID, l.Resources, sem.GetName(), sem.GetMaxResources()-sem.AcquiredResources(),
)
}
if !l.Expires.IsZero() && l.Expires.Before(s.Clock().Now()) {
return nil, trace.BadParameter("the lease %v has expired at %v", l.ID, l.Expires)
}
key := backend.Key(semaphoresPrefix, sem.GetSubKind(), sem.GetName())
item, err := s.Get(ctx, key)
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
// Semaphore does not exist, create a new semaphore from the specs with
// acquired lease
sem.SetLeases([]services.SemaphoreLease{l})
value, err := services.GetSemaphoreMarshaler().Marshal(sem)
if err != nil {
return nil, trace.Wrap(err)
}
item := backend.Item{
Key: key,
Value: value,
Expires: sem.Expiry(),
}
_, err = s.Create(ctx, item)
if err != nil {
return nil, trace.Wrap(err)
}
return &l, nil
}
existing, err := services.GetSemaphoreMarshaler().Unmarshal(item.Value)
lease, err := sem.Acquire(leaseID, req)
if err != nil {
return nil, trace.Wrap(err)
}
existing.RemoveExpiredLeases(s.Clock().Now().UTC())
if l.Resources > existing.GetMaxResources()-existing.AcquiredResources() {
return nil, trace.LimitExceeded(
"lease %v can't acquire %v resources, semaphore %v has %v available",
l.ID, l.Resources, sem.GetName(), existing.GetMaxResources()-existing.AcquiredResources(),
)
}
for _, lease := range existing.GetLeases() {
if lease.ID == l.ID {
return nil, trace.BadParameter(
"semaphore %v already has lease %v, use KeepAliveSemaphoreLease to renew the lease",
sem, l.ID,
)
}
}
if sem.Expiry().After(existing.Expiry()) {
existing.SetExpiry(sem.Expiry())
}
if l.Expires.After(existing.Expiry()) {
existing.SetExpiry(l.Expires)
}
existing.AddLease(l)
newValue, err := services.GetSemaphoreMarshaler().Marshal(existing)
newValue, err := services.GetSemaphoreMarshaler().Marshal(sem)
if err != nil {
return nil, trace.Wrap(err)
}
@ -773,31 +806,78 @@ func (s *PresenceService) TryAcquireSemaphore(ctx context.Context, sem services.
newItem := backend.Item{
Key: key,
Value: newValue,
Expires: existing.Expiry(),
Expires: sem.Expiry(),
}
if _, err := s.CompareAndSwap(ctx, *item, newItem); err != nil {
return nil, trace.Wrap(err)
}
return lease, nil
}
// KeepAliveSemaphoreLease updates semaphore lease, if the lease expiry is updated,
// semaphore is renewed
func (s *PresenceService) KeepAliveSemaphoreLease(ctx context.Context, lease services.SemaphoreLease) error {
if err := lease.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
if lease.Expires.Before(s.Clock().Now().UTC()) {
return trace.BadParameter("lease %v has expired at %v", lease.LeaseID, lease.Expires)
}
key := backend.Key(semaphoresPrefix, lease.SemaphoreKind, lease.SemaphoreName)
item, err := s.Get(ctx, key)
if err != nil {
if trace.IsNotFound(err) {
return trace.NotFound("cannot keepalive, semaphore not found: %s/%s", lease.SemaphoreKind, lease.SemaphoreName)
}
return trace.Wrap(err)
}
sem, err := services.GetSemaphoreMarshaler().Unmarshal(item.Value)
if err != nil {
return trace.Wrap(err)
}
sem.RemoveExpiredLeases(s.Clock().Now().UTC())
if err := sem.KeepAlive(lease); err != nil {
return trace.Wrap(err)
}
newValue, err := services.GetSemaphoreMarshaler().Marshal(sem)
if err != nil {
return trace.Wrap(err)
}
newItem := backend.Item{
Key: key,
Value: newValue,
Expires: sem.Expiry(),
}
_, err = s.CompareAndSwap(ctx, *item, newItem)
if err != nil {
if trace.IsCompareFailed(err) {
return nil, trace.CompareFailed("semaphore %v hase been concurrently updated, try again", existing.GetName())
return trace.CompareFailed("semaphore %v/%v has been concurrently updated, try again", sem.GetSubKind(), sem.GetName())
}
return nil, trace.Wrap(err)
return trace.Wrap(err)
}
return &l, nil
return nil
}
// KeepAliveSemaphoreLease updates semaphore lease, if the lease expiry is updated,
// semaphore is renewed
func (s *PresenceService) KeepAliveSemaphoreLease(ctx context.Context, l services.SemaphoreLease) error {
if err := l.CheckAndSetDefaults(); err != nil {
// CancelSemaphoreLease cancels semaphore lease early.
func (s *PresenceService) CancelSemaphoreLease(ctx context.Context, lease services.SemaphoreLease) error {
if err := lease.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
if l.Expires.Before(s.Clock().Now()) {
return trace.BadParameter("the lease %v has expired at %v", l.ID, l.Expires)
if lease.Expires.Before(s.Clock().Now()) {
return trace.BadParameter("the lease %v has expired at %v", lease.LeaseID, lease.Expires)
}
key := backend.Key(semaphoresPrefix, l.SemaphoreSubKind, l.SemaphoreName)
key := backend.Key(semaphoresPrefix, lease.SemaphoreKind, lease.SemaphoreName)
item, err := s.Get(ctx, key)
if err != nil {
return trace.Wrap(err)
@ -808,23 +888,8 @@ func (s *PresenceService) KeepAliveSemaphoreLease(ctx context.Context, l service
return trace.Wrap(err)
}
sem.RemoveExpiredLeases(s.Clock().Now().UTC())
leases := sem.GetLeases()
leaseIndex := -1
for i := range leases {
if leases[i].ID == l.ID {
leaseIndex = i
break
}
}
if leaseIndex == -1 {
return trace.NotFound("the lease %v was not found in semaphore %v", l.ID, l.SemaphoreName)
}
leases[leaseIndex] = l
sem.SetLeases(leases)
if l.Expires.After(sem.Expiry()) {
sem.SetExpiry(l.Expires)
if err := sem.Cancel(lease); err != nil {
return trace.Wrap(err)
}
newValue, err := services.GetSemaphoreMarshaler().Marshal(sem)
@ -833,46 +898,72 @@ func (s *PresenceService) KeepAliveSemaphoreLease(ctx context.Context, l service
}
newItem := backend.Item{
Key: key,
Value: newValue,
Key: key,
Value: newValue,
Expires: sem.Expiry(),
}
_, err = s.CompareAndSwap(ctx, *item, newItem)
if err != nil {
if trace.IsCompareFailed(err) {
return trace.CompareFailed("semaphore %v hase been concurrently updated, try again", sem.GetName())
return trace.CompareFailed("semaphore %v/%v has been concurrently updated, try again", sem.GetSubKind(), sem.GetName())
}
return trace.Wrap(err)
}
return nil
}
// GetAllSemaphores returns a list of all semaphores in the system
func (s *PresenceService) GetAllSemaphores(ctx context.Context, opts ...services.MarshalOption) ([]services.Semaphore, error) {
startKey := backend.Key(semaphoresPrefix)
result, err := s.GetRange(ctx, startKey, backend.RangeEnd(startKey), backend.NoLimit)
if err != nil {
return nil, trace.Wrap(err)
// GetSemaphores returns all semaphores matching the supplied filter.
func (s *PresenceService) GetSemaphores(ctx context.Context, filter services.SemaphoreFilter) ([]services.Semaphore, error) {
var items []backend.Item
if filter.SemaphoreKind != "" && filter.SemaphoreName != "" {
// special case: filter corresponds to a single semaphore
item, err := s.Get(ctx, backend.Key(semaphoresPrefix, filter.SemaphoreKind, filter.SemaphoreName))
if err != nil {
if trace.IsNotFound(err) {
return nil, nil
}
return nil, trace.Wrap(err)
}
items = append(items, *item)
} else {
var startKey []byte
if filter.SemaphoreKind != "" {
startKey = backend.Key(semaphoresPrefix, filter.SemaphoreKind)
} else {
startKey = backend.Key(semaphoresPrefix)
}
result, err := s.GetRange(ctx, startKey, backend.RangeEnd(startKey), backend.NoLimit)
if err != nil {
if trace.IsNotFound(err) {
return nil, nil
}
return nil, trace.Wrap(err)
}
items = result.Items
}
sems := make([]services.Semaphore, len(result.Items))
for i, item := range result.Items {
conn, err := services.GetSemaphoreMarshaler().Unmarshal(item.Value,
services.AddOptions(opts,
services.WithResourceID(item.ID),
services.WithExpires(item.Expires))...)
sems := make([]services.Semaphore, 0, len(items))
for _, item := range items {
sem, err := services.GetSemaphoreMarshaler().Unmarshal(item.Value)
if err != nil {
return nil, trace.Wrap(err)
}
sems[i] = conn
if filter.Match(sem) {
sems = append(sems, sem)
}
}
return sems, nil
}
// DeleteAllSemaphores deletes all semaphores in the system
func (s *PresenceService) DeleteAllSemaphores(ctx context.Context) error {
return s.DeleteRange(ctx, backend.Key(semaphoresPrefix), backend.RangeEnd(backend.Key(semaphoresPrefix)))
// DeleteSemaphore deletes a semaphore matching the supplied filter
func (s *PresenceService) DeleteSemaphore(ctx context.Context, filter services.SemaphoreFilter) error {
if filter.SemaphoreKind == "" || filter.SemaphoreName == "" {
return trace.BadParameter("semaphore kind and name must be specified for deletion")
}
return trace.Wrap(s.Delete(ctx, backend.Key(semaphoresPrefix, filter.SemaphoreKind, filter.SemaphoreName)))
}
const (

View file

@ -165,14 +165,6 @@ func (s *ServicesSuite) TestProxyWatcher(c *check.C) {
s.suite.ProxyWatcher(c)
}
func (s *ServicesSuite) TestSemaphore(c *check.C) {
s.suite.Semaphore(c)
}
func (s *ServicesSuite) TestSemaphoreExpiry(c *check.C) {
s.suite.SemaphoreExpiry(c)
}
func (s *ServicesSuite) TestSemaphoreLock(c *check.C) {
s.suite.SemaphoreLock(c)
}
@ -180,3 +172,11 @@ func (s *ServicesSuite) TestSemaphoreLock(c *check.C) {
func (s *ServicesSuite) TestSemaphoreConcurrency(c *check.C) {
s.suite.SemaphoreConcurrency(c)
}
func (s *ServicesSuite) TestSemaphoreContention(c *check.C) {
s.suite.SemaphoreContention(c)
}
func (s *ServicesSuite) TestSemaphoreFlakiness(c *check.C) {
s.suite.SemaphoreFlakiness(c)
}

View file

@ -761,6 +761,8 @@ func ParseShortcut(in string) (string, error) {
return KindClusterAuthPreference, nil
case KindRemoteCluster, "remote_clusters", "rc", "rcs":
return KindRemoteCluster, nil
case KindSemaphore, "semaphores", "sem", "sems":
return KindSemaphore, nil
}
return "", trace.BadParameter("unsupported resource: %q - resources should be expressed as 'type/name', for example 'connector/github'", in)
}
@ -784,6 +786,12 @@ func ParseRef(ref string) (*Ref, error) {
return nil, trace.Wrap(err)
}
return &Ref{Kind: shortcut, Name: parts[1]}, nil
case 3:
shortcut, err := ParseShortcut(parts[0])
if err != nil {
return nil, trace.Wrap(err)
}
return &Ref{Kind: shortcut, SubKind: parts[1], Name: parts[2]}, nil
}
return nil, trace.BadParameter("failed to parse '%v'", ref)
}
@ -797,10 +805,12 @@ func isDelimiter(r rune) bool {
return false
}
// Ref is a resource reference
// Ref is a resource reference. Typically of the form kind/name,
// but sometimes of the form kind/subkind/name.
type Ref struct {
Kind string
Name string
Kind string
SubKind string
Name string
}
// IsEmpty checks whether the provided resource name is empty
@ -819,7 +829,11 @@ func (r *Ref) Set(v string) error {
}
func (r *Ref) String() string {
return fmt.Sprintf("%s/%s", r.Kind, r.Name)
if r.SubKind == "" {
return fmt.Sprintf("%s/%s", r.Kind, r.Name)
} else {
return fmt.Sprintf("%s/%s/%s", r.Kind, r.SubKind, r.Name)
}
}
// Refs is a set of resource references

View file

@ -1635,6 +1635,32 @@ func (set RoleSet) AdjustSessionTTL(ttl time.Duration) time.Duration {
return ttl
}
// MaxConnections returns the maximum number of concurrent ssh connections
// allowed. If MaxConnections is zero then no maximum was defined
// and the number of concurrent connections is unconstrained.
func (set RoleSet) MaxConnections() int64 {
var mcs int64
for _, role := range set {
if m := role.GetOptions().MaxConnections; m != 0 && (m < mcs || mcs == 0) {
mcs = m
}
}
return mcs
}
// MaxSessions returns the maximum number of concurrent ssh sessions
// per connection. If MaxSessions is zero then no maximum was defined
// and the number of sessions is unconstrained.
func (set RoleSet) MaxSessions() int64 {
var ms int64
for _, role := range set {
if m := role.GetOptions().MaxSessions; m != 0 && (m < ms || ms == 0) {
ms = m
}
}
return ms
}
// AdjustClientIdleTimeout adjusts requested idle timeout
// to the lowest max allowed timeout, the most restrictive
// option will be picked, negative values will be assumed as 0
@ -2301,7 +2327,9 @@ const RoleSpecV3SchemaTemplate = `{
"enhanced_recording": {
"type": "array",
"items": { "type": "string" }
}
},
"max_connections": { "type": "number" },
"max_sessions": {"type": "number"}
}
},
"allow": { "$ref": "#/definitions/role_condition" },

View file

@ -48,6 +48,58 @@ func (s *RoleSuite) SetUpSuite(c *C) {
utils.InitLoggerForTests()
}
// TestConnAndSessLimits verifies that role sets correctly calculate
// a user's MaxConnections and MaxSessions values from multiple
// roles with different individual values. These are tested together since
// both values use the same resolution rules.
func (s *RoleSuite) TestConnAndSessLimits(c *C) {
tts := []struct {
desc string
vals []int64
want int64
}{
{
desc: "smallest nonzero value is selected from mixed values",
vals: []int64{8, 6, 7, 5, 3, 0, 9},
want: 3,
},
{
desc: "smallest value selected from all nonzero values",
vals: []int64{5, 6, 7, 8},
want: 5,
},
{
desc: "all zero values results in a zero value",
vals: []int64{0, 0, 0, 0, 0, 0, 0},
want: 0,
},
}
for ti, tt := range tts {
cmt := Commentf("test case %d: %s", ti, tt.desc)
var set RoleSet
for i, val := range tt.vals {
role := &RoleV3{
Kind: KindRole,
Version: V3,
Metadata: Metadata{
Name: fmt.Sprintf("role-%d", i),
Namespace: defaults.Namespace,
},
Spec: RoleSpecV3{
Options: RoleOptions{
MaxConnections: val,
MaxSessions: val,
},
},
}
c.Assert(role.CheckAndSetDefaults(), IsNil, cmt)
set = append(set, role)
}
c.Assert(set.MaxConnections(), Equals, tt.want, cmt)
c.Assert(set.MaxSessions(), Equals, tt.want, cmt)
}
}
func (s *RoleSuite) TestRoleExtension(c *C) {
type Spec struct {
RoleSpecV2

View file

@ -3,243 +3,412 @@ package services
import (
"context"
"fmt"
"sync"
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/pborman/uuid"
log "github.com/sirupsen/logrus"
)
// SemaphoreKindConnection is the semaphore kind used by
// the Concurrent Session Control feature to limit concurrent
// connections (corresponds to the `max_connections`
// role option).
const SemaphoreKindConnection = "connection"
// Semaphores provides ability to control
// how many shared resources of some kind are acquired at the same time,
// used to implement concurrent sessions control in a distributed environment
type Semaphores interface {
// TryAcquireSemaphore acquires lease with requested resources from semaphore
TryAcquireSemaphore(ctx context.Context, sem Semaphore, l SemaphoreLease) (*SemaphoreLease, error)
// AcquireSemaphore acquires lease with requested resources from semaphore
AcquireSemaphore(ctx context.Context, params AcquireSemaphoreRequest) (*SemaphoreLease, error)
// KeepAliveSemaphoreLease updates semaphore lease
KeepAliveSemaphoreLease(ctx context.Context, l SemaphoreLease) error
// GetAllSemaphores returns a list of all semaphores in the system
GetAllSemaphores(ctx context.Context, opts ...MarshalOption) ([]Semaphore, error)
// DeleteAllSemaphores deletes all semaphores in the system
DeleteAllSemaphores(ctx context.Context) error
KeepAliveSemaphoreLease(ctx context.Context, lease SemaphoreLease) error
// CancelSemaphoreLease cancels semaphore lease early
CancelSemaphoreLease(ctx context.Context, lease SemaphoreLease) error
// GetSemaphores returns a list of semaphores matching supplied filter.
GetSemaphores(ctx context.Context, filter SemaphoreFilter) ([]Semaphore, error)
// DeleteSemaphore deletes a semaphore matching supplied filter.
DeleteSemaphore(ctx context.Context, filter SemaphoreFilter) error
}
// Match checks if the supplied semaphore matches this filter.
func (f *SemaphoreFilter) Match(sem Semaphore) bool {
if f.SemaphoreKind != "" && f.SemaphoreKind != sem.GetSubKind() {
return false
}
if f.SemaphoreName != "" && f.SemaphoreName != sem.GetName() {
return false
}
return true
}
// SemaphoreLockConfig holds semaphore lock configuration
type SemaphoreLockConfig struct {
// Service is a semaphores service
// Service is the service against which all semaphore
// operations are performed.
Service Semaphores
// Lease is a lease name to lock, will
// be auto generated to UUID if empty
Lease SemaphoreLease
// Semaphore is a semaphore name/kind to lock
Semaphore Semaphore
// Clock helps to override time in tests
Clock clockwork.Clock
// Retry is semaphore locking retry logic
Retry utils.Retry
// TTL is a TTL to set for lease
TTL time.Duration
// Retries is approximate amount of lock retries on contention
// or other errors
Retries int
// Expiry is an optional lease expiry parameter.
Expiry time.Duration
// TickRate is the rate at which lease renewals are attempted
// and defaults to 1/2 expiry. Used to accelerate tests.
TickRate time.Duration
// Params holds the semaphore lease acquisition parameters.
Params AcquireSemaphoreRequest
}
// CheckAndSetDefaults checks and sets default values
func (s *SemaphoreLockConfig) CheckAndSetDefaults() error {
if s.Service == nil {
return trace.BadParameter("missing parameter Service")
// CheckAndSetDefaults checks and sets default parameters
func (l *SemaphoreLockConfig) CheckAndSetDefaults() error {
if l.Service == nil {
return trace.BadParameter("missing semaphore service")
}
if s.Semaphore == nil {
return trace.BadParameter("missing parameter semaphore")
if l.Expiry == 0 {
l.Expiry = defaults.SessionControlTimeout
}
if s.Clock == nil {
s.Clock = clockwork.NewRealClock()
if l.Expiry < time.Millisecond {
return trace.BadParameter("sub-millisecond lease expiry is not supported: %v", l.Expiry)
}
if s.Lease.ID == "" {
s.Lease.ID = uuid.New()
if l.TickRate == 0 {
l.TickRate = l.Expiry / 2
}
if s.Retries == 0 {
s.Retries = defaults.SemaphoreRetries
if l.TickRate >= l.Expiry {
return trace.BadParameter("tick-rate must be less than expiry")
}
if s.Retry == nil {
var err error
s.Retry, err = utils.NewLinear(utils.LinearConfig{
Step: defaults.SemaphoreRetryPeriod,
Max: defaults.SemaphoreTimeout,
Jitter: utils.NewJitter(),
})
if err != nil {
return trace.Wrap(err)
}
if l.Params.Expires.IsZero() {
l.Params.Expires = time.Now().UTC().Add(l.Expiry)
}
if s.TTL == 0 {
s.TTL = s.Retry.MaxPeriod() * time.Duration(s.Retries)
if s.TTL < time.Second {
return trace.BadParameter(
"TTL should be >= 1 second, because it is a minimum granular unit backend understands, got %v", s.TTL)
}
if err := l.Params.Check(); err != nil {
return trace.Wrap(err)
}
return nil
}
// SemaphoreLock is a high level object
// that acquires semaphore lease and keeps the lease until is closed
// SemaphoreLock provides a convenient interface for managing
// semaphore lease keepalive operations.
type SemaphoreLock struct {
SemaphoreLockConfig
ctx context.Context
cancel context.CancelFunc
cfg SemaphoreLockConfig
lease0 SemaphoreLease
retry utils.Retry
ticker *time.Ticker
doneC chan struct{}
closeOnce sync.Once
renewalC chan struct{}
cond *sync.Cond
err error
fin bool
}
// KeepAliveCycle runs one keep alive cycle
func (s *SemaphoreLock) KeepAliveCycle() error {
s.Lease.Expires = s.Clock.Now().UTC().Add(s.TTL)
return s.Service.KeepAliveSemaphoreLease(s.ctx, s.Lease)
// finish registers the final result of the background
// goroutine. must be called even if err is nil in
// order to wake any goroutines waiting on the error
// and mark the lock as finished.
func (l *SemaphoreLock) finish(err error) {
l.cond.L.Lock()
defer l.cond.L.Unlock()
l.err = err
l.fin = true
l.cond.Broadcast()
}
// KeepAlive keeps the acquired lease alive, by running
// KeepAliveAttempt in a cycle
func (s *SemaphoreLock) KeepAlive() {
defer s.cancel()
// Done signals that lease keepalive operations
// have stopped.
func (l *SemaphoreLock) Done() <-chan struct{} {
return l.doneC
}
// Wait blocks until the final result is available. Note that
// this method may block longer than desired since cancellation of
// the parent context triggers the *start* of the release operation.
func (l *SemaphoreLock) Wait() error {
l.cond.L.Lock()
defer l.cond.L.Unlock()
for !l.fin {
l.cond.Wait()
}
return l.err
}
// Stop stops associated lease keepalive.
func (l *SemaphoreLock) Stop() {
l.closeOnce.Do(func() {
l.ticker.Stop()
close(l.doneC)
})
}
// Renewed notifies on next successful lease keepalive.
// Used in tests to block until next renewal.
func (l *SemaphoreLock) Renewed() <-chan struct{} {
return l.renewalC
}
func (lock *SemaphoreLock) KeepAlive(ctx context.Context) {
var nodrop bool
var err error
lease := lock.lease0
ctx, cancel := context.WithCancel(ctx)
defer func() {
cancel()
lock.Stop()
defer lock.finish(err)
if nodrop {
// non-standard exit conditions; don't bother handling
// cancellation/expiry.
return
}
if lease.Expires.After(time.Now().UTC()) {
// parent context is closed. create orphan context with generous
// timeout for lease cancellation scope. this will not block any
// caller that is not explicitly waiting on the final error value.
cancelContext, cancel := context.WithTimeout(context.Background(), lock.cfg.Expiry/4)
defer cancel()
err = lock.cfg.Service.CancelSemaphoreLease(cancelContext, lease)
if err != nil {
log.Warnf("Failed to cancel semaphore lease %s/%s: %v", lease.SemaphoreKind, lease.SemaphoreName, err)
}
} else {
log.Errorf("Semaphore lease expired: %s/%s", lease.SemaphoreKind, lease.SemaphoreName)
}
}()
Outer:
for {
select {
case <-s.Clock.After(s.TTL / time.Duration(s.Retries)):
err := s.KeepAliveCycle()
if err == nil {
continue
case tick := <-lock.ticker.C:
leaseContext, leaseCancel := context.WithDeadline(ctx, lease.Expires)
nextLease := lease
nextLease.Expires = tick.Add(lock.cfg.Expiry)
for {
err = lock.cfg.Service.KeepAliveSemaphoreLease(leaseContext, nextLease)
if trace.IsNotFound(err) {
leaseCancel()
// semaphore and/or lease no longer exist; best to log the error
// and exit immediately.
log.Warnf("Halting keepalive on semaphore %s/%s early: %v", lease.SemaphoreKind, lease.SemaphoreName, err)
nodrop = true
return
}
if err == nil {
leaseCancel()
lease = nextLease
lock.retry.Reset()
select {
case lock.renewalC <- struct{}{}:
default:
}
continue Outer
}
log.Debugf("Failed to renew semaphore lease %s/%s: %v", lease.SemaphoreKind, lease.SemaphoreName, err)
lock.retry.Inc()
select {
case <-lock.retry.After():
case <-leaseContext.Done():
leaseCancel() // demanded by linter
return
case <-lock.Done():
leaseCancel()
return
}
}
if trace.IsCompareFailed(err) {
log.WithError(err).Debugf("Semaphore is being modified, retrying %v.", s.Lease)
continue
}
if trace.IsConnectionProblem(err) {
log.WithError(err).Debugf("Connection problem, retrying %v.", s.Lease)
continue
}
log.WithError(err).Warningf("Failed to keep alive lease %v.", s.Lease)
case <-ctx.Done():
return
case <-s.ctx.Done():
case <-lock.Done():
return
}
}
}
// Done indicates that either semaphore has been closed
// or some other error occured and semaphore no longer holds the lock
func (s *SemaphoreLock) Done() <-chan struct{} {
return s.ctx.Done()
}
// Close stops lease renewal attempts
func (s *SemaphoreLock) Close() error {
s.cancel()
return nil
}
// AcquireSemaphore blocks until the context expires or it successfully acquires required amount of resources
// if lease.ID is not specified, this method generates one.
// KeepAlive should be started in a separate goroutine explicitly
// Lock should be closed if to release associated KeepAlive goroutine
func AcquireSemaphore(ctx context.Context, lock SemaphoreLockConfig) (*SemaphoreLock, error) {
if err := lock.CheckAndSetDefaults(); err != nil {
// AcquireSemaphoreLock attempts to acquire and hold a semaphore lease. If successfully acquired,
// background keepalive processes are started and an associated lock handle is returned. Cancelling
// the supplied context releases the semaphore.
func AcquireSemaphoreLock(ctx context.Context, cfg SemaphoreLockConfig) (*SemaphoreLock, error) {
if err := cfg.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
for {
lock.Lease.Expires = lock.Clock.Now().UTC().Add(lock.TTL)
out, err := lock.Service.TryAcquireSemaphore(ctx, lock.Semaphore, lock.Lease)
if err == nil {
ctx, cancel := context.WithCancel(context.TODO())
lock.Lease = *out
l := &SemaphoreLock{
SemaphoreLockConfig: lock,
ctx: ctx,
cancel: cancel,
}
return l, nil
}
// Optimistic locking failed, retry after wait
if !trace.IsCompareFailed(err) {
return nil, trace.Wrap(err)
}
log.WithError(err).Debugf("Failed to acquire, going to sleep for %v", lock.Retry.Duration())
select {
case <-lock.Clock.After(lock.Retry.Duration()):
continue
case <-ctx.Done():
return nil, trace.ConnectionProblem(ctx.Err(), "context is cancelled")
}
// set up retry with a ratio which will result in 3-4 retries before the lease expires
retry, err := utils.NewLinear(utils.LinearConfig{
Max: cfg.Expiry / 4,
Step: cfg.Expiry / 16,
Jitter: utils.NewJitter(),
})
if err != nil {
return nil, trace.Wrap(err)
}
lease, err := cfg.Service.AcquireSemaphore(ctx, cfg.Params)
if err != nil {
return nil, trace.Wrap(err)
}
lock := &SemaphoreLock{
cfg: cfg,
lease0: *lease,
retry: retry,
ticker: time.NewTicker(cfg.TickRate),
doneC: make(chan struct{}),
renewalC: make(chan struct{}),
cond: sync.NewCond(&sync.Mutex{}),
}
return lock, nil
}
// NewSemaphore is a convenience wrapper to create a Semaphore
func NewSemaphore(name, subKind string, expires time.Time, spec SemaphoreSpecV3) (Semaphore, error) {
cc := SemaphoreV3{
// Check verifies that all required parameters have been supplied.
func (s *AcquireSemaphoreRequest) Check() error {
if s.SemaphoreKind == "" {
return trace.BadParameter("missing parameter SemaphoreKind")
}
if s.SemaphoreName == "" {
return trace.BadParameter("missing parameter SemaphoreName")
}
if s.MaxLeases == 0 {
return trace.BadParameter("missing parameter MaxLeases")
}
if s.Expires.IsZero() {
return trace.BadParameter("missing parameter Expires")
}
return nil
}
// ConfigureSemaphore configures an empty semaphore resource matching
// these acquire parameters.
func (s *AcquireSemaphoreRequest) ConfigureSemaphore() (Semaphore, error) {
sem := SemaphoreV3{
Kind: KindSemaphore,
SubKind: subKind,
SubKind: s.SemaphoreKind,
Version: V3,
Metadata: Metadata{
Name: name,
Name: s.SemaphoreName,
Namespace: defaults.Namespace,
},
Spec: spec,
}
cc.SetExpiry(expires)
if err := cc.CheckAndSetDefaults(); err != nil {
sem.SetExpiry(s.Expires)
if err := sem.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
return &cc, nil
return &sem, nil
}
// Semaphore represents distributed semaphore concept
type Semaphore interface {
// Resource contains common resource values
Resource
// CheckAndSetDefaults checks and sets default parameter
// CheckAndSetDefaults checks and sets default parameters
CheckAndSetDefaults() error
// GetMaxResources returns maximum available amount
// of resources consumed by the semaphore
GetMaxResources() int64
// AddLease adds a lease to the list
AddLease(SemaphoreLease)
// SetLeases sets the lease list to the value
SetLeases([]SemaphoreLease)
// RemoveLease removes lease from the list
RemoveLease(l SemaphoreLease) error
// GetLeases returns all leases used by semaphore
GetLeases() []SemaphoreLease
// AcquiredResources computes and returns the amount of acquired resources
AcquiredResources() int64
// Contains checks if lease is member of this semaphore.
Contains(lease SemaphoreLease) bool
// Acquire attempts to acquire a lease with this semaphore.
Acquire(leaseID string, params AcquireSemaphoreRequest) (*SemaphoreLease, error)
// KeepAlive attempts to update the expiry of an existent lease.
KeepAlive(lease SemaphoreLease) error
// Cancel attempts to cancel an existent lease.
Cancel(lease SemaphoreLease) error
// LeaseRefs grants access to the underlying list
// of lease references.
LeaseRefs() []SemaphoreLeaseRef
// RemoveExpiredLeases removes expired leases
RemoveExpiredLeases(time.Time)
RemoveExpiredLeases(now time.Time)
}
// CheckAndSetDefaults checks and sets default values
func (l *SemaphoreLease) CheckAndSetDefaults() error {
if l == nil {
return trace.BadParameter("missing lease object")
}
if l.Resources <= 0 {
return trace.BadParameter("parameter Resources should be >= 0")
if l.SemaphoreKind == "" {
return trace.BadParameter("missing parameter SemaphoreKind")
}
if l.SemaphoreName == "" {
return trace.BadParameter("missing parameter SemaphoreName")
}
if l.SemaphoreSubKind == "" {
return trace.BadParameter("missing parameter SemaphoreSubKind")
}
if l.ID == "" {
return trace.BadParameter("missing parameter ID")
if l.LeaseID == "" {
return trace.BadParameter("missing parameter LeaseID")
}
if l.Expires.IsZero() {
return trace.BadParameter("set lease expiry time")
return trace.BadParameter("missing lease expiry time")
}
return nil
}
// Contains checks if lease is member of this semaphore.
func (c *SemaphoreV3) Contains(lease SemaphoreLease) bool {
if lease.SemaphoreKind != c.GetSubKind() || lease.SemaphoreName != c.GetName() {
return false
}
for _, ref := range c.Spec.Leases {
if ref.LeaseID == lease.LeaseID {
return true
}
}
return false
}
// Acquire attempts to acquire a lease with this semaphore.
func (c *SemaphoreV3) Acquire(leaseID string, params AcquireSemaphoreRequest) (*SemaphoreLease, error) {
if params.SemaphoreKind != c.GetSubKind() || params.SemaphoreName != c.GetName() {
return nil, trace.BadParameter("cannot acquire, params do not match")
}
if c.leaseCount() >= params.MaxLeases {
return nil, trace.LimitExceeded("cannot acquire semaphore %s/%s (%s)",
c.GetSubKind(),
c.GetName(),
teleport.MaxLeases,
)
}
for _, ref := range c.Spec.Leases {
if ref.LeaseID == leaseID {
return nil, trace.AlreadyExists("semaphore lease already exists: %q", leaseID)
}
}
if params.Expires.After(c.Expiry()) {
c.SetExpiry(params.Expires)
}
c.Spec.Leases = append(c.Spec.Leases, SemaphoreLeaseRef{
LeaseID: leaseID,
Expires: params.Expires,
Holder: params.Holder,
})
return &SemaphoreLease{
SemaphoreKind: params.SemaphoreKind,
SemaphoreName: params.SemaphoreName,
LeaseID: leaseID,
Expires: params.Expires,
}, nil
}
// KeepAlive attempts to update the expiry of an existent lease.
func (c *SemaphoreV3) KeepAlive(lease SemaphoreLease) error {
if lease.SemaphoreKind != c.GetSubKind() || lease.SemaphoreName != c.GetName() {
return trace.BadParameter("cannot keepalive, lease does not match")
}
for i := range c.Spec.Leases {
if c.Spec.Leases[i].LeaseID == lease.LeaseID {
c.Spec.Leases[i].Expires = lease.Expires
if lease.Expires.After(c.Expiry()) {
c.SetExpiry(lease.Expires)
}
return nil
}
}
return trace.NotFound("cannot keepalive, lease not found: %q", lease.LeaseID)
}
// Cancel attempts to cancel an existent lease.
func (c *SemaphoreV3) Cancel(lease SemaphoreLease) error {
if lease.SemaphoreKind != c.GetSubKind() || lease.SemaphoreName != c.GetName() {
return trace.BadParameter("cannot cancel, lease does not match")
}
for i, ref := range c.Spec.Leases {
if ref.LeaseID == lease.LeaseID {
c.Spec.Leases = append(c.Spec.Leases[:i], c.Spec.Leases[i+1:]...)
return nil
}
}
return trace.NotFound("cannot cancel, lease not found: %q", lease.LeaseID)
}
// RemoveExpiredLeases removes expired leases
func (c *SemaphoreV3) RemoveExpiredLeases(now time.Time) {
// See https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
@ -252,45 +421,17 @@ func (c *SemaphoreV3) RemoveExpiredLeases(now time.Time) {
c.Spec.Leases = filtered
}
// AcquiredResources computes and returns the amount of acquired resources
func (c *SemaphoreV3) AcquiredResources() int64 {
total := int64(0)
for _, lease := range c.Spec.Leases {
total += lease.Resources
}
return total
// leaseCount returns the number of active leases
func (c *SemaphoreV3) leaseCount() int64 {
return int64(len(c.Spec.Leases))
}
// AddLease adds a lease to the list
func (c *SemaphoreV3) AddLease(l SemaphoreLease) {
c.Spec.Leases = append(c.Spec.Leases, l)
}
// SetLeases sets leases list
func (c *SemaphoreV3) SetLeases(l []SemaphoreLease) {
c.Spec.Leases = l
}
// RemoveLease removes lease from the list
func (c *SemaphoreV3) RemoveLease(l SemaphoreLease) error {
for i := range c.Spec.Leases {
if c.Spec.Leases[i].ID == l.ID {
c.Spec.Leases = append(c.Spec.Leases[:i], c.Spec.Leases[i+1:]...)
return nil
}
}
return trace.NotFound("lease %v is not found", l.ID)
}
// GetLeases returns all leases used by semaphore
func (c *SemaphoreV3) GetLeases() []SemaphoreLease {
// LeaseRefs grants access to the underlying list
// of lease references
func (c *SemaphoreV3) LeaseRefs() []SemaphoreLeaseRef {
return c.Spec.Leases
}
func (c *SemaphoreV3) GetMaxResources() int64 {
return c.Spec.MaxResources
}
// GetVersion returns resource version
func (c *SemaphoreV3) GetVersion() string {
return c.Version
@ -353,8 +494,8 @@ func (c *SemaphoreV3) GetMetadata() Metadata {
// String represents a human readable version of the semaphore.
func (c *SemaphoreV3) String() string {
return fmt.Sprintf("Semaphore(%v, subKind=%v, maxResources=%v)",
c.Metadata.Name, c.SubKind, c.Spec.MaxResources)
return fmt.Sprintf("Semaphore(kind=%v, name=%v, leases=%v)",
c.SubKind, c.Metadata.Name, c.leaseCount())
}
// CheckAndSetDefaults checks validity of all parameters and sets defaults.
@ -379,12 +520,23 @@ func (c *SemaphoreV3) CheckAndSetDefaults() error {
// SemaphoreSpecSchemaTemplate is a template for Semaphore schema.
const SemaphoreSpecSchemaTemplate = `{
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"leases": {
"type": "array",
"items": {
"type": "object",
"properties": {
"lease_id": { "type": "string" },
"expires": { "type": "string" },
"holder": { "type": "string" }
}
}
}
}
}`
// GetSemaphoreSchema returns the validattion schema for this object
// GetSemaphoreSchema returns the validation schema for this object
func GetSemaphoreSchema() string {
return fmt.Sprintf(V2SchemaTemplate, MetadataSchema, SemaphoreSpecSchemaTemplate, DefaultDefinitions)
}

View file

@ -0,0 +1,82 @@
/*
Copyright 2020 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 services
import (
"time"
"github.com/gravitational/teleport/lib/utils"
"gopkg.in/check.v1"
)
type SemaphoreSuite struct {
}
var _ = check.Suite(&SemaphoreSuite{})
func (s *SemaphoreSuite) SetUpSuite(c *check.C) {
utils.InitLoggerForTests()
}
func (s *SemaphoreSuite) TestAcquireSemaphoreRequest(c *check.C) {
ok := AcquireSemaphoreRequest{
SemaphoreKind: "foo",
SemaphoreName: "bar",
MaxLeases: 1,
Expires: time.Now(),
}
ok2 := ok
c.Assert(ok.Check(), check.IsNil)
c.Assert(ok2.Check(), check.IsNil)
// Check that all the required fields have their
// zero values rejected.
bad := ok
bad.SemaphoreKind = ""
c.Assert(bad.Check(), check.NotNil)
bad = ok
bad.SemaphoreName = ""
c.Assert(bad.Check(), check.NotNil)
bad = ok
bad.MaxLeases = 0
c.Assert(bad.Check(), check.NotNil)
bad = ok
bad.Expires = time.Time{}
c.Assert(bad.Check(), check.NotNil)
// ensure that well formed acquire params can configure
// a well formed semaphore.
sem, err := ok.ConfigureSemaphore()
c.Assert(err, check.IsNil)
// verify acquisition works and semaphore state is
// correctly updated.
lease, err := sem.Acquire("sem-id", ok)
c.Assert(err, check.IsNil)
c.Assert(sem.Contains(*lease), check.Equals, true)
// verify keepalive succeeds and correctly updates
// semaphore expiry.
newLease := *lease
newLease.Expires = sem.Expiry().Add(time.Second)
c.Assert(sem.KeepAlive(newLease), check.IsNil)
c.Assert(sem.Expiry(), check.Equals, newLease.Expires)
c.Assert(sem.Cancel(newLease), check.IsNil)
c.Assert(sem.Contains(newLease), check.Equals, false)
}

View file

@ -25,6 +25,8 @@ import (
"encoding/base64"
"fmt"
"sort"
"sync"
"sync/atomic"
"time"
"github.com/gravitational/teleport"
@ -1011,261 +1013,205 @@ func (s *ServicesTestSuite) ClusterConfig(c *check.C, opts ...SuiteOption) {
fixtures.DeepCompare(c, clusterName, gotName)
}
// Semaphore tests semaphore basic operations
func (s *ServicesTestSuite) Semaphore(c *check.C) {
// non-expiring semaphores are not allowed
_, err := services.NewSemaphore("alice", services.KindUser, time.Time{}, services.SemaphoreSpecV3{
MaxResources: 2,
})
fixtures.ExpectBadParameter(c, err)
expires := s.Clock.Now().Add(time.Hour).UTC()
sem, err := services.NewSemaphore("alice", services.KindUser, expires, services.SemaphoreSpecV3{
MaxResources: 2,
})
c.Assert(err, check.IsNil)
lease := services.SemaphoreLease{
ID: "1",
Resources: 1,
Expires: s.Clock.Now().Add(time.Hour).UTC(),
}
out, err := s.PresenceS.TryAcquireSemaphore(context.TODO(), sem, lease)
c.Assert(err, check.IsNil)
c.Assert(out, check.NotNil)
sems, err := s.PresenceS.GetAllSemaphores(context.TODO())
c.Assert(err, check.IsNil)
c.Assert(sems, check.HasLen, 1)
// lease tries to acquire too many resources
newLease := services.SemaphoreLease{
ID: "2",
Resources: 2,
Expires: s.Clock.Now().Add(time.Hour).UTC(),
}
out, err = s.PresenceS.TryAcquireSemaphore(context.TODO(), sem, newLease)
fixtures.ExpectLimitExceeded(c, err)
c.Assert(out, check.IsNil)
// lease acquires enough resources
newLease = services.SemaphoreLease{
SemaphoreName: sem.GetName(),
SemaphoreSubKind: sem.GetSubKind(),
ID: "2",
Resources: 1,
Expires: s.Clock.Now().Add(time.Hour).UTC(),
}
out, err = s.PresenceS.TryAcquireSemaphore(context.TODO(), sem, newLease)
c.Assert(err, check.IsNil)
c.Assert(out, check.NotNil)
// Renew the lease
newLease.Expires = s.Clock.Now().Add(time.Hour).UTC()
err = s.PresenceS.KeepAliveSemaphoreLease(context.TODO(), newLease)
c.Assert(err, check.IsNil)
// Can't renew nonexistent lease
badLease := newLease
badLease.ID = "not here"
err = s.PresenceS.KeepAliveSemaphoreLease(context.TODO(), badLease)
fixtures.ExpectNotFound(c, err)
// Can't renew expired lease
expiredLease := newLease
expiredLease.Expires = time.Now().Add(-1 * time.Hour)
err = s.PresenceS.KeepAliveSemaphoreLease(context.TODO(), expiredLease)
fixtures.ExpectBadParameter(c, err)
err = s.PresenceS.DeleteAllSemaphores(context.TODO())
c.Assert(err, check.IsNil)
sems, err = s.PresenceS.GetAllSemaphores(context.TODO())
c.Assert(err, check.IsNil)
c.Assert(sems, check.HasLen, 0)
// sem wrapper is a helper for overriding the keepalive
// method on the semaphore service.
type semWrapper struct {
services.Semaphores
keepAlive func(context.Context, services.SemaphoreLease) error
}
// SemaphoreExpiry tests semaphore and lease expiry
func (s *ServicesTestSuite) SemaphoreExpiry(c *check.C) {
expires := s.Clock.Now().Add(1 * time.Second).UTC()
sem, err := services.NewSemaphore("alice", services.KindUser, expires, services.SemaphoreSpecV3{
MaxResources: 2,
})
c.Assert(err, check.IsNil)
// lease acquires all the resources
lease := services.SemaphoreLease{
ID: "1",
Resources: 2,
Expires: expires,
}
out, err := s.PresenceS.TryAcquireSemaphore(context.TODO(), sem, lease)
c.Assert(err, check.IsNil)
c.Assert(out, check.NotNil)
// lease renews the lease
out.Expires = expires.Add(3 * time.Second)
err = s.PresenceS.KeepAliveSemaphoreLease(context.TODO(), *out)
c.Assert(err, check.IsNil)
// If previous keep alive haven't reneweed the lease, semaphore would have expired
s.Clock.Advance(2 * time.Second)
sems, err := s.PresenceS.GetAllSemaphores(context.TODO())
c.Assert(err, check.IsNil)
c.Assert(sems, check.HasLen, 1)
// lease 2 can't acquire resources
lease2 := services.SemaphoreLease{
ID: "2",
Resources: 2,
Expires: s.Clock.Now().Add(3 * time.Second),
}
_, err = s.PresenceS.TryAcquireSemaphore(context.TODO(), sem, lease2)
fixtures.ExpectLimitExceeded(c, err)
// wait until lease expries semaphore should be reacquired
lease2.Expires = s.Clock.Now().Add(10 * time.Second)
s.Clock.Advance(5 * time.Second)
_, err = s.PresenceS.TryAcquireSemaphore(context.TODO(), sem, lease2)
c.Assert(err, check.IsNil)
// wait until semaphore expires
sems, err = s.PresenceS.GetAllSemaphores(context.TODO())
c.Assert(err, check.IsNil)
c.Assert(sems, check.HasLen, 1)
s.Clock.Advance(11 * time.Second)
sems, err = s.PresenceS.GetAllSemaphores(context.TODO())
c.Assert(err, check.IsNil)
c.Assert(sems, check.HasLen, 0)
func (w *semWrapper) KeepAliveSemaphoreLease(ctx context.Context, lease services.SemaphoreLease) error {
return w.keepAlive(ctx, lease)
}
// SemaphoreLock tests semaphore lock high level methods
func (s *ServicesTestSuite) SemaphoreLock(c *check.C) {
expires := s.Clock.Now().Add(1 * time.Second).UTC()
sem, err := services.NewSemaphore("alice", services.KindUser, expires, services.SemaphoreSpecV3{
MaxResources: 2,
})
c.Assert(err, check.IsNil)
ctx := context.TODO()
lock, err := services.AcquireSemaphore(ctx, services.SemaphoreLockConfig{
Clock: s.Clock,
Service: s.PresenceS,
Semaphore: sem,
Lease: services.SemaphoreLease{
ID: "lease-1",
Resources: 1,
func (s *ServicesTestSuite) SemaphoreFlakiness(c *check.C) {
const renewals = 3
// wrap our services.Semaphores instance to cause two out of three lease
// keepalive attempts fail with a meaningless error. Locks should make
// at *least* three attempts before their expiry, so locks should not
// fail under these conditions.
keepAlives := new(uint64)
wrapper := &semWrapper{
Semaphores: s.PresenceS,
keepAlive: func(ctx context.Context, lease services.SemaphoreLease) error {
kn := atomic.AddUint64(keepAlives, 1)
if kn%3 == 0 {
return s.PresenceS.KeepAliveSemaphoreLease(ctx, lease)
}
return trace.Errorf("uh-oh!")
},
})
defer lock.Close()
c.Assert(err, check.IsNil)
c.Assert(lock, check.NotNil)
}
// lease attempts to acquire too many resources
_, err = services.AcquireSemaphore(ctx, services.SemaphoreLockConfig{
Clock: s.Clock,
Service: s.PresenceS,
Semaphore: sem,
Lease: services.SemaphoreLease{
ID: "lease-greedy",
Resources: 2,
cfg := services.SemaphoreLockConfig{
Service: wrapper,
Expiry: time.Second,
TickRate: time.Millisecond * 50,
Params: services.AcquireSemaphoreRequest{
SemaphoreKind: services.SemaphoreKindConnection,
SemaphoreName: "alice",
MaxLeases: 1,
},
})
fixtures.ExpectLimitExceeded(c, err)
}
// acquire one resource left
lock2, err := services.AcquireSemaphore(ctx, services.SemaphoreLockConfig{
Clock: s.Clock,
Service: s.PresenceS,
Semaphore: sem,
Lease: services.SemaphoreLease{
ID: "lease-2",
Resources: 1,
},
})
defer lock2.Close()
c.Assert(err, check.IsNil)
c.Assert(lock2, check.NotNil)
// Advance the clock and run keep-alive cycle loop on the first lock
s.Clock.Advance(2 * lock.TTL / 3)
err = lock.KeepAliveCycle()
c.Assert(err, check.IsNil)
// Advance the clock past initial TTL boundary, the lock 2 should expire
// and attempt to renew the lease on it should fail
s.Clock.Advance(lock.TTL/3 + time.Second)
err = lock2.KeepAliveCycle()
fixtures.ExpectNotFound(c, err)
// this one should succeed
err = lock.KeepAliveCycle()
c.Assert(err, check.IsNil)
// After TTL passed, the lease is not found and semaphore has expired
s.Clock.Advance(lock.TTL * 2)
err = lock.KeepAliveCycle()
fixtures.ExpectNotFound(c, err)
}
// SemaphoreConcurrency tests concurrent lock functionality
func (s *ServicesTestSuite) SemaphoreConcurrency(c *check.C) {
const maxResources = 2
expires := s.Clock.Now().Add(1 * time.Second).UTC()
sem, err := services.NewSemaphore("bob", services.KindUser, expires, services.SemaphoreSpecV3{
MaxResources: maxResources,
})
c.Assert(err, check.IsNil)
// out of 20 locks, 2 should succeed, others should time out
ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second)
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
locksC := make(chan *services.SemaphoreLock, 2)
for i := 0; i < 20; i++ {
go func(threadID int) {
lock, err := services.AcquireSemaphore(ctx, services.SemaphoreLockConfig{
Service: s.PresenceS,
Semaphore: sem,
Lease: services.SemaphoreLease{
ID: fmt.Sprintf("lease-%v", threadID),
Resources: 1,
},
})
if err != nil {
return
lock, err := services.AcquireSemaphoreLock(ctx, cfg)
c.Assert(err, check.IsNil)
go lock.KeepAlive(ctx)
for i := 0; i < renewals; i++ {
select {
case <-lock.Renewed():
continue
case <-lock.Done():
c.Fatalf("Lost semaphore lock: %v", lock.Wait())
case <-time.After(time.Second):
c.Fatalf("Timeout waiting for renewals")
}
}
}
// SemaphoreContention checks that a large number of concurrent acquisitions
// all succeed if MaxLeases is sufficiently high. Note that we do not test
// the same property holds for releasing semaphores; release operations are
// best-effort and allowed to fail. Also, "large" in this context is still
// fairly small. Semaphores aren't cheap and the auth server is expected
// to start returning "too much contention" errors at around 100 concurrent
// attempts.
func (s *ServicesTestSuite) SemaphoreContention(c *check.C) {
const locks int64 = 50
const iters = 5
for i := 0; i < iters; i++ {
cfg := services.SemaphoreLockConfig{
Service: s.PresenceS,
Expiry: time.Hour,
Params: services.AcquireSemaphoreRequest{
SemaphoreKind: services.SemaphoreKindConnection,
SemaphoreName: "alice",
MaxLeases: locks,
},
}
// we leak lock handles in the spawned goroutines, so
// context-based cancellation is needed to cleanup the
// background keepalive activity.
ctx, cancel := context.WithCancel(context.TODO())
var wg sync.WaitGroup
for i := int64(0); i < locks; i++ {
wg.Add(1)
go func() {
defer wg.Done()
lock, err := services.AcquireSemaphoreLock(ctx, cfg)
c.Assert(err, check.IsNil)
go lock.KeepAlive(ctx)
}()
}
wg.Wait()
cancel()
c.Assert(s.PresenceS.DeleteSemaphore(context.TODO(), services.SemaphoreFilter{
SemaphoreKind: cfg.Params.SemaphoreKind,
SemaphoreName: cfg.Params.SemaphoreName,
}), check.IsNil)
}
}
// SemaphoreConcurrency verifies that a large number of concurrent
// acquisitions result in the correct number of successful acquisitions.
func (s *ServicesTestSuite) SemaphoreConcurrency(c *check.C) {
const maxLeases int64 = 20
const attempts int64 = 200
cfg := services.SemaphoreLockConfig{
Service: s.PresenceS,
Expiry: time.Hour,
Params: services.AcquireSemaphoreRequest{
SemaphoreKind: services.SemaphoreKindConnection,
SemaphoreName: "alice",
MaxLeases: maxLeases,
},
}
// we leak lock handles in the spawned goroutines, so
// context-based cancellation is needed to cleanup the
// background keepalive activity.
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
var success int64
var failure int64
var wg sync.WaitGroup
for i := int64(0); i < attempts; i++ {
wg.Add(1)
go func() {
lock, err := services.AcquireSemaphoreLock(ctx, cfg)
if err == nil {
go lock.KeepAlive(ctx)
atomic.AddInt64(&success, 1)
} else {
atomic.AddInt64(&failure, 1)
}
select {
case locksC <- lock:
case <-ctx.Done():
lock.Close()
return
}
}(i)
wg.Done()
}()
}
wg.Wait()
c.Assert(atomic.LoadInt64(&success), check.Equals, maxLeases)
c.Assert(atomic.LoadInt64(&failure), check.Equals, attempts-maxLeases)
}
// SemaphoreLock verifies correct functionality of the basic
// semaphore lock scenarios.
func (s *ServicesTestSuite) SemaphoreLock(c *check.C) {
cfg := services.SemaphoreLockConfig{
Service: s.PresenceS,
Expiry: time.Hour,
Params: services.AcquireSemaphoreRequest{
SemaphoreKind: services.SemaphoreKindConnection,
SemaphoreName: "alice",
MaxLeases: 1,
},
}
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
lock, err := services.AcquireSemaphoreLock(ctx, cfg)
c.Assert(err, check.IsNil)
go lock.KeepAlive(ctx)
// MaxLeases is 1, so second acquire op fails.
_, err = services.AcquireSemaphoreLock(ctx, cfg)
fixtures.ExpectLimitExceeded(c, err)
// Lock is successfully released.
lock.Stop()
c.Assert(lock.Wait(), check.IsNil)
// Acquire new lock with short expiry
// and high tick rate to verify renewals.
cfg.Expiry = time.Second
cfg.TickRate = time.Millisecond * 50
lock, err = services.AcquireSemaphoreLock(ctx, cfg)
c.Assert(err, check.IsNil)
go lock.KeepAlive(ctx)
timeout := time.After(time.Second)
for i := 0; i < 3; i++ {
select {
case <-lock.Done():
c.Fatalf("Unexpected lock failure: %v", lock.Wait())
case <-timeout:
c.Fatalf("Timeout waiting for lock renewal %d", i)
case <-lock.Renewed():
}
}
locks := []*services.SemaphoreLock{}
defer func() {
for _, l := range locks {
l.Close()
}
}()
for i := 0; i < maxResources; i++ {
select {
case lock := <-locksC:
locks = append(locks, lock)
case <-ctx.Done():
c.Fatalf("Timeout waiting for acquire lock to complete")
}
}
// make sure no additional locks are acquired
// forcibly delete the semaphore
c.Assert(s.PresenceS.DeleteSemaphore(context.TODO(), services.SemaphoreFilter{
SemaphoreKind: cfg.Params.SemaphoreKind,
SemaphoreName: cfg.Params.SemaphoreName,
}), check.IsNil)
select {
case lock := <-locksC:
c.Fatalf("Unexpected lock acquisition: %+v", lock)
case <-ctx.Done():
case <-lock.Done():
fixtures.ExpectNotFound(c, lock.Wait())
case <-time.After(time.Millisecond * 1500):
c.Errorf("timeout waiting for semaphore lock failure")
}
}

File diff suppressed because it is too large Load diff

View file

@ -396,6 +396,12 @@ message ClusterConfigSpecV3 {
// LocalAuth is true if local authentication is enabled.
bool LocalAuth = 9 [ (gogoproto.jsontag) = "local_auth", (gogoproto.casttype) = "Bool" ];
// SessionControlTimeout is the session control lease expiry and defines
// the upper limit of how long a node may be out of contact with the auth
// server before it begins terminating controlled sessions.
int64 SessionControlTimeout = 10
[ (gogoproto.jsontag) = "session_control_timeout", (gogoproto.casttype) = "Duration" ];
}
// AuditConfig represents audit log settings in the cluster
@ -701,6 +707,14 @@ message RoleOptions {
(gogoproto.jsontag) = "permit_x11_forwarding,omitempty",
(gogoproto.casttype) = "Bool"
];
// MaxConnections defines the maximum number of
// concurrent connections a user may hold.
int64 MaxConnections = 9 [ (gogoproto.jsontag) = "max_connections,omitempty" ];
// MaxSessions defines the maximum number of
// concurrent sessions per connection.
int64 MaxSessions = 10 [ (gogoproto.jsontag) = "max_sessions,omitempty" ];
}
// RoleConditions is a set of conditions that must all match to be allowed or
@ -976,29 +990,65 @@ message TunnelConnectionSpecV2 {
string Type = 4 [ (gogoproto.jsontag) = "type", (gogoproto.casttype) = "TunnelType" ];
}
// SemaphoreLeaseOwner holds semaphore lease owner information
message SemaphoreLeaseOwner {
// ServerName is a name of the server
string ServerName = 1 [ (gogoproto.jsontag) = "server_name" ];
// SemaphoreFilter encodes semaphore filtering params.
// A semaphore filter matches a semaphore if all nonzero fields
// match the corresponding semaphore fileds (e.g. a filter which
// specifies only `kind=foo` would match all semaphores of
// kind `foo`).
message SemaphoreFilter {
// SemaphoreKind is the kind of the semaphore.
string SemaphoreKind = 1 [ (gogoproto.jsontag) = "kind" ];
// SemaphoreName is the name of the semaphore.
string SemaphoreName = 2 [ (gogoproto.jsontag) = "name" ];
}
// AcquireSemaphoreRequest holds semaphore lease acquisition parameters.
message AcquireSemaphoreRequest {
// SemaphoreKind is the kind of the semaphore.
string SemaphoreKind = 1 [ (gogoproto.jsontag) = "kind" ];
// SemaphoreName is the name of the semaphore.
string SemaphoreName = 2 [ (gogoproto.jsontag) = "name" ];
// MaxLeases is the maximum number of concurrent leases. If acquisition
// would cause more than MaxLeases to exist, acquisition must fail.
int64 MaxLeases = 3 [ (gogoproto.jsontag) = "max_resources" ];
// Expires is the time at which this lease expires.
google.protobuf.Timestamp Expires = 4 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "expires"
];
// Holder identifies the entitiy holding the lease.
string Holder = 5 [ (gogoproto.jsontag) = "holder" ];
}
// SemaphoreLease represents lease acquired for semaphore
message SemaphoreLease {
// ID is an auto generated ID of the lease
string ID = 1 [ (gogoproto.jsontag) = "id" ];
// SemaphoreName is a name of the semaphore
string SemaphoreName = 2 [ (gogoproto.jsontag) = "semaphore_name" ];
// SemaphoreSubKind is a subkind of the semaphore
string SemaphoreSubKind = 3 [ (gogoproto.jsontag) = "semaphore_subkind" ];
// Resources is amount of resources acquired by this lease
int64 Resources = 4 [ (gogoproto.jsontag) = "resources" ];
// Expires is set to update expiry time
// SemaphoreKind is the kind of the semaphore.
string SemaphoreKind = 1 [ (gogoproto.jsontag) = "kind" ];
// SemaphoreName is the name of the semaphore.
string SemaphoreName = 2 [ (gogoproto.jsontag) = "name" ];
// LeaseID uniquely identifies this lease.
string LeaseID = 3 [ (gogoproto.jsontag) = "lease_id" ];
// Expires is the time at which this lease expires.
google.protobuf.Timestamp Expires = 5 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "expires"
];
SemaphoreLeaseOwner Owner = 6 [ (gogoproto.jsontag) = "owner" ];
}
// SemaphoreLeaseRef identifies an existent lease.
message SemaphoreLeaseRef {
// LeaseID is the unique ID of the lease.
string LeaseID = 1 [ (gogoproto.jsontag) = "lease_id" ];
// Expires is the time at which the lease expires.
google.protobuf.Timestamp Expires = 2 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "expires"
];
// Holder identifies the lease holder.
string Holder = 3 [ (gogoproto.jsontag) = "holder" ];
}
// SemaphoreV3 implements Semaphore interface
@ -1012,7 +1062,7 @@ message SemaphoreV3 {
string SubKind = 2 [ (gogoproto.jsontag) = "sub_kind,omitempty" ];
// Version is version
string Version = 3 [ (gogoproto.jsontag) = "version" ];
// Metadata is User metadata
// Metadata is Semaphore metadata
Metadata Metadata = 4 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "metadata" ];
// Spec is a lease V3 spec
SemaphoreSpecV3 Spec = 5 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "spec" ];
@ -1020,9 +1070,7 @@ message SemaphoreV3 {
// SemaphoreSpecV3 contains the data about lease
message SemaphoreSpecV3 {
// MaxResources is a maximum number of allowed leases for this semaphore
int64 MaxResources = 1 [ (gogoproto.jsontag) = "max_resources" ];
// Leases is a list of leases acquired
repeated SemaphoreLease Leases = 2
// Leases is a list of all currently acquired leases.
repeated SemaphoreLeaseRef Leases = 1
[ (gogoproto.nullable) = false, (gogoproto.jsontag) = "leases" ];
}

View file

@ -741,9 +741,10 @@ func (s *Server) handleSessionChannel(ctx context.Context, nch ssh.NewChannel) {
scx.ChannelType = teleport.ChanSession
defer scx.Close()
ch = scx.TrackActivity(ch)
// Create a "session" channel on the remote host.
// Create a "session" channel on the remote host. Note that we
// create the remote session channel before accepting the local
// channel request; this allows us to propagate the rejection
// reason/message in the event the channel is rejected.
remoteSession, err := s.remoteClient.NewSession()
if err != nil {
s.log.Warnf("Remote session open failed: %v", err)
@ -758,6 +759,19 @@ func (s *Server) handleSessionChannel(ctx context.Context, nch ssh.NewChannel) {
}
scx.RemoteSession = remoteSession
// Accept the session channel request
ch, in, err := nch.Accept()
if err != nil {
s.log.Warnf("Unable to accept channel: %v", err)
if err := nch.Reject(ssh.ConnectionFailed, fmt.Sprintf("unable to accept channel: %v", err)); err != nil {
s.log.Warnf("Failed to reject channel: %v", err)
}
return
}
scx.AddCloser(ch)
ch = scx.TrackActivity(ch)
s.log.Debugf("Opening session request to %v in context %v.", s.sconn.RemoteAddr(), scx.ID())
defer s.log.Debugf("Closing session request to %v in context %v.", s.sconn.RemoteAddr(), scx.ID())

View file

@ -561,6 +561,7 @@ func New(addr utils.NetAddr,
sshutils.AuthMethods{PublicKey: s.authHandlers.UserKeyAuth},
sshutils.SetLimiter(s.limiter),
sshutils.SetRequestHandler(s),
sshutils.SetNewConnHandler(s),
sshutils.SetCiphers(s.ciphers),
sshutils.SetKEXAlgorithms(s.kexAlgorithms),
sshutils.SetMACAlgorithms(s.macAlgorithms),
@ -860,6 +861,74 @@ func (s *Server) HandleRequest(r *ssh.Request) {
}
}
// HandleNewConn is called by sshutils.Server once for each new incoming connection,
// prior to handling any channels or requests. Currently this callback's only
// function is to apply concurrent session control limits.
func (s *Server) HandleNewConn(ctx context.Context, ccx *sshutils.ConnectionContext) (context.Context, error) {
// we don't currently have any work to do in non-node contexts.
if s.Component() != teleport.ComponentNode {
return ctx, nil
}
identityContext, err := s.authHandlers.CreateIdentityContext(ccx.ServerConn)
if err != nil {
return ctx, trace.Wrap(err)
}
maxConnections := identityContext.RoleSet.MaxConnections()
if maxConnections == 0 {
// concurrent session control is not active, nothing
// else needs to be done here.
return ctx, nil
}
cfg, err := s.authService.GetClusterConfig()
if err != nil {
return ctx, trace.Wrap(err)
}
lock, err := services.AcquireSemaphoreLock(ctx, services.SemaphoreLockConfig{
Service: s.authService,
Expiry: cfg.GetSessionControlTimeout(),
Params: services.AcquireSemaphoreRequest{
SemaphoreKind: services.SemaphoreKindConnection,
SemaphoreName: identityContext.TeleportUser,
MaxLeases: maxConnections,
Holder: s.uuid,
},
})
if err != nil {
if strings.Contains(err.Error(), teleport.MaxLeases) {
// user has exceeded their max concurrent ssh connections.
s.EmitAuditEvent(events.SessionRejected, events.EventFields{
events.Reason: events.SessionRejectedReasonMaxConnections,
events.Maximum: maxConnections,
events.EventProtocol: events.EventProtocolSSH,
events.EventUser: identityContext.TeleportUser,
events.SessionServerID: s.uuid,
})
err = trace.AccessDenied("too many concurrent ssh connections for user %q (max=%d)",
identityContext.TeleportUser,
maxConnections,
)
}
return ctx, trace.Wrap(err)
}
go lock.KeepAlive(ctx)
// ensure that losing the lock closes the connection context. Under normal
// conditions, cancellation propagates from the connection context to the
// lock, but if we lose the lock due to some error (e.g. poor connectivity
// to auth server) then cancellation propagates in the other direction.
go func() {
// TODO(fspmarshall): If lock was lost due to error, find a way to propagate
// an error message to user.
<-lock.Done()
ccx.Close()
}()
return ctx, nil
}
// HandleNewChan is called when new channel is opened
func (s *Server) HandleNewChan(ctx context.Context, ccx *sshutils.ConnectionContext, nch ssh.NewChannel) {
identityContext, err := s.authHandlers.CreateIdentityContext(ccx.ServerConn)
@ -910,13 +979,38 @@ func (s *Server) HandleNewChan(ctx context.Context, ccx *sshutils.ConnectionCont
// Channels of type "session" handle requests that are involved in running
// commands on a server, subsystem requests, and agent forwarding.
case teleport.ChanSession:
var decr func()
if max := identityContext.RoleSet.MaxSessions(); max != 0 {
d, ok := ccx.IncrSessions(max)
if !ok {
// user has exceeded their max concurrent ssh sessions.
s.EmitAuditEvent(events.SessionRejected, events.EventFields{
events.Reason: events.SessionRejectedReasonMaxSessions,
events.Maximum: max,
events.EventProtocol: events.EventProtocolSSH,
events.EventUser: identityContext.TeleportUser,
events.SessionServerID: s.uuid,
})
rejectChannel(nch, ssh.Prohibited, fmt.Sprintf("too many session channels for user %q (max=%d)", identityContext.TeleportUser, max))
return
}
decr = d
}
ch, requests, err := nch.Accept()
if err != nil {
log.Warnf("Unable to accept channel: %v.", err)
rejectChannel(nch, ssh.ConnectionFailed, fmt.Sprintf("unable to accept channel: %v", err))
if decr != nil {
decr()
}
return
}
go s.handleSessionRequests(ctx, ccx, identityContext, ch, requests)
go func() {
s.handleSessionRequests(ctx, ccx, identityContext, ch, requests)
if decr != nil {
decr()
}
}()
// Channels of type "direct-tcpip" handles request for port forwarding.
case teleport.ChanDirectTCPIP:
req, err := sshutils.ParseDirectTCPIPReq(nch.ExtraData())

View file

@ -304,6 +304,39 @@ func (s *SrvSuite) TestAgentForwardPermission(c *C) {
c.Assert(strings.Contains(string(output), "SSH_AUTH_SOCK"), Equals, false)
}
// TestMaxSesssions makes sure that MaxSessions RBAC rules prevent
// too many concurrent sessions.
func (s *SrvSuite) TestMaxSessions(c *C) {
const maxSessions int64 = 2
ctx := context.Background()
// make sure the role does not allow agent forwarding
roleName := services.RoleNameForUser(s.user)
role, err := s.server.Auth().GetRole(roleName)
c.Assert(err, IsNil)
roleOptions := role.GetOptions()
roleOptions.MaxSessions = maxSessions
role.SetOptions(roleOptions)
err = s.server.Auth().UpsertRole(ctx, role)
c.Assert(err, IsNil)
for i := int64(0); i < maxSessions; i++ {
se, err := s.clt.NewSession()
c.Assert(err, IsNil)
defer se.Close()
}
_, err = s.clt.NewSession()
c.Assert(err, NotNil)
c.Assert(strings.Contains(err.Error(), "too many session channels"), Equals, true)
// verfiy that max sessions does not affect max connections.
for i := int64(0); i <= maxSessions; i++ {
clt, err := ssh.Dial("tcp", s.srv.Addr(), s.cltConfig)
c.Assert(err, IsNil)
c.Assert(clt.Close(), IsNil)
}
}
// TestOpenExecSessionSetsSession tests that OpenExecSession()
// sets ServerContext session.
func (s *SrvSuite) TestOpenExecSessionSetsSession(c *C) {

View file

@ -49,6 +49,10 @@ type ConnectionContext struct {
// been requested for this connection.
forwardAgent bool
// sessions is the number of currently active session channels; only tracked
// when handling node-side connections for users with MaxSessions applied.
sessions int64
// closers is a list of io.Closer that will be called when session closes
// this is handy as sometimes client closes session, in this case resources
// will be properly closed and deallocated, otherwise they could be kept hanging.
@ -143,6 +147,30 @@ func (c *ConnectionContext) GetForwardAgent() bool {
return c.forwardAgent
}
// TryIncrSessions tries to increment the active session count; if ok the
// returned decr function *must* be called when the associated session is closed.
func (c *ConnectionContext) IncrSessions(max int64) (decr func(), ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.sessions >= max {
return func() {}, false
}
c.sessions++
var decrOnce sync.Once
return func() {
decrOnce.Do(c.decrSessions)
}, true
}
func (c *ConnectionContext) decrSessions() {
c.mu.Lock()
defer c.mu.Unlock()
c.sessions--
if c.sessions < 0 {
panic("underflow")
}
}
// AddCloser adds any closer in ctx that will be called
// when the underlying connection is closed.
func (c *ConnectionContext) AddCloser(closer io.Closer) {

View file

@ -59,6 +59,7 @@ type Server struct {
newChanHandler NewChanHandler
reqHandler RequestHandler
newConnHandler NewConnHandler
cfg ssh.ServerConfig
limiter *limiter.Limiter
@ -196,6 +197,13 @@ func SetRequestHandler(req RequestHandler) ServerOption {
}
}
func SetNewConnHandler(handler NewConnHandler) ServerOption {
return func(s *Server) error {
s.newConnHandler = handler
return nil
}
}
func SetCiphers(ciphers []string) ServerOption {
return func(s *Server) error {
s.Debugf("Supported ciphers: %q.", ciphers)
@ -444,6 +452,32 @@ func (s *Server) HandleConnection(conn net.Conn) {
ctx, ccx := NewConnectionContext(context.Background(), wconn, sconn)
defer ccx.Close()
if s.newConnHandler != nil {
// if newConnHandler was set, then we have additional setup work
// to do before we can begin serving normally. Errors returned
// from a NewConnHandler are rejections.
ctx, err = s.newConnHandler.HandleNewConn(ctx, ccx)
if err != nil {
s.Warnf("Dropping inbound ssh connection due to error: %v", err)
// Immediately dropping the ssh connection results in an
// EOF error for the client. We therefore wait briefly
// to see if the client opens a channel, which will give
// us the opportunity to respond with a human-readable
// error.
select {
case firstChan := <-chans:
if firstChan != nil {
firstChan.Reject(ssh.Prohibited, err.Error())
}
case <-s.closeContext.Done():
case <-time.After(time.Second * 1):
}
sconn.Close()
conn.Close()
return
}
}
for {
select {
// handle out of band ssh requests
@ -472,6 +506,7 @@ func (s *Server) HandleConnection(conn net.Conn) {
}
case <-ctx.Done():
log.Debugf("Connection context canceled: %v -> %v", conn.RemoteAddr(), conn.LocalAddr())
return
}
}
}
@ -490,6 +525,13 @@ func (f NewChanHandlerFunc) HandleNewChan(ctx context.Context, ccx *ConnectionCo
f(ctx, ccx, ch)
}
// NewConnHandler is called once per incoming connection.
// Errors terminate the incoming connection. The returned context
// must be the same as, or a child of, the passed in context.
type NewConnHandler interface {
HandleNewConn(ctx context.Context, ccx *ConnectionContext) (context.Context, error)
}
type AuthMethods struct {
PublicKey PublicKeyFunc
Password PasswordFunc

View file

@ -60,8 +60,6 @@ type Retry interface {
// that fires after Duration delay,
// could fire right away if Duration is 0
After() <-chan time.Time
// MaxPeriod returns maximum period between retries
MaxPeriod() time.Duration
// Clone creates a copy of this retry in a
// reset state.
Clone() Retry
@ -127,11 +125,6 @@ type Linear struct {
closedChan chan time.Time
}
// MaxPeriod returns maximum period between retries
func (r *Linear) MaxPeriod() time.Duration {
return r.Max
}
// Reset resetes retry period to initial state
func (r *Linear) Reset() {
r.attempt = 0

View file

@ -642,3 +642,47 @@ func (c *remoteClusterCollection) toMarshal() interface{} {
func (c *remoteClusterCollection) writeYAML(w io.Writer) error {
return utils.WriteYAML(w, c.toMarshal())
}
type semaphoreCollection struct {
sems []services.Semaphore
}
func (c *semaphoreCollection) resources() (r []services.Resource) {
for _, resource := range c.sems {
r = append(r, resource)
}
return r
}
func (c *semaphoreCollection) writeText(w io.Writer) error {
t := asciitable.MakeTable([]string{"Kind", "Name", "LeaseID", "Holder", "Expires"})
for _, sem := range c.sems {
for _, ref := range sem.LeaseRefs() {
t.AddRow([]string{
sem.GetSubKind(), sem.GetName(), ref.LeaseID, ref.Holder, ref.Expires.Format(time.RFC822),
})
}
}
_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
}
func (c *semaphoreCollection) writeJSON(w io.Writer) error {
data, err := json.MarshalIndent(c.toMarshal(), "", " ")
if err != nil {
return trace.Wrap(err)
}
_, err = w.Write(data)
return trace.Wrap(err)
}
func (c *semaphoreCollection) toMarshal() interface{} {
if len(c.sems) == 1 {
return c.sems[0]
}
return c.sems
}
func (c *semaphoreCollection) writeYAML(w io.Writer) error {
return utils.WriteYAML(w, c.toMarshal())
}

View file

@ -398,6 +398,21 @@ func (rc *ResourceCommand) Delete(client auth.ClientI) (err error) {
return trace.Wrap(err)
}
fmt.Printf("remote cluster %q has been deleted\n", rc.ref.Name)
case services.KindSemaphore:
if rc.ref.SubKind == "" || rc.ref.Name == "" {
return trace.BadParameter(
"full semaphore path must be specified (e.g. '%s/%s/alice@example.com')",
services.KindSemaphore, services.SemaphoreKindConnection,
)
}
err := client.DeleteSemaphore(ctx, services.SemaphoreFilter{
SemaphoreKind: rc.ref.SubKind,
SemaphoreName: rc.ref.Name,
})
if err != nil {
return trace.Wrap(err)
}
fmt.Printf("semaphore '%s/%s' has been deleted\n", rc.ref.SubKind, rc.ref.Name)
default:
return trace.BadParameter("deleting resources of type %q is not supported", rc.ref.Kind)
}
@ -556,6 +571,15 @@ func (rc *ResourceCommand) getCollection(client auth.ClientI) (c ResourceCollect
return nil, trace.Wrap(err)
}
return &remoteClusterCollection{remoteClusters: []services.RemoteCluster{remoteCluster}}, nil
case services.KindSemaphore:
sems, err := client.GetSemaphores(context.TODO(), services.SemaphoreFilter{
SemaphoreKind: rc.ref.SubKind,
SemaphoreName: rc.ref.Name,
})
if err != nil {
return nil, trace.Wrap(err)
}
return &semaphoreCollection{sems: sems}, nil
}
return nil, trace.BadParameter("'%v' is not supported", rc.ref.Kind)
}