Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Stable UNIX users: functionality #51200

Merged
merged 12 commits into from
Jan 29, 2025
6 changes: 6 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import (
resourceusagepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/resourceusage/v1"
samlidppb "github.com/gravitational/teleport/api/gen/proto/go/teleport/samlidp/v1"
secreportsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/secreports/v1"
stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
userloginstatev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/userloginstate/v1"
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
Expand Down Expand Up @@ -4970,6 +4971,11 @@ func (c *Client) GitServerReadOnlyClient() gitserverclient.ReadOnlyClient {
return c.GitServerClient()
}

// StableUNIXUsersClient returns a client for the stable UNIX users API.
func (c *Client) StableUNIXUsersClient() stableunixusersv1.StableUNIXUsersServiceClient {
return stableunixusersv1.NewStableUNIXUsersServiceClient(c.conn)
}

// GetCertAuthority retrieves a CA by type and domain.
func (c *Client) GetCertAuthority(ctx context.Context, id types.CertAuthID, loadKeys bool) (types.CertAuthority, error) {
ca, err := c.TrustClient().GetCertAuthority(ctx, &trustpb.GetCertAuthorityRequest{
Expand Down
26 changes: 26 additions & 0 deletions api/proto/teleport/legacy/types/events/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4749,6 +4749,7 @@ message OneOf {
events.GitCommand GitCommand = 197;
events.UserLoginAccessListInvalid UserLoginAccessListInvalid = 198;
events.AccessRequestExpire AccessRequestExpire = 199;
events.StableUNIXUserCreate StableUNIXUserCreate = 200;
}
}

Expand Down Expand Up @@ -7950,3 +7951,28 @@ message UserLoginAccessListInvalid {
(gogoproto.jsontag) = ""
];
}

// StableUNIXUserCreate is emitted whenever a new stable UNIX user is written in
// the cluster state storage.
message StableUNIXUserCreate {
// Metadata is common event metadata
Metadata Metadata = 1 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];

// User is a common user event metadata
UserMetadata User = 2 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];

StableUNIXUser stable_unix_user = 3;
}

message StableUNIXUser {
string username = 1;
int32 uid = 2;
}
5 changes: 5 additions & 0 deletions api/types/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -2434,3 +2434,8 @@ func (m *WorkloadIdentityDelete) TrimToMaxSize(_ int) AuditEvent {
func (m *GitCommand) TrimToMaxSize(_ int) AuditEvent {
return m
}

// TrimToMaxSize implements [AuditEvent].
func (m *StableUNIXUserCreate) TrimToMaxSize(int) AuditEvent {
return m
}
2,816 changes: 1,693 additions & 1,123 deletions api/types/events/events.pb.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions api/types/events/oneof.go
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,10 @@ func ToOneOf(in AuditEvent) (*OneOf, error) {
out.Event = &OneOf_GitCommand{
GitCommand: e,
}
case *StableUNIXUserCreate:
out.Event = &OneOf_StableUNIXUserCreate{
StableUNIXUserCreate: e,
}
default:
slog.ErrorContext(context.Background(), "Attempted to convert dynamic event of unknown type into protobuf event.", "event_type", in.GetType())
unknown := &Unknown{}
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/authclient/clt.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import (
provisioningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/provisioning/v1"
resourceusagepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/resourceusage/v1"
samlidppb "github.com/gravitational/teleport/api/gen/proto/go/teleport/samlidp/v1"
stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
Expand Down Expand Up @@ -1872,6 +1873,9 @@ type ClientI interface {
// will return "not implemented" errors (as per the default gRPC behavior).
StaticHostUserClient() services.StaticHostUser

// StableUNIXUsersClient returns a client for the stable UNIX users API.
StableUNIXUsersClient() stableunixusersv1.StableUNIXUsersServiceClient

// CloneHTTPClient creates a new HTTP client with the same configuration.
CloneHTTPClient(params ...roundtrip.ClientParam) (*HTTPClient, error)

Expand Down
2 changes: 2 additions & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5312,6 +5312,8 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) {

stableUNIXUsersServiceServer, err := stableunixusers.New(stableunixusers.Config{
Authorizer: cfg.Authorizer,
Emitter: cfg.Emitter,
Logger: cfg.AuthServer.logger.With(teleport.ComponentKey, "stable_unix_users"),

Backend: cfg.AuthServer.bk,
ReadOnlyCache: cfg.AuthServer.ReadOnlyCache,
Expand Down
72 changes: 60 additions & 12 deletions lib/auth/stableunixusers/stableunixusers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ package stableunixusers
import (
"context"
"errors"
"log/slog"
"time"

"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"

stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/readonly"
"github.com/gravitational/teleport/lib/utils"
Expand All @@ -38,6 +41,8 @@ const uidCacheTTL = 30 * time.Second
// Config contains the parameters for [New].
type Config struct {
Authorizer authz.Authorizer
Emitter apievents.Emitter
Logger *slog.Logger

Backend backend.Backend
ReadOnlyCache *readonly.Cache
Expand All @@ -54,25 +59,49 @@ type Config struct {

// New returns the auth server implementation for the stable UNIX users service,
// including the gRPC interface, authz enforcement, and business logic.
func New(params Config) (stableunixusersv1.StableUNIXUsersServiceServer, error) {
func New(c Config) (stableunixusersv1.StableUNIXUsersServiceServer, error) {
if c.Authorizer == nil {
return nil, trace.BadParameter("missing Authorizer")
}
if c.Emitter == nil {
return nil, trace.BadParameter("missing Emitter")
}
if c.Logger == nil {
return nil, trace.BadParameter("missing Logger")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use slog.Default instead of failing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the one place where we call this in real code we have access to the logger from the auth server - we can let the caller specify a default or noop logger, IMO.

}
if c.Backend == nil {
return nil, trace.BadParameter("missing Backend")
}
if c.ReadOnlyCache == nil {
return nil, trace.BadParameter("missing ReadOnlyCache")
}
if c.StableUNIXUsers == nil {
return nil, trace.BadParameter("missing StableUNIXUsers")
}
if c.ClusterConfiguration == nil {
return nil, trace.BadParameter("missing ClusterConfiguration")
}

uidCache, err := utils.NewFnCache(utils.FnCacheConfig{
TTL: uidCacheTTL,
Clock: params.CacheClock,
Context: params.CacheContext,
Clock: c.CacheClock,
Context: c.CacheContext,
ReloadOnErr: true,
})
if err != nil {
return nil, trace.Wrap(err)
}

return &server{
authorizer: params.Authorizer,
authorizer: c.Authorizer,
emitter: c.Emitter,
logger: c.Logger,

backend: params.Backend,
readOnlyCache: params.ReadOnlyCache,
backend: c.Backend,
readOnlyCache: c.ReadOnlyCache,

stableUNIXUsers: params.StableUNIXUsers,
clusterConfiguration: params.ClusterConfiguration,
stableUNIXUsers: c.StableUNIXUsers,
clusterConfiguration: c.ClusterConfiguration,

uidCache: uidCache,

Expand All @@ -86,6 +115,8 @@ type server struct {
stableunixusersv1.UnsafeStableUNIXUsersServiceServer

authorizer authz.Authorizer
emitter apievents.Emitter
logger *slog.Logger

backend backend.Backend
readOnlyCache *readonly.Cache
Expand Down Expand Up @@ -161,9 +192,17 @@ func (s *server) ObtainUIDForUsername(ctx context.Context, req *stableunixusersv
// obtainUIDForUsernameCached calls [*server.obtainUIDForUsernameUncached]
// through the UID FnCache.
func (s *server) obtainUIDForUsernameCached(ctx context.Context, username string) (int32, error) {
idGetter, err := authz.UserFromContext(ctx)
if err != nil {
idGetter = nil
}

uid, err := utils.FnCacheGet(ctx, s.uidCache, username, func(ctx context.Context) (int32, error) {
ctx, cancel := context.WithTimeout(ctx, uidCacheTTL)
defer cancel()
if idGetter != nil {
ctx = authz.ContextWithUser(ctx, idGetter)
}
return s.obtainUIDForUsernameUncached(ctx, username)
})
if err != nil {
Expand Down Expand Up @@ -192,8 +231,6 @@ func (s *server) obtainUIDForUsernameUncached(ctx context.Context, username stri

uid, err := s.stableUNIXUsers.GetUIDForUsername(ctx, username)
if err == nil {
// TODO(espadolini): _potentially_ emit an audit log event with
// username and UID (it might spam the audit log unnecessarily)
return uid, nil
}
if !trace.IsNotFound(err) {
Expand Down Expand Up @@ -233,8 +270,19 @@ func (s *server) obtainUIDForUsernameUncached(ctx context.Context, username stri
return 0, trace.Wrap(err)
}

// TODO(espadolini): emit an audit log event with the username and UID
// that was just created
if err := s.emitter.EmitAuditEvent(ctx, &apievents.StableUNIXUserCreate{
Metadata: apievents.Metadata{
Type: events.StableUNIXUserCreateEvent,
Code: events.StableUNIXUserCreateCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
StableUnixUser: &apievents.StableUNIXUser{
Username: username,
Uid: uid,
},
}); err != nil {
s.logger.WarnContext(ctx, "Failed to emit stable_unix_user.create", "error", err)
}

return uid, nil
}
Expand Down
40 changes: 40 additions & 0 deletions lib/auth/stableunixusers/stableunixusers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package stableunixusers_test
import (
"context"
"fmt"
"log/slog"
"os"
"testing"
"time"

Expand All @@ -29,13 +31,20 @@ import (

stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/auth/stableunixusers"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/backend/memory"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/services/readonly"
"github.com/gravitational/teleport/lib/utils"
)

func TestMain(m *testing.M) {
utils.InitLoggerForTests()
os.Exit(m.Run())
}

func TestStableUNIXUsers(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Expand All @@ -58,11 +67,15 @@ func TestStableUNIXUsers(t *testing.T) {
Backend: bk,
}

emitter := new(mockEmitter)

var authorizer authz.AuthorizerFunc

cacheClock := clockwork.NewFakeClock()
svc, err := stableunixusers.New(stableunixusers.Config{
Authorizer: &authorizer,
Emitter: emitter,
Logger: slog.Default(),

Backend: bk,
ReadOnlyCache: readOnlyCache,
Expand Down Expand Up @@ -114,38 +127,46 @@ func TestStableUNIXUsers(t *testing.T) {
uid1, err := obtainUIDForUsername("user1")
require.NoError(t, err)
require.Equal(t, firstUID, uid1)
require.True(t, emitter.getAndReset())

// this will panic unless the internal time-based cache is working
stableUNIXUsers.Backend = nil
uid1, err = obtainUIDForUsername("user1")
require.NoError(t, err)
require.Equal(t, firstUID, uid1)
require.False(t, emitter.getAndReset())

stableUNIXUsers.Backend = bk

uid2, err := obtainUIDForUsernameUncached("user2")
require.NoError(t, err)
require.Equal(t, firstUID+1, uid2)
require.True(t, emitter.getAndReset())

uid1, err = obtainUIDForUsernameUncached("user1")
require.NoError(t, err)
require.Equal(t, firstUID, uid1)
require.False(t, emitter.getAndReset())

uid2, err = obtainUIDForUsernameUncached("user2")
require.NoError(t, err)
require.Equal(t, firstUID+1, uid2)
require.False(t, emitter.getAndReset())

uid3, err := obtainUIDForUsernameUncached("user3")
require.NoError(t, err)
require.Equal(t, firstUID+2, uid3)
require.True(t, emitter.getAndReset())

uid4, err := obtainUIDForUsernameUncached("user4")
require.NoError(t, err)
require.Equal(t, firstUID+3, uid4)
require.True(t, emitter.getAndReset())

// 90000-90003 is only four spots, we can't store the fifth user
_, err = obtainUIDForUsernameUncached("user5")
require.ErrorAs(t, err, new(*trace.LimitExceededError))
require.False(t, emitter.getAndReset())

// nodes are not allowed to list users
_, err = svc.ListStableUNIXUsers(ctx, &stableunixusersv1.ListStableUNIXUsersRequest{
Expand Down Expand Up @@ -193,6 +214,7 @@ func TestStableUNIXUsers(t *testing.T) {
_, err = clusterConfiguration.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)

emitter.emitted = true
eg, ctx := errgroup.WithContext(ctx)
for i := range 1000 {
eg.Go(func() error {
Expand All @@ -202,3 +224,21 @@ func TestStableUNIXUsers(t *testing.T) {
}
require.NoError(t, eg.Wait())
}

type mockEmitter struct {
emitted bool
}

func (e *mockEmitter) EmitAuditEvent(ctx context.Context, ev apievents.AuditEvent) error {
if !e.emitted {
// avoid racing a write if the flag is already set
e.emitted = true
}
return nil
}

func (e *mockEmitter) getAndReset() bool {
r := e.emitted
e.emitted = false
return r
}
Loading
Loading