Skip to content

Commit

Permalink
Improve docstrings (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
maratori authored Dec 10, 2024
1 parent a07a929 commit d606205
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 83 deletions.
14 changes: 8 additions & 6 deletions auth/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import (
"context"
)

// Authenticator is used in gRPC interceptors.
// Authenticator is an interface for adding authentication data to the [context.Context] in gRPC interceptors.
// There are two main implementations: [AuthenticatorFromBearerTokener] and [PropagateAuthorizationHeader],
// but custom implementations can also be provided.
type Authenticator interface {
// Auth returns a [context.Context] with necessary grpc metadata for authorization.
// Auth enriches the provided [context.Context] with necessary gRPC metadata for authentication.
Auth(context.Context) (context.Context, error)

// HandleError is called with a [context.Context] received from [Authenticator.Auth]
// and an error from a gRPC call if it has the Unauthenticated code.
// If HandleError returns nil, a new auth will be requested to retry the gRPC call.
// If the gRPC call should not be retried, HandleError must return the incoming error.
// HandleError processes errors from gRPC calls that fail with the Unauthenticated status code.
// It receives the [context.Context] returned by [Authenticator.Auth] and the error from the gRPC call.
// If HandleError returns nil, the system will attempt to re-authenticate and retry the call.
// If the call should not be retried, HandleError should return the original error.
HandleError(context.Context, error) error
}
36 changes: 27 additions & 9 deletions auth/bearer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,37 @@ import (

const AuthorizationHeader = "Authorization"

// BearerToken is a token used in the Bearer authentication scheme.
type BearerToken struct {
Token string
ExpiresAt time.Time
}

// BearerTokener is an interface for providing a [BearerToken] for authentication.
// Most implementations provided in this package are decorators,
// allowing you to layer additional behavior on top of a base implementation.
// These can be combined or extended to fit custom authentication requirements.
type BearerTokener interface {
// BearerToken returns a token to be used in "Authorization" header.
// BearerToken returns a [BearerToken] for use in the [AuthorizationHeader].
BearerToken(context.Context) (BearerToken, error)

// HandleError is called with the [BearerToken] received from [BearerTokener.BearerToken]
// and an error from a gRPC call if it has the Unauthenticated code.
// If HandleError returns nil, a new auth will be requested to retry the gRPC call.
// If the gRPC call should not be retried, HandleError must return the incoming error.
// HandleError processes errors from gRPC calls that fail with the Unauthenticated status code.
// It receives the [BearerToken] returned by [BearerTokener.BearerToken] and the error from the gRPC call.
// If HandleError returns nil, the system will attempt to re-authenticate and retry the call.
// If the call should not be retried, HandleError should return the original error.
HandleError(context.Context, BearerToken, error) error
}

// StaticBearerToken implement [BearerTokener] with constant token.
// StaticBearerToken is a [BearerTokener] that always returns a fixed [BearerToken].
type StaticBearerToken string

var _ BearerTokener = StaticBearerToken("")

// NewStaticBearerToken returns a [BearerTokener] that always returns a fixed [BearerToken].
func NewStaticBearerToken(token string) StaticBearerToken {
return StaticBearerToken(token)
}

func (t StaticBearerToken) BearerToken(context.Context) (BearerToken, error) {
return BearerToken{
Token: string(t),
Expand All @@ -38,15 +48,19 @@ func (t StaticBearerToken) BearerToken(context.Context) (BearerToken, error) {
}

func (t StaticBearerToken) HandleError(_ context.Context, _ BearerToken, err error) error {
return err
return err // Unauthenticated error should not be retried
}

// AuthenticatorFromBearerTokener is an [Authenticator] that uses a [BearerTokener]
// to populate the [AuthorizationHeader] with a "Bearer " prefix.
type AuthenticatorFromBearerTokener struct {
tokener BearerTokener
}

var _ Authenticator = AuthenticatorFromBearerTokener{}

// NewAuthenticatorFromBearerTokener returns an [Authenticator] that uses a [BearerTokener]
// to populate the [AuthorizationHeader] with a "Bearer " prefix.
func NewAuthenticatorFromBearerTokener(tokener BearerTokener) AuthenticatorFromBearerTokener {
return AuthenticatorFromBearerTokener{
tokener: tokener,
Expand All @@ -71,17 +85,21 @@ func (a AuthenticatorFromBearerTokener) Auth(ctx context.Context) (context.Conte
func (a AuthenticatorFromBearerTokener) HandleError(ctx context.Context, err error) error {
token, ok := ctx.Value(ctxKeyBearerToken{}).(BearerToken)
if !ok {
return err
return err // Unauthenticated error should not be retried
}
return a.tokener.HandleError(ctx, token, err)
}

type ctxKeyBearerToken struct{}

// PropagateAuthorizationHeader is an [Authenticator] that transfers the [AuthorizationHeader]
// from incoming gRPC metadata to outgoing metadata.
type PropagateAuthorizationHeader struct{}

var _ Authenticator = PropagateAuthorizationHeader{}

// NewPropagateAuthorizationHeader returns an [Authenticator] that transfers the [AuthorizationHeader]
// from incoming gRPC metadata to outgoing metadata.
func NewPropagateAuthorizationHeader() PropagateAuthorizationHeader {
return PropagateAuthorizationHeader{}
}
Expand All @@ -98,5 +116,5 @@ func (PropagateAuthorizationHeader) Auth(ctx context.Context) (context.Context,
}

func (PropagateAuthorizationHeader) HandleError(_ context.Context, err error) error {
return err
return err // Unauthenticated error should not be retried
}
25 changes: 18 additions & 7 deletions auth/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ import (
"golang.org/x/sync/singleflight"
)

// CachedServiceTokener is a middleware that enhances the functionality of the [BearerTokener] by caching the token.
// It also automatically refreshes the token in the background before it expires, ensuring seamless authentication.
// Recommended parameters from IAM:
// - lifetime 90%
// - initial retry 1 second
// - max retry 1 minute
// CachedServiceTokener is a [BearerTokener] decorator that enhances its functionality
// with [BearerToken] caching and automatic background refresh to ensure that token is always valid.
//
// Recommended parameters from the IAM team:
// - lifetime: 90% of token lifespan
// - initial retry: 1 second
// - max retry: 1 minute
type CachedServiceTokener struct {
logger *slog.Logger
tokener BearerTokener
lifetime float64
lifetime float64 // lifetime is fraction of token lifespan after which the token is refreshed
initialRetry time.Duration
retryMultiplier float64
maxRetry time.Duration
Expand All @@ -36,6 +37,13 @@ type CachedServiceTokener struct {

var _ BearerTokener = (*CachedServiceTokener)(nil)

// NewCachedServiceTokener returns a decorated [BearerTokener] that enhances its functionality
// with [BearerToken] caching and automatic background refresh to ensure that token is always valid.
//
// Recommended parameters from the IAM team:
// - lifetime: 90% of token lifespan
// - initial retry: 1 second
// - max retry: 1 minute
func NewCachedServiceTokener(
logger *slog.Logger,
tokener BearerTokener,
Expand Down Expand Up @@ -202,6 +210,8 @@ func (c *CachedServiceTokener) requestToken(ctx context.Context, background bool
return res.(BearerToken), nil //nolint:errcheck // ok to panic
}

// CachedBearerTokener is a [BearerTokener] decorator that caches the [BearerToken].
// Method [CachedBearerTokener.HandleError] invalidates the cache on any error.
type CachedBearerTokener struct {
tokener BearerTokener
group singleflight.Group
Expand All @@ -212,6 +222,7 @@ type CachedBearerTokener struct {

var _ BearerTokener = (*CachedBearerTokener)(nil)

// NewCachedBearerTokener returns a decorated [BearerTokener] that caches the [BearerToken].
func NewCachedBearerTokener(tokener BearerTokener) *CachedBearerTokener {
return &CachedBearerTokener{
tokener: tokener,
Expand Down
20 changes: 16 additions & 4 deletions auth/exchangable.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ type ExchangeTokenRequester interface {
GetExchangeTokenRequest(context.Context) (*iampb.ExchangeTokenRequest, error)
}

// ExchangeableBearerTokener sends request to [iampb.TokenExchangeServiceClient.Exchange] to get a [BearerToken].
// ExchangeableBearerTokener is a [BearerTokener] that exchanges tokens
// using the [iampb.TokenExchangeServiceClient.Exchange] method.
// It relies on an [ExchangeTokenRequester] to generate the request payload for the exchange.
type ExchangeableBearerTokener struct {
creds ExchangeTokenRequester
client iampb.TokenExchangeServiceClient
Expand All @@ -25,6 +27,9 @@ type ExchangeableBearerTokener struct {

var _ BearerTokener = (*ExchangeableBearerTokener)(nil)

// NewExchangeableBearerTokener returns a [BearerTokener] that exchanges tokens
// using the [iampb.TokenExchangeServiceClient.Exchange] method.
// It relies on an [ExchangeTokenRequester] to generate the request payload for the exchange.
func NewExchangeableBearerTokener(
creds ExchangeTokenRequester,
client iampb.TokenExchangeServiceClient,
Expand All @@ -36,6 +41,9 @@ func NewExchangeableBearerTokener(
}
}

// SetClient updates the gRPC client.
//
// Note: This method is not thread-safe and should be called during initialization.
func (t *ExchangeableBearerTokener) SetClient(client iampb.TokenExchangeServiceClient) {
t.client = client
}
Expand Down Expand Up @@ -68,18 +76,22 @@ func (t *ExchangeableBearerTokener) BearerToken(ctx context.Context) (BearerToke
}

func (t *ExchangeableBearerTokener) HandleError(context.Context, BearerToken, error) error {
return nil
return nil // Unauthenticated error can be retried
}

// ServiceAccountExchangeTokenRequester generates a signed JWT using the credentials of a [ServiceAccount],
// intended for exchange to obtain a [BearerToken].
// ServiceAccountExchangeTokenRequester is an [ExchangeTokenRequester] that generates a signed JWT
// using the credentials of a [ServiceAccount].
// The JWT is intended for exchange to obtain a [BearerToken] via a token exchange service.
type ServiceAccountExchangeTokenRequester struct {
account ServiceAccountReader
now func() time.Time
}

var _ ExchangeTokenRequester = ServiceAccountExchangeTokenRequester{}

// NewServiceAccountExchangeTokenRequester returns an [ExchangeTokenRequester] that generates a signed JWT
// using the credentials of a [ServiceAccount].
// The JWT is intended for exchange to obtain a [BearerToken] via a token exchange service.
func NewServiceAccountExchangeTokenRequester(account ServiceAccountReader) ServiceAccountExchangeTokenRequester {
return ServiceAccountExchangeTokenRequester{
account: account,
Expand Down
16 changes: 9 additions & 7 deletions auth/interceptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,37 @@ import (
"github.com/nebius/gosdk/serviceerror"
)

// Disable is a [grpc.CallOption] that disables authentication on the client for a specific request.
// Disable is a [grpc.CallOption] that disables authentication for a specific gRPC request.
type Disable struct {
grpc.EmptyCallOption
}

// The following selectors can be used in common scenarios.
// Commonly used selectors for choosing authenticators in typical scenarios.
//
//nolint:gochecknoglobals // const
var /* const */ (
Base = Select("base")
Propagate = Select("propagate")
)

// Selector is a [grpc.CallOption] to select one of configured authenticators for a specific request.
// Selector is a [grpc.CallOption] that allows specifying which authenticator to use for a particular gRPC request.
// The selected authenticator must be pre-configured in the client.
type Selector struct {
grpc.EmptyCallOption
Name string
}

// Select returns [Selector] to select one of configured authenticators for a specific request.
// Select returns a [Selector] for specifying an authenticator by name to use for a particular gRPC request.
func Select(name string) Selector {
return Selector{
Name: name,
}
}

// Error wraps any authentication error.
// It doesn't have an Unwrap method to avoid bugs.
// To get an underlying error you should do the following:
// Error wraps an authentication-related error.
//
// It intentionally does not provide an Unwrap method to avoid unintended behavior or bugs.
// To access the underlying error, use [errors.As] as follows:
//
// var authErr *auth.Error
// if errors.As(err, &authErr) {
Expand Down
Loading

0 comments on commit d606205

Please sign in to comment.