diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index 5eb64ee3c..cf3445c44 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -8,3 +8,6 @@ jobs: uses: gravitational/shared-workflows/.github/workflows/dependency-review.yaml@main permissions: contents: read + with: + # gravitational/teleport misdetected as "v0" + allow-ghsas: GHSA-6xf3-5hp7-xqqg diff --git a/Makefile b/Makefile index d4cbb611a..4b8837297 100644 --- a/Makefile +++ b/Makefile @@ -130,11 +130,7 @@ test-tooling: (cd tooling; go test -v -race ./...) .PHONY: test-unit -test-unit: test-tooling test-lib test-access test-event-handler - -.PHONY: test-lib -test-lib: - (cd lib; go test -v -race ./...) +test-unit: test-tooling test-access test-event-handler .PHONY: test-access test-access: @@ -224,6 +220,11 @@ update-helm-version-%: # Update snapshots @helm unittest -u -3 charts/$(subst access-,access/,$*) || { echo "Please install unittest as described in .cloudbuild/helm-unittest.yaml" ; exit 1; } +TELEPORT_DEP_VERSION ?= v12.1.1 +.PHONY: update-teleport-dep-version +update-teleport-dep-version: + ./update_teleport_dep_version.sh $(TELEPORT_DEP_VERSION) + .PHONY: update-tag update-tag: # Make sure VERSION is set on the command line "make update-tag VERSION=x.y.z". diff --git a/access/Dockerfile b/access/Dockerfile index 674657c9f..988425431 100644 --- a/access/Dockerfile +++ b/access/Dockerfile @@ -16,8 +16,6 @@ RUN --mount=type=cache,target=/go/pkg/mod go mod download # Copy the go source COPY access/${ACCESS_PLUGIN} access/${ACCESS_PLUGIN} -COPY access/common access/common -COPY lib lib # Build RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build make -C access/${ACCESS_PLUGIN} GITREF=${GITREF} @@ -28,4 +26,4 @@ FROM gcr.io/distroless/base@sha256:03dcbf61f859d0ae4c69c6242c9e5c3d7e1a42e5d3b69 ARG ACCESS_PLUGIN COPY --from=builder /workspace/access/${ACCESS_PLUGIN}/build/teleport-${ACCESS_PLUGIN} /usr/local/bin/teleport-plugin -ENTRYPOINT ["/usr/local/bin/teleport-plugin"] \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/teleport-plugin"] diff --git a/access/common/app.go b/access/common/app.go deleted file mode 100644 index e5a7a4a8b..000000000 --- a/access/common/app.go +++ /dev/null @@ -1,447 +0,0 @@ -/* -Copyright 2022 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 common - -import ( - "context" - "time" - - "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/access/common/teleport" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - pd "github.com/gravitational/teleport-plugins/lib/plugindata" - "github.com/gravitational/teleport-plugins/lib/watcherjob" -) - -const ( - // minServerVersion is the minimal teleport version the plugin supports. - minServerVersion = "6.1.0-beta.1" - // grpcBackoffMaxDelay is a maximum time GRPC client waits before reconnection attempt. - grpcBackoffMaxDelay = time.Second * 2 - // InitTimeout is used to bound execution time of health check and teleport version check. - initTimeout = time.Second * 10 - // handlerTimeout is used to bound the execution time of watcher event handler. - handlerTimeout = time.Second * 5 -) - -// BaseApp is responsible for handling all the access-request logic. -// It will start a Teleport client, listen for events and treat them. -// It also handles signals and watches its thread. -// To instantiate a new BaseApp, use NewApp() -type BaseApp struct { - PluginName string - apiClient teleport.Client - bot MessagingBot - mainJob lib.ServiceJob - pluginData *pd.CompareAndSwap[GenericPluginData] - Conf PluginConfiguration - - *lib.Process -} - -// NewApp creates a new BaseApp and initialize its main job -func NewApp(conf PluginConfiguration, pluginName string) *BaseApp { - app := BaseApp{ - PluginName: pluginName, - Conf: conf, - } - app.mainJob = lib.NewServiceJob(app.run) - return &app -} - -// Run initializes and runs a watcher and a callback server -func (a *BaseApp) Run(ctx context.Context) error { - // Initialize the process. - a.Process = lib.NewProcess(ctx) - a.SpawnCriticalJob(a.mainJob) - <-a.Process.Done() - return a.Err() -} - -// Err returns the error app finished with. -func (a *BaseApp) Err() error { - return trace.Wrap(a.mainJob.Err()) -} - -// WaitReady waits for http and watcher service to start up. -func (a *BaseApp) WaitReady(ctx context.Context) (bool, error) { - return a.mainJob.WaitReady(ctx) -} - -func (a *BaseApp) checkTeleportVersion(ctx context.Context) (proto.PingResponse, error) { - log := logger.Get(ctx) - log.Debug("Checking Teleport server version") - - pong, err := a.apiClient.Ping(ctx) - if err != nil { - if trace.IsNotImplemented(err) { - return pong, trace.Wrap(err, "server version must be at least %s", minServerVersion) - } - return pong, trace.Wrap(err, "Unable to get Teleport server version") - } - err = lib.AssertServerVersion(pong, minServerVersion) - return pong, trace.Wrap(err) -} - -// initTeleport creates a Teleport client and validates Teleport connectivity. -func (a *BaseApp) initTeleport(ctx context.Context, conf PluginConfiguration) (clusterName, webProxyAddr string, err error) { - clt, err := conf.GetTeleportClient(ctx) - if err != nil { - return "", "", trace.Wrap(err) - } - - a.apiClient = clt - pong, err := a.checkTeleportVersion(ctx) - if err != nil { - return "", "", trace.Wrap(err) - } - - if pong.ServerFeatures.AdvancedAccessWorkflows { - webProxyAddr = pong.ProxyPublicAddr - } - - return pong.ClusterName, webProxyAddr, nil -} - -// onWatcherEvent is called for every cluster Event. It will filter out non-access-request events and -// call onPendingRequest, onResolvedRequest and on DeletedRequest depending on the event. -func (a *BaseApp) onWatcherEvent(ctx context.Context, event types.Event) error { - if kind := event.Resource.GetKind(); kind != types.KindAccessRequest { - return trace.Errorf("unexpected kind %s", kind) - } - op := event.Type - reqID := event.Resource.GetName() - ctx, _ = logger.WithField(ctx, "request_id", reqID) - - switch op { - case types.OpPut: - ctx, _ = logger.WithField(ctx, "request_op", "put") - req, ok := event.Resource.(types.AccessRequest) - if !ok { - return trace.Errorf("unexpected resource type %T", event.Resource) - } - ctx, log := logger.WithField(ctx, "request_state", req.GetState().String()) - - var err error - switch { - case req.GetState().IsPending(): - err = a.onPendingRequest(ctx, req) - case req.GetState().IsApproved(), req.GetState().IsDenied(): - err = a.onResolvedRequest(ctx, req) - default: - log.WithField("event", event).Warn("Unknown request state") - return nil - } - - if err != nil { - log.WithError(err).Errorf("Failed to process request") - return trace.Wrap(err) - } - - return nil - case types.OpDelete: - ctx, log := logger.WithField(ctx, "request_op", "delete") - - if err := a.onDeletedRequest(ctx, reqID); err != nil { - log.WithError(err).Errorf("Failed to process deleted request") - return trace.Wrap(err) - } - return nil - default: - return trace.BadParameter("unexpected event operation %s", op) - } -} - -// run starts the event watcher job and blocks utils it stops -func (a *BaseApp) run(ctx context.Context) error { - log := logger.Get(ctx) - - if err := a.init(ctx); err != nil { - return trace.Wrap(err) - } - watcherJob := watcherjob.NewJob( - a.apiClient, - watcherjob.Config{ - Watch: types.Watch{Kinds: []types.WatchKind{{Kind: types.KindAccessRequest}}}, - EventFuncTimeout: handlerTimeout, - }, - a.onWatcherEvent, - ) - a.SpawnCriticalJob(watcherJob) - ok, err := watcherJob.WaitReady(ctx) - if err != nil { - return trace.Wrap(err) - } - - a.mainJob.SetReady(ok) - if ok { - log.Info("Plugin is ready") - } else { - log.Error("Plugin is not ready") - } - - <-watcherJob.Done() - - return trace.Wrap(watcherJob.Err()) -} - -func (a *BaseApp) init(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, initTimeout) - defer cancel() - log := logger.Get(ctx) - - clusterName, webProxyAddr, err := a.initTeleport(ctx, a.Conf) - if err != nil { - return trace.Wrap(err) - } - - a.bot, err = a.Conf.NewBot(clusterName, webProxyAddr) - if err != nil { - return trace.Wrap(err) - } - - a.pluginData = pd.NewCAS( - a.apiClient, - a.PluginName, - types.KindAccessRequest, - EncodePluginData, - DecodePluginData, - ) - - log.Debug("Starting API health check...") - if err = a.bot.CheckHealth(ctx); err != nil { - return trace.Wrap(err, "API health check failed") - } - - log.Debug("API health check finished ok") - return nil -} - -func (a *BaseApp) onPendingRequest(ctx context.Context, req types.AccessRequest) error { - log := logger.Get(ctx) - - reqID := req.GetName() - reqData := pd.AccessRequestData{ - User: req.GetUser(), - Roles: req.GetRoles(), - RequestReason: req.GetRequestReason(), - } - - _, err := a.pluginData.Create(ctx, reqID, GenericPluginData{AccessRequestData: reqData}) - switch { - case err == nil: - // This is a new access-request, we have to broadcast it first - if recipients := a.getMessageRecipients(ctx, req); len(recipients) > 0 { - if err := a.broadcastMessages(ctx, recipients, reqID, reqData); err != nil { - return trace.Wrap(err) - } - } else { - log.Warning("No channel to post") - } - case trace.IsAlreadyExists(err): - // The messages were already sent, nothing to do, we can update the reviews - default: - // This is an unexpected error, returning - return trace.Wrap(err) - } - - // This is an already existing access request, we post reviews and update its status - if reqReviews := req.GetReviews(); len(reqReviews) > 0 { - if err := a.postReviewReplies(ctx, reqID, reqReviews); err != nil { - return trace.Wrap(err) - } - - err := a.updateMessages(ctx, reqID, pd.Unresolved, "", reqReviews) - if err != nil { - return trace.Wrap(err) - } - } - - return nil -} - -func (a *BaseApp) onResolvedRequest(ctx context.Context, req types.AccessRequest) error { - // We always post review replies in thread. If the messaging service does not support - // threading this will do nothing - replyErr := a.postReviewReplies(ctx, req.GetName(), req.GetReviews()) - - reason := req.GetResolveReason() - state := req.GetState() - var tag pd.ResolutionTag - - switch state { - case types.RequestState_APPROVED: - tag = pd.ResolvedApproved - case types.RequestState_DENIED: - tag = pd.ResolvedDenied - default: - logger.Get(ctx).Warningf("Unknown state %v (%s)", state, state.String()) - return replyErr - } - err := trace.Wrap(a.updateMessages(ctx, req.GetName(), tag, reason, req.GetReviews())) - return trace.NewAggregate(replyErr, err) -} - -func (a *BaseApp) onDeletedRequest(ctx context.Context, reqID string) error { - return a.updateMessages(ctx, reqID, pd.ResolvedExpired, "", nil) -} - -// broadcastMessages sends nessages to each recipient for an access-request. -// This method is only called when for new access-requests. -func (a *BaseApp) broadcastMessages(ctx context.Context, recipients []Recipient, reqID string, reqData pd.AccessRequestData) error { - sentMessages, err := a.bot.Broadcast(ctx, recipients, reqID, reqData) - if len(sentMessages) == 0 && err != nil { - return trace.Wrap(err) - } - for _, data := range sentMessages { - logger.Get(ctx).WithFields(logger.Fields{ - "channel_id": data.ChannelID, - "message_id": data.MessageID, - }).Info("Successfully posted messages") - } - if err != nil { - logger.Get(ctx).WithError(err).Error("Failed to post one or more messages") - } - - _, err = a.pluginData.Update(ctx, reqID, func(existing GenericPluginData) (GenericPluginData, error) { - existing.SentMessages = sentMessages - return existing, nil - }) - - return trace.Wrap(err) -} - -// postReviewReplies lists and updates existing messages belonging to an access request. -// Posting reviews is done both by updating the original message and by replying in thread if possible. -func (a *BaseApp) postReviewReplies(ctx context.Context, reqID string, reqReviews []types.AccessReview) error { - var oldCount int - - pd, err := a.pluginData.Update(ctx, reqID, func(existing GenericPluginData) (GenericPluginData, error) { - sentMessages := existing.SentMessages - if len(sentMessages) == 0 { - // wait for the plugin data to be updated with SentMessages - return GenericPluginData{}, trace.CompareFailed("existing sentMessages is empty") - } - - count := len(reqReviews) - oldCount = existing.ReviewsCount - if oldCount >= count { - return GenericPluginData{}, trace.AlreadyExists("reviews are sent already") - } - - existing.ReviewsCount = count - return existing, nil - }) - if trace.IsAlreadyExists(err) { - logger.Get(ctx).Debug("Failed to post reply: replies are already sent") - return nil - } - if err != nil { - return trace.Wrap(err) - } - - slice := reqReviews[oldCount:] - if len(slice) == 0 { - return nil - } - - errors := make([]error, 0, len(slice)) - for _, data := range pd.SentMessages { - ctx, _ = logger.WithFields(ctx, logger.Fields{"channel_id": data.ChannelID, "message_id": data.MessageID}) - for _, review := range slice { - if err := a.bot.PostReviewReply(ctx, data.ChannelID, data.MessageID, review); err != nil { - errors = append(errors, err) - } - } - } - return trace.NewAggregate(errors...) -} - -// getMessageRecipients takes an access request and returns a list of channelIDs that should be messaged. -// channelIDs can represent any communication channel depending on the MessagingBot implementation: -// a public channel, a private one, or a user direct message channel. -func (a *BaseApp) getMessageRecipients(ctx context.Context, req types.AccessRequest) []Recipient { - log := logger.Get(ctx) - - // We receive a set from GetRawRecipientsFor but we still might end up with duplicate channel names. - // This can happen if this set contains the channel `C` and the email for channel `C`. - recipientSet := NewRecipientSet() - - validEmailSuggReviewers := []string{} - for _, reviewer := range req.GetSuggestedReviewers() { - if !lib.IsEmail(reviewer) { - log.Warningf("Failed to notify a suggested reviewer: %q does not look like a valid email", reviewer) - continue - } - - validEmailSuggReviewers = append(validEmailSuggReviewers, reviewer) - } - rawRecipients := a.Conf.GetRecipients().GetRawRecipientsFor(req.GetRoles(), validEmailSuggReviewers) - for _, rawRecipient := range rawRecipients { - recipient, err := a.bot.FetchRecipient(ctx, rawRecipient) - if err != nil { - // Something wrong happened, we log the error and continue to treat valid rawRecipients - log.Warning(err) - } else { - recipientSet.Add(*recipient) - } - } - - return recipientSet.ToSlice() -} - -// updateMessages updates the messages status and adds the resolve reason. -func (a *BaseApp) updateMessages(ctx context.Context, reqID string, tag pd.ResolutionTag, reason string, reviews []types.AccessReview) error { - log := logger.Get(ctx) - - pluginData, err := a.pluginData.Update(ctx, reqID, func(existing GenericPluginData) (GenericPluginData, error) { - if len(existing.SentMessages) == 0 { - return GenericPluginData{}, trace.NotFound("plugin data not found") - } - - // If resolution field is not empty then we already resolved the incident before. In this case we just quit. - if existing.AccessRequestData.ResolutionTag != pd.Unresolved { - return GenericPluginData{}, trace.CompareFailed("request is already resolved") - } - - // Mark plugin data as resolved. - existing.ResolutionTag = tag - existing.ResolutionReason = reason - - return existing, nil - }) - if trace.IsNotFound(err) { - log.Debug("Failed to update messages: plugin data is missing") - return nil - } - if err != nil { - return trace.Wrap(err) - } - - reqData, sentMessages := pluginData.AccessRequestData, pluginData.SentMessages - if err := a.bot.UpdateMessages(ctx, reqID, reqData, sentMessages, reviews); err != nil { - return trace.Wrap(err) - } - - log.Infof("Successfully marked request as %s in all messages", tag) - - return nil -} diff --git a/access/common/auth/oauth/oauth.go b/access/common/auth/oauth/oauth.go deleted file mode 100644 index af0a938a9..000000000 --- a/access/common/auth/oauth/oauth.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2023 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 oauth - -import ( - "context" - - storage "github.com/gravitational/teleport-plugins/access/common/auth/storage" -) - -// Authorizer is the composite interface of Exchanger and Refresher. -type Authorizer interface { - Exchanger - Refresher -} - -// Exchanger implements the OAuth2 authorization code exchange operation. -type Exchanger interface { - Exchange(ctx context.Context, authorizationCode string, redirectURI string) (*storage.Credentials, error) -} - -// Refresher implements the OAuth2 bearer token refresh operation. -type Refresher interface { - Refresh(ctx context.Context, refreshToken string) (*storage.Credentials, error) -} diff --git a/access/common/auth/storage/storage.go b/access/common/auth/storage/storage.go deleted file mode 100644 index 001d556e6..000000000 --- a/access/common/auth/storage/storage.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2023 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 storage - -import ( - "context" - "time" -) - -// Credentials represents the short-lived OAuth2 credentials. -type Credentials struct { - // AccessToken is the Bearer token used to access the provider's API - AccessToken string - // RefreshToken is used to acquire a new access token. - RefreshToken string - // ExpiresAt marks the end of validity period for the access token. - // The application must use the refresh token to acquire a new access token - // before this time. - ExpiresAt time.Time -} - -// Store defines the interface for persisting the short-lived OAuth2 credentials. -type Store interface { - GetCredentials(context.Context) (*Credentials, error) - PutCredentials(context.Context, *Credentials) error -} diff --git a/access/common/auth/token_provider.go b/access/common/auth/token_provider.go deleted file mode 100644 index 8650f6acd..000000000 --- a/access/common/auth/token_provider.go +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2023 Gravitational, Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package auth - -import ( - "context" - "sync" - "time" - - "github.com/gravitational/trace" - "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" - - "github.com/gravitational/teleport-plugins/access/common/auth/oauth" - "github.com/gravitational/teleport-plugins/access/common/auth/storage" -) - -const defaultRefreshRetryInterval = 5 * time.Minute -const defaultTokenBufferInterval = 1 * time.Hour - -// AccessTokenProvider provides a method to get the bearer token -// for use when authorizing to a 3rd-party provider API. -type AccessTokenProvider interface { - GetAccessToken() (string, error) -} - -// StaticAccessTokenProvider is an implementation of AccessTokenProvider -// that always returns the specified token. -type StaticAccessTokenProvider struct { - token string -} - -// NewStaticAccessTokenProvider creates a new StaticAccessTokenProvider. -func NewStaticAccessTokenProvider(token string) *StaticAccessTokenProvider { - return &StaticAccessTokenProvider{token: token} -} - -// GetAccessToken implements AccessTokenProvider -func (s *StaticAccessTokenProvider) GetAccessToken() (string, error) { - return s.token, nil -} - -// RotatedAccessTokenProviderConfig contains parameters and dependencies for RotatedAccessTokenProvider -type RotatedAccessTokenProviderConfig struct { - RetryInterval time.Duration - TokenBufferInterval time.Duration - - Store storage.Store - Refresher oauth.Refresher - Clock clockwork.Clock - - Log *logrus.Entry -} - -// CheckAndSetDefaults validates a configuration and sets default values -func (c *RotatedAccessTokenProviderConfig) CheckAndSetDefaults() error { - if c.RetryInterval == 0 { - c.RetryInterval = defaultRefreshRetryInterval - } - if c.TokenBufferInterval == 0 { - c.TokenBufferInterval = defaultTokenBufferInterval - } - - if c.Store == nil { - return trace.BadParameter("Store must be set") - } - if c.Refresher == nil { - return trace.BadParameter("Refresher must be set") - } - if c.Clock == nil { - c.Clock = clockwork.NewRealClock() - } - if c.Log == nil { - c.Log = logrus.NewEntry(logrus.StandardLogger()) - } - return nil -} - -// RotatedAccessTokenProvider is an implementation of AccessTokenProvider -// that uses OAuth2 refresh token flow to renew the acess token. -// The credentials are stored in the given persistent store. -// -// To have an up-to-date token, one must run RefreshLoop() in a background goroutine. -type RotatedAccessTokenProvider struct { - retryInterval time.Duration - tokenBufferInterval time.Duration - store storage.Store - refresher oauth.Refresher - clock clockwork.Clock - - log logrus.FieldLogger - - lock sync.RWMutex // protects the below fields - creds *storage.Credentials -} - -// NewRotatedTokenProvider creates a new RotatedAccessTokenProvider from the given config. -// NewRotatedTokenProvider will return an error if the store does not have existing credentials, -// meaning they need to be acquired first (e.g. via OAuth2 authorization code flow). -func NewRotatedTokenProvider(ctx context.Context, cfg RotatedAccessTokenProviderConfig) (*RotatedAccessTokenProvider, error) { - if err := cfg.CheckAndSetDefaults(); err != nil { - return nil, trace.Wrap(err) - } - - provider := &RotatedAccessTokenProvider{ - retryInterval: cfg.RetryInterval, - tokenBufferInterval: cfg.TokenBufferInterval, - store: cfg.Store, - refresher: cfg.Refresher, - clock: cfg.Clock, - log: cfg.Log, - } - - var err error - provider.creds, err = provider.store.GetCredentials(ctx) - if err != nil { - return nil, trace.Wrap(err) - } - - return provider, nil -} - -// GetAccessToken implements AccessTokenProvider() -func (r *RotatedAccessTokenProvider) GetAccessToken() (string, error) { - r.lock.RLock() - defer r.lock.RUnlock() - return r.creds.AccessToken, nil -} - -// RefreshLoop runs the credential refresh process. -func (r *RotatedAccessTokenProvider) RefreshLoop(ctx context.Context) { - r.lock.RLock() - creds := r.creds - r.lock.RUnlock() - - interval := r.getRefreshInterval(creds) - - timer := r.clock.NewTimer(interval) - defer timer.Stop() - r.log.Infof("Will attempt token refresh in: %s", interval) - - for { - select { - case <-ctx.Done(): - r.log.Info("Shutting down") - return - case <-timer.Chan(): - creds, _ := r.store.GetCredentials(ctx) - - // Skip if the credentials are sufficiently fresh - // (in an HA setup another instance might have refreshed the credentials). - // This is just an optimistic check to potentially reduce API calls. - // There is no synchronization between several instances of the plugin. - if creds != nil && !r.shouldRefresh(creds) { - r.lock.Lock() - r.creds = creds - r.lock.Unlock() - - interval := r.getRefreshInterval(creds) - timer.Reset(interval) - r.log.Infof("Next refresh in: %s", interval) - continue - } - - creds, err := r.refresh(ctx) - if err != nil { - r.log.Errorf("Error while refreshing: %s. Will retry after: %s", err, r.retryInterval) - timer.Reset(r.retryInterval) - } else { - err := r.store.PutCredentials(ctx, creds) - if err != nil { - r.log.Errorf("Error while storing the refreshed credentials: %s", err) - timer.Reset(r.retryInterval) - continue - } - - r.lock.Lock() - r.creds = creds - r.lock.Unlock() - - interval := r.getRefreshInterval(creds) - timer.Reset(interval) - r.log.Infof("Successfully refreshed credentials. Next refresh in: %s", interval) - } - } - } -} - -func (r *RotatedAccessTokenProvider) getRefreshInterval(creds *storage.Credentials) time.Duration { - d := creds.ExpiresAt.Sub(r.clock.Now()) - r.tokenBufferInterval - - // Timer panics of duration is negative - if d < 0 { - d = time.Duration(1) - } - return d -} - -func (r *RotatedAccessTokenProvider) refresh(ctx context.Context) (*storage.Credentials, error) { - creds, err := r.refresher.Refresh(ctx, r.creds.RefreshToken) - if err != nil { - return nil, trace.Wrap(err) - } - return creds, nil -} - -func (r *RotatedAccessTokenProvider) shouldRefresh(creds *storage.Credentials) bool { - now := r.clock.Now() - refreshAt := creds.ExpiresAt.Add(-r.tokenBufferInterval) - return now.After(refreshAt) || now.Equal(refreshAt) -} diff --git a/access/common/auth/token_provider_test.go b/access/common/auth/token_provider_test.go deleted file mode 100644 index 15103dd91..000000000 --- a/access/common/auth/token_provider_test.go +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2023 Gravitational, Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package auth - -import ( - "context" - "testing" - "time" - - "github.com/gravitational/trace" - "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/require" - - "github.com/gravitational/teleport-plugins/access/common/auth/oauth" - "github.com/gravitational/teleport-plugins/access/common/auth/storage" -) - -type mockRefresher struct { - refresh func(string) (*storage.Credentials, error) -} - -// Refresh implements oauth.Refresher -func (r *mockRefresher) Refresh(ctx context.Context, refreshToken string) (*storage.Credentials, error) { - return r.refresh(refreshToken) -} - -type mockStore struct { - getCredentials func() (*storage.Credentials, error) - putCredentials func(*storage.Credentials) error -} - -// GetCredentials implements storage.Store -func (s *mockStore) GetCredentials(ctx context.Context) (*storage.Credentials, error) { - return s.getCredentials() -} - -// PutCredentials implements storage.Store -func (s *mockStore) PutCredentials(ctx context.Context, creds *storage.Credentials) error { - return s.putCredentials(creds) -} - -func TestRotatedAccessTokenProvider(t *testing.T) { - log := logrus.New() - log.Level = logrus.DebugLevel - - newProvider := func(ctx context.Context, store storage.Store, refresher oauth.Refresher, clock clockwork.Clock, initialCreds *storage.Credentials) *RotatedAccessTokenProvider { - return &RotatedAccessTokenProvider{ - store: store, - refresher: refresher, - clock: clock, - - retryInterval: 1 * time.Minute, - tokenBufferInterval: 1 * time.Hour, - - creds: initialCreds, - log: log, - } - } - - t.Run("Init", func(t *testing.T) { - clock := clockwork.NewFakeClock() - initialCreds := &storage.Credentials{ - AccessToken: "my-access-token", - RefreshToken: "my-refresh-token", - ExpiresAt: clock.Now().Add(2 * time.Hour), - } - - refresher := &mockRefresher{} - mockStore := &mockStore{ - getCredentials: func() (*storage.Credentials, error) { - return initialCreds, nil - }, - } - - provider, err := NewRotatedTokenProvider(context.Background(), RotatedAccessTokenProviderConfig{ - Store: mockStore, - Refresher: refresher, - Clock: clock, - }) - require.NoError(t, err) - creds, err := provider.GetAccessToken() - require.NoError(t, err) - require.Equal(t, initialCreds.AccessToken, creds) - }) - - t.Run("InitFail", func(t *testing.T) { - clock := clockwork.NewFakeClock() - refresher := &mockRefresher{} - mockStore := &mockStore{ - getCredentials: func() (*storage.Credentials, error) { - return nil, trace.NotFound("not found") - }, - } - - provider, err := NewRotatedTokenProvider(context.Background(), RotatedAccessTokenProviderConfig{ - Store: mockStore, - Refresher: refresher, - Clock: clock, - }) - require.Error(t, err) - require.Nil(t, provider) - }) - - t.Run("Refresh", func(t *testing.T) { - clock := clockwork.NewFakeClock() - initialCreds := &storage.Credentials{ - AccessToken: "my-access-token", - RefreshToken: "my-refresh-token", - ExpiresAt: clock.Now().Add(2 * time.Hour), - } - newCreds := &storage.Credentials{ - AccessToken: "my-access-token2", - RefreshToken: "my-refresh-token2", - ExpiresAt: clock.Now().Add(4 * time.Hour), - } - - var storedCreds *storage.Credentials - var refreshCalled int - - refresher := &mockRefresher{ - refresh: func(refreshToken string) (*storage.Credentials, error) { - refreshCalled++ - require.Equal(t, refreshToken, initialCreds.RefreshToken) - - // fail the first call - if refreshCalled == 1 { - return nil, trace.Errorf("some error") - } - - return newCreds, nil - }, - } - mockStore := &mockStore{ - getCredentials: func() (*storage.Credentials, error) { - return initialCreds, nil - }, - putCredentials: func(creds *storage.Credentials) error { - storedCreds = creds - return nil - }, - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - provider := newProvider(ctx, mockStore, refresher, clock, initialCreds) - - go provider.RefreshLoop(ctx) - - clock.BlockUntil(1) - require.Nil(t, storedCreds) // before attempting refresh - - clock.Advance(1 * time.Hour) // trigger refresh (2 hours - 1 hour buffer) - clock.BlockUntil(1) - require.Nil(t, storedCreds) // after first refresh has failed - - clock.Advance(1 * time.Minute) // trigger refresh (after retry period) - clock.BlockUntil(1) - require.Equal(t, newCreds, storedCreds) - }) - - t.Run("Cancel", func(t *testing.T) { - clock := clockwork.NewFakeClock() - refresher := &mockRefresher{} - mockStore := &mockStore{} - - initialCreds := &storage.Credentials{ - AccessToken: "my-access-token", - RefreshToken: "my-refresh-token", - ExpiresAt: clock.Now().Add(2 * time.Hour), - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - provider := newProvider(ctx, mockStore, refresher, clock, initialCreds) - finished := make(chan struct{}, 1) - - go func() { - provider.RefreshLoop(ctx) - finished <- struct{}{} - }() - - cancel() - require.Eventually(t, func() bool { - select { - case <-finished: - return true - default: - return false - } - }, time.Second, time.Second/10) - }) -} diff --git a/access/common/bot.go b/access/common/bot.go deleted file mode 100644 index 2e1e006cd..000000000 --- a/access/common/bot.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2022 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 common - -import ( - "github.com/gravitational/teleport/api/types" - "golang.org/x/net/context" - - pd "github.com/gravitational/teleport-plugins/lib/plugindata" -) - -// MessagingBot is a generic interface with all methods required to send notifications through a messaging service. -// A messaging bot contains an API client to send/edit messages and is able to resolve a Recipient from a string. -// Implementing this interface allows to leverage BaseApp logic without customization. -type MessagingBot interface { - // CheckHealth checks if the bot can connect to its messaging service - CheckHealth(ctx context.Context) error - // Broadcast sends an access request message to a list of Recipient - Broadcast(ctx context.Context, recipients []Recipient, reqID string, reqData pd.AccessRequestData) (data SentMessages, err error) - // PostReviewReply posts in thread an access request review. This does nothing if the messaging service - // does not support threaded replies. - PostReviewReply(ctx context.Context, channelID string, threadID string, review types.AccessReview) error - // UpdateMessages updates access request messages that were previously sent via Broadcast - // This is used to change the access-request status and number of required approval remaining - UpdateMessages(ctx context.Context, reqID string, data pd.AccessRequestData, messageData SentMessages, reviews []types.AccessReview) error - // FetchRecipient fetches recipient data from the messaging service API. It can also be used to check and initialize - // a communication channel (e.g. MsTeams needs to install the app for the user before being able to send - // notifications) - FetchRecipient(ctx context.Context, recipient string) (*Recipient, error) -} diff --git a/access/common/config.go b/access/common/config.go deleted file mode 100644 index cda9035f6..000000000 --- a/access/common/config.go +++ /dev/null @@ -1,88 +0,0 @@ -/* -Copyright 2022 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 common - -import ( - "context" - - "github.com/gravitational/teleport/api/client" - "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" - "google.golang.org/grpc" - grpcbackoff "google.golang.org/grpc/backoff" - - "github.com/gravitational/teleport-plugins/access/common/teleport" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/credentials" - "github.com/gravitational/teleport-plugins/lib/logger" -) - -type PluginConfiguration interface { - GetTeleportClient(ctx context.Context) (teleport.Client, error) - GetRecipients() RawRecipientsMap - NewBot(clusterName string, webProxyAddr string) (MessagingBot, error) -} - -type BaseConfig struct { - Teleport lib.TeleportConfig - Recipients RawRecipientsMap `toml:"role_to_recipients"` - Log logger.Config -} - -func (c BaseConfig) GetRecipients() RawRecipientsMap { - return c.Recipients -} - -func (c BaseConfig) GetTeleportClient(ctx context.Context) (teleport.Client, error) { - if validCred, err := credentials.CheckIfExpired(c.Teleport.Credentials()); err != nil { - log.Warn(err) - if !validCred { - return nil, trace.BadParameter( - "No valid credentials found, this likely means credentials are expired. In this case, please sign new credentials and increase their TTL if needed.", - ) - } - log.Info("At least one non-expired credential has been found, continuing startup") - } - - bk := grpcbackoff.DefaultConfig - bk.MaxDelay = grpcBackoffMaxDelay - - clt, err := client.New(ctx, client.Config{ - Addrs: c.Teleport.GetAddrs(), - Credentials: c.Teleport.Credentials(), - DialOpts: []grpc.DialOption{ - grpc.WithConnectParams(grpc.ConnectParams{Backoff: bk, MinConnectTimeout: initTimeout}), - grpc.WithReturnConnectionError(), - }, - }) - if err != nil { - return nil, trace.Wrap(err) - } - clt = clt.WithCallOptions(grpc.WaitForReady(true)) - - return clt, nil -} - -// GenericAPIConfig holds common configuration use by a messaging service. -// MessagingBots requiring more custom configuration (MSTeams for example) can -// implement their own APIConfig instead. -type GenericAPIConfig struct { - Token string - // DELETE IN 11.0.0 (Joerger) - use "role_to_recipients["*"]" instead - Recipients []string - APIURL string -} diff --git a/access/common/message.go b/access/common/message.go deleted file mode 100644 index d3d9fc3a4..000000000 --- a/access/common/message.go +++ /dev/null @@ -1,138 +0,0 @@ -/* -Copyright 2022 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 common - -import ( - "fmt" - "net/url" - "strings" - "text/template" - "time" - - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - pd "github.com/gravitational/teleport-plugins/lib/plugindata" -) - -// Slack has a 4000 character limit for message texts and 3000 character limit -// for message section texts, so we truncate all reasons to a generous but -// conservative limit -const ( - requestReasonLimit = 500 - resolutionReasonLimit - ReviewReasonLimit -) - -var reviewReplyTemplate = template.Must(template.New("review reply").Parse( - `{{.Author}} reviewed the request at {{.Created.Format .TimeFormat}}. -Resolution: {{.ProposedStateEmoji}} {{.ProposedState}}. -{{if .Reason}}Reason: {{.Reason}}.{{end}}`, -)) - -func MsgStatusText(tag pd.ResolutionTag, reason string) string { - var statusEmoji string - status := string(tag) - switch tag { - case pd.Unresolved: - status = "PENDING" - statusEmoji = "⏳" - case pd.ResolvedApproved: - statusEmoji = "✅" - case pd.ResolvedDenied: - statusEmoji = "❌" - case pd.ResolvedExpired: - statusEmoji = "⌛" - } - - statusText := fmt.Sprintf("*Status*: %s %s", statusEmoji, status) - if reason != "" { - statusText += fmt.Sprintf("\n*Resolution reason*: %s", lib.MarkdownEscape(reason, resolutionReasonLimit)) - } - - return statusText -} - -func MsgFields(reqID string, reqData pd.AccessRequestData, clusterName string, webProxyURL *url.URL) string { - var builder strings.Builder - builder.Grow(128) - - msgFieldToBuilder(&builder, "ID", reqID) - msgFieldToBuilder(&builder, "Cluster", clusterName) - - if len(reqData.User) > 0 { - msgFieldToBuilder(&builder, "User", reqData.User) - } - if reqData.Roles != nil { - msgFieldToBuilder(&builder, "Role(s)", strings.Join(reqData.Roles, ",")) - } - if reqData.RequestReason != "" { - msgFieldToBuilder(&builder, "Reason", lib.MarkdownEscape(reqData.RequestReason, requestReasonLimit)) - } - if webProxyURL != nil { - reqURL := *webProxyURL - reqURL.Path = lib.BuildURLPath("web", "requests", reqID) - msgFieldToBuilder(&builder, "Link", reqURL.String()) - } else { - if reqData.ResolutionTag == pd.Unresolved { - msgFieldToBuilder(&builder, "Approve", fmt.Sprintf("`tsh request review --approve %s`", reqID)) - msgFieldToBuilder(&builder, "Deny", fmt.Sprintf("`tsh request review --deny %s`", reqID)) - } - } - - return builder.String() -} - -func MsgReview(review types.AccessReview) (string, error) { - if review.Reason != "" { - review.Reason = lib.MarkdownEscape(review.Reason, ReviewReasonLimit) - } - - var proposedStateEmoji string - switch review.ProposedState { - case types.RequestState_APPROVED: - proposedStateEmoji = "✅" - case types.RequestState_DENIED: - proposedStateEmoji = "❌" - } - - var builder strings.Builder - err := reviewReplyTemplate.Execute(&builder, struct { - types.AccessReview - ProposedState string - ProposedStateEmoji string - TimeFormat string - }{ - review, - review.ProposedState.String(), - proposedStateEmoji, - time.RFC822, - }) - if err != nil { - return "", trace.Wrap(err) - } - return builder.String(), nil -} - -func msgFieldToBuilder(b *strings.Builder, field, value string) { - b.WriteString("*") - b.WriteString(field) - b.WriteString("*: ") - b.WriteString(value) - b.WriteString("\n") -} diff --git a/access/common/plugindata.go b/access/common/plugindata.go deleted file mode 100644 index 032212f3b..000000000 --- a/access/common/plugindata.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2022 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 common - -import ( - "fmt" - "strings" - - "github.com/gravitational/teleport-plugins/lib/plugindata" -) - -// GenericPluginData is a data associated with access request that we store in Teleport using UpdatePluginData API. -type GenericPluginData struct { - plugindata.AccessRequestData - SentMessages -} - -// MessageData contains all the required information to identify and edit a message. -type MessageData struct { - // ChannelID identifies a channel. - ChannelID string - // MessageID identifies a specific message in the channel. - // For example: on Discord this is an ID while on Slack this is a timestamp. - MessageID string -} - -type SentMessages = []MessageData - -// DecodePluginData deserializes a string map to GenericPluginData struct. -func DecodePluginData(dataMap map[string]string) (GenericPluginData, error) { - data := GenericPluginData{} - - data.AccessRequestData = plugindata.DecodeAccessRequestData(dataMap) - - if channelID, timestamp := dataMap["channel_id"], dataMap["timestamp"]; channelID != "" && timestamp != "" { - data.SentMessages = append(data.SentMessages, MessageData{ChannelID: channelID, MessageID: timestamp}) - } - - if str := dataMap["messages"]; str != "" { - for _, encodedMsg := range strings.Split(str, ",") { - if parts := strings.Split(encodedMsg, "/"); len(parts) == 2 { - data.SentMessages = append(data.SentMessages, MessageData{ChannelID: parts[0], MessageID: parts[1]}) - } - } - } - return data, nil -} - -// EncodePluginData serializes a GenericPluginData struct into a string map. -func EncodePluginData(data GenericPluginData) (map[string]string, error) { - result := plugindata.EncodeAccessRequestData(data.AccessRequestData) - - var encodedMessages []string - for _, msg := range data.SentMessages { - // TODO(hugoShaka): switch to base64 encode to avoid having / and , characters that could lead to bad parsing - encodedMessages = append(encodedMessages, fmt.Sprintf("%s/%s", msg.ChannelID, msg.MessageID)) - } - result["messages"] = strings.Join(encodedMessages, ",") - - return result, nil -} diff --git a/access/common/plugindata_test.go b/access/common/plugindata_test.go deleted file mode 100644 index 533e03e94..000000000 --- a/access/common/plugindata_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2023 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 common - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/gravitational/teleport-plugins/lib/plugindata" -) - -var samplePluginData = GenericPluginData{ - AccessRequestData: plugindata.AccessRequestData{ - User: "user-foo", - Roles: []string{"role-foo", "role-bar"}, - RequestReason: "foo reason", - ReviewsCount: 3, - ResolutionTag: plugindata.ResolvedApproved, - ResolutionReason: "foo ok", - }, - SentMessages: SentMessages{ - {ChannelID: "CHANNEL1", MessageID: "0000001"}, - {ChannelID: "CHANNEL2", MessageID: "0000002"}, - }, -} - -func TestEncodePluginData(t *testing.T) { - dataMap, err := EncodePluginData(samplePluginData) - assert.NoError(t, err) - assert.Len(t, dataMap, 7) - assert.Equal(t, "user-foo", dataMap["user"]) - assert.Equal(t, "role-foo,role-bar", dataMap["roles"]) - assert.Equal(t, "foo reason", dataMap["request_reason"]) - assert.Equal(t, "3", dataMap["reviews_count"]) - assert.Equal(t, "APPROVED", dataMap["resolution"]) - assert.Equal(t, "foo ok", dataMap["resolve_reason"]) - assert.Equal(t, "CHANNEL1/0000001,CHANNEL2/0000002", dataMap["messages"]) -} - -func TestDecodePluginData(t *testing.T) { - pluginData, err := DecodePluginData(map[string]string{ - "user": "user-foo", - "roles": "role-foo,role-bar", - "request_reason": "foo reason", - "reviews_count": "3", - "resolution": "APPROVED", - "resolve_reason": "foo ok", - "messages": "CHANNEL1/0000001,CHANNEL2/0000002", - }) - assert.NoError(t, err) - assert.Equal(t, samplePluginData, pluginData) -} - -func TestEncodeEmptyPluginData(t *testing.T) { - dataMap, err := EncodePluginData(GenericPluginData{}) - assert.NoError(t, err) - assert.Len(t, dataMap, 7) - for key, value := range dataMap { - assert.Emptyf(t, value, "value at key %q must be empty", key) - } -} - -func TestDecodeEmptyPluginData(t *testing.T) { - result, err := DecodePluginData(nil) - assert.NoError(t, err) - assert.Empty(t, result) - - result, err = DecodePluginData(make(map[string]string)) - assert.NoError(t, err) - assert.Empty(t, result) -} diff --git a/access/common/recipient.go b/access/common/recipient.go deleted file mode 100644 index 308953023..000000000 --- a/access/common/recipient.go +++ /dev/null @@ -1,141 +0,0 @@ -/* -Copyright 2022 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 common - -import ( - "fmt" - - "github.com/gravitational/teleport/api/types" - - "github.com/gravitational/teleport-plugins/lib/stringset" -) - -// RawRecipientsMap is a mapping of roles to recipient(s). -type RawRecipientsMap map[string][]string - -// UnmarshalTOML will convert the input into map[string][]string -// The input can be one of the following: -// "key" = "value" -// "key" = ["multiple", "values"] -func (r *RawRecipientsMap) UnmarshalTOML(in interface{}) error { - *r = make(RawRecipientsMap) - - recipientsMap, ok := in.(map[string]interface{}) - if !ok { - return fmt.Errorf("unexpected type for recipients %T", in) - } - - for k, v := range recipientsMap { - switch val := v.(type) { - case string: - (*r)[k] = []string{val} - case []interface{}: - for _, str := range val { - str, ok := str.(string) - if !ok { - return fmt.Errorf("unexpected type for recipients value %T", v) - } - (*r)[k] = append((*r)[k], str) - } - default: - return fmt.Errorf("unexpected type for recipients value %T", v) - } - } - - return nil -} - -// GetRawRecipientsFor will return the set of raw recipients given a list of roles and suggested reviewers. -// We create a unique list based on: -// - the list of suggestedReviewers -// - for each role, the list of reviewers -// - if the role doesn't exist in the map (or it's empty), we add the list of recipients for the default role ("*") instead -func (r RawRecipientsMap) GetRawRecipientsFor(roles, suggestedReviewers []string) []string { - recipients := stringset.New() - - for _, role := range roles { - roleRecipients := r[role] - if len(roleRecipients) == 0 { - roleRecipients = r[types.Wildcard] - } - - recipients.Add(roleRecipients...) - } - - recipients.Add(suggestedReviewers...) - - return recipients.ToSlice() -} - -// GetAllRawRecipients returns unique set of raw recipients -func (r RawRecipientsMap) GetAllRawRecipients() []string { - recipients := stringset.New() - - for _, r := range r { - recipients.Add(r...) - } - - return recipients.ToSlice() -} - -// Recipient is a generic representation of a message recipient. Its nature depends on the messaging service used. -// It can be a user, a public/private channel, or something else. A Recipient should contain enough information to -// identify uniquely where to send a message. -type Recipient struct { - // Name is the original string that was passed to create the recipient. This can be an id, email, channel name - // URL, ... This is the user input (through suggested reviewers or plugin configuration) - Name string - // ID represents the recipient from the messaging service point of view. - // e.g. if Name is a Slack user email address, ID will be the Slack user id. - // This information should be sufficient to send a new message to a recipient. - ID string - // Kind is the recipient kind inferred from the Recipient Name. This is a messaging service concept, most common - // values are "User" or "Channel". - Kind string - // Data allows MessagingBot to store required data for the recipient - Data interface{} -} - -// RecipientSet is a Set of Recipient. Recipient items are deduplicated based on Recipient.ID -type RecipientSet struct { - recipients map[string]Recipient -} - -// NewRecipientSet returns an initialized RecipientSet -func NewRecipientSet() RecipientSet { - return RecipientSet{recipients: make(map[string]Recipient)} -} - -// Add adds an item to an existing RecipientSet. If an item with the same Recipient.ID already exists it is overridden. -func (s *RecipientSet) Add(recipient Recipient) { - s.recipients[recipient.ID] = recipient -} - -// Contains checks if the RecipientSet contains a Recipient for a given recipientID. -func (s *RecipientSet) Contains(recipientID string) bool { - _, isPresent := s.recipients[recipientID] - return isPresent -} - -// ToSlice returns a Recipient slice from a RecipientSet. Items are copied but not deep-copied. -func (s *RecipientSet) ToSlice() []Recipient { - recipientSlice := make([]Recipient, 0, len(s.recipients)) - for _, recipient := range s.recipients { - recipientSlice = append(recipientSlice, recipient) - } - return recipientSlice -} diff --git a/access/common/recipient_test.go b/access/common/recipient_test.go deleted file mode 100644 index ae9b633cf..000000000 --- a/access/common/recipient_test.go +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright 2023 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 common - -import ( - "testing" - - "github.com/gravitational/teleport/api/types" - "github.com/pelletier/go-toml" - "github.com/stretchr/testify/require" -) - -type wrapRecipientsMap struct { - RecipientsMap RawRecipientsMap `toml:"role_to_recipients"` -} - -func TestRawRecipientsMap(t *testing.T) { - testCases := []struct { - desc string - in string - expectRecipients RawRecipientsMap - }{ - { - desc: "test role_to_recipients multiple format", - in: ` - [role_to_recipients] - "dev" = ["dev-channel", "admin-channel"] - "*" = "admin-channel" - `, - expectRecipients: RawRecipientsMap{ - "dev": []string{"dev-channel", "admin-channel"}, - types.Wildcard: []string{"admin-channel"}, - }, - }, - { - desc: "test role_to_recipients role to list of recipients", - in: ` - [role_to_recipients] - "dev" = ["dev-channel", "admin-channel"] - "prod" = ["sre-channel", "oncall-channel"] - `, - expectRecipients: RawRecipientsMap{ - "dev": []string{"dev-channel", "admin-channel"}, - "prod": []string{"sre-channel", "oncall-channel"}, - }, - }, - { - desc: "test role_to_recipients role to string recipient", - in: ` - [role_to_recipients] - "single" = "admin-channel" - `, - expectRecipients: RawRecipientsMap{ - "single": []string{"admin-channel"}, - }, - }, - { - desc: "test role_to_recipients multiple format", - in: ` - [role_to_recipients] - "dev" = ["dev-channel", "admin-channel"] - "*" = "admin-channel" - `, - expectRecipients: RawRecipientsMap{ - "dev": []string{"dev-channel", "admin-channel"}, - types.Wildcard: []string{"admin-channel"}, - }, - }, - { - desc: "test role_to_recipients no mapping", - in: ` - [role_to_recipients] - `, - expectRecipients: RawRecipientsMap{}, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - w := wrapRecipientsMap{} - err := toml.Unmarshal([]byte(tc.in), &w) - require.NoError(t, err) - - require.Equal(t, tc.expectRecipients, w.RecipientsMap) - }) - } -} - -func TestRawRecipientsMapGetRecipients(t *testing.T) { - testCases := []struct { - desc string - m RawRecipientsMap - roles []string - suggestedReviewers []string - output []string - }{ - { - desc: "test match exact role", - m: RawRecipientsMap{ - "dev": []string{"chanDev"}, - "*": []string{"chanA", "chanB"}, - }, - roles: []string{"dev"}, - suggestedReviewers: []string{}, - output: []string{"chanDev"}, - }, - { - desc: "test only default recipient", - m: RawRecipientsMap{ - "*": []string{"chanA", "chanB"}, - }, - roles: []string{"dev"}, - suggestedReviewers: []string{}, - output: []string{"chanA", "chanB"}, - }, - { - desc: "test deduplicate recipients", - m: RawRecipientsMap{ - "dev": []string{"chanA", "chanB"}, - "*": []string{"chanC"}, - }, - roles: []string{"dev"}, - suggestedReviewers: []string{"chanA", "chanB"}, - output: []string{"chanA", "chanB"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - recipients := tc.m.GetRawRecipientsFor(tc.roles, tc.suggestedReviewers) - require.ElementsMatch(t, recipients, tc.output) - }) - } -} - -func TestNewRecipientSet(t *testing.T) { - actual := NewRecipientSet() - expected := RecipientSet{recipients: make(map[string]Recipient)} - require.Equal(t, expected, actual) -} - -func TestRecipientSet_Add(t *testing.T) { - // Setup - set := NewRecipientSet() - a := Recipient{ - Name: "Recipient A", - ID: "A", - Kind: "Test", - } - b := Recipient{ - Name: "Recipient B", - ID: "B", - Kind: "Test", - } - a2 := Recipient{ - Name: "Recipient A2", - ID: "A", - Kind: "Test", - Data: nil, - } - - // Testing with a single element - set.Add(a) - require.Equal(t, map[string]Recipient{"A": a}, set.recipients) - - // Testing with a second element - set.Add(b) - require.Equal(t, map[string]Recipient{"A": a, "B": b}, set.recipients) - - // Testing with an element with the same ID - set.Add(a2) - require.Equal(t, map[string]Recipient{"A": a2, "B": b}, set.recipients) -} - -func TestRecipientSet_Contains(t *testing.T) { - // Setup - a := Recipient{ - Name: "Recipient A", - ID: "A", - Kind: "Test", - } - b := Recipient{ - Name: "Recipient B", - ID: "B", - Kind: "Test", - } - set := RecipientSet{recipients: map[string]Recipient{"A": a, "B": b}} - - // Testing contains on a couple elements - require.True(t, set.Contains(a.ID)) - require.True(t, set.Contains(b.ID)) - - // Testing contains on an absent element - require.False(t, set.Contains("non-existent")) -} - -func TestRecipientSet_ToSlice(t *testing.T) { - // Setup - emptySet := NewRecipientSet() - a := Recipient{ - Name: "Recipient A", - ID: "A", - Kind: "Test", - } - b := Recipient{ - Name: "Recipient B", - ID: "B", - Kind: "Test", - } - set := RecipientSet{recipients: map[string]Recipient{"A": a, "B": b}} - - // Testing with an empty set - require.Equal(t, []Recipient{}, emptySet.ToSlice()) - // Testing with a non-empty set - require.ElementsMatch(t, []Recipient{a, b}, set.ToSlice()) -} diff --git a/access/common/teleport/client.go b/access/common/teleport/client.go deleted file mode 100644 index 37455c03c..000000000 --- a/access/common/teleport/client.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2023 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 teleport - -import ( - "context" - - "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/types" - - "github.com/gravitational/teleport-plugins/lib/plugindata" -) - -// Client aggregates the parts of Teleport API client interface -// (as implemented by github.com/gravitational/teleport/api/client.Client) -// that are used by the access plugins. -type Client interface { - plugindata.Client - types.Events - Ping(context.Context) (proto.PingResponse, error) -} diff --git a/access/discord/app.go b/access/discord/app.go index 2346385b2..05bab56e0 100644 --- a/access/discord/app.go +++ b/access/discord/app.go @@ -17,7 +17,7 @@ limitations under the License. package main import ( - "github.com/gravitational/teleport-plugins/access/common" + "github.com/gravitational/teleport/integrations/access/common" ) const ( diff --git a/access/discord/bot.go b/access/discord/bot.go index ff92a9d99..0a18db1ef 100644 --- a/access/discord/bot.go +++ b/access/discord/bot.go @@ -25,11 +25,10 @@ import ( "github.com/go-resty/resty/v2" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/access/common" + "github.com/gravitational/teleport/integrations/lib" + pd "github.com/gravitational/teleport/integrations/lib/plugindata" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/access/common" - "github.com/gravitational/teleport-plugins/lib" - pd "github.com/gravitational/teleport-plugins/lib/plugindata" ) const discordMaxConns = 100 diff --git a/access/discord/config.go b/access/discord/config.go index 059500805..5a1f69f01 100644 --- a/access/discord/config.go +++ b/access/discord/config.go @@ -23,11 +23,10 @@ import ( "github.com/go-resty/resty/v2" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/access/common" + "github.com/gravitational/teleport/integrations/lib" "github.com/gravitational/trace" "github.com/pelletier/go-toml" - - "github.com/gravitational/teleport-plugins/access/common" - "github.com/gravitational/teleport-plugins/lib" ) const discordAPIUrl = "https://discord.com/api/" diff --git a/access/discord/discord_test.go b/access/discord/discord_test.go index 67e65e5e2..f44b741b8 100644 --- a/access/discord/discord_test.go +++ b/access/discord/discord_test.go @@ -29,15 +29,14 @@ import ( "github.com/google/uuid" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/access/common" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/testing/integration" "github.com/gravitational/trace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - - "github.com/gravitational/teleport-plugins/access/common" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/testing/integration" ) var msgFieldRegexp = regexp.MustCompile(`(?im)^\*([a-zA-Z ]+)\*: (.+)$`) diff --git a/access/discord/helpers_test.go b/access/discord/helpers_test.go index 9b5f591fa..31827a096 100644 --- a/access/discord/helpers_test.go +++ b/access/discord/helpers_test.go @@ -14,7 +14,7 @@ package main -import "github.com/gravitational/teleport-plugins/access/common" +import "github.com/gravitational/teleport/integrations/access/common" type MessageSlice []DiscordMsg type MessageSet map[common.MessageData]struct{} diff --git a/access/discord/main.go b/access/discord/main.go index 2afea1e8f..7c7e64945 100644 --- a/access/discord/main.go +++ b/access/discord/main.go @@ -24,10 +24,9 @@ import ( "time" "github.com/gravitational/kingpin" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) //go:embed example_config.toml diff --git a/access/email/app.go b/access/email/app.go index 8824022f1..403aa41f2 100644 --- a/access/email/app.go +++ b/access/email/app.go @@ -23,14 +23,13 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/credentials" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/watcherjob" "github.com/gravitational/trace" "google.golang.org/grpc" grpcbackoff "google.golang.org/grpc/backoff" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/credentials" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/watcherjob" ) const ( diff --git a/access/email/client.go b/access/email/client.go index 919e13942..91c1f58b0 100644 --- a/access/email/client.go +++ b/access/email/client.go @@ -25,10 +25,9 @@ import ( "time" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) var reviewReplyTemplate = template.Must(template.New("review reply").Parse( diff --git a/access/email/config.go b/access/email/config.go index d3b7eea8e..f42b06b4f 100644 --- a/access/email/config.go +++ b/access/email/config.go @@ -21,13 +21,12 @@ import ( "fmt" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/access/common" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" "github.com/pelletier/go-toml" "gopkg.in/mail.v2" - - "github.com/gravitational/teleport-plugins/access/common" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) // DeliveryConfig represents email recipients config diff --git a/access/email/config_test.go b/access/email/config_test.go index 708a29a90..27c9add1f 100644 --- a/access/email/config_test.go +++ b/access/email/config_test.go @@ -22,11 +22,10 @@ import ( "testing" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/access/common" "github.com/gravitational/trace" "github.com/stretchr/testify/require" "gopkg.in/mail.v2" - - "github.com/gravitational/teleport-plugins/access/common" ) func TestRecipients(t *testing.T) { diff --git a/access/email/email_test.go b/access/email/email_test.go index 8a4d74037..89dce9265 100644 --- a/access/email/email_test.go +++ b/access/email/email_test.go @@ -31,14 +31,13 @@ import ( "github.com/google/uuid" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/testing/integration" "github.com/gravitational/trace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/testing/integration" ) const ( diff --git a/access/email/main.go b/access/email/main.go index 0b925f298..8722a6a9c 100644 --- a/access/email/main.go +++ b/access/email/main.go @@ -23,10 +23,9 @@ import ( "time" "github.com/gravitational/kingpin" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) func main() { diff --git a/access/jira/app.go b/access/jira/app.go index 1a198b8e9..30db08619 100644 --- a/access/jira/app.go +++ b/access/jira/app.go @@ -28,16 +28,15 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" apiutils "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/credentials" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/watcherjob" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "google.golang.org/grpc" grpcbackoff "google.golang.org/grpc/backoff" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/backoff" - "github.com/gravitational/teleport-plugins/lib/credentials" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/watcherjob" ) const ( diff --git a/access/jira/client.go b/access/jira/client.go index c42a3aa2c..63a9964cc 100644 --- a/access/jira/client.go +++ b/access/jira/client.go @@ -28,10 +28,9 @@ import ( "github.com/go-resty/resty/v2" "github.com/google/go-querystring/query" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) const ( diff --git a/access/jira/config.go b/access/jira/config.go index a86affbcb..ecec664b8 100644 --- a/access/jira/config.go +++ b/access/jira/config.go @@ -23,11 +23,10 @@ import ( "os" "strings" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" "github.com/pelletier/go-toml" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) type Config struct { diff --git a/access/jira/jira_test.go b/access/jira/jira_test.go index 7272ec05c..e3df96551 100644 --- a/access/jira/jira_test.go +++ b/access/jira/jira_test.go @@ -33,14 +33,13 @@ import ( "github.com/google/uuid" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/testing/integration" "github.com/gravitational/trace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/testing/integration" ) type JiraSuite struct { diff --git a/access/jira/main.go b/access/jira/main.go index b95e044f5..080d6b608 100644 --- a/access/jira/main.go +++ b/access/jira/main.go @@ -23,10 +23,9 @@ import ( "time" "github.com/gravitational/kingpin" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) const ( diff --git a/access/jira/webhook_server.go b/access/jira/webhook_server.go index 0b9fe5d8e..d6c31baea 100644 --- a/access/jira/webhook_server.go +++ b/access/jira/webhook_server.go @@ -26,11 +26,10 @@ import ( "sync/atomic" "time" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" "github.com/julienschmidt/httprouter" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) const ( diff --git a/access/mattermost/app.go b/access/mattermost/app.go index 68967119a..95f887fb7 100644 --- a/access/mattermost/app.go +++ b/access/mattermost/app.go @@ -22,17 +22,16 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/credentials" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/stringset" + "github.com/gravitational/teleport/integrations/lib/watcherjob" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "google.golang.org/grpc" grpcbackoff "google.golang.org/grpc/backoff" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/backoff" - "github.com/gravitational/teleport-plugins/lib/credentials" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/stringset" - "github.com/gravitational/teleport-plugins/lib/watcherjob" ) const ( diff --git a/access/mattermost/bot.go b/access/mattermost/bot.go index 7337e15fa..5c148265e 100644 --- a/access/mattermost/bot.go +++ b/access/mattermost/bot.go @@ -24,10 +24,9 @@ import ( "github.com/go-resty/resty/v2" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" "github.com/gravitational/trace" "github.com/mailgun/holster/v3/collections" - - "github.com/gravitational/teleport-plugins/lib" ) const ( diff --git a/access/mattermost/config.go b/access/mattermost/config.go index ce1a18006..8fff1b9df 100644 --- a/access/mattermost/config.go +++ b/access/mattermost/config.go @@ -17,11 +17,10 @@ package main import ( "strings" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" "github.com/pelletier/go-toml" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) type Config struct { diff --git a/access/mattermost/main.go b/access/mattermost/main.go index 6d65fe92c..e07ec700c 100644 --- a/access/mattermost/main.go +++ b/access/mattermost/main.go @@ -23,10 +23,9 @@ import ( "time" "github.com/gravitational/kingpin" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) func main() { diff --git a/access/mattermost/mattermost_test.go b/access/mattermost/mattermost_test.go index 26a302807..37fc5f766 100644 --- a/access/mattermost/mattermost_test.go +++ b/access/mattermost/mattermost_test.go @@ -29,14 +29,13 @@ import ( "github.com/google/uuid" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/testing/integration" "github.com/gravitational/trace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/testing/integration" ) var msgFieldRegexp = regexp.MustCompile(`(?im)^\*\*([a-zA-Z ]+)\*\*:\ +(.+)$`) diff --git a/access/msteams/app.go b/access/msteams/app.go index c2e2ed0e1..95b7caeb1 100644 --- a/access/msteams/app.go +++ b/access/msteams/app.go @@ -21,16 +21,15 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/credentials" + "github.com/gravitational/teleport/integrations/lib/logger" + pd "github.com/gravitational/teleport/integrations/lib/plugindata" + "github.com/gravitational/teleport/integrations/lib/stringset" + "github.com/gravitational/teleport/integrations/lib/watcherjob" "github.com/gravitational/trace" "google.golang.org/grpc" grpcbackoff "google.golang.org/grpc/backoff" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/credentials" - "github.com/gravitational/teleport-plugins/lib/logger" - pd "github.com/gravitational/teleport-plugins/lib/plugindata" - "github.com/gravitational/teleport-plugins/lib/stringset" - "github.com/gravitational/teleport-plugins/lib/watcherjob" ) const ( diff --git a/access/msteams/bot.go b/access/msteams/bot.go index 4df4677f3..236650dd2 100644 --- a/access/msteams/bot.go +++ b/access/msteams/bot.go @@ -24,11 +24,11 @@ import ( "time" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/plugindata" "github.com/gravitational/trace" "github.com/gravitational/teleport-plugins/access/msteams/msapi" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/plugindata" ) const ( diff --git a/access/msteams/card.go b/access/msteams/card.go index 383387ac3..391eb58f7 100644 --- a/access/msteams/card.go +++ b/access/msteams/card.go @@ -22,9 +22,8 @@ import ( cards "github.com/DanielTitkov/go-adaptive-cards" "github.com/gravitational/teleport/api/types" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/plugindata" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/plugindata" ) // BuildCard builds the MS Teams message from a request data diff --git a/access/msteams/config.go b/access/msteams/config.go index 35936e10e..448e1d628 100644 --- a/access/msteams/config.go +++ b/access/msteams/config.go @@ -18,13 +18,13 @@ import ( "strings" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/access/common" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" "github.com/pelletier/go-toml" - "github.com/gravitational/teleport-plugins/access/common" "github.com/gravitational/teleport-plugins/access/msteams/msapi" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) // Config represents plugin configuration diff --git a/access/msteams/configure.go b/access/msteams/configure.go index 2be2e9da6..b23734bd8 100644 --- a/access/msteams/configure.go +++ b/access/msteams/configure.go @@ -24,9 +24,8 @@ import ( "path" "github.com/google/uuid" + "github.com/gravitational/teleport/integrations/lib" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" ) const ( diff --git a/access/msteams/main.go b/access/msteams/main.go index 5ada00088..9742e004c 100644 --- a/access/msteams/main.go +++ b/access/msteams/main.go @@ -20,10 +20,9 @@ import ( "time" "github.com/gravitational/kingpin" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) var ( diff --git a/access/msteams/ms_teams_test.go b/access/msteams/ms_teams_test.go index 64b4469d8..44a097697 100644 --- a/access/msteams/ms_teams_test.go +++ b/access/msteams/ms_teams_test.go @@ -28,17 +28,17 @@ import ( "github.com/google/uuid" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/access/common" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/testing/integration" "github.com/gravitational/trace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/tidwall/gjson" - "github.com/gravitational/teleport-plugins/access/common" "github.com/gravitational/teleport-plugins/access/msteams/msapi" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/testing/integration" ) type TeamsSuite struct { diff --git a/access/msteams/msapi/client.go b/access/msteams/msapi/client.go index f305759a4..b0b9a1be5 100644 --- a/access/msteams/msapi/client.go +++ b/access/msteams/msapi/client.go @@ -23,10 +23,9 @@ import ( "net/url" "strings" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - - "github.com/gravitational/teleport-plugins/lib/backoff" ) // Client represents generic MS API client diff --git a/access/msteams/msapi/token.go b/access/msteams/msapi/token.go index 90f696f50..d65b43693 100644 --- a/access/msteams/msapi/token.go +++ b/access/msteams/msapi/token.go @@ -25,10 +25,9 @@ import ( "sync" "time" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - - "github.com/gravitational/teleport-plugins/lib/backoff" ) const ( diff --git a/access/msteams/plugindata.go b/access/msteams/plugindata.go index 25b01afe8..67c1424a4 100644 --- a/access/msteams/plugindata.go +++ b/access/msteams/plugindata.go @@ -19,9 +19,8 @@ import ( "encoding/json" "strings" + "github.com/gravitational/teleport/integrations/lib/plugindata" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib/plugindata" ) // PluginData is a data associated with access request that we store in Teleport using UpdatePluginData API. diff --git a/access/msteams/plugindata_test.go b/access/msteams/plugindata_test.go index 8d5af16b7..1b2dd69b7 100644 --- a/access/msteams/plugindata_test.go +++ b/access/msteams/plugindata_test.go @@ -17,9 +17,8 @@ package main import ( "testing" + "github.com/gravitational/teleport/integrations/lib/plugindata" "github.com/stretchr/testify/assert" - - "github.com/gravitational/teleport-plugins/lib/plugindata" ) var samplePluginData = PluginData{ diff --git a/access/msteams/validate.go b/access/msteams/validate.go index 416b5093b..0ae6a738b 100644 --- a/access/msteams/validate.go +++ b/access/msteams/validate.go @@ -22,10 +22,9 @@ import ( cards "github.com/DanielTitkov/go-adaptive-cards" "github.com/google/uuid" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/plugindata" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/plugindata" ) // validate installs the application for a user if required and sends the Hello, world! message diff --git a/access/pagerduty/app.go b/access/pagerduty/app.go index f56fd8b60..66f703c23 100644 --- a/access/pagerduty/app.go +++ b/access/pagerduty/app.go @@ -26,16 +26,15 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/credentials" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/watcherjob" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "google.golang.org/grpc" grpcbackoff "google.golang.org/grpc/backoff" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/backoff" - "github.com/gravitational/teleport-plugins/lib/credentials" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/watcherjob" ) const ( diff --git a/access/pagerduty/client.go b/access/pagerduty/client.go index e3d72616a..156764630 100644 --- a/access/pagerduty/client.go +++ b/access/pagerduty/client.go @@ -28,11 +28,10 @@ import ( "github.com/go-resty/resty/v2" "github.com/google/go-querystring/query" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/stringset" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/stringset" ) const ( diff --git a/access/pagerduty/config.go b/access/pagerduty/config.go index 8c9eba630..14908063c 100644 --- a/access/pagerduty/config.go +++ b/access/pagerduty/config.go @@ -19,11 +19,10 @@ package main import ( "strings" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" "github.com/pelletier/go-toml" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) type Config struct { diff --git a/access/pagerduty/fake_pagerduty_test.go b/access/pagerduty/fake_pagerduty_test.go index 7ae6ff5e8..b8cc02eeb 100644 --- a/access/pagerduty/fake_pagerduty_test.go +++ b/access/pagerduty/fake_pagerduty_test.go @@ -28,11 +28,10 @@ import ( "sync" "sync/atomic" + "github.com/gravitational/teleport/integrations/lib/stringset" "github.com/gravitational/trace" "github.com/julienschmidt/httprouter" log "github.com/sirupsen/logrus" - - "github.com/gravitational/teleport-plugins/lib/stringset" ) type FakePagerduty struct { diff --git a/access/pagerduty/main.go b/access/pagerduty/main.go index e7f0b754e..8807e009e 100644 --- a/access/pagerduty/main.go +++ b/access/pagerduty/main.go @@ -23,10 +23,9 @@ import ( "time" "github.com/gravitational/kingpin" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) func main() { diff --git a/access/pagerduty/pagerduty_test.go b/access/pagerduty/pagerduty_test.go index ece3de176..07ec65ad2 100644 --- a/access/pagerduty/pagerduty_test.go +++ b/access/pagerduty/pagerduty_test.go @@ -30,14 +30,13 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/wrappers" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/testing/integration" "github.com/gravitational/trace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/testing/integration" ) const ( diff --git a/access/slack/app.go b/access/slack/app.go deleted file mode 100644 index 04c084975..000000000 --- a/access/slack/app.go +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2022 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 slack - -import ( - "github.com/gravitational/teleport-plugins/access/common" -) - -const ( - // slackPluginName is used to tag Slack GenericPluginData and as a Delegator in Audit log. - slackPluginName = "slack" -) - -// NewSlackApp initializes a new teleport-slack app and returns it. -func NewSlackApp(conf *Config) *common.BaseApp { - return common.NewApp(conf, slackPluginName) -} diff --git a/access/slack/bot.go b/access/slack/bot.go deleted file mode 100644 index 20d4b495a..000000000 --- a/access/slack/bot.go +++ /dev/null @@ -1,205 +0,0 @@ -/* -Copyright 2022 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 slack - -import ( - "context" - "encoding/json" - "net/url" - "time" - - "github.com/go-resty/resty/v2" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/access/common" - "github.com/gravitational/teleport-plugins/lib" - pd "github.com/gravitational/teleport-plugins/lib/plugindata" -) - -const slackMaxConns = 100 -const slackHTTPTimeout = 10 * time.Second - -// Bot is a slack client that works with AccessRequest. -// It's responsible for formatting and posting a message on Slack when an -// action occurs with an access request: a new request popped up, or a -// request is processed/updated. -type Bot struct { - client *resty.Client - clusterName string - webProxyURL *url.URL -} - -// onAfterResponseSlack resty error function for Slack -func onAfterResponseSlack(_ *resty.Client, resp *resty.Response) error { - if !resp.IsSuccess() { - return trace.Errorf("slack api returned unexpected code %v", resp.StatusCode()) - } - - var result APIResponse - if err := json.Unmarshal(resp.Body(), &result); err != nil { - return trace.Wrap(err) - } - - if !result.Ok { - return trace.Errorf("%s", result.Error) - } - - return nil -} - -func (b Bot) CheckHealth(ctx context.Context) error { - _, err := b.client.NewRequest(). - SetContext(ctx). - Post("auth.test") - if err != nil { - if err.Error() == "invalid_auth" { - return trace.Wrap(err, "authentication failed, probably invalid token") - } - return trace.Wrap(err) - } - return nil -} - -// Broadcast posts request info to Slack with action buttons. -func (b Bot) Broadcast(ctx context.Context, recipients []common.Recipient, reqID string, reqData pd.AccessRequestData) (common.SentMessages, error) { - var data common.SentMessages - var errors []error - - for _, recipient := range recipients { - var result ChatMsgResponse - _, err := b.client.NewRequest(). - SetContext(ctx). - SetBody(Message{BaseMessage: BaseMessage{Channel: recipient.ID}, BlockItems: b.slackMsgSections(reqID, reqData)}). - SetResult(&result). - Post("chat.postMessage") - if err != nil { - errors = append(errors, trace.Wrap(err)) - continue - } - data = append(data, common.MessageData{ChannelID: result.Channel, MessageID: result.Timestamp}) - } - - return data, trace.NewAggregate(errors...) -} - -func (b Bot) PostReviewReply(ctx context.Context, channelID, timestamp string, review types.AccessReview) error { - text, err := common.MsgReview(review) - if err != nil { - return trace.Wrap(err) - } - - _, err = b.client.NewRequest(). - SetContext(ctx). - SetBody(Message{BaseMessage: BaseMessage{Channel: channelID, ThreadTs: timestamp}, Text: text}). - Post("chat.postMessage") - return trace.Wrap(err) -} - -// LookupDirectChannelByEmail fetches user's id by email. -func (b Bot) lookupDirectChannelByEmail(ctx context.Context, email string) (string, error) { - var result struct { - APIResponse - User User `json:"user"` - } - _, err := b.client.NewRequest(). - SetContext(ctx). - SetQueryParam("email", email). - SetResult(&result). - Get("users.lookupByEmail") - if err != nil { - return "", trace.Wrap(err) - } - - return result.User.ID, nil -} - -// Expire updates request's Slack post with EXPIRED status and removes action buttons. -func (b Bot) UpdateMessages(ctx context.Context, reqID string, reqData pd.AccessRequestData, slackData common.SentMessages, reviews []types.AccessReview) error { - var errors []error - for _, msg := range slackData { - _, err := b.client.NewRequest(). - SetContext(ctx). - SetBody(Message{BaseMessage: BaseMessage{ - Channel: msg.ChannelID, - Timestamp: msg.MessageID, - }, BlockItems: b.slackMsgSections(reqID, reqData)}). - Post("chat.update") - if err != nil { - switch err.Error() { - case "message_not_found": - err = trace.Wrap(err, "cannot find message with timestamp %s in channel %s", msg.MessageID, msg.ChannelID) - default: - err = trace.Wrap(err) - } - errors = append(errors, trace.Wrap(err)) - } - } - - if len(errors) > 0 { - return errors[0] - } - - return nil -} - -func (b Bot) FetchRecipient(ctx context.Context, recipient string) (*common.Recipient, error) { - if lib.IsEmail(recipient) { - channel, err := b.lookupDirectChannelByEmail(ctx, recipient) - if err != nil { - if err.Error() == "users_not_found" { - return nil, trace.NotFound("email recipient '%s' not found: %s", recipient, err) - } - return nil, trace.Errorf("error resolving email recipient %s: %s", recipient, err) - } - return &common.Recipient{ - Name: recipient, - ID: channel, - Kind: "Email", - Data: nil, - }, nil - } - // TODO: check if channel exists ? - return &common.Recipient{ - Name: recipient, - ID: recipient, - Kind: "Channel", - Data: nil, - }, nil -} - -// msgSection builds a Slack message section (obeys markdown). -func (b Bot) slackMsgSections(reqID string, reqData pd.AccessRequestData) []BlockItem { - fields := common.MsgFields(reqID, reqData, b.clusterName, b.webProxyURL) - statusText := common.MsgStatusText(reqData.ResolutionTag, reqData.ResolutionReason) - - sections := []BlockItem{ - NewBlockItem(SectionBlock{ - Text: NewTextObjectItem(MarkdownObject{Text: "You have a new Role Request:"}), - }), - NewBlockItem(SectionBlock{ - Text: NewTextObjectItem(MarkdownObject{Text: fields}), - }), - NewBlockItem(ContextBlock{ - ElementItems: []ContextElementItem{ - NewContextElementItem(MarkdownObject{Text: statusText}), - }, - }), - } - - return sections -} diff --git a/access/slack/cmd/slack/main.go b/access/slack/cmd/slack/main.go index d26fe8bdc..d1beb695d 100644 --- a/access/slack/cmd/slack/main.go +++ b/access/slack/cmd/slack/main.go @@ -24,11 +24,10 @@ import ( "time" "github.com/gravitational/kingpin" + "github.com/gravitational/teleport/integrations/access/slack" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/access/slack" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) //go:embed example_config.toml diff --git a/access/slack/config.go b/access/slack/config.go deleted file mode 100644 index efcdeefca..000000000 --- a/access/slack/config.go +++ /dev/null @@ -1,135 +0,0 @@ -/* -Copyright 2022 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 slack - -import ( - "net/url" - "strings" - - "github.com/go-resty/resty/v2" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - "github.com/pelletier/go-toml" - - "github.com/gravitational/teleport-plugins/access/common" - "github.com/gravitational/teleport-plugins/access/common/auth" - "github.com/gravitational/teleport-plugins/lib" -) - -// Config stores the full configuration for the teleport-slack plugin to run. -type Config struct { - common.BaseConfig - Slack common.GenericAPIConfig - AccessTokenProvider auth.AccessTokenProvider -} - -// LoadSlackConfig reads the config file, initializes a new SlackConfig struct object, and returns it. -// Optionally returns an error if the file is not readable, or if file format is invalid. -func LoadSlackConfig(filepath string) (*Config, error) { - t, err := toml.LoadFile(filepath) - if err != nil { - return nil, trace.Wrap(err) - } - - conf := &Config{} - if err := t.Unmarshal(conf); err != nil { - return nil, trace.Wrap(err) - } - - if strings.HasPrefix(conf.Slack.Token, "/") { - conf.Slack.Token, err = lib.ReadPassword(conf.Slack.Token) - if err != nil { - return nil, trace.Wrap(err) - } - } - - if err := conf.CheckAndSetDefaults(); err != nil { - return nil, trace.Wrap(err) - } - return conf, nil -} - -// CheckAndSetDefaults checks the config struct for any logical errors, and sets default values -// if some values are missing. -// If critical values are missing and we can't set defaults for them, this will return an error. -func (c *Config) CheckAndSetDefaults() error { - if err := c.Teleport.CheckAndSetDefaults(); err != nil { - return trace.Wrap(err) - } - - if c.AccessTokenProvider == nil { - if c.Slack.Token == "" { - return trace.BadParameter("missing required value slack.token") - } - c.AccessTokenProvider = auth.NewStaticAccessTokenProvider(c.Slack.Token) - } else { - if c.Slack.Token != "" { - return trace.BadParameter("exactly one of slack.token and AccessTokenProvider must be set") - } - } - - if c.Log.Output == "" { - c.Log.Output = "stderr" - } - if c.Log.Severity == "" { - c.Log.Severity = "info" - } - - if len(c.Recipients) == 0 { - return trace.BadParameter("missing required value role_to_recipients.") - } else if len(c.Recipients[types.Wildcard]) == 0 { - return trace.BadParameter("missing required value role_to_recipients[%v].", types.Wildcard) - } - - return nil -} - -// NewBot initializes the new Slack message generator (SlackBot) -// takes GenericAPIConfig as an argument. -func (c *Config) NewBot(clusterName, webProxyAddr string) (common.MessagingBot, error) { - var ( - webProxyURL *url.URL - err error - ) - if webProxyAddr != "" { - if webProxyURL, err = lib.AddrToURL(webProxyAddr); err != nil { - return Bot{}, trace.Wrap(err) - } - } - - var apiURL = slackAPIURL - if endpoint := c.Slack.APIURL; endpoint != "" { - apiURL = endpoint - } - - client := makeSlackClient(apiURL). - OnBeforeRequest(func(_ *resty.Client, r *resty.Request) error { - token, err := c.AccessTokenProvider.GetAccessToken() - if err != nil { - return trace.Wrap(err) - } - r.SetHeader("Authorization", "Bearer "+token) - return nil - }). - OnAfterResponse(onAfterResponseSlack) - - return Bot{ - client: client, - clusterName: clusterName, - webProxyURL: webProxyURL, - }, nil -} diff --git a/access/slack/fake_slack_test.go b/access/slack/fake_slack_test.go deleted file mode 100644 index 1ae3e28ca..000000000 --- a/access/slack/fake_slack_test.go +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright 2023 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 slack - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "runtime/debug" - "sync" - "sync/atomic" - "time" - - "github.com/gravitational/trace" - "github.com/julienschmidt/httprouter" - log "github.com/sirupsen/logrus" -) - -type FakeSlack struct { - srv *httptest.Server - - botUser User - objects sync.Map - newMessages chan Message - messageUpdatesByAPI chan Message - messageUpdatesByResponding chan Message - messageCounter uint64 - userIDCounter uint64 - startTime time.Time -} - -func NewFakeSlack(botUser User, concurrency int) *FakeSlack { - router := httprouter.New() - - s := &FakeSlack{ - newMessages: make(chan Message, concurrency*6), - messageUpdatesByAPI: make(chan Message, concurrency*2), - messageUpdatesByResponding: make(chan Message, concurrency), - startTime: time.Now(), - srv: httptest.NewServer(router), - } - - s.botUser = s.StoreUser(botUser) - - router.POST("/auth.test", func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(rw).Encode(APIResponse{Ok: true}) - panicIf(err) - }) - - router.POST("/chat.postMessage", func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - var payload Message - err := json.NewDecoder(r.Body).Decode(&payload) - panicIf(err) - - // text limit and block text limit as per - // https://api.slack.com/methods/chat.postMessage and - // https://api.slack.com/reference/block-kit/blocks#section - if len(payload.Text) > 4000 || func() bool { - for _, block := range payload.BlockItems { - sectionBlock, ok := block.Block.(SectionBlock) - if !ok { - continue - } - if len(sectionBlock.Text.GetText()) > 3000 { - return true - } - } - return false - }() { - rw.WriteHeader(http.StatusBadRequest) - return - } - - msg := s.StoreMessage(Message{BaseMessage: BaseMessage{ - Type: "message", - Channel: payload.Channel, - ThreadTs: payload.ThreadTs, - User: s.botUser.ID, - Username: s.botUser.Name, - }, - BlockItems: payload.BlockItems, - Text: payload.Text, - }) - s.newMessages <- msg - - response := ChatMsgResponse{ - APIResponse: APIResponse{Ok: true}, - Channel: msg.Channel, - Timestamp: msg.Timestamp, - Text: msg.Text, - } - err = json.NewEncoder(rw).Encode(response) - panicIf(err) - }) - - router.POST("/chat.update", func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - var payload Message - err := json.NewDecoder(r.Body).Decode(&payload) - panicIf(err) - - msg, found := s.GetMessage(payload.Timestamp) - if !found { - err := json.NewEncoder(rw).Encode(APIResponse{Ok: false, Error: "message_not_found"}) - panicIf(err) - return - } - - msg.Text = payload.Text - msg.BlockItems = payload.BlockItems - - s.messageUpdatesByAPI <- s.StoreMessage(msg) - - response := ChatMsgResponse{ - APIResponse: APIResponse{Ok: true}, - Channel: msg.Channel, - Timestamp: msg.Timestamp, - Text: msg.Text, - } - err = json.NewEncoder(rw).Encode(&response) - panicIf(err) - }) - - router.POST("/_response/:ts", func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - var payload struct { - Message - ReplaceOriginal bool `json:"replace_original"` - } - err := json.NewDecoder(r.Body).Decode(&payload) - panicIf(err) - - timestamp := ps.ByName("ts") - msg, found := s.GetMessage(timestamp) - if !found { - err := json.NewEncoder(rw).Encode(APIResponse{Ok: false, Error: "message_not_found"}) - panicIf(err) - return - } - - if payload.ReplaceOriginal { - msg.BlockItems = payload.BlockItems - s.messageUpdatesByResponding <- s.StoreMessage(msg) - } else { - newMsg := s.StoreMessage(Message{BaseMessage: BaseMessage{ - Type: "message", - Channel: msg.Channel, - User: s.botUser.ID, - Username: s.botUser.Name, - }, - BlockItems: payload.BlockItems, - }) - s.newMessages <- newMsg - } - err = json.NewEncoder(rw).Encode(APIResponse{Ok: true}) - panicIf(err) - }) - - router.GET("/users.info", func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - id := r.URL.Query().Get("user") - if id == "" { - err := json.NewEncoder(rw).Encode(APIResponse{Ok: false, Error: "invalid_arguments"}) - panicIf(err) - return - } - - user, found := s.GetUser(id) - if !found { - err := json.NewEncoder(rw).Encode(APIResponse{Ok: false, Error: "user_not_found"}) - panicIf(err) - return - } - - err := json.NewEncoder(rw).Encode(struct { - User User `json:"user"` - Ok bool `json:"ok"` - }{user, true}) - panicIf(err) - }) - - router.GET("/users.lookupByEmail", func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - email := r.URL.Query().Get("email") - if email == "" { - err := json.NewEncoder(rw).Encode(APIResponse{Ok: false, Error: "invalid_arguments"}) - panicIf(err) - return - } - - user, found := s.GetUserByEmail(email) - if !found { - err := json.NewEncoder(rw).Encode(APIResponse{Ok: false, Error: "users_not_found"}) - panicIf(err) - return - } - - err := json.NewEncoder(rw).Encode(struct { - User User `json:"user"` - Ok bool `json:"ok"` - }{user, true}) - panicIf(err) - }) - - return s -} - -func (s *FakeSlack) URL() string { - return s.srv.URL -} - -func (s *FakeSlack) Close() { - s.srv.Close() - close(s.newMessages) - close(s.messageUpdatesByAPI) - close(s.messageUpdatesByResponding) -} - -func (s *FakeSlack) StoreMessage(msg Message) Message { - if msg.Timestamp == "" { - now := s.startTime.Add(time.Since(s.startTime)) // get monotonic timestamp - uniq := atomic.AddUint64(&s.messageCounter, 1) // generate uniq int to prevent races - msg.Timestamp = fmt.Sprintf("%d.%d", now.UnixNano(), uniq) - } - s.objects.Store(fmt.Sprintf("msg-%s", msg.Timestamp), msg) - return msg -} - -func (s *FakeSlack) GetMessage(id string) (Message, bool) { - if obj, ok := s.objects.Load(fmt.Sprintf("msg-%s", id)); ok { - msg, ok := obj.(Message) - return msg, ok - } - return Message{}, false -} - -func (s *FakeSlack) StoreUser(user User) User { - if user.ID == "" { - user.ID = fmt.Sprintf("U%d", atomic.AddUint64(&s.userIDCounter, 1)) - } - s.objects.Store(fmt.Sprintf("user-%s", user.ID), user) - s.objects.Store(fmt.Sprintf("userByEmail-%s", user.Profile.Email), user) - return user -} - -func (s *FakeSlack) GetUser(id string) (User, bool) { - if obj, ok := s.objects.Load(fmt.Sprintf("user-%s", id)); ok { - user, ok := obj.(User) - return user, ok - } - return User{}, false -} - -func (s *FakeSlack) GetUserByEmail(email string) (User, bool) { - if obj, ok := s.objects.Load(fmt.Sprintf("userByEmail-%s", email)); ok { - user, ok := obj.(User) - return user, ok - } - return User{}, false -} - -func (s *FakeSlack) CheckNewMessage(ctx context.Context) (Message, error) { - select { - case message := <-s.newMessages: - return message, nil - case <-ctx.Done(): - return Message{}, trace.Wrap(ctx.Err()) - } -} - -func (s *FakeSlack) CheckMessageUpdateByAPI(ctx context.Context) (Message, error) { - select { - case message := <-s.messageUpdatesByAPI: - return message, nil - case <-ctx.Done(): - return Message{}, trace.Wrap(ctx.Err()) - } -} - -func (s *FakeSlack) CheckMessageUpdateByResponding(ctx context.Context) (Message, error) { - select { - case message := <-s.messageUpdatesByResponding: - return message, nil - case <-ctx.Done(): - return Message{}, trace.Wrap(ctx.Err()) - } -} - -func panicIf(err error) { - if err != nil { - log.Panicf("%v at %v", err, string(debug.Stack())) - } -} diff --git a/access/slack/helpers_test.go b/access/slack/helpers_test.go deleted file mode 100644 index dad43674e..000000000 --- a/access/slack/helpers_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2023 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 slack - -import "github.com/gravitational/teleport-plugins/access/common" - -type SlackMessageSlice []Message -type SlackDataMessageSet map[common.MessageData]struct{} - -func (slice SlackMessageSlice) Len() int { - return len(slice) -} - -func (slice SlackMessageSlice) Less(i, j int) bool { - if slice[i].Channel < slice[j].Channel { - return true - } - return slice[i].Timestamp < slice[j].Timestamp -} - -func (slice SlackMessageSlice) Swap(i, j int) { - slice[i], slice[j] = slice[j], slice[i] -} - -func (set SlackDataMessageSet) Add(msg common.MessageData) { - set[msg] = struct{}{} -} - -func (set SlackDataMessageSet) Contains(msg common.MessageData) bool { - _, ok := set[msg] - return ok -} diff --git a/access/slack/http.go b/access/slack/http.go deleted file mode 100644 index c913ab362..000000000 --- a/access/slack/http.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2023 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 slack - -import ( - "net/http" - - "github.com/go-resty/resty/v2" -) - -const slackAPIURL = "https://slack.com/api/" - -func makeSlackClient(apiURL string) *resty.Client { - return resty. - NewWithClient(&http.Client{ - Timeout: slackHTTPTimeout, - Transport: &http.Transport{ - MaxConnsPerHost: slackMaxConns, - MaxIdleConnsPerHost: slackMaxConns, - }, - }). - SetHeader("Content-Type", "application/json"). - SetHeader("Accept", "application/json"). - SetHostURL(apiURL) -} diff --git a/access/slack/oauth.go b/access/slack/oauth.go deleted file mode 100644 index e29175ac8..000000000 --- a/access/slack/oauth.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2023 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 slack - -import ( - "context" - "time" - - "github.com/go-resty/resty/v2" - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/access/common/auth/oauth" - "github.com/gravitational/teleport-plugins/access/common/auth/storage" -) - -// Authorizer implements oauth2.Authorizer for Slack API. -type Authorizer struct { - client *resty.Client - - clientID string - clientSecret string -} - -func newAuthorizer(client *resty.Client, clientID string, clientSecret string) *Authorizer { - return &Authorizer{ - client: client, - clientID: clientID, - clientSecret: clientSecret, - } -} - -// NewAuthorizer returns a new Authorizer. -// -// clientID is the Client ID for this Slack app as specified by OAuth2. -// clientSecret is the Client Secret for this Slack app as specified by OAuth2. -func NewAuthorizer(clientID string, clientSecret string) *Authorizer { - client := makeSlackClient(slackAPIURL) - return newAuthorizer(client, clientID, clientSecret) -} - -// Exchange implements oauth.Exchanger -func (a *Authorizer) Exchange(ctx context.Context, authorizationCode string, redirectURI string) (*storage.Credentials, error) { - var result AccessResponse - - _, err := a.client.R(). - SetQueryParam("client_id", a.clientID). - SetQueryParam("client_secret", a.clientSecret). - SetQueryParam("code", authorizationCode). - SetQueryParam("redirect_uri", redirectURI). - SetResult(&result). - Post("oauth.v2.access") - - if err != nil { - return nil, trace.Wrap(err) - } - - if !result.Ok { - return nil, trace.Errorf("%s", result.Error) - } - - return &storage.Credentials{ - AccessToken: result.AccessToken, - RefreshToken: result.RefreshToken, - ExpiresAt: time.Now().UTC().Add(time.Duration(result.ExpiresInSeconds) * time.Second), - }, nil -} - -// Refresh implements oauth.Refresher -func (a *Authorizer) Refresh(ctx context.Context, refreshToken string) (*storage.Credentials, error) { - var result AccessResponse - _, err := a.client.R(). - SetQueryParam("client_id", a.clientID). - SetQueryParam("client_secret", a.clientSecret). - SetQueryParam("grant_type", "refresh_token"). - SetQueryParam("refresh_token", refreshToken). - SetResult(&result). - Post("oauth.v2.access") - - if err != nil { - return nil, trace.Wrap(err) - } - - if !result.Ok { - return nil, trace.Errorf("%s", result.Error) - } - - return &storage.Credentials{ - AccessToken: result.AccessToken, - RefreshToken: result.RefreshToken, - ExpiresAt: time.Now().UTC().Add(time.Duration(result.ExpiresInSeconds) * time.Second), - }, nil -} - -var _ oauth.Authorizer = &Authorizer{} diff --git a/access/slack/oauth_test.go b/access/slack/oauth_test.go deleted file mode 100644 index c48e3ea47..000000000 --- a/access/slack/oauth_test.go +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2023 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 slack - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/julienschmidt/httprouter" - "github.com/stretchr/testify/require" -) - -type testOAuthServer struct { - clientID string - clientSecret string - authorizationCode string - redirectURI string - refreshToken string - - exchangeResponse AccessResponse - refreshResponse AccessResponse - - srv *httptest.Server - t *testing.T -} - -func (s *testOAuthServer) handler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - if grantType := r.URL.Query().Get("grant_type"); grantType == "refresh_token" { - s.refresh(w, r) - } else { - s.exchange(w, r) - } -} - -func (s *testOAuthServer) exchange(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - require.Equal(s.t, s.clientID, q.Get("client_id")) - require.Equal(s.t, s.clientSecret, q.Get("client_secret")) - require.Equal(s.t, s.redirectURI, q.Get("redirect_uri")) - require.Equal(s.t, s.authorizationCode, q.Get("code")) - - w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(s.exchangeResponse) - require.NoError(s.t, err) -} - -func (s *testOAuthServer) refresh(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - require.Equal(s.t, s.clientID, q.Get("client_id")) - require.Equal(s.t, s.clientSecret, q.Get("client_secret")) - require.Equal(s.t, s.refreshToken, q.Get("refresh_token")) - - w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(s.refreshResponse) - require.NoError(s.t, err) -} - -func (s *testOAuthServer) start() { - router := httprouter.New() - router.POST("/oauth.v2.access", s.handler) - - s.srv = httptest.NewServer(router) -} - -func (s *testOAuthServer) url() string { - return s.srv.URL + "/" -} - -func (s *testOAuthServer) close() { - s.srv.Close() -} - -func TestOAuth(t *testing.T) { - const ( - clientID = "my-client-id" - clientSecret = "my-client-secret" - authorizationCode = "12345678" - redirectURI = "https://foobar.com/callback" - refreshToken = "my-refresh-token1" - expiresInSeconds = 43200 - ) - - newServer := func(t *testing.T) *testOAuthServer { - s := &testOAuthServer{ - clientID: clientID, - clientSecret: clientSecret, - authorizationCode: authorizationCode, - redirectURI: redirectURI, - refreshToken: refreshToken, - - t: t, - } - s.start() - return s - } - - ok := func(accessToken string, refreshToken string, expiresInSeconds int) AccessResponse { - return AccessResponse{ - APIResponse: APIResponse{Ok: true}, - AccessToken: accessToken, - RefreshToken: refreshToken, - ExpiresInSeconds: expiresInSeconds, - } - } - - fail := func(e string) AccessResponse { - return AccessResponse{ - APIResponse: APIResponse{ - Ok: false, - Error: e, - }, - } - } - - t.Run("ExchangeOK", func(t *testing.T) { - s := newServer(t) - defer s.close() - s.exchangeResponse = ok("my-access-token1", "my-refresh-token2", expiresInSeconds) - - authorizer := newAuthorizer(makeSlackClient(s.url()), clientID, clientSecret) - - creds, err := authorizer.Exchange(context.Background(), s.authorizationCode, s.redirectURI) - require.NoError(t, err) - require.Equal(t, s.exchangeResponse.AccessToken, creds.AccessToken) - require.Equal(t, s.exchangeResponse.RefreshToken, creds.RefreshToken) - require.WithinDuration(t, time.Now().Add(time.Duration(expiresInSeconds)*time.Second), creds.ExpiresAt, 1*time.Second) - }) - - t.Run("ExchangeFail", func(t *testing.T) { - s := newServer(t) - defer s.close() - s.exchangeResponse = fail("invalid_code") - - authorizer := newAuthorizer(makeSlackClient(s.url()), clientID, clientSecret) - - _, err := authorizer.Exchange(context.Background(), s.authorizationCode, s.redirectURI) - require.Error(t, err) - require.ErrorContains(t, err, "invalid_code") - - }) - - t.Run("RefreshOK", func(t *testing.T) { - s := newServer(t) - defer s.close() - s.refreshResponse = ok("my-access-token2", "my-refresh-token3", expiresInSeconds) - - authorizer := newAuthorizer(makeSlackClient(s.url()), clientID, clientSecret) - - creds, err := authorizer.Refresh(context.Background(), refreshToken) - require.NoError(t, err) - require.Equal(t, s.refreshResponse.AccessToken, creds.AccessToken) - require.Equal(t, s.refreshResponse.RefreshToken, creds.RefreshToken) - require.WithinDuration(t, time.Now().Add(time.Duration(expiresInSeconds)*time.Second), creds.ExpiresAt, 1*time.Second) - }) - - t.Run("RefreshFail", func(t *testing.T) { - - s := newServer(t) - defer s.close() - s.refreshResponse = fail("expired_token") - - authorizer := newAuthorizer(makeSlackClient(s.url()), clientID, clientSecret) - - _, err := authorizer.Refresh(context.Background(), refreshToken) - require.Error(t, err) - require.ErrorContains(t, err, "expired_token") - }) -} diff --git a/access/slack/slack_test.go b/access/slack/slack_test.go deleted file mode 100644 index d669c8399..000000000 --- a/access/slack/slack_test.go +++ /dev/null @@ -1,795 +0,0 @@ -// Copyright 2023 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 slack - -import ( - "context" - "os/user" - "regexp" - "runtime" - "sort" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/google/uuid" - "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - - "github.com/gravitational/teleport-plugins/access/common" - "github.com/gravitational/teleport-plugins/access/common/auth" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/testing/integration" -) - -var msgFieldRegexp = regexp.MustCompile(`(?im)^\*([a-zA-Z ]+)\*: (.+)$`) -var requestReasonRegexp = regexp.MustCompile("(?im)^\\*Reason\\*:\\ ```\\n(.*?)```(.*?)$") - -type SlackSuite struct { - integration.Suite - appConfig *Config - userNames struct { - ruler string - requestor string - reviewer1 string - reviewer2 string - plugin string - } - raceNumber int - fakeSlack *FakeSlack - - clients map[string]*integration.Client - teleportFeatures *proto.Features - teleportConfig lib.TeleportConfig -} - -func TestSlackbot(t *testing.T) { suite.Run(t, &SlackSuite{}) } - -func (s *SlackSuite) SetupSuite() { - var err error - t := s.T() - - logger.Init() - err = logger.Setup(logger.Config{Severity: "debug"}) - require.NoError(t, err) - s.raceNumber = runtime.GOMAXPROCS(0) - me, err := user.Current() - require.NoError(t, err) - - // We set such a big timeout because integration.NewFromEnv could start - // downloading a Teleport *-bin.tar.gz file which can take a long time. - ctx := s.SetContextTimeout(2 * time.Minute) - - teleport, err := integration.NewFromEnv(ctx) - require.NoError(t, err) - t.Cleanup(teleport.Close) - - auth, err := teleport.NewAuthService() - require.NoError(t, err) - s.StartApp(auth) - - s.clients = make(map[string]*integration.Client) - - // Set up the user who has an access to all kinds of resources. - - s.userNames.ruler = me.Username + "-ruler@example.com" - client, err := teleport.MakeAdmin(ctx, auth, s.userNames.ruler) - require.NoError(t, err) - s.clients[s.userNames.ruler] = client - - // Get the server features. - - pong, err := client.Ping(ctx) - require.NoError(t, err) - teleportFeatures := pong.GetServerFeatures() - - var bootstrap integration.Bootstrap - - // Set up user who can request the access to role "editor". - - conditions := types.RoleConditions{Request: &types.AccessRequestConditions{Roles: []string{"editor"}}} - if teleportFeatures.AdvancedAccessWorkflows { - conditions.Request.Thresholds = []types.AccessReviewThreshold{types.AccessReviewThreshold{Approve: 2, Deny: 2}} - } - role, err := bootstrap.AddRole("foo", types.RoleSpecV6{Allow: conditions}) - require.NoError(t, err) - - user, err := bootstrap.AddUserWithRoles(me.Username+"@example.com", role.GetName()) - require.NoError(t, err) - s.userNames.requestor = user.GetName() - - // Set up TWO users who can review access requests to role "editor". - - conditions = types.RoleConditions{} - if teleportFeatures.AdvancedAccessWorkflows { - conditions.ReviewRequests = &types.AccessReviewConditions{Roles: []string{"editor"}} - } - role, err = bootstrap.AddRole("foo-reviewer", types.RoleSpecV6{Allow: conditions}) - require.NoError(t, err) - - user, err = bootstrap.AddUserWithRoles(me.Username+"-reviewer1@example.com", role.GetName()) - require.NoError(t, err) - s.userNames.reviewer1 = user.GetName() - - user, err = bootstrap.AddUserWithRoles(me.Username+"-reviewer2@example.com", role.GetName()) - require.NoError(t, err) - s.userNames.reviewer2 = user.GetName() - - // Set up plugin user. - - role, err = bootstrap.AddRole("access-slack", types.RoleSpecV6{ - Allow: types.RoleConditions{ - Rules: []types.Rule{ - types.NewRule("access_request", []string{"list", "read"}), - types.NewRule("access_plugin_data", []string{"update"}), - }, - }, - }) - require.NoError(t, err) - - user, err = bootstrap.AddUserWithRoles("access-slack", role.GetName()) - require.NoError(t, err) - s.userNames.plugin = user.GetName() - - // Bake all the resources. - - err = teleport.Bootstrap(ctx, auth, bootstrap.Resources()) - require.NoError(t, err) - - // Initialize the clients. - - client, err = teleport.NewClient(ctx, auth, s.userNames.requestor) - require.NoError(t, err) - s.clients[s.userNames.requestor] = client - - if teleportFeatures.AdvancedAccessWorkflows { - client, err = teleport.NewClient(ctx, auth, s.userNames.reviewer1) - require.NoError(t, err) - s.clients[s.userNames.reviewer1] = client - - client, err = teleport.NewClient(ctx, auth, s.userNames.reviewer2) - require.NoError(t, err) - s.clients[s.userNames.reviewer2] = client - } - - identityPath, err := teleport.Sign(ctx, auth, s.userNames.plugin) - require.NoError(t, err) - - s.teleportConfig.Addr = auth.AuthAddr().String() - s.teleportConfig.Identity = identityPath - s.teleportFeatures = teleportFeatures -} - -func (s *SlackSuite) SetupTest() { - t := s.T() - - err := logger.Setup(logger.Config{Severity: "debug"}) - require.NoError(t, err) - - s.fakeSlack = NewFakeSlack(User{Name: "slackbot"}, s.raceNumber) - t.Cleanup(s.fakeSlack.Close) - - s.fakeSlack.StoreUser(User{Name: "Vladimir", Profile: UserProfile{Email: s.userNames.requestor}}) - - var conf Config - conf.Teleport = s.teleportConfig - conf.Slack.Token = "000000" - conf.Slack.APIURL = s.fakeSlack.URL() + "/" - conf.AccessTokenProvider = auth.NewStaticAccessTokenProvider(conf.Slack.Token) - - s.appConfig = &conf - s.SetContextTimeout(5 * time.Second) -} - -func (s *SlackSuite) startApp() { - t := s.T() - t.Helper() - - app := NewSlackApp(s.appConfig) - s.StartApp(app) -} - -func (s *SlackSuite) ruler() *integration.Client { - return s.clients[s.userNames.ruler] -} - -func (s *SlackSuite) requestor() *integration.Client { - return s.clients[s.userNames.requestor] -} - -func (s *SlackSuite) reviewer1() *integration.Client { - return s.clients[s.userNames.reviewer1] -} - -func (s *SlackSuite) reviewer2() *integration.Client { - return s.clients[s.userNames.reviewer2] -} - -func (s *SlackSuite) newAccessRequest(reviewers []User) types.AccessRequest { - t := s.T() - t.Helper() - - req, err := types.NewAccessRequest(uuid.New().String(), s.userNames.requestor, "editor") - require.NoError(t, err) - // max size of request was decreased here: https://github.com/gravitational/teleport/pull/13298 - req.SetRequestReason("because of " + strings.Repeat("A", 4000)) - var suggestedReviewers []string - for _, user := range reviewers { - suggestedReviewers = append(suggestedReviewers, user.Profile.Email) - } - req.SetSuggestedReviewers(suggestedReviewers) - return req -} - -func (s *SlackSuite) createAccessRequest(reviewers []User) types.AccessRequest { - t := s.T() - t.Helper() - - req := s.newAccessRequest(reviewers) - err := s.requestor().CreateAccessRequest(s.Context(), req) - require.NoError(t, err) - return req -} - -func (s *SlackSuite) checkPluginData(reqID string, cond func(common.GenericPluginData) bool) common.GenericPluginData { - t := s.T() - t.Helper() - - for { - rawData, err := s.ruler().PollAccessRequestPluginData(s.Context(), "slack", reqID) - require.NoError(t, err) - data, err := common.DecodePluginData(rawData) - require.NoError(t, err) - if cond(data) { - return data - } - } -} - -func (s *SlackSuite) TestMessagePosting() { - t := s.T() - - reviewer1 := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer1}}) - reviewer2 := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer2}}) - - s.startApp() - request := s.createAccessRequest([]User{reviewer2, reviewer1}) - - pluginData := s.checkPluginData(request.GetName(), func(data common.GenericPluginData) bool { - return len(data.SentMessages) > 0 - }) - assert.Len(t, pluginData.SentMessages, 2) - - var messages []Message - messageSet := make(SlackDataMessageSet) - for i := 0; i < 2; i++ { - msg, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - messageSet.Add(common.MessageData{ChannelID: msg.Channel, MessageID: msg.Timestamp}) - messages = append(messages, msg) - } - - assert.Len(t, messageSet, 2) - assert.Contains(t, messageSet, pluginData.SentMessages[0]) - assert.Contains(t, messageSet, pluginData.SentMessages[1]) - - sort.Sort(SlackMessageSlice(messages)) - - assert.Equal(t, reviewer1.ID, messages[0].Channel) - assert.Equal(t, reviewer2.ID, messages[1].Channel) - - msgUser, err := parseMessageField(messages[0], "User") - require.NoError(t, err) - assert.Equal(t, s.userNames.requestor, msgUser) - - block, ok := messages[0].BlockItems[1].Block.(SectionBlock) - require.True(t, ok) - t.Logf("%q", block.Text.GetText()) - matches := requestReasonRegexp.FindAllStringSubmatch(block.Text.GetText(), -1) - require.Equal(t, 1, len(matches)) - require.Equal(t, 3, len(matches[0])) - assert.Equal(t, "because of "+strings.Repeat("A", 489), matches[0][1]) - assert.Equal(t, " (truncated)", matches[0][2]) - - statusLine, err := getStatusLine(messages[0]) - require.NoError(t, err) - assert.Equal(t, "*Status*: ⏳ PENDING", statusLine) -} - -func (s *SlackSuite) TestRecipientsConfig() { - t := s.T() - - reviewer1 := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer1}}) - reviewer2 := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer2}}) - s.appConfig.Recipients = common.RawRecipientsMap{ - types.Wildcard: []string{reviewer2.Profile.Email, reviewer1.ID}, - } - - s.startApp() - - request := s.createAccessRequest(nil) - pluginData := s.checkPluginData(request.GetName(), func(data common.GenericPluginData) bool { - return len(data.SentMessages) > 0 - }) - assert.Len(t, pluginData.SentMessages, 2) - - var ( - msg Message - messages []Message - ) - - messageSet := make(SlackDataMessageSet) - - msg, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - messageSet.Add(common.MessageData{ChannelID: msg.Channel, MessageID: msg.Timestamp}) - messages = append(messages, msg) - - msg, err = s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - messageSet.Add(common.MessageData{ChannelID: msg.Channel, MessageID: msg.Timestamp}) - messages = append(messages, msg) - - assert.Len(t, messageSet, 2) - assert.Contains(t, messageSet, pluginData.SentMessages[0]) - assert.Contains(t, messageSet, pluginData.SentMessages[1]) - - sort.Sort(SlackMessageSlice(messages)) - - assert.Equal(t, reviewer1.ID, messages[0].Channel) - assert.Equal(t, reviewer2.ID, messages[1].Channel) -} - -func (s *SlackSuite) TestApproval() { - t := s.T() - - reviewer := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer1}}) - - s.startApp() - - req := s.createAccessRequest([]User{reviewer}) - msg, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msg.Channel) - - err = s.ruler().ApproveAccessRequest(s.Context(), req.GetName(), "okay") - require.NoError(t, err) - - msgUpdate, err := s.fakeSlack.CheckMessageUpdateByAPI(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msgUpdate.Channel) - assert.Equal(t, msg.Timestamp, msgUpdate.Timestamp) - - statusLine, err := getStatusLine(msgUpdate) - require.NoError(t, err) - assert.Equal(t, "*Status*: ✅ APPROVED\n*Resolution reason*: ```\nokay```", statusLine) -} - -func (s *SlackSuite) TestDenial() { - t := s.T() - - reviewer := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer1}}) - - s.startApp() - - req := s.createAccessRequest([]User{reviewer}) - msg, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msg.Channel) - - // max size of request was decreased here: https://github.com/gravitational/teleport/pull/13298 - err = s.ruler().DenyAccessRequest(s.Context(), req.GetName(), "not okay "+strings.Repeat("A", 4000)) - require.NoError(t, err) - - msgUpdate, err := s.fakeSlack.CheckMessageUpdateByAPI(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msgUpdate.Channel) - assert.Equal(t, msg.Timestamp, msgUpdate.Timestamp) - - statusLine, err := getStatusLine(msgUpdate) - require.NoError(t, err) - assert.Equal(t, "*Status*: ❌ DENIED\n*Resolution reason*: ```\nnot okay "+strings.Repeat("A", 491)+"``` (truncated)", statusLine) -} - -func (s *SlackSuite) TestReviewReplies() { - t := s.T() - - if !s.teleportFeatures.AdvancedAccessWorkflows { - t.Skip("Doesn't work in OSS version") - } - - reviewer := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer1}}) - - s.startApp() - - req := s.createAccessRequest([]User{reviewer}) - s.checkPluginData(req.GetName(), func(data common.GenericPluginData) bool { - return len(data.SentMessages) > 0 - }) - - msg, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msg.Channel) - - err = s.reviewer1().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer1, - ProposedState: types.RequestState_APPROVED, - Created: time.Now(), - Reason: "okay", - }) - require.NoError(t, err) - - reply, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, msg.Channel, reply.Channel) - assert.Equal(t, msg.Timestamp, reply.ThreadTs) - assert.Contains(t, reply.Text, s.userNames.reviewer1+" reviewed the request", "reply must contain a review author") - assert.Contains(t, reply.Text, "Resolution: ✅ APPROVED", "reply must contain a proposed state") - assert.Contains(t, reply.Text, "Reason: ```\nokay```", "reply must contain a reason") - - err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer2, - ProposedState: types.RequestState_DENIED, - Created: time.Now(), - Reason: "not okay", - }) - require.NoError(t, err) - - reply, err = s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, msg.Channel, reply.Channel) - assert.Equal(t, msg.Timestamp, reply.ThreadTs) - assert.Contains(t, reply.Text, s.userNames.reviewer2+" reviewed the request", "reply must contain a review author") - assert.Contains(t, reply.Text, "Resolution: ❌ DENIED", "reply must contain a proposed state") - assert.Contains(t, reply.Text, "Reason: ```\nnot okay```", "reply must contain a reason") -} - -func (s *SlackSuite) TestApprovalByReview() { - t := s.T() - - if !s.teleportFeatures.AdvancedAccessWorkflows { - t.Skip("Doesn't work in OSS version") - } - - reviewer := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer1}}) - - s.startApp() - - req := s.createAccessRequest([]User{reviewer}) - msg, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msg.Channel) - - err = s.reviewer1().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer1, - ProposedState: types.RequestState_APPROVED, - Created: time.Now(), - Reason: "okay", - }) - require.NoError(t, err) - - reply, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, msg.Channel, reply.Channel) - assert.Equal(t, msg.Timestamp, reply.ThreadTs) - assert.Contains(t, reply.Text, s.userNames.reviewer1+" reviewed the request", "reply must contain a review author") - - err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer2, - ProposedState: types.RequestState_APPROVED, - Created: time.Now(), - Reason: "finally okay", - }) - require.NoError(t, err) - - reply, err = s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, msg.Channel, reply.Channel) - assert.Equal(t, msg.Timestamp, reply.ThreadTs) - assert.Contains(t, reply.Text, s.userNames.reviewer2+" reviewed the request", "reply must contain a review author") - // When posting a review, the slack bot also updates the message to add the amount of reviewrs - // This update is soon superseded by the "access allowed" update - _, _ = s.fakeSlack.CheckMessageUpdateByAPI(s.Context()) - - msgUpdate, err := s.fakeSlack.CheckMessageUpdateByAPI(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msgUpdate.Channel) - assert.Equal(t, msg.Timestamp, msgUpdate.Timestamp) - - statusLine, err := getStatusLine(msgUpdate) - require.NoError(t, err) - assert.Equal(t, "*Status*: ✅ APPROVED\n*Resolution reason*: ```\nfinally okay```", statusLine) -} - -func (s *SlackSuite) TestDenialByReview() { - t := s.T() - - if !s.teleportFeatures.AdvancedAccessWorkflows { - t.Skip("Doesn't work in OSS version") - } - - reviewer := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer1}}) - - s.startApp() - - req := s.createAccessRequest([]User{reviewer}) - msg, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msg.Channel) - - err = s.reviewer1().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer1, - ProposedState: types.RequestState_DENIED, - Created: time.Now(), - Reason: "not okay", - }) - require.NoError(t, err) - - reply, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, msg.Channel, reply.Channel) - assert.Equal(t, msg.Timestamp, reply.ThreadTs) - assert.Contains(t, reply.Text, s.userNames.reviewer1+" reviewed the request", "reply must contain a review author") - - err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer2, - ProposedState: types.RequestState_DENIED, - Created: time.Now(), - Reason: "finally not okay", - }) - require.NoError(t, err) - - reply, err = s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, msg.Channel, reply.Channel) - assert.Equal(t, msg.Timestamp, reply.ThreadTs) - assert.Contains(t, reply.Text, s.userNames.reviewer2+" reviewed the request", "reply must contain a review author") - // When posting a review, the slack bot also updates the message to add the amount of reviewrs - // This update is soon superseded by the "access allowed" update - _, _ = s.fakeSlack.CheckMessageUpdateByAPI(s.Context()) - - msgUpdate, err := s.fakeSlack.CheckMessageUpdateByAPI(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msgUpdate.Channel) - assert.Equal(t, msg.Timestamp, msgUpdate.Timestamp) - - statusLine, err := getStatusLine(msgUpdate) - require.NoError(t, err) - assert.Equal(t, "*Status*: ❌ DENIED\n*Resolution reason*: ```\nfinally not okay```", statusLine) -} - -func (s *SlackSuite) TestExpiration() { - t := s.T() - - reviewer := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer1}}) - - s.startApp() - - request := s.createAccessRequest([]User{reviewer}) - msg, err := s.fakeSlack.CheckNewMessage(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msg.Channel) - - s.checkPluginData(request.GetName(), func(data common.GenericPluginData) bool { - return len(data.SentMessages) > 0 - }) - - err = s.ruler().DeleteAccessRequest(s.Context(), request.GetName()) // simulate expiration - require.NoError(t, err) - - msgUpdate, err := s.fakeSlack.CheckMessageUpdateByAPI(s.Context()) - require.NoError(t, err) - assert.Equal(t, reviewer.ID, msgUpdate.Channel) - assert.Equal(t, msg.Timestamp, msgUpdate.Timestamp) - - statusLine, err := getStatusLine(msgUpdate) - require.NoError(t, err) - assert.Equal(t, "*Status*: ⌛ EXPIRED", statusLine) -} - -func (s *SlackSuite) TestRace() { - t := s.T() - - if !s.teleportFeatures.AdvancedAccessWorkflows { - t.Skip("Doesn't work in OSS version") - } - - err := logger.Setup(logger.Config{Severity: "info"}) // Turn off noisy debug logging - require.NoError(t, err) - - reviewer1 := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer1}}) - reviewer2 := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer2}}) - - s.SetContextTimeout(20 * time.Second) - s.startApp() - - var ( - raceErr error - raceErrOnce sync.Once - threadMsgIDs sync.Map - threadMsgsCount int32 - msgUpdateCounters sync.Map - reviewReplyCounters sync.Map - ) - setRaceErr := func(err error) error { - raceErrOnce.Do(func() { - raceErr = err - }) - return err - } - - process := lib.NewProcess(s.Context()) - for i := 0; i < s.raceNumber; i++ { - process.SpawnCritical(func(ctx context.Context) error { - req, err := types.NewAccessRequest(uuid.New().String(), s.userNames.requestor, "editor") - if err != nil { - return setRaceErr(trace.Wrap(err)) - } - req.SetSuggestedReviewers([]string{reviewer1.Profile.Email, reviewer2.Profile.Email}) - if err := s.requestor().CreateAccessRequest(ctx, req); err != nil { - return setRaceErr(trace.Wrap(err)) - } - return nil - }) - } - - // Having TWO suggested reviewers will post TWO messages for each request. - // We also have approval threshold of TWO set in the role properties - // so lets simply submit the approval from each of the suggested reviewers. - // - // Multiplier SIX means that we handle TWO messages for each request and also - // TWO comments for each message: 2 * (1 message + 2 comments). - for i := 0; i < 6*s.raceNumber; i++ { - process.SpawnCritical(func(ctx context.Context) error { - msg, err := s.fakeSlack.CheckNewMessage(ctx) - if err != nil { - return setRaceErr(trace.Wrap(err)) - } - - if msg.ThreadTs == "" { - // Handle "root" notifications. - - threadMsgKey := common.MessageData{ChannelID: msg.Channel, MessageID: msg.Timestamp} - if _, loaded := threadMsgIDs.LoadOrStore(threadMsgKey, struct{}{}); loaded { - return setRaceErr(trace.Errorf("thread %v already stored", threadMsgKey)) - } - atomic.AddInt32(&threadMsgsCount, 1) - - user, ok := s.fakeSlack.GetUser(msg.Channel) - if !ok { - return setRaceErr(trace.Errorf("user %s is not found", msg.Channel)) - } - - reqID, err := parseMessageField(msg, "ID") - if err != nil { - return setRaceErr(trace.Wrap(err)) - } - - if err = s.clients[user.Profile.Email].SubmitAccessRequestReview(ctx, reqID, types.AccessReview{ - Author: user.Profile.Email, - ProposedState: types.RequestState_APPROVED, - Created: time.Now(), - Reason: "okay", - }); err != nil { - return setRaceErr(trace.Wrap(err)) - } - } else { - // Handle review comments. - - threadMsgKey := common.MessageData{ChannelID: msg.Channel, MessageID: msg.ThreadTs} - var newCounter int32 - val, _ := reviewReplyCounters.LoadOrStore(threadMsgKey, &newCounter) - counterPtr := val.(*int32) - atomic.AddInt32(counterPtr, 1) - } - - return nil - }) - } - - // Multiplier TWO means that we handle the 2 updates for each of the two messages posted to reviewers. - for i := 0; i < 2*2*s.raceNumber; i++ { - process.SpawnCritical(func(ctx context.Context) error { - msg, err := s.fakeSlack.CheckMessageUpdateByAPI(ctx) - if err != nil { - return setRaceErr(trace.Wrap(err)) - } - - threadMsgKey := common.MessageData{ChannelID: msg.Channel, MessageID: msg.Timestamp} - var newCounter int32 - val, _ := msgUpdateCounters.LoadOrStore(threadMsgKey, &newCounter) - counterPtr := val.(*int32) - atomic.AddInt32(counterPtr, 1) - - return nil - }) - } - - process.Terminate() - <-process.Done() - require.NoError(t, raceErr) - - assert.Equal(t, int32(2*s.raceNumber), threadMsgsCount) - threadMsgIDs.Range(func(key, value interface{}) bool { - next := true - - val, loaded := reviewReplyCounters.LoadAndDelete(key) - next = next && assert.True(t, loaded) - counterPtr := val.(*int32) - next = next && assert.Equal(t, int32(2), *counterPtr) - - val, loaded = msgUpdateCounters.LoadAndDelete(key) - next = next && assert.True(t, loaded) - counterPtr = val.(*int32) - // Each message should be updated 2 times - next = next && assert.Equal(t, int32(2), *counterPtr) - - return next - }) -} - -func parseMessageField(msg Message, field string) (string, error) { - block := msg.BlockItems[1].Block - sectionBlock, ok := block.(SectionBlock) - if !ok { - return "", trace.Errorf("invalid block type %T", block) - } - - if sectionBlock.Text.TextObject == nil { - return "", trace.Errorf("section block does not contain text") - } - - text := sectionBlock.Text.GetText() - matches := msgFieldRegexp.FindAllStringSubmatch(text, -1) - if matches == nil { - return "", trace.Errorf("cannot parse fields from text %s", text) - } - var fields []string - for _, match := range matches { - if match[1] == field { - return match[2], nil - } - fields = append(fields, match[1]) - } - return "", trace.Errorf("cannot find field %s in %v", field, fields) -} - -func getStatusLine(msg Message) (string, error) { - block := msg.BlockItems[2].Block - contextBlock, ok := block.(ContextBlock) - if !ok { - return "", trace.Errorf("invalid block type %T", block) - } - - elementItems := contextBlock.ElementItems - if n := len(elementItems); n != 1 { - return "", trace.Errorf("expected only one context element, got %v", n) - } - - element := elementItems[0].ContextElement - textBlock, ok := element.(TextObject) - if !ok { - return "", trace.Errorf("invalid element type %T", element) - } - - return textBlock.GetText(), nil -} diff --git a/access/slack/types.go b/access/slack/types.go deleted file mode 100644 index 0452685c2..000000000 --- a/access/slack/types.go +++ /dev/null @@ -1,431 +0,0 @@ -/* -Copyright 2022 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 slack - -import ( - "encoding/json" - - "github.com/gravitational/trace" -) - -// Slack API types - -type APIResponse struct { - Ok bool `json:"ok"` - Error string `json:"error,omitempty"` -} - -type ChatMsgResponse struct { - APIResponse - Channel string `json:"channel"` - Timestamp string `json:"ts"` - Text string `json:"text"` -} - -type BaseMessage struct { - Type string `json:"type,omitempty"` - Channel string `json:"channel,omitempty"` - User string `json:"user,omitempty"` - Username string `json:"username,omitempty"` - Timestamp string `json:"ts,omitempty"` - ThreadTs string `json:"thread_ts,omitempty"` -} - -type Message struct { - BaseMessage - BlockItems []BlockItem `json:"blocks,omitempty"` - Text string `json:"text,omitempty"` -} - -type User struct { - ID string `json:"id"` - Name string `json:"name"` - Profile UserProfile `json:"profile"` -} - -type UserProfile struct { - Email string `json:"email"` -} - -// Slack API: OAuth - -type AccessResponse struct { - APIResponse - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - ExpiresInSeconds int `json:"expires_in"` -} - -// Slack API: blocks - -type BlockType string - -type Block interface { - BlockType() BlockType -} - -type BlockItem struct{ Block } - -func (p *BlockItem) UnmarshalJSON(data []byte) error { - var itemType struct { - Type BlockType `json:"type"` - } - if err := json.Unmarshal(data, &itemType); err != nil { - return trace.Wrap(err) - } - var block Block - var err error - switch itemType.Type { - case ActionsBlock{}.BlockType(): - var val ActionsBlock - err = trace.Wrap(json.Unmarshal(data, &val)) - block = val - case ContextBlock{}.BlockType(): - var val ContextBlock - err = trace.Wrap(json.Unmarshal(data, &val)) - block = val - case DividerBlock{}.BlockType(): - var val DividerBlock - err = trace.Wrap(json.Unmarshal(data, &val)) - block = val - case FileBlock{}.BlockType(): - var val FileBlock - err = trace.Wrap(json.Unmarshal(data, &val)) - block = val - case HeaderBlock{}.BlockType(): - var val HeaderBlock - err = trace.Wrap(json.Unmarshal(data, &val)) - block = val - case ImageBlock{}.BlockType(): - var val ImageBlock - err = trace.Wrap(json.Unmarshal(data, &val)) - block = val - case InputBlock{}.BlockType(): - var val InputBlock - err = trace.Wrap(json.Unmarshal(data, &val)) - block = val - case SectionBlock{}.BlockType(): - var val SectionBlock - err = trace.Wrap(json.Unmarshal(data, &val)) - block = val - } - if err != nil { - return err - } - p.Block = block - return nil -} - -func (p BlockItem) MarshalJSON() ([]byte, error) { - typeVal := string(p.BlockType()) - switch val := p.Block.(type) { - case ActionsBlock: - return json.Marshal(struct { - Type string `json:"type"` - ActionsBlock - }{typeVal, val}) - case ContextBlock: - return json.Marshal(struct { - Type string `json:"type"` - ContextBlock - }{typeVal, val}) - case DividerBlock: - return json.Marshal(struct { - Type string `json:"type"` - DividerBlock - }{typeVal, val}) - case FileBlock: - return json.Marshal(struct { - Type string `json:"type"` - FileBlock - }{typeVal, val}) - case HeaderBlock: - return json.Marshal(struct { - Type string `json:"type"` - HeaderBlock - }{typeVal, val}) - case ImageBlock: - return json.Marshal(struct { - Type string `json:"type"` - ImageBlock - }{typeVal, val}) - case InputBlock: - return json.Marshal(struct { - Type string `json:"type"` - InputBlock - }{typeVal, val}) - case SectionBlock: - return json.Marshal(struct { - Type string `json:"type"` - SectionBlock - }{typeVal, val}) - default: - return json.Marshal(val) - } -} - -func NewBlockItem(block Block) BlockItem { - return BlockItem{block} -} - -// Slack API: actions blocks - -type ActionsBlock struct { - Elements []json.RawMessage `json:"elements"` - BlockID string `json:"block_id,omitempty"` -} - -func (b ActionsBlock) BlockType() BlockType { - return BlockType("actions") -} - -// Slack API: context blocks - -type ContextBlock struct { - ElementItems []ContextElementItem `json:"elements"` - BlockID string `json:"block_id,omitempty"` -} - -func (b ContextBlock) BlockType() BlockType { - return BlockType("context") -} - -type ContextElementType string - -type ContextElement interface { - ContextElementType() ContextElementType -} - -type ContextElementItem struct{ ContextElement } - -func NewContextElementItem(element ContextElement) ContextElementItem { - return ContextElementItem{element} -} - -func (p *ContextElementItem) UnmarshalJSON(data []byte) error { - var itemType struct { - Type ContextElementType `json:"type"` - } - if err := json.Unmarshal(data, &itemType); err != nil { - return trace.Wrap(err) - } - var element ContextElement - var err error - switch itemType.Type { - case PlainTextObject{}.ContextElementType(): - var val PlainTextObject - err = trace.Wrap(json.Unmarshal(data, &val)) - element = val - case MarkdownObject{}.ContextElementType(): - var val MarkdownObject - err = trace.Wrap(json.Unmarshal(data, &val)) - element = val - } - if err != nil { - return err - } - p.ContextElement = element - return nil -} - -func (p ContextElementItem) MarshalJSON() ([]byte, error) { - typeVal := string(p.ContextElementType()) - switch val := p.ContextElement.(type) { - case PlainTextObject: - return json.Marshal(struct { - Type string `json:"type"` - PlainTextObject - }{typeVal, val}) - case MarkdownObject: - return json.Marshal(struct { - Type string `json:"type"` - MarkdownObject - }{typeVal, val}) - default: - return json.Marshal(val) - } -} - -// Slack API: divider blocks - -type DividerBlock struct { - BlockID string `json:"block_id,omitempty"` -} - -func (b DividerBlock) BlockType() BlockType { - return BlockType("divider") -} - -// Slack API: file blocks - -type FileBlock struct { - ExternalID string `json:"external_id"` - Source string `json:"source"` - BlockID string `json:"block_id,omitempty"` -} - -func (b FileBlock) BlockType() BlockType { - return BlockType("file") -} - -// Slack API: header blocks - -type HeaderBlock struct { - Text string `json:"text"` - BlockID string `json:"block_id,omitempty"` -} - -func (b HeaderBlock) BlockType() BlockType { - return BlockType("header") -} - -// Slack API: image blocks - -type ImageBlock struct { - ImageURL string `json:"image_url"` - AltText string `json:"alt_text,omitempty"` - Title json.RawMessage `json:"title,omitempty"` - BlockID string `json:"block_id,omitempty"` -} - -func (b ImageBlock) BlockType() BlockType { - return BlockType("image") -} - -// Slack API: input blocks - -type InputBlock struct { - Label json.RawMessage `json:"label"` - Element json.RawMessage `json:"element"` - DispatchAction bool `json:"dispatch_action,omitempty"` - BlockID string `json:"block_id,omitempty"` - Hint json.RawMessage `json:"hint,omitempty"` - Optional bool `json:"optional,omitempty"` -} - -func (b InputBlock) BlockType() BlockType { - return BlockType("input") -} - -// Slack API: section blocks - -type SectionBlock struct { - Text TextObjectItem `json:"text,omitempty"` - BlockID string `json:"block_id,omitempty"` - Fields []TextObjectItem `json:"fields,omitempty"` -} - -func (b SectionBlock) BlockType() BlockType { - return BlockType("section") -} - -// Slack API: text objects - -type TextObjectType string -type TextObject interface { - TextObjectType() TextObjectType - GetText() string -} - -type TextObjectItem struct{ TextObject } - -func (p *TextObjectItem) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - p.TextObject = nil - return nil - } - - var itemType struct { - Type TextObjectType `json:"type"` - } - if err := json.Unmarshal(data, &itemType); err != nil { - return trace.Wrap(err) - } - var object TextObject - var err error - switch itemType.Type { - case PlainTextObject{}.TextObjectType(): - var val PlainTextObject - err = trace.Wrap(json.Unmarshal(data, &val)) - object = val - case MarkdownObject{}.TextObjectType(): - var val MarkdownObject - err = trace.Wrap(json.Unmarshal(data, &val)) - object = val - } - if err != nil { - return trace.Wrap(err) - } - p.TextObject = object - return nil -} - -func (p TextObjectItem) MarshalJSON() ([]byte, error) { - typeVal := string(p.TextObjectType()) - switch val := p.TextObject.(type) { - case PlainTextObject: - return json.Marshal(struct { - Type string `json:"type"` - PlainTextObject - }{typeVal, val}) - case MarkdownObject: - return json.Marshal(struct { - Type string `json:"type"` - MarkdownObject - }{typeVal, val}) - default: - return json.Marshal(val) - } -} - -func NewTextObjectItem(object TextObject) TextObjectItem { - return TextObjectItem{object} -} - -type PlainTextObject struct { - Text string `json:"text"` - Emoji bool `json:"emoji,omitempty"` -} - -func (t PlainTextObject) TextObjectType() TextObjectType { - return TextObjectType("plain_text") -} - -func (t PlainTextObject) ContextElementType() ContextElementType { - return ContextElementType("plain_text") -} - -func (t PlainTextObject) GetText() string { - return t.Text -} - -type MarkdownObject struct { - Text string `json:"text"` - Verbatim bool `json:"verbatim,omitempty"` -} - -func (t MarkdownObject) TextObjectType() TextObjectType { - return TextObjectType("mrkdwn") -} - -func (t MarkdownObject) ContextElementType() ContextElementType { - return ContextElementType("mrkdwn") -} - -func (t MarkdownObject) GetText() string { - return t.Text -} diff --git a/event-handler/Dockerfile b/event-handler/Dockerfile index eef222e58..2781ee04c 100644 --- a/event-handler/Dockerfile +++ b/event-handler/Dockerfile @@ -15,7 +15,6 @@ RUN --mount=type=cache,target=/go/pkg/mod go mod download # Copy the go source COPY event-handler event-handler -COPY lib lib # Build RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build make -C event-handler GITREF=${GITREF} @@ -26,4 +25,4 @@ FROM gcr.io/distroless/base@sha256:03dcbf61f859d0ae4c69c6242c9e5c3d7e1a42e5d3b69 COPY --from=builder /workspace/event-handler/build/teleport-event-handler /usr/local/bin/teleport-event-handler -ENTRYPOINT ["/usr/local/bin/teleport-event-handler"] \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/teleport-event-handler"] diff --git a/event-handler/app.go b/event-handler/app.go index f7f774a12..b1c4e5242 100644 --- a/event-handler/app.go +++ b/event-handler/app.go @@ -20,13 +20,12 @@ import ( "context" "time" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/sirupsen/logrus" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/backoff" - "github.com/gravitational/teleport-plugins/lib/logger" ) // App is the app structure diff --git a/event-handler/cli.go b/event-handler/cli.go index e2567ec13..126badb5d 100644 --- a/event-handler/cli.go +++ b/event-handler/cli.go @@ -22,11 +22,11 @@ import ( "time" "github.com/alecthomas/kong" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/stringset" "github.com/gravitational/trace" "github.com/gravitational/teleport-plugins/event-handler/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/stringset" ) // FluentdConfig represents fluentd instance configuration diff --git a/event-handler/event_handler_test.go b/event-handler/event_handler_test.go index 742a77560..1c51872c8 100644 --- a/event-handler/event_handler_test.go +++ b/event-handler/event_handler_test.go @@ -27,12 +27,11 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" + "github.com/gravitational/teleport/integrations/lib/testing/integration" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/testing/integration" ) type EventHandlerSuite struct { diff --git a/event-handler/events_job.go b/event-handler/events_job.go index e845a18f2..77ec56985 100644 --- a/event-handler/events_job.go +++ b/event-handler/events_job.go @@ -17,12 +17,11 @@ package main import ( "context" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" limiter "github.com/sethvargo/go-limiter" "github.com/sethvargo/go-limiter/memorystore" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) // EventsJob incapsulates audit log event consumption logic diff --git a/event-handler/fake_fluentd_test.go b/event-handler/fake_fluentd_test.go index 92ee25961..d7d577849 100644 --- a/event-handler/fake_fluentd_test.go +++ b/event-handler/fake_fluentd_test.go @@ -28,9 +28,8 @@ import ( "strings" "time" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib/logger" ) type FakeFluentd struct { diff --git a/event-handler/fluentd_client.go b/event-handler/fluentd_client.go index 96ade2078..a1f2b7acc 100644 --- a/event-handler/fluentd_client.go +++ b/event-handler/fluentd_client.go @@ -25,11 +25,11 @@ import ( "os" "time" + tlib "github.com/gravitational/teleport/integrations/lib" "github.com/gravitational/trace" log "github.com/sirupsen/logrus" "github.com/gravitational/teleport-plugins/event-handler/lib" - tlib "github.com/gravitational/teleport-plugins/lib" ) const ( diff --git a/event-handler/main.go b/event-handler/main.go index 5a5908b85..f01b988e2 100644 --- a/event-handler/main.go +++ b/event-handler/main.go @@ -24,10 +24,9 @@ import ( "time" "github.com/alecthomas/kong" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) // cli is CLI configuration diff --git a/event-handler/session_events_job.go b/event-handler/session_events_job.go index 496e77b17..8fd7f6a08 100644 --- a/event-handler/session_events_job.go +++ b/event-handler/session_events_job.go @@ -18,14 +18,13 @@ import ( "context" "time" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" log "github.com/sirupsen/logrus" "golang.org/x/sync/semaphore" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/backoff" - "github.com/gravitational/teleport-plugins/lib/logger" ) const ( diff --git a/event-handler/state.go b/event-handler/state.go index 18ad371ca..6ce1bae35 100644 --- a/event-handler/state.go +++ b/event-handler/state.go @@ -24,11 +24,11 @@ import ( "strings" "time" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" "github.com/peterbourgon/diskv/v3" "github.com/gravitational/teleport-plugins/event-handler/lib" - "github.com/gravitational/teleport-plugins/lib/logger" ) const ( diff --git a/event-handler/teleport_events_watcher.go b/event-handler/teleport_events_watcher.go index f9128bd42..ed2fe6aa1 100644 --- a/event-handler/teleport_events_watcher.go +++ b/event-handler/teleport_events_watcher.go @@ -24,12 +24,11 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/events" + "github.com/gravitational/teleport/integrations/lib/credentials" + "github.com/gravitational/teleport/integrations/lib/logger" "github.com/gravitational/trace" log "github.com/sirupsen/logrus" "golang.org/x/net/context" - - "github.com/gravitational/teleport-plugins/lib/credentials" - "github.com/gravitational/teleport-plugins/lib/logger" ) const ( diff --git a/go.mod b/go.mod index 77c742408..53b60054a 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,14 @@ go 1.19 require ( github.com/DanielTitkov/go-adaptive-cards v0.2.2 github.com/alecthomas/kong v0.2.22 - github.com/ghodss/yaml v1.0.0 github.com/go-resty/resty/v2 v2.3.0 github.com/gogo/protobuf v1.3.2 github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.3.0 - github.com/gravitational/kingpin v2.1.11-0.20190130013101-742f2714c145+incompatible - github.com/gravitational/teleport/api v0.0.0-20230313221333-b9ee4832ecd8 // tag v12.1.0 + github.com/gravitational/kingpin v2.1.11-0.20220901134012-2a1956e29525+incompatible + github.com/gravitational/teleport v0.0.0-20230329091501-300bc0bb5ba8 // ref: heads/branch/v12, TODO: set to v12.1.2 when it's out + github.com/gravitational/teleport/api v0.0.0 // replaced github.com/gravitational/trace v1.2.1 - github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/terraform-plugin-framework v0.10.0 github.com/hashicorp/terraform-plugin-go v0.12.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1 @@ -23,35 +22,21 @@ require ( github.com/mailgun/holster/v3 v3.15.2 github.com/mailgun/mailgun-go/v4 v4.5.3 github.com/manifoldco/promptui v0.8.0 - github.com/pelletier/go-toml v1.8.1 + github.com/pelletier/go-toml v1.9.5 github.com/peterbourgon/diskv/v3 v3.0.1 github.com/sethvargo/go-limiter v0.7.2 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 github.com/tidwall/gjson v1.14.4 - golang.org/x/crypto v0.6.0 - golang.org/x/net v0.7.0 + golang.org/x/net v0.8.0 golang.org/x/sync v0.1.0 google.golang.org/grpc v1.52.3 google.golang.org/protobuf v1.28.1 gopkg.in/mail.v2 v2.3.1 - k8s.io/apimachinery v0.26.1 ) -// Pin these packages to the same versions as teleport v12 branch, -// to avoid bumping them when plugins are imported in teleport. See: -// https://github.com/gravitational/teleport/blob/3e8d5e3775397e3dee25f62ecf0ec50552615f85/go.mod#L158 -// TODO: remove this in v13. require ( - cloud.google.com/go/iam v0.8.0 // indirect - google.golang.org/api v0.103.0 // indirect - google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c // indirect -) - -require ( - cloud.google.com/go/storage v1.28.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect github.com/alecthomas/colour v0.1.0 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect @@ -65,16 +50,16 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-piv/piv-go v1.10.0 // indirect github.com/golang-jwt/jwt/v4 v4.4.3 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/btree v1.0.1 // indirect + github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/gorilla/mux v1.8.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -83,6 +68,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.4.8 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/hc-install v0.3.2 // indirect github.com/hashicorp/hcl/v2 v2.3.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect @@ -92,6 +78,7 @@ require ( github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c // indirect github.com/hashicorp/terraform-svchost v0.0.1 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect + github.com/jhump/protoreflect v1.6.1 // indirect github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect @@ -100,7 +87,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -125,20 +112,23 @@ require ( go.opentelemetry.io/otel/sdk v1.13.0 // indirect go.opentelemetry.io/otel/trace v1.13.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect + golang.org/x/crypto v0.6.0 // indirect golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb // indirect - golang.org/x/oauth2 v0.5.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apimachinery v0.26.1 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) replace ( github.com/gogo/protobuf => github.com/gravitational/protobuf v1.3.2-0.20201123192827-2b9fcfaffcbf + github.com/gravitational/teleport/api => github.com/gravitational/teleport/api v0.0.0-20230329091501-300bc0bb5ba8 // ref: heads/branch/v12, TODO: set to v12.1.2 when it's out github.com/julienschmidt/httprouter => github.com/rw-access/httprouter v1.3.1-0.20210321233808-98e93175c124 ) diff --git a/go.sum b/go.sum index 346ad795a..8b77ed135 100644 --- a/go.sum +++ b/go.sum @@ -14,19 +14,18 @@ cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZ cloud.google.com/go v0.61.0/go.mod h1:XukKJg4Y7QsUu0Hxg3qQKUWR4VuWivmyMK2+rUyxAqw= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= +cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= +cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU= cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -36,8 +35,7 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY= -cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI= +cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -47,8 +45,8 @@ github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy86 github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= @@ -61,8 +59,7 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/ahmetb/go-linq v3.0.0+incompatible h1:qQkjjOXKrKOTy83X8OpRmnKflXKQIL/mC/gMVVDMhOA= github.com/ahmetb/go-linq v3.0.0+incompatible/go.mod h1:PFffvbdbtw+QTB0WKRP0cNht7vnCfnGlEpak/DVg5cY= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o= github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kong v0.2.22 h1:lRcQYT2/yJ+coDNA5ws0mRL0pwSqjbP/6AcRkyKhomk= @@ -93,8 +90,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= -github.com/aws/aws-sdk-go v1.25.3 h1:uM16hIw9BotjZKMZlX05SN2EFtaWfi/NonPKIARiBLQ= github.com/aws/aws-sdk-go v1.25.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.44.180 h1:VLZuAHI9fa/3WME5JjpVjcPCNfpGHVMiHx8sLHWhMgI= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -199,7 +196,6 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -227,8 +223,8 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -266,12 +262,14 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gravitational/kingpin v2.1.11-0.20190130013101-742f2714c145+incompatible h1:CfyZl3nyo9K5lLqOmqvl9/IElY1UCnOWKZiQxJ8HKdA= -github.com/gravitational/kingpin v2.1.11-0.20190130013101-742f2714c145+incompatible/go.mod h1:LWxG30M3FcrjhOn3T4zz7JmBoQJ45MWZmOXgy9Ganoc= +github.com/gravitational/kingpin v2.1.11-0.20220901134012-2a1956e29525+incompatible h1:TEGeCHqyhYjjYs1YatUJfZ8GgOhZGVfnJeC+BfdxoLQ= +github.com/gravitational/kingpin v2.1.11-0.20220901134012-2a1956e29525+incompatible/go.mod h1:LWxG30M3FcrjhOn3T4zz7JmBoQJ45MWZmOXgy9Ganoc= github.com/gravitational/protobuf v1.3.2-0.20201123192827-2b9fcfaffcbf h1:MQ4e8XcxvZTeuOmRl7yE519vcWc2h/lyvYzsvt41cdY= github.com/gravitational/protobuf v1.3.2-0.20201123192827-2b9fcfaffcbf/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gravitational/teleport/api v0.0.0-20230313221333-b9ee4832ecd8 h1:HzwXifXCXMRjmtjiNKAbD/891w9W/PIL7bmy7wULJHw= -github.com/gravitational/teleport/api v0.0.0-20230313221333-b9ee4832ecd8/go.mod h1:XMxqHqyL2cjzPC2iiK6cKoJluPNYW3TwDgsqQ/np+dM= +github.com/gravitational/teleport v0.0.0-20230329091501-300bc0bb5ba8 h1:LtbI1PY+XSVVTpIJ4WIE+g6HH9blohzFRUb+vcgrMJY= +github.com/gravitational/teleport v0.0.0-20230329091501-300bc0bb5ba8/go.mod h1:yZjUeFt5j7ZG3JoUpKuCFnOY6WPcsq+t2hlEuVgaxLA= +github.com/gravitational/teleport/api v0.0.0-20230329091501-300bc0bb5ba8 h1:7MgFrtdVy2c7lN1013C6+wrbZDg/YBpKGiJB1z6PZBk= +github.com/gravitational/teleport/api v0.0.0-20230329091501-300bc0bb5ba8/go.mod h1:T/zz2cKNBC22GvChCGtTJk7BbYdjK8CRlfwLaZrHOT0= github.com/gravitational/trace v1.2.1 h1:Iaf43aqbKV5H8bdiRs1qByjEHgAfADJ0lt0JwRyu+q8= github.com/gravitational/trace v1.2.1/go.mod h1:n0ijrq6psJY0sOI/NzLp+xdd8xl79jjwzVOFHDY6+kQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -279,8 +277,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.10.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 h1:I8MsauTJQXZ8df8qJvEln0kYNc3bSapuaSsEsnFdEFU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3/go.mod h1:lZdb/YAJUSj9OqrCHs2ihjtoO3+xK3G53wTYXFWRGDo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/hashicorp/consul/api v1.7.0/go.mod h1:1NSuaUUkFaJzMasbfq/11wKYWSR67Xn6r2DXKhuDNFg= github.com/hashicorp/consul/sdk v0.6.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -365,16 +363,17 @@ github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKe github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jhump/protoreflect v1.6.1 h1:4/2yi5LyDPP7nN+Hiird1SAJ6YoxUm13/oxHGRnbPd8= +github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= @@ -392,8 +391,8 @@ github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgy github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.11.2 h1:MiK62aErc3gIiVEtyzKfeOHgW7atJb5g/KNX5m3c2nQ= github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.13 h1:NFn1Wr8cfnenSJSA46lLq4wHCcBzKTSjnBIexDMMOV0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -458,8 +457,9 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -477,8 +477,8 @@ github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4 github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -660,6 +660,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -700,8 +701,8 @@ golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5o golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -709,7 +710,6 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -777,12 +777,12 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -791,8 +791,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -831,6 +831,7 @@ golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -839,6 +840,7 @@ golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -861,7 +863,6 @@ google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/lib/addr.go b/lib/addr.go deleted file mode 100644 index 2da2ee63a..000000000 --- a/lib/addr.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023 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 lib - -import ( - "net/url" - "strings" - - "github.com/gravitational/trace" -) - -func AddrToURL(addr string) (*url.URL, error) { - var ( - result *url.URL - err error - ) - if !strings.HasPrefix(addr, "http://") && !strings.HasPrefix(addr, "https://") { - addr = "https://" + addr - } - if result, err = url.Parse(addr); err != nil { - return nil, trace.Wrap(err) - } - if result.Scheme == "https" && result.Port() == "443" { - // Cut off redundant :443 - result.Host = result.Hostname() - } - return result, nil -} diff --git a/lib/addr_test.go b/lib/addr_test.go deleted file mode 100644 index 9fe219f2a..000000000 --- a/lib/addr_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2023 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 lib - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAddrToURL(t *testing.T) { - url, err := AddrToURL("foo") - assert.NoError(t, err) - assert.Equal(t, "https://foo", url.String()) - - url, err = AddrToURL("foo:443") - assert.NoError(t, err) - assert.Equal(t, "https://foo", url.String()) - - url, err = AddrToURL("foo:3080") - assert.NoError(t, err) - assert.Equal(t, "https://foo:3080", url.String()) -} diff --git a/lib/backoff/backoff.go b/lib/backoff/backoff.go deleted file mode 100644 index fd642b23f..000000000 --- a/lib/backoff/backoff.go +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package backoff - -import "context" - -// Backoff is an interface to some (exponential) backoff algorithm. -type Backoff interface { - Do(context.Context) error -} diff --git a/lib/backoff/backoff_test.go b/lib/backoff/backoff_test.go deleted file mode 100644 index e68dc9f4e..000000000 --- a/lib/backoff/backoff_test.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package backoff - -import ( - "context" - "testing" - "time" - - "github.com/jonboulle/clockwork" - "github.com/stretchr/testify/require" -) - -func TestDecorr(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - t.Cleanup(cancel) - - clock := clockwork.NewFakeClockAt(time.Unix(0, 0)) - base := 20 * time.Millisecond - cap := 2 * time.Second - backoff := NewDecorr(base, cap, clock) - - // Check exponential bounds. - for max := 3 * base; max < cap; max = 3 * max { - dur, err := measure(ctx, clock, func() error { return backoff.Do(ctx) }) - require.NoError(t, err) - require.Greater(t, dur, base) - require.LessOrEqual(t, dur, max) - } - - // Check that exponential growth threshold. - for i := 0; i < 2; i++ { - dur, err := measure(ctx, clock, func() error { return backoff.Do(ctx) }) - require.NoError(t, err) - require.Greater(t, dur, base) - require.LessOrEqual(t, dur, cap) - } -} diff --git a/lib/backoff/decorr.go b/lib/backoff/decorr.go deleted file mode 100644 index 4b99c350b..000000000 --- a/lib/backoff/decorr.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package backoff - -import ( - "context" - "math/rand" - "time" - - "github.com/gravitational/trace" - "github.com/jonboulle/clockwork" -) - -// Decorr is a "decorrelated jitter" inspired by https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/. -type decorr struct { - base int64 - cap int64 - mul int64 - sleep int64 - clock clockwork.Clock -} - -// NewDecorr initializes an algorithm. -func NewDecorr(base, cap time.Duration, clock clockwork.Clock) Backoff { - return NewDecorrWithMul(base, cap, 3, clock) -} - -// NewDecorrWithMul initializes a backoff algorithm with a given multiplier. -func NewDecorrWithMul(base, cap time.Duration, mul int64, clock clockwork.Clock) Backoff { - return &decorr{ - base: int64(base), - cap: int64(cap), - mul: mul, - sleep: int64(base), - clock: clock, - } -} - -func (backoff *decorr) Do(ctx context.Context) error { - backoff.sleep = backoff.base + rand.Int63n(backoff.sleep*backoff.mul-backoff.base) - if backoff.sleep > backoff.cap { - backoff.sleep = backoff.cap - } - select { - case <-backoff.clock.After(time.Duration(backoff.sleep)): - return nil - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } -} diff --git a/lib/backoff/measure.go b/lib/backoff/measure.go deleted file mode 100644 index 6a094ff2c..000000000 --- a/lib/backoff/measure.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package backoff - -import ( - "context" - "runtime" - "time" - - "github.com/gravitational/trace" - "github.com/jonboulle/clockwork" -) - -func measure(ctx context.Context, clock clockwork.FakeClock, fn func() error) (time.Duration, error) { - done := make(chan struct{}) - var dur time.Duration - var err error - go func() { - before := clock.Now() - err = fn() - after := clock.Now() - dur = after.Sub(before) - close(done) - }() - clock.BlockUntil(1) - for { - /* - What does runtime.Gosched() do? - > Gosched yields the processor, allowing other goroutines to run. It does not - > suspend the current goroutine, so execution resumes automatically. - - Why do we need it? - There are two concurrent goroutines at this point: - - this one - - the one that executes `fn()` - When this one is scheduled to run it advances the clock a bit more. - It might happen that this one keeps running over and over, while the other one is not scheduled. - When that happens, the other 'select' (the one in decorr.Do) gets called and returns nil, - the goroutine sets the `dur` value. - However, it's too late because the observed time (`dur`) is already larger than expected. - - If both goroutines ran sequentially, this would work. - Calling runtime.Gosched here, tries to give priority to the other goroutine. - So, when the other goroutine's select is ready (the clock.After returns), it immediately returns and - `dur` has the expected value. - */ - runtime.Gosched() - select { - case <-done: - return dur, trace.Wrap(err) - case <-ctx.Done(): - return time.Duration(0), trace.Wrap(ctx.Err()) - default: - clock.Advance(5 * time.Millisecond) - runtime.Gosched() - } - } -} diff --git a/lib/bail.go b/lib/bail.go deleted file mode 100644 index 0dcf20504..000000000 --- a/lib/bail.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2023 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 lib - -import ( - "os" - - "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" -) - -// Bail exits with nonzero exit code and prints an error to a log. -func Bail(err error) { - if agg, ok := trace.Unwrap(err).(trace.Aggregate); ok { - for i, err := range agg.Errors() { - log.WithError(err).Errorf("Terminating with fatal error [%d]...", i+1) - } - } else { - log.WithError(err).Error("Terminating with fatal error...") - } - os.Exit(1) -} diff --git a/lib/config.go b/lib/config.go deleted file mode 100644 index 262fa1bd3..000000000 --- a/lib/config.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2023 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 lib - -import ( - "io" - "os" - "strings" - - "github.com/gravitational/teleport/api/client" - "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" - - "github.com/gravitational/teleport-plugins/lib/stringset" -) - -// TeleportConfig stores config options for where -// the Teleport's Auth server is listening, and what certificates to -// use to authenticate in it. -type TeleportConfig struct { - AuthServer string `toml:"auth_server"` - Addr string `toml:"addr"` - Identity string `toml:"identity"` - ClientKey string `toml:"client_key"` - ClientCrt string `toml:"client_crt"` - RootCAs string `toml:"root_cas"` -} - -func (cfg TeleportConfig) GetAddrs() []string { - if cfg.Addr != "" { - return []string{cfg.Addr} - } else if cfg.AuthServer != "" { - return []string{cfg.AuthServer} - } - return nil -} - -func (cfg *TeleportConfig) CheckAndSetDefaults() error { - if cfg.Addr == "" && cfg.AuthServer == "" { - cfg.Addr = "localhost:3025" - } else if cfg.AuthServer != "" { - log.Warn("Configuration setting `auth_server` is deprecated, consider to change it to `addr`") - } - - if err := cfg.CheckTLSConfig(); err != nil { - return trace.Wrap(err) - } - - if cfg.Identity != "" && cfg.ClientCrt != "" { - return trace.BadParameter("configuration setting `identity` is mutually exclusive with all the `client_crt`, `client_key` and `root_cas` settings") - } - - return nil -} - -func (cfg *TeleportConfig) CheckTLSConfig() error { - provided := stringset.NewWithCap(3) - missing := stringset.NewWithCap(3) - - if cfg.ClientCrt != "" { - provided.Add("`client_crt`") - } else { - missing.Add("`client_crt`") - } - - if cfg.ClientKey != "" { - provided.Add("`client_key`") - } else { - missing.Add("`client_key`") - } - - if cfg.RootCAs != "" { - provided.Add("`root_cas`") - } else { - missing.Add("`root_cas`") - } - - if len(provided) > 0 && len(provided) < 3 { - return trace.BadParameter( - "configuration setting(s) %s are provided but setting(s) %s are missing", - strings.Join(provided.ToSlice(), ", "), - strings.Join(missing.ToSlice(), ", "), - ) - } - - return nil -} - -func (cfg TeleportConfig) Credentials() []client.Credentials { - switch true { - case cfg.Identity != "": - return []client.Credentials{client.LoadIdentityFile(cfg.Identity)} - case cfg.ClientCrt != "" && cfg.ClientKey != "" && cfg.RootCAs != "": - return []client.Credentials{client.LoadKeyPair(cfg.ClientCrt, cfg.ClientKey, cfg.RootCAs)} - default: - return nil - } -} - -// ReadPassword reads password from file or env var, trims and returns -func ReadPassword(filename string) (string, error) { - f, err := os.Open(filename) - if os.IsNotExist(err) { - return "", trace.BadParameter("Error reading password from %v", filename) - } - if err != nil { - return "", trace.Wrap(err) - } - pass := make([]byte, 2000) - l, err := f.Read(pass) - if err != nil && err != io.EOF { - return "", err - } - pass = pass[:l] // truncate \0 - return strings.TrimSpace(string(pass)), nil -} diff --git a/lib/credentials/credentials.go b/lib/credentials/credentials.go deleted file mode 100644 index 4abd40de3..000000000 --- a/lib/credentials/credentials.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2023 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 credentials - -import ( - "crypto/tls" - "crypto/x509" - "time" - - "github.com/gravitational/teleport/api/client" - "github.com/gravitational/trace" -) - -// CheckIfExpired returns true if there's at least 1 non-expired credential in the creds list. It also returns -// an error aggregate with an individual error for each invalid credential -func CheckIfExpired(credentials []client.Credentials) (bool, error) { - var errors []error - validCredentials := false - - for _, credential := range credentials { - tlsConfig, err := credential.TLSConfig() - if err != nil { - errors = append(errors, err) - continue - } - // If tlsConfig is nil, it means this is a credential for an insecure client, we let it pass - if tlsConfig == nil { - continue - } - - isValid := true - // client.Credentials.TLSConfig() does not populate tlsConfig.Certificate: - // it only sets tlsConfig.GetClientCertificate. - // We have to invoke the function to retrieve the certificate chain. - certificateChain, _ := tlsConfig.GetClientCertificate(&tls.CertificateRequestInfo{}) - if len(certificateChain.Certificate) == 0 { - isValid = false - } - - // We consider a chain valid if all its certs are not expired - for _, certificate := range certificateChain.Certificate { - parsedCert, err := x509.ParseCertificate(certificate) - if err != nil { - errors = append(errors, trace.WrapWithMessage(err, "failed to parse certificate while checking credential validity")) - isValid = false - break - } - - if time.Now().After(parsedCert.NotAfter) { - isValid = false - errors = append( - errors, - trace.CompareFailed( - "expired credential found: the certificate for '%s', issued by '%s' is not valid after '%s'", - parsedCert.Subject.CommonName, - parsedCert.Issuer.CommonName, - parsedCert.NotAfter, - ), - ) - } - } - if isValid { - validCredentials = true - } - } - - return validCredentials, trace.NewAggregate(errors...) -} diff --git a/lib/credentials/credentials_test.go b/lib/credentials/credentials_test.go deleted file mode 100644 index b16bda93a..000000000 --- a/lib/credentials/credentials_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2023 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 credentials - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "math/big" - "testing" - "time" - - "github.com/gravitational/teleport/api/client" - "github.com/gravitational/trace" - "github.com/stretchr/testify/require" - "golang.org/x/crypto/ssh" -) - -// mockTLSCredentials mocks insecure Client credentials. -// it returns a nil tlsConfig which allows the client to run in insecure mode. -type mockTLSCredentials struct { - CertificateChain *tls.Certificate -} - -func (mc *mockTLSCredentials) Dialer(_ client.Config) (client.ContextDialer, error) { - return nil, trace.NotImplemented("no dialer") -} - -func (mc *mockTLSCredentials) TLSConfig() (*tls.Config, error) { - if mc.CertificateChain == nil { - return nil, nil - } - return &tls.Config{GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) { - return mc.CertificateChain, nil - }}, nil -} - -func (mc *mockTLSCredentials) SSHClientConfig() (*ssh.ClientConfig, error) { - return nil, trace.NotImplemented("no ssh config") -} - -func TestCheckExpiredCredentials(t *testing.T) { - // Setup the CA and sign the client certs - ca := &x509.Certificate{ - SerialNumber: big.NewInt(0), - Subject: pkix.Name{ - CommonName: "teleport-cluster", - }, - NotBefore: time.Now().Add(-2 * time.Hour), - NotAfter: time.Now().Add(2 * time.Hour), - } - expiredCert := &x509.Certificate{ - SerialNumber: big.NewInt(0), - Subject: pkix.Name{ - CommonName: "access-plugin", - }, - NotBefore: time.Now().Add(-2 * time.Minute), - NotAfter: time.Now().Add(-1 * time.Minute), - } - validCert := &x509.Certificate{ - SerialNumber: big.NewInt(0), - Subject: pkix.Name{ - CommonName: "access-plugin", - }, - NotBefore: time.Now().Add(-1 * time.Hour), - NotAfter: time.Now().Add(1 * time.Hour), - } - - caKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(t, err) - clientKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(t, err) - validCertBytes, err := x509.CreateCertificate(rand.Reader, validCert, ca, &clientKey.PublicKey, caKey) - require.NoError(t, err) - invalidCertBytes, err := x509.CreateCertificate(rand.Reader, expiredCert, ca, &clientKey.PublicKey, caKey) - require.NoError(t, err) - - expiredCred := &mockTLSCredentials{CertificateChain: &tls.Certificate{Certificate: [][]byte{invalidCertBytes}}} - validCred := &mockTLSCredentials{CertificateChain: &tls.Certificate{Certificate: [][]byte{validCertBytes}}} - - // Doing the real tests - testCases := []struct { - name string - credentials []client.Credentials - expectNumErrors int - expectIsValid bool - }{ - { - name: "Empty credentials (no certs)", - credentials: []client.Credentials{ - &mockTLSCredentials{ - CertificateChain: &tls.Certificate{ - Certificate: [][]byte{}, - }, - }, - }, - expectNumErrors: 0, - expectIsValid: false, - }, - { - name: "Empty credentials (no TLS config)", - credentials: []client.Credentials{&mockTLSCredentials{}}, - expectNumErrors: 0, - expectIsValid: false, - }, - { - name: "Single valid credential", - credentials: []client.Credentials{validCred}, - expectNumErrors: 0, - expectIsValid: true, - }, - { - name: "Single invalid credential", - credentials: []client.Credentials{expiredCred}, - expectNumErrors: 1, - expectIsValid: false, - }, - { - name: "Valid and invalid credential", - credentials: []client.Credentials{validCred, expiredCred}, - expectNumErrors: 1, - expectIsValid: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - isValid, err := CheckIfExpired(tc.credentials) - require.Equal(t, tc.expectIsValid, isValid, "check validity") - if tc.expectNumErrors == 0 { - require.NoError(t, err) - } else { - require.Error(t, err) - aggregate, ok := trace.Unwrap(err).(trace.Aggregate) - require.True(t, ok) - require.Equal(t, len(aggregate.Errors()), tc.expectNumErrors, "check number of errors reported") - } - }) - } - -} diff --git a/lib/download.go b/lib/download.go deleted file mode 100644 index 455fd6f4c..000000000 --- a/lib/download.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package lib - -import ( - "context" - "io" - "net/http" - - "github.com/gravitational/trace" -) - -// DownloadAndCheck gets a file from the Internet and checks its SHA256 sum. -func DownloadAndCheck(ctx context.Context, url string, out io.Writer, checksum SHA256Sum) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return trace.Wrap(err) - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return trace.Wrap(err) - } - defer resp.Body.Close() - - sha256 := NewSHA256() - if _, err = io.Copy(out, io.TeeReader(resp.Body, sha256)); err != nil { - return trace.Wrap(err) - } - - if sha256.Sum() != checksum { - return trace.CompareFailed("sha256 sum of downloaded file does not match") - } - return nil -} diff --git a/lib/email.go b/lib/email.go deleted file mode 100644 index 32239c894..000000000 --- a/lib/email.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2023 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 lib - -import "net/mail" - -func IsEmail(str string) bool { - address, err := mail.ParseAddress(str) - if err != nil { - return false - } - return str == address.Address -} diff --git a/lib/errors.go b/lib/errors.go deleted file mode 100644 index c1f9d640a..000000000 --- a/lib/errors.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2023 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 lib - -import ( - "context" - "io" - - "github.com/gravitational/trace" - "github.com/gravitational/trace/trail" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -// TODO: remove this when trail.FromGRPC will understand additional error codes -func FromGRPC(err error) error { - switch { - case err == io.EOF: - fallthrough - case status.Code(err) == codes.Canceled, err == context.Canceled: - fallthrough - case status.Code(err) == codes.DeadlineExceeded, err == context.DeadlineExceeded: - return trace.Wrap(err) - default: - return trail.FromGRPC(err) - } -} - -// TODO: remove this when trail.FromGRPC will understand additional error codes -func IsCanceled(err error) bool { - err = trace.Unwrap(err) - return err == context.Canceled || status.Code(err) == codes.Canceled -} - -// TODO: remove this when trail.FromGRPC will understand additional error codes -func IsDeadline(err error) bool { - err = trace.Unwrap(err) - return err == context.DeadlineExceeded || status.Code(err) == codes.DeadlineExceeded -} diff --git a/lib/escape.go b/lib/escape.go deleted file mode 100644 index 90af22839..000000000 --- a/lib/escape.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2023 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 lib - -import "strings" - -// MarkdownEscape wraps some text `t` in triple backticks (escaping any backtick -// inside the message), limiting the length of the message to `n` runes (inside -// the single preformatted block). The text is trimmed before escaping. -// Backticks are escaped and thus count as two runes for the purpose of the -// truncation. -func MarkdownEscape(t string, n int) string { - t = strings.TrimSpace(t) - if t == "" { - return "(empty)" - } - var b strings.Builder - b.WriteString("```\n") - for i, r := range t { - if i >= n { - b.WriteString("``` (truncated)") - return b.String() - } - b.WriteRune(r) - if r == '`' { - // byte order mark, as a zero width no-break space; seems to result - // in escaped backticks with no spurious characters in the message - b.WriteRune('\ufeff') - n-- - } - } - b.WriteString("```") - return b.String() -} diff --git a/lib/escape_test.go b/lib/escape_test.go deleted file mode 100644 index 54cdfbb92..000000000 --- a/lib/escape_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2023 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 lib - -import "fmt" - -func ExampleMarkdownEscape() { - fmt.Printf("%q\n", MarkdownEscape(" ", 1000)) - fmt.Printf("%q\n", MarkdownEscape("abc", 1000)) - fmt.Printf("%q\n", MarkdownEscape("`foo` `bar`", 1000)) - fmt.Printf("%q\n", MarkdownEscape(" 123456789012345 ", 10)) - - // Output: "(empty)" - // "```\nabc```" - // "```\n`\ufefffoo`\ufeff `\ufeffbar`\ufeff```" - // "```\n1234567890``` (truncated)" -} diff --git a/lib/http.go b/lib/http.go deleted file mode 100644 index 32027693b..000000000 --- a/lib/http.go +++ /dev/null @@ -1,342 +0,0 @@ -// Copyright 2023 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 lib - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "net" - "net/http" - "net/url" - "path" - "sync" - "time" - - "github.com/gravitational/trace" - "github.com/julienschmidt/httprouter" - log "github.com/sirupsen/logrus" -) - -// TLSConfig stores TLS configuration for a http service -type TLSConfig struct { - VerifyClientCertificate bool `toml:"verify_client_cert"` - - VerifyClientCertificateFunc func(chains [][]*x509.Certificate) error -} - -// HTTPConfig stores configuration of an HTTP service -// including it's public address, listen host and port, -// TLS certificate and key path, and extra TLS configuration -// options, represented as TLSConfig. -type HTTPConfig struct { - ListenAddr string `toml:"listen_addr"` - PublicAddr string `toml:"public_addr"` - KeyFile string `toml:"https_key_file"` - CertFile string `toml:"https_cert_file"` - BasicAuth HTTPBasicAuthConfig `toml:"basic_auth"` - TLS TLSConfig `toml:"tls"` - - Insecure bool -} - -// HTTPBasicAuthConfig stores configuration for -// HTTP Basic Authentication -type HTTPBasicAuthConfig struct { - Username string `toml:"user"` - Password string `toml:"password"` -} - -// HTTP is a tiny wrapper around standard net/http. -// It starts either insecure server or secure one with TLS, depending on the settings. -// It also adds a context to its handlers and the server itself has context to. -// So you are guaranteed that server will be closed when the context is canceled. -type HTTP struct { - HTTPConfig - mu sync.Mutex - addr net.Addr - baseURL *url.URL - *httprouter.Router - server http.Server -} - -// HTTPBasicAuth wraps a http.Handler with HTTP Basic Auth check. -type HTTPBasicAuth struct { - HTTPBasicAuthConfig - handler http.Handler -} - -type httpListenChanKey struct{} - -func (conf *HTTPConfig) defaultScheme() (scheme string) { - if conf.Insecure { - scheme = "http" - } else { - scheme = "https" - } - return -} - -// BaseURL builds a base url depending on "public_addr" parameter. -func (conf *HTTPConfig) BaseURL() (*url.URL, error) { - if conf.PublicAddr == "" { - return &url.URL{Scheme: conf.defaultScheme()}, nil - } - url, err := url.Parse(conf.PublicAddr) - if err != nil { - return nil, err - } - - scheme := url.Scheme - if scheme == "" { - scheme = conf.defaultScheme() - return url.Parse(fmt.Sprintf("%s://%s", scheme, conf.PublicAddr)) - } - - if scheme != "http" && scheme != "https" { - return nil, trace.BadParameter("wrong scheme in public_addr parameter: %q", scheme) - } - - return url, nil -} - -// Check validates the http server configuration. -func (conf *HTTPConfig) Check() error { - baseURL, err := conf.BaseURL() - if err != nil { - return trace.Wrap(err) - } - if conf.KeyFile != "" && conf.CertFile == "" { - return trace.BadParameter("https_cert_file is required when https_key_file is specified") - } - if conf.CertFile != "" && conf.KeyFile == "" { - return trace.BadParameter("https_key_file is required when https_cert_file is specified") - } - if conf.BasicAuth.Password != "" && conf.BasicAuth.Username == "" { - return trace.BadParameter("basic_auth.user is required when basic_auth.password is specified") - } - if conf.BasicAuth.Username != "" && baseURL != nil && baseURL.User != nil { - return trace.BadParameter("passing credentials both in basic_auth section and public_addr parameter is not supported") - } - return nil -} - -// ServeHTTP processes one http request. -func (auth *HTTPBasicAuth) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - username, password, ok := r.BasicAuth() - - if ok && username == auth.Username && password == auth.Password { - auth.handler.ServeHTTP(rw, r) - } else { - rw.Header().Set("WWW-Authenticate", "Basic realm=Restricted") - http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - } -} - -// NewHTTP creates a new HTTP wrapper -func NewHTTP(config HTTPConfig) (*HTTP, error) { - baseURL, err := config.BaseURL() - if err != nil { - return nil, trace.Wrap(err) - } - router := httprouter.New() - - if userInfo := baseURL.User; userInfo != nil { - password, _ := userInfo.Password() - config.BasicAuth = HTTPBasicAuthConfig{Username: userInfo.Username(), Password: password} - } - - var handler http.Handler - handler = router - if config.BasicAuth.Username != "" { - handler = &HTTPBasicAuth{config.BasicAuth, handler} - } - - var tlsConfig *tls.Config - if !config.Insecure { - tlsConfig = &tls.Config{} - if config.TLS.VerifyClientCertificate { - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - if verify := config.TLS.VerifyClientCertificateFunc; verify != nil { - tlsConfig.VerifyPeerCertificate = func(_ [][]byte, chains [][]*x509.Certificate) error { - if err := verify(chains); err != nil { - log.WithError(err).Error("HTTPS client certificate verification failed") - return err - } - return nil - } - } - } else { - tlsConfig.ClientAuth = tls.NoClientCert - } - } - - return &HTTP{ - HTTPConfig: config, - Router: router, - baseURL: baseURL, - server: http.Server{Handler: handler, TLSConfig: tlsConfig}, - }, nil -} - -// BuildURLPath returns a URI with args represented as query params -// If any supplied argument is not a string, BuildURLPath will use -// fmt.Sprintf(value) to stringify it. -func BuildURLPath(args ...interface{}) string { - var pathArgs []string - for _, a := range args { - var str string - switch v := a.(type) { - case string: - str = v - default: - str = fmt.Sprint(v) - } - pathArgs = append(pathArgs, url.PathEscape(str)) - } - return path.Join(pathArgs...) -} - -// ListenAndServe runs a http(s) server on a provided port. -func (h *HTTP) ListenAndServe(ctx context.Context) error { - defer log.Debug("HTTP server terminated") - var err error - - h.server.BaseContext = func(_ net.Listener) context.Context { - return ctx - } - go func() { - <-ctx.Done() - h.server.Close() - }() - - listen := h.ListenAddr - if listen == "" { - if h.Insecure { - listen = ":http" - } else { - listen = ":https" - } - } - - listenCh, _ := ctx.Value(httpListenChanKey{}).(chan<- net.Addr) - listener, err := net.Listen("tcp", listen) - if err != nil { - if listenCh != nil { - listenCh <- nil - } - return trace.Wrap(err) - } - addr := listener.Addr() - - h.mu.Lock() - h.addr = addr - h.mu.Unlock() - - if listenCh != nil { - listenCh <- addr - } - - if h.Insecure { - log.Debugf("Starting insecure HTTP server on %s", addr) - err = h.server.Serve(listener) - } else { - log.Debugf("Starting secure HTTPS server on %s", addr) - err = h.server.ServeTLS(listener, h.CertFile, h.KeyFile) - } - if err == http.ErrServerClosed { - return nil - } - return trace.Wrap(err) -} - -// Shutdown stops the server gracefully. -func (h *HTTP) Shutdown(ctx context.Context) error { - return h.server.Shutdown(ctx) -} - -// ShutdownWithTimeout stops the server gracefully. -func (h *HTTP) ShutdownWithTimeout(ctx context.Context, duration time.Duration) error { - ctx, cancel := context.WithTimeout(ctx, duration) - defer cancel() - - return h.Shutdown(ctx) -} - -// ServiceJob creates a service job for the HTTP service, -// wraps it with a termination handler so it shuts down and -// logs when it quits. -func (h *HTTP) ServiceJob() ServiceJob { - return NewServiceJob(func(ctx context.Context) error { - MustGetProcess(ctx).OnTerminate(func(ctx context.Context) error { - if err := h.ShutdownWithTimeout(ctx, time.Second*5); err != nil { - log.Error("HTTP server graceful shutdown failed") - return err - } - return nil - }) - listenChan := make(chan net.Addr) - var outChan chan<- net.Addr = listenChan - ctx = context.WithValue(ctx, httpListenChanKey{}, outChan) - go func() { - addr := <-listenChan - close(listenChan) - MustGetServiceJob(ctx).SetReady(addr != nil) - }() - return h.ListenAndServe(ctx) - }) -} - -// BaseURL returns an url on which the server is accessible externally. -func (h *HTTP) BaseURL() *url.URL { - h.mu.Lock() - defer h.mu.Unlock() - url := *h.baseURL - if url.Host == "" && h.addr != nil { - url.Host = h.addr.String() - } - return &url -} - -// NewURL builds an external url for a specific path and query parameters. -func (h *HTTP) NewURL(subpath string, values url.Values) *url.URL { - url := h.BaseURL() - url.Path = path.Join(url.Path, subpath) - - if values != nil { - url.RawQuery = values.Encode() - } - - return url -} - -// EnsureCert checks cert and key files consistency. -func (h *HTTP) EnsureCert(defaultPath string) error { - if h.Insecure { - return nil - } - - if h.CertFile != "" && h.KeyFile == "" { - return trace.Errorf("you should specify https_key_file parameter") - } - - if h.CertFile == "" && h.KeyFile != "" { - return trace.Errorf("you should specify https_cert_file parameter") - } - - _, err := tls.LoadX509KeyPair(h.CertFile, h.KeyFile) - return trace.Wrap(err) -} diff --git a/lib/logger/logger.go b/lib/logger/logger.go deleted file mode 100644 index cc128615d..000000000 --- a/lib/logger/logger.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2023 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 logger - -import ( - "context" - "os" - "strings" - - "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" -) - -type Config struct { - Output string `toml:"output"` - Severity string `toml:"severity"` -} - -type Fields = log.Fields - -type contextKey struct{} - -// InitLogger sets up logger for a typical daemon scenario until configuration -// file is parsed -func Init() { - log.SetFormatter(&trace.TextFormatter{ - DisableTimestamp: true, - EnableColors: trace.IsTerminal(os.Stderr), - ComponentPadding: 1, // We don't use components so strip the padding - }) - log.SetOutput(os.Stderr) -} - -func Setup(conf Config) error { - switch conf.Output { - case "stderr", "error", "2": - log.SetOutput(os.Stderr) - case "", "stdout", "out", "1": - log.SetOutput(os.Stdout) - default: - // assume it's a file path: - logFile, err := os.Create(conf.Output) - if err != nil { - return trace.Wrap(err, "failed to create the log file") - } - log.SetOutput(logFile) - } - - switch strings.ToLower(conf.Severity) { - case "info": - log.SetLevel(log.InfoLevel) - case "err", "error": - log.SetLevel(log.ErrorLevel) - case "debug": - log.SetLevel(log.DebugLevel) - case "warn", "warning": - log.SetLevel(log.WarnLevel) - default: - return trace.BadParameter("unsupported logger severity: '%v'", conf.Severity) - } - - return nil -} - -func withLogger(ctx context.Context, logger log.FieldLogger) context.Context { - return context.WithValue(ctx, contextKey{}, logger) -} - -func WithField(ctx context.Context, key string, value interface{}) (context.Context, log.FieldLogger) { - logger := Get(ctx).WithField(key, value) - return withLogger(ctx, logger), logger -} - -func WithFields(ctx context.Context, logFields Fields) (context.Context, log.FieldLogger) { - logger := Get(ctx).WithFields(logFields) - return withLogger(ctx, logger), logger -} - -func SetField(ctx context.Context, key string, value interface{}) context.Context { - ctx, _ = WithField(ctx, key, value) - return ctx -} - -func SetFields(ctx context.Context, logFields Fields) context.Context { - ctx, _ = WithFields(ctx, logFields) - return ctx -} - -func Get(ctx context.Context) log.FieldLogger { - if logger, ok := ctx.Value(contextKey{}).(log.FieldLogger); ok && logger != nil { - return logger - } - - return Standard() -} - -func Standard() log.FieldLogger { - return log.StandardLogger() -} diff --git a/lib/plugindata/access_request.go b/lib/plugindata/access_request.go deleted file mode 100644 index 891df51b3..000000000 --- a/lib/plugindata/access_request.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2023 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 plugindata - -import ( - "fmt" - "strings" -) - -// ResolutionTag represents enum type of access request resolution constant -type ResolutionTag string - -const ( - Unresolved = ResolutionTag("") - ResolvedApproved = ResolutionTag("APPROVED") - ResolvedDenied = ResolutionTag("DENIED") - ResolvedExpired = ResolutionTag("EXPIRED") -) - -// AccessRequestData represents generic plugin data required for access request processing -type AccessRequestData struct { - User string - Roles []string - RequestReason string - ReviewsCount int - ResolutionTag ResolutionTag - ResolutionReason string -} - -// DecodeAccessRequestData deserializes a string map to PluginData struct. -func DecodeAccessRequestData(dataMap map[string]string) (data AccessRequestData) { - data.User = dataMap["user"] - if str := dataMap["roles"]; str != "" { - data.Roles = strings.Split(str, ",") - } - data.RequestReason = dataMap["request_reason"] - if str := dataMap["reviews_count"]; str != "" { - fmt.Sscanf(str, "%d", &data.ReviewsCount) - } - data.ResolutionTag = ResolutionTag(dataMap["resolution"]) - data.ResolutionReason = dataMap["resolve_reason"] - - return -} - -// EncodeAccessRequestData deserializes a string map to PluginData struct. -func EncodeAccessRequestData(data AccessRequestData) map[string]string { - result := make(map[string]string) - - result["user"] = data.User - result["roles"] = strings.Join(data.Roles, ",") - result["request_reason"] = data.RequestReason - - var reviewsCountStr string - if data.ReviewsCount > 0 { - reviewsCountStr = fmt.Sprintf("%d", data.ReviewsCount) - } - result["reviews_count"] = reviewsCountStr - result["resolution"] = string(data.ResolutionTag) - result["resolve_reason"] = data.ResolutionReason - - return result -} diff --git a/lib/plugindata/access_request_test.go b/lib/plugindata/access_request_test.go deleted file mode 100644 index b7e148055..000000000 --- a/lib/plugindata/access_request_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2023 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 plugindata - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -var sampleAccessRequestData = AccessRequestData{ - User: "user-foo", - Roles: []string{"role-foo", "role-bar"}, - RequestReason: "foo reason", - ReviewsCount: 3, - ResolutionTag: ResolvedApproved, - ResolutionReason: "foo ok", -} - -func TestEncodeAccessRequestData(t *testing.T) { - dataMap := EncodeAccessRequestData(sampleAccessRequestData) - assert.Len(t, dataMap, 6) - assert.Equal(t, "user-foo", dataMap["user"]) - assert.Equal(t, "role-foo,role-bar", dataMap["roles"]) - assert.Equal(t, "foo reason", dataMap["request_reason"]) - assert.Equal(t, "3", dataMap["reviews_count"]) - assert.Equal(t, "APPROVED", dataMap["resolution"]) - assert.Equal(t, "foo ok", dataMap["resolve_reason"]) -} - -func TestDecodeAccessRequestData(t *testing.T) { - pluginData := DecodeAccessRequestData(map[string]string{ - "user": "user-foo", - "roles": "role-foo,role-bar", - "request_reason": "foo reason", - "reviews_count": "3", - "resolution": "APPROVED", - "resolve_reason": "foo ok", - }) - assert.Equal(t, sampleAccessRequestData, pluginData) -} - -func TestEncodeEmptyAccessRequestData(t *testing.T) { - dataMap := EncodeAccessRequestData(AccessRequestData{}) - assert.Len(t, dataMap, 6) - for key, value := range dataMap { - assert.Emptyf(t, value, "value at key %q must be empty", key) - } -} - -func TestDecodeEmptyAccessRequestData(t *testing.T) { - assert.Empty(t, DecodeAccessRequestData(nil)) - assert.Empty(t, DecodeAccessRequestData(make(map[string]string))) -} diff --git a/lib/plugindata/cas.go b/lib/plugindata/cas.go deleted file mode 100644 index f7b1e706e..000000000 --- a/lib/plugindata/cas.go +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright 2023 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 plugindata - -import ( - "context" - "time" - - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - "github.com/jonboulle/clockwork" - - "github.com/gravitational/teleport-plugins/lib/backoff" -) - -const ( - // backoffBase is an initial (minimum) backoff value. - backoffBase = time.Millisecond - // backoffMax is a backoff threshold - backoffMax = time.Second -) - -// Client represents an interface to Teleport API client -type Client interface { - GetPluginData(context.Context, types.PluginDataFilter) ([]types.PluginData, error) - UpdatePluginData(context.Context, types.PluginDataUpdateParams) error -} - -// CompareAndSwap represents modifier struct -type CompareAndSwap[T any] struct { - client Client - name string - kind string - backoffBase time.Duration - backoffMax time.Duration - encode func(T) (map[string]string, error) - decode func(map[string]string) (T, error) -} - -// NewCAS returns modifier struct -func NewCAS[T any]( - client Client, name, - kind string, - encode func(T) (map[string]string, error), - decode func(map[string]string) (T, error), -) *CompareAndSwap[T] { - return &CompareAndSwap[T]{ - client, - name, - kind, - backoffBase, - backoffMax, - encode, - decode, - } -} - -// Create tries to perform compare-and-swap update of a plugin data assuming that it does not exist -// -// fn callback function receives current plugin data value and returns modified value and -// error. -// -// Please note that fn might be called several times due to CAS backoff, hence, you must be careful -// with things like I/O ops and channels. -func (c *CompareAndSwap[T]) Create( - ctx context.Context, - resource string, - newData T, -) (T, error) { - emptyData := *new(T) - - existingData, err := c.getPluginData(ctx, resource) - if err != nil && !trace.IsNotFound(err) { - return emptyData, trace.Wrap(err) - } - - if existingData != nil { - return emptyData, trace.AlreadyExists("plugin data already exists") - } - - err = c.updatePluginData(ctx, resource, newData, emptyData) - if err == nil { - return newData, nil - } - - return emptyData, trace.Wrap(err) -} - -// Update tries to perform compare-and-swap update of a plugin data assuming that it exist -// -// modifyT will receive existing plugin data and should return a modified version of the data. - -// If existing plugin data does not match expected data, then a trace.CompareFailed error should -// be returned to backoff and try again. - -// To abort the update, modifyT should return an error other, than trace.CompareFailed, which -// will be propagated back to the caller of `Update`. -func (c *CompareAndSwap[T]) Update( - ctx context.Context, - resource string, - modifyT func(T) (T, error), -) (T, error) { - emptyData := *new(T) - var failedAttempts []error - - backoff := backoff.NewDecorr(c.backoffBase, c.backoffMax, clockwork.NewRealClock()) - for { - // Get existing data - oldData, err := c.getPluginData(ctx, resource) - if err != nil { - return emptyData, trace.Wrap(err) - } - - cbData := *oldData - expectData := *oldData - - // Modify data - newData, err := modifyT(cbData) - if trace.IsCompareFailed(err) { - failedAttempts = append(failedAttempts, trace.Wrap(err)) - backoffErr := backoff.Do(ctx) - if backoffErr != nil { - failedAttempts = append(failedAttempts, trace.Wrap(backoffErr)) - return emptyData, trace.NewAggregate(failedAttempts...) - } - - continue - } else if err != nil { - return emptyData, trace.Wrap(err) - } - - // Submit modifications - err = c.updatePluginData(ctx, resource, newData, expectData) - if err == nil { - return newData, nil - } - if !trace.IsCompareFailed(err) { - return emptyData, trace.Wrap(err) - } - // A conflict happened, we register the failed attempt and wait before retrying - failedAttempts = append(failedAttempts, trace.Wrap(err)) - backoffErr := backoff.Do(ctx) - if backoffErr != nil { - failedAttempts = append(failedAttempts, trace.Wrap(backoffErr)) - return emptyData, trace.NewAggregate(failedAttempts...) - } - } -} - -// NOTE: Implement Upsert method when it will be required - -// getPluginData loads a plugin data for a given resource. It returns nil if it's not found. -func (c *CompareAndSwap[T]) getPluginData(ctx context.Context, resource string) (*T, error) { - dataMaps, err := c.client.GetPluginData(ctx, types.PluginDataFilter{ - Kind: c.kind, - Resource: resource, - Plugin: c.name, - }) - if err != nil { - return nil, trace.Wrap(err) - } - if len(dataMaps) == 0 { - return nil, trace.NotFound("plugin data not found") - } - entry := dataMaps[0].Entries()[c.name] - if entry == nil || entry.Data == nil { - return nil, trace.NotFound("plugin data entry not found") - } - d, err := c.decode(entry.Data) - return &d, err -} - -// updatePluginData updates an existing plugin data or sets a new one if it didn't exist. -func (c *CompareAndSwap[T]) updatePluginData(ctx context.Context, resource string, data T, expectData T) error { - set, err := c.encode(data) - if err != nil { - return err - } - expect, err := c.encode(expectData) - if err != nil { - return err - } - return c.client.UpdatePluginData(ctx, types.PluginDataUpdateParams{ - Kind: c.kind, - Resource: resource, - Plugin: c.name, - Set: set, - Expect: expect, - }) -} diff --git a/lib/plugindata/cas_test.go b/lib/plugindata/cas_test.go deleted file mode 100644 index 59fe721dc..000000000 --- a/lib/plugindata/cas_test.go +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2023 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 plugindata - -import ( - "context" - "testing" - - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - "github.com/stretchr/testify/require" -) - -const ( - resourceKind = "test" -) - -type mockData struct { - Foo string - Bar string -} - -func mockEncode(source mockData) (map[string]string, error) { - result := make(map[string]string) - - result["foo"] = source.Foo - result["bar"] = source.Bar - - return result, nil -} - -func mockDecode(source map[string]string) (mockData, error) { - result := mockData{} - - result.Foo = source["foo"] - result.Bar = source["bar"] - - return result, nil -} - -func mockDecodeFail(source map[string]string) (mockData, error) { - return mockData{}, trace.BadParameter("Failed to decode data") -} - -type mockClient struct { - oldDataCursor int - oldData []map[string]string - updateResult []error - updateResultCursor int -} - -func (c *mockClient) GetPluginData(_ context.Context, f types.PluginDataFilter) ([]types.PluginData, error) { - i, err := types.NewPluginData(f.Resource, resourceKind) - if err != nil { - return nil, trace.Wrap(err) - } - - d, ok := i.(*types.PluginDataV3) - if !ok { - return nil, trace.Errorf("Failed to convert %T to types.PluginDataV3", i) - } - - var data map[string]string - if c.oldDataCursor < len(c.oldData) { - data = c.oldData[c.oldDataCursor] - } - c.oldDataCursor++ - - d.Spec.Entries = map[string]*types.PluginDataEntry{ - resourceKind: {Data: data}, - } - - return []types.PluginData{d}, nil -} - -func (c *mockClient) UpdatePluginData(context.Context, types.PluginDataUpdateParams) error { - if c.updateResultCursor+1 > len(c.updateResult) { - return nil - } - err := c.updateResult[c.updateResultCursor] - c.updateResultCursor++ - return err -} - -func TestModifyFailed(t *testing.T) { - c := &mockClient{ - oldData: []map[string]string{{"foo": "value"}}, - } - cas := NewCAS(c, resourceKind, types.KindAccessRequest, mockEncode, mockDecode) - - r, err := cas.Update(context.Background(), "foo", func(data mockData) (mockData, error) { - return mockData{}, trace.Errorf("fail") - }) - - require.Error(t, err, "fail") - require.Equal(t, r, mockData{}) -} - -// We test cas is retrying modityT properly if modifyT returns a CompareFailedError during the first iteration. -func TestModifyCompareFailed(t *testing.T) { - c := &mockClient{ - oldData: []map[string]string{ - {"foo": "0"}, - {"foo": "1"}, - }, - } - cas := NewCAS(c, resourceKind, types.KindAccessRequest, mockEncode, mockDecode) - - r, err := cas.Update(context.Background(), "foo", func(data mockData) (mockData, error) { - // If this is the first time we're called we fail - if data.Foo == "0" { - return mockData{}, &trace.CompareFailedError{Message: "does not exist yet"} - } - data.Bar = "other value" - return data, nil - }) - - require.NoError(t, err) - require.NotNil(t, r) - require.Equal(t, r.Bar, "other value") -} - -func TestModifySuccess(t *testing.T) { - c := &mockClient{ - oldData: []map[string]string{{"foo": "value"}}, - } - cas := NewCAS(c, resourceKind, types.KindAccessRequest, mockEncode, mockDecode) - - r, err := cas.Update(context.Background(), "foo", func(i mockData) (mockData, error) { - i.Foo = "other value" - return i, nil - }) - - require.NoError(t, err) - require.NotNil(t, r) - require.Equal(t, r.Foo, "other value") -} - -func TestBackoff(t *testing.T) { - c := &mockClient{ - oldData: []map[string]string{{"foo": "value"}, {"foo": "value"}}, - updateResult: []error{trace.CompareFailed("fail"), nil}, - } - cas := NewCAS(c, resourceKind, types.KindAccessRequest, mockEncode, mockDecode) - - r, err := cas.Update(context.Background(), "foo", func(_ mockData) (mockData, error) { - return mockData{Foo: "yes"}, nil - }) - - require.NoError(t, err) - require.NotNil(t, r) - require.Equal(t, r.Foo, "yes") -} - -func TestWrongData(t *testing.T) { - c := &mockClient{ - oldData: []map[string]string{{"foo": "value"}}, - } - cas := NewCAS(c, resourceKind, types.KindAccessRequest, mockEncode, mockDecodeFail) - - _, err := cas.Update(context.Background(), "foo", func(i mockData) (mockData, error) { - i.Foo = "other value" - return i, nil - }) - - require.Error(t, err) - require.ErrorIs(t, err, trace.BadParameter("Failed to decode data")) -} diff --git a/lib/process.go b/lib/process.go deleted file mode 100644 index ea197b2f7..000000000 --- a/lib/process.go +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright 2023 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 lib - -import ( - "context" - "sync" - - "github.com/gravitational/trace" -) - -type Job interface { - DoJob(context.Context) error -} - -type ServiceJob interface { - Job - IsReady() bool - SetReady(ready bool) - WaitReady(ctx context.Context) (bool, error) - Done() <-chan struct{} - Err() error -} - -type serviceJob struct { - mu sync.Mutex - do func(context.Context) error - ready bool - err error - readyCh chan struct{} - doneCh chan struct{} -} - -type Process struct { - sync.Mutex - // doneCh is closed when all the jobs are completed. - doneCh chan struct{} - // spawn runs a goroutine in the app's context as a job with waiting for - // its completion on shutdown. - spawn func(Job, bool) - // terminate signals the app to terminate gracefully. - terminate func() - // cancel signals the app to terminate immediately - cancel context.CancelFunc - // onTerminate is a list of callbacks called on terminate. - onTerminate []jobFunc - // terminated flags out that process has been signaled for termination. - terminated bool - // criticalErrors is a list of errors returned by critical jobs. - criticalErrors []error -} - -type jobFunc func(context.Context) error - -type processKey struct{} -type jobKey struct{} - -var closedChan = make(chan struct{}) - -func init() { - close(closedChan) -} - -func NewProcess(ctx context.Context) *Process { - ctx, cancel := context.WithCancel(ctx) - doneCh := make(chan struct{}) - process := &Process{ - doneCh: doneCh, - cancel: cancel, - onTerminate: make([]jobFunc, 0), - } - ctx = context.WithValue(ctx, processKey{}, process) - - var jobs sync.WaitGroup - - jobs.Add(1) // Start the main "job". We have to do it for Wait() not being returned beforehand. - go func() { - jobs.Wait() - close(doneCh) - }() - process.spawn = func(job Job, critical bool) { - jobs.Add(1) - jctx, jcancel := context.WithCancel(context.WithValue(ctx, jobKey{}, job)) - go func() { - err := job.DoJob(jctx) - jcancel() - jobs.Done() - if err != nil && critical { - process.Terminate() - } - }() - } - - var once sync.Once - process.terminate = func() { - once.Do(func() { - process.Lock() - process.terminated = true - for _, j := range process.onTerminate { - process.spawn(j, false) - } - process.onTerminate = nil - process.Unlock() - jobs.Done() // Stop the main "job". - }) - } - - return process -} - -func (p *Process) SpawnJob(job Job) { - if p == nil { - panic("spawning a job on a nil process") - } - select { - case <-p.doneCh: - panic("spawning a job on a finished process") - default: - p.spawn(job, false) - } -} - -func (p *Process) SpawnCriticalJob(job Job) { - if p == nil { - panic("spawning a job on a nil process") - } - select { - case <-p.doneCh: - panic("spawning a job on a finished process") - default: - p.spawn(job, true) - } -} - -func (p *Process) Spawn(fn func(ctx context.Context) error) { - p.SpawnJob(jobFunc(fn)) -} - -func (p *Process) SpawnCritical(fn func(ctx context.Context) error) { - p.SpawnCriticalJob(jobFunc(fn)) -} - -func (p *Process) OnTerminate(fn func(ctx context.Context) error) { - if p == nil { - panic("calling OnTerminate a nil process") - } - p.Lock() - defer p.Unlock() - if p.terminated { - p.Spawn(fn) - } else { - p.onTerminate = append(p.onTerminate, fn) - } -} - -// Done channel is used to wait for jobs completion. -func (p *Process) Done() <-chan struct{} { - if p == nil { - return closedChan - } - return p.doneCh -} - -// Terminate signals a process to terminate. You should avoid spawning new jobs after termination. -func (p *Process) Terminate() { - if p == nil { - return - } - p.terminate() -} - -// Shutdown signals a process to terminate and waits for completion of all jobs. -func (p *Process) Shutdown(ctx context.Context) error { - p.Terminate() - select { - case <-ctx.Done(): - return ctx.Err() - case <-p.Done(): - return nil - } -} - -// Close shuts down all process jobs immediately. -func (p *Process) Close() { - if p == nil { - return - } - p.cancel() - <-p.doneCh -} - -func (p *Process) CriticalError() error { - return trace.NewAggregate(p.criticalErrors...) -} - -func (j jobFunc) DoJob(ctx context.Context) error { - return j(ctx) -} - -func MustGetProcess(ctx context.Context) *Process { - return ctx.Value(processKey{}).(*Process) -} - -func MustGetJob(ctx context.Context) Job { - return ctx.Value(jobKey{}).(Job) -} - -func MustGetServiceJob(ctx context.Context) ServiceJob { - return MustGetJob(ctx).(ServiceJob) -} - -func NewServiceJob(fn func(ctx context.Context) error) ServiceJob { - job := &serviceJob{ - readyCh: make(chan struct{}), - doneCh: make(chan struct{}), - } - job.do = func(ctx context.Context) error { - err := fn(ctx) - job.finish(err) - return err - } - return job -} - -func (job *serviceJob) finish(err error) { - job.mu.Lock() - defer job.mu.Unlock() - - select { - case <-job.readyCh: - default: - close(job.readyCh) - } - job.err = err - close(job.doneCh) -} - -func (job *serviceJob) DoJob(ctx context.Context) error { - return job.do(ctx) -} - -func (job *serviceJob) IsReady() bool { - job.mu.Lock() - defer job.mu.Unlock() - - return job.ready -} - -func (job *serviceJob) SetReady(ready bool) { - job.mu.Lock() - defer job.mu.Unlock() - - job.ready = ready - select { - case <-job.readyCh: - default: - close(job.readyCh) - } -} - -func (job *serviceJob) WaitReady(ctx context.Context) (bool, error) { - select { - case <-job.readyCh: - return job.IsReady(), nil - case <-ctx.Done(): - return false, trace.Wrap(ctx.Err()) - } -} - -func (job *serviceJob) Done() <-chan struct{} { - return job.doneCh -} - -func (job *serviceJob) Err() error { - job.mu.Lock() - defer job.mu.Unlock() - - return job.err -} diff --git a/lib/runner.go b/lib/runner.go deleted file mode 100644 index 4bbe16055..000000000 --- a/lib/runner.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2023 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 lib - -import ( - "fmt" - "runtime" -) - -// PrintVersion prints the specified app version to STDOUT -func PrintVersion(appName string, version string, gitref string) { - if gitref != "" { - fmt.Printf("%v v%v git:%v %v\n", appName, version, gitref, runtime.Version()) - } else { - fmt.Printf("%v v%v %v\n", appName, version, runtime.Version()) - } -} diff --git a/lib/sha256.go b/lib/sha256.go deleted file mode 100644 index 7be662fd4..000000000 --- a/lib/sha256.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package lib - -import ( - "crypto/sha256" - "encoding/hex" - "hash" - "io" - "os" - - "github.com/gravitational/trace" -) - -type SHA256Sum [sha256.Size]byte - -type SHA256 struct { - hash hash.Hash -} - -func NewSHA256() SHA256 { - hash := sha256.New() - return SHA256{hash: hash} -} - -func (s SHA256) Write(p []byte) (n int, err error) { - return s.hash.Write(p) -} - -func (s SHA256) Sum() SHA256Sum { - var result SHA256Sum - copy(result[:], s.hash.Sum(nil)[:sha256.Size]) - return result -} - -func ReadFileSHA256(fileName string) (SHA256Sum, error) { - file, err := os.Open(fileName) - if err != nil { - return SHA256Sum{}, trace.Wrap(err) - } - sha256 := NewSHA256() - _, err = io.Copy(sha256, file) - if err = trace.NewAggregate(err, file.Close()); err != nil { - return SHA256Sum{}, trace.Wrap(err) - } - return sha256.Sum(), nil -} - -func MustHexSHA256(str string) SHA256Sum { - data, err := hex.DecodeString(str) - if err != nil { - panic(err) - } - var result SHA256Sum - copy(result[:], data[:sha256.Size]) - return result -} diff --git a/lib/signals.go b/lib/signals.go deleted file mode 100644 index 4d187da4e..000000000 --- a/lib/signals.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2023 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 lib - -import ( - "context" - "os" - "os/signal" - "syscall" - "time" - - log "github.com/sirupsen/logrus" -) - -type Terminable interface { - // Shutdown attempts to gracefully terminate. - Shutdown(context.Context) error - // Close does a fast (force) termination. - Close() -} - -func ServeSignals(app Terminable, shutdownTimeout time.Duration) { - ctx := context.Background() - sigC := make(chan os.Signal, 1) - signal.Notify(sigC, - syscall.SIGTERM, // graceful shutdown - syscall.SIGINT, // graceful-then-fast shutdown - ) - defer signal.Stop(sigC) - - gracefulShutdown := func() { - tctx, tcancel := context.WithTimeout(ctx, shutdownTimeout) - defer tcancel() - log.Infof("Attempting graceful shutdown...") - if err := app.Shutdown(tctx); err != nil { - log.Infof("Graceful shutdown failed. Trying fast shutdown...") - app.Close() - } - } - var alreadyInterrupted bool - for { - signal := <-sigC - switch signal { - case syscall.SIGTERM: - gracefulShutdown() - return - case syscall.SIGINT: - if alreadyInterrupted { - app.Close() - return - } - go gracefulShutdown() - alreadyInterrupted = true - } - } -} diff --git a/lib/stringset/stringset.go b/lib/stringset/stringset.go deleted file mode 100644 index 5f80155b8..000000000 --- a/lib/stringset/stringset.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2023 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 stringset - -// StringSet is string container in which every string is contained at most once i.e. a set data structure. -type StringSet map[string]struct{} - -// New builds a string set with elements from a given slice. -func New(elems ...string) StringSet { - set := NewWithCap(len(elems)) - set.Add(elems...) - return set -} - -// NewWithCap builds an empty string set with a given capacity. -func NewWithCap(cap int) StringSet { - return make(StringSet, cap) -} - -// Add inserts a string to the set. -func (set StringSet) Add(elems ...string) { - for _, str := range elems { - set[str] = struct{}{} - } -} - -// Del removes a string from the set. -func (set StringSet) Del(str string) { - delete(set, str) -} - -// Len returns a set size. -func (set StringSet) Len() int { - return len(set) -} - -// Contains checks if the set includes a given string. -func (set StringSet) Contains(str string) bool { - _, ok := set[str] - return ok -} - -// ToSlice returns a slice with set contents. -func (set StringSet) ToSlice() []string { - if n := set.Len(); n > 0 { - result := make([]string, 0, n) - for str := range set { - result = append(result, str) - } - return result - } - return nil -} diff --git a/lib/tar/extract.go b/lib/tar/extract.go deleted file mode 100644 index a0b0fef4e..000000000 --- a/lib/tar/extract.go +++ /dev/null @@ -1,144 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package tar - -import ( - "archive/tar" - "compress/gzip" - "io" - "os" - "path" - "path/filepath" - "strings" - - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib/stringset" -) - -// Compression is a compression flag. -type Compression int - -const ( - NoCompression Compression = iota - GzipCompression -) - -// ExtractOptions is the options for Extract and ExtractFile functions. -type ExtractOptions struct { - // OutDir is a directory where to extract the files. Current working directory by default. - OutDir string - // Compression indicates is the tarball compressed or not. - Compression Compression - // StripComponents is like --strip-components of tar utility. It strips first N components from the path by a given depth. - StripComponents uint - // Files is a list of files to extract. If empty, extract all the files. - Files []string - // OutFiles is a resulting map passed by user. If non-nil then it will store the mapping from tar file names to file system paths. - OutFiles map[string]string -} - -// ExtractFile extracts a tar file contents. -func ExtractFile(fileName string, options ExtractOptions) error { - file, err := os.Open(fileName) - if err != nil { - return trace.Wrap(err) - } - return trace.NewAggregate( - Extract(file, options), - file.Close(), - ) -} - -// Extract extracts a tarball given as a reader interface. -func Extract(reader io.Reader, options ExtractOptions) error { - var err error - - outDir := options.OutDir - if outDir == "" { - outDir, err = os.Getwd() - if err != nil { - return trace.Wrap(err) - } - } - - switch options.Compression { - case NoCompression: - case GzipCompression: - reader, err = gzip.NewReader(reader) - if err != nil { - return trace.Wrap(err) - } - default: - return trace.BadParameter("unknown compression options %v", options.Compression) - } - - tarReader := tar.NewReader(reader) - - var filesDone stringset.StringSet - if len(options.Files) > 0 { - filesDone = stringset.New(options.Files...) - } - for filesDone == nil || filesDone.Len() > 0 { - header, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return trace.Wrap(err) - } - - if filesDone != nil && !filesDone.Contains(header.Name) { - continue - } - filesDone.Del(header.Name) - - outFileName := header.Name - if strip := int(options.StripComponents); strip > 0 { - parts := strings.Split(outFileName, "/") - if strip > len(parts)-1 { - strip = len(parts) - 1 - } - outFileName = path.Join(parts[strip:]...) - } - - outFilePath := path.Join(outDir, outFileName) - outFilePerm := os.FileMode(header.Mode).Perm() - - // fail if the outFilePath is outside outDir, see the "zip slip" vulnerability - if !strings.HasPrefix(filepath.Clean(outFilePath), filepath.Clean(outDir)+string(os.PathSeparator)) { - return trace.Errorf("extraction target outside the root: %s", header.Name) - } - outFile, err := os.OpenFile(outFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, outFilePerm) - if err != nil { - return trace.Wrap(err) - } - _, err = io.Copy(outFile, tarReader) - if err = trace.NewAggregate(err, outFile.Close()); err != nil { - return trace.Wrap(err) - } - if options.OutFiles != nil { - options.OutFiles[header.Name] = outFile.Name() - } - } - - if filesDone.Len() > 0 { - return trace.Errorf("files not found in the archive: %s", strings.Join(filesDone.ToSlice(), ", ")) - } - - return nil -} diff --git a/lib/tctl/resources.go b/lib/tctl/resources.go deleted file mode 100644 index 303c1cb8d..000000000 --- a/lib/tctl/resources.go +++ /dev/null @@ -1,112 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package tctl - -import ( - "encoding/json" - "io" - - "github.com/ghodss/yaml" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - kyaml "k8s.io/apimachinery/pkg/util/yaml" -) - -func writeResourcesYAML(w io.Writer, resources []types.Resource) error { - for i, resource := range resources { - data, err := yaml.Marshal(resource) - if err != nil { - return trace.Wrap(err) - } - if _, err := w.Write(data); err != nil { - return trace.Wrap(err) - } - if i != len(resources) { - if _, err := io.WriteString(w, "\n---\n"); err != nil { - return trace.Wrap(err) - } - } - } - return nil -} - -func readResourcesYAMLOrJSON(r io.Reader) ([]types.Resource, error) { - var resources []types.Resource - decoder := kyaml.NewYAMLOrJSONDecoder(r, 32768) - for { - var res streamResource - err := decoder.Decode(&res) - if err != nil { - if err == io.EOF { - break - } - return nil, trace.Wrap(err) - } - resources = append(resources, res.Resource) - } - return resources, nil -} - -type streamResource struct{ types.Resource } - -func (res *streamResource) UnmarshalJSON(raw []byte) error { - var header types.ResourceHeader - if err := json.Unmarshal(raw, &header); err != nil { - return trace.Wrap(err) - } - - var resource types.Resource - switch header.Kind { - case types.KindNode: - switch header.Version { - case types.V2: - resource = &types.ServerV2{} - default: - return trace.BadParameter("unsupported resource version %s", header.Version) - } - case types.KindUser: - switch header.Version { - case types.V2: - resource = &types.UserV2{} - default: - return trace.BadParameter("unsupported resource version %s", header.Version) - } - case types.KindRole: - switch header.Version { - case types.V4, types.V5, types.V6: - resource = &types.RoleV6{} - default: - return trace.BadParameter("unsupported resource version %s", header.Version) - } - case types.KindCertAuthority: - switch header.Version { - case types.V2: - resource = &types.CertAuthorityV2{} - default: - return trace.BadParameter("unsupported resource version %s", header.Version) - } - default: - return trace.BadParameter("unsupported resource kind %s", header.Kind) - } - - if err := json.Unmarshal(raw, resource); err != nil { - return trace.Wrap(err) - } - - res.Resource = resource - return nil -} diff --git a/lib/tctl/tctl.go b/lib/tctl/tctl.go deleted file mode 100644 index 495d223f4..000000000 --- a/lib/tctl/tctl.go +++ /dev/null @@ -1,169 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package tctl - -import ( - "context" - "os/exec" - "regexp" - - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib/logger" -) - -var regexpStatusCAPin = regexp.MustCompile(`CA pin +(sha256:[a-zA-Z0-9]+)`) - -// Tctl is a runner of tctl command. -type Tctl struct { - Path string - ConfigPath string - AuthServer string -} - -// CheckExecutable checks if `tctl` executable exists in the system. -func (tctl Tctl) CheckExecutable() error { - _, err := exec.LookPath(tctl.cmd()) - return trace.Wrap(err, "tctl executable is not found") -} - -// Sign generates Teleport client credentials at a given path. -func (tctl Tctl) Sign(ctx context.Context, username, format, outPath string) error { - log := logger.Get(ctx) - args := append(tctl.baseArgs(), - "auth", - "sign", - "--user", - username, - "--format", - format, - "--overwrite", - "--out", - outPath, - ) - cmd := exec.CommandContext(ctx, tctl.cmd(), args...) - log.Debugf("Running %s", cmd) - output, err := cmd.CombinedOutput() - if err != nil { - log.WithError(err).WithField("args", args).Debug("tctl auth sign failed:", string(output)) - return trace.Wrap(err, "tctl auth sign failed") - } - return nil -} - -// Create creates or updates a set of Teleport resources. -func (tctl Tctl) Create(ctx context.Context, resources []types.Resource) error { - log := logger.Get(ctx) - args := append(tctl.baseArgs(), "create") - cmd := exec.CommandContext(ctx, tctl.cmd(), args...) - log.Debugf("Running %s", cmd) - stdinPipe, err := cmd.StdinPipe() - if err != nil { - return trace.Wrap(err, "failed to get stdin pipe") - } - go func() { - defer func() { - if err := stdinPipe.Close(); err != nil { - log.WithError(trace.Wrap(err)).Error("Failed to close stdin pipe") - } - }() - if err := writeResourcesYAML(stdinPipe, resources); err != nil { - log.WithError(trace.Wrap(err)).Error("Failed to serialize resources stdin") - } - }() - output, err := cmd.CombinedOutput() - if err != nil { - log.WithError(err).Debug("tctl create failed:", string(output)) - return trace.Wrap(err, "tctl create failed") - } - return nil -} - -// GetAll loads a bunch of Teleport resources by a given query. -func (tctl Tctl) GetAll(ctx context.Context, query string) ([]types.Resource, error) { - log := logger.Get(ctx) - args := append(tctl.baseArgs(), "get", query) - cmd := exec.CommandContext(ctx, tctl.cmd(), args...) - - log.Debugf("Running %s", cmd) - stdoutPipe, err := cmd.StdoutPipe() - if err != nil { - return nil, trace.Wrap(err, "failed to get stdout") - } - if err := cmd.Start(); err != nil { - return nil, trace.Wrap(err, "failed to start tctl") - } - resources, err := readResourcesYAMLOrJSON(stdoutPipe) - if err != nil { - return nil, trace.Wrap(err) - } - if err := cmd.Wait(); err != nil { - return nil, trace.Wrap(err) - } - return resources, nil -} - -// Get loads a singular resource by its kind and name identifiers. -func (tctl Tctl) Get(ctx context.Context, kind, name string) (types.Resource, error) { - query := kind + "/" + name - resources, err := tctl.GetAll(ctx, query) - if err != nil { - return nil, trace.Wrap(err) - } - if len(resources) == 0 { - return nil, trace.NotFound("resource %q is not found", query) - } - return resources[0], nil -} - -// GetCAPin sets the auth service CA Pin using output from tctl. -func (tctl Tctl) GetCAPin(ctx context.Context) (string, error) { - log := logger.Get(ctx) - - args := append(tctl.baseArgs(), "status") - cmd := exec.CommandContext(ctx, tctl.cmd(), args...) - - log.Debugf("Running %s", cmd) - output, err := cmd.Output() - if err != nil { - return "", trace.Wrap(err, "failed to get auth status") - } - - submatch := regexpStatusCAPin.FindStringSubmatch(string(output)) - if len(submatch) < 2 || submatch[1] == "" { - return "", trace.Errorf("failed to find CA Pin in auth status") - } - return submatch[1], nil -} - -func (tctl Tctl) cmd() string { - if tctl.Path != "" { - return tctl.Path - } - return "tctl" -} - -func (tctl Tctl) baseArgs() (args []string) { - if tctl.ConfigPath != "" { - args = append(args, "--config", tctl.ConfigPath) - } - if tctl.AuthServer != "" { - args = append(args, "--auth-server", tctl.AuthServer) - } - return -} diff --git a/lib/testing/integration/auth_test.go b/lib/testing/integration/auth_test.go deleted file mode 100644 index 2284074ca..000000000 --- a/lib/testing/integration/auth_test.go +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "testing" - - "github.com/gravitational/teleport/api/types" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -type IntegrationAuthSuite struct { - AuthSetup -} - -func TestIntegrationAuth(t *testing.T) { suite.Run(t, &IntegrationAuthSuite{}) } - -func (s *IntegrationAuthSuite) SetupTest() { - s.AuthSetup.SetupService() -} - -func (s *IntegrationAuthSuite) TestBootstrap() { - t := s.T() - - var bootstrap Bootstrap - role, err := bootstrap.AddRole("foo", types.RoleSpecV6{}) - require.NoError(t, err) - _, err = bootstrap.AddUserWithRoles("vladimir", role.GetName()) - require.NoError(t, err) - err = s.Integration.Bootstrap(s.Context(), s.Auth, bootstrap.Resources()) - require.NoError(t, err) -} - -func (s *IntegrationAuthSuite) TestPing() { - t := s.T() - - var bootstrap Bootstrap - user, err := bootstrap.AddUserWithRoles("vladimir", "editor") - require.NoError(t, err) - err = s.Integration.Bootstrap(s.Context(), s.Auth, bootstrap.Resources()) - require.NoError(t, err) - - client, err := s.Integration.NewClient(s.Context(), s.Auth, user.GetName()) - require.NoError(t, err) - _, err = client.Ping(s.Context()) - require.NoError(t, err) -} diff --git a/lib/testing/integration/authservice.go b/lib/testing/integration/authservice.go deleted file mode 100644 index 6839accf7..000000000 --- a/lib/testing/integration/authservice.go +++ /dev/null @@ -1,305 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "bufio" - "bytes" - "context" - "io" - "os/exec" - "regexp" - "strings" - "sync" - "syscall" - "time" - - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib/logger" -) - -var regexpAuthStarting = regexp.MustCompile(`Auth service [^ ]+ is starting on [^ ]+:(\d+)`) - -type AuthService struct { - mu sync.Mutex - teleportPath string - configPath string - authAddr Addr - isReady bool - readyCh chan struct{} - doneCh chan struct{} - terminate context.CancelFunc - setErr func(error) - setReady func(bool) - error error - stdout strings.Builder - stderr bytes.Buffer -} - -func newAuthService(teleportPath, configPath string) *AuthService { - var auth AuthService - var setErrOnce, setReadyOnce sync.Once - readyCh := make(chan struct{}) - auth = AuthService{ - teleportPath: teleportPath, - configPath: configPath, - readyCh: readyCh, - doneCh: make(chan struct{}), - terminate: func() {}, // dummy noop that will be overridden by Run(), - setErr: func(err error) { - setErrOnce.Do(func() { - auth.mu.Lock() - defer auth.mu.Unlock() - auth.error = err - }) - }, - setReady: func(isReady bool) { - setReadyOnce.Do(func() { - auth.mu.Lock() - auth.isReady = isReady - auth.mu.Unlock() - close(readyCh) - }) - }, - } - return &auth -} - -// Run spawns an auth server instance. -func (auth *AuthService) Run(ctx context.Context) error { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - log := logger.Get(ctx) - - cmd := exec.CommandContext(ctx, auth.teleportPath, "start", "--debug", "--config", auth.configPath) - log.Debugf("Running Auth service: %s", cmd) - - stdoutPipe, err := cmd.StdoutPipe() - if err != nil { - err = trace.Wrap(err, "failed to get stdout") - auth.setErr(err) - return err - } - - stderrPipe, err := cmd.StderrPipe() - if err != nil { - err = trace.Wrap(err, "failed to get stderr") - auth.setErr(err) - return err - } - - if err := cmd.Start(); err != nil { - err = trace.Wrap(err, "failed to start teleport") - auth.setErr(err) - return err - } - - ctx, log = logger.WithField(ctx, "pid", cmd.Process.Pid) - log.Debug("Auth service process has been started") - - auth.mu.Lock() - var terminateOnce sync.Once - auth.terminate = func() { - terminateOnce.Do(func() { - log.Debug("Terminating Auth service process") - // Signal the process to gracefully terminate by sending SIGQUIT. - if err := cmd.Process.Signal(syscall.SIGQUIT); err != nil { - log.Warn(err) - } - // If we're not done in 5 minutes, just kill the process by canceling its context. - go func() { - select { - case <-auth.doneCh: - case <-time.After(serviceShutdownTimeout): - log.Debug("Killing Auth service process") - } - // cancel() results in sending SIGKILL to a process if it's still alive. - cancel() - }() - }) - } - auth.mu.Unlock() - - var ioWork sync.WaitGroup - ioWork.Add(2) - - // Parse stdout of a Teleport process. - go func() { - defer ioWork.Done() - - stdout := bufio.NewReader(stdoutPipe) - for { - line, err := stdout.ReadString('\n') - if err == io.EOF { - return - } - if err := trace.Wrap(err); err != nil { - log.WithError(err).Error("failed to read process stdout") - return - } - - auth.saveStdout(line) - - if auth.IsReady() { - continue - } - - auth.parseLine(ctx, line) - if addr := auth.AuthAddr(); !addr.IsEmpty() { - log.Debugf("Found addr of Auth service process: %v", addr) - auth.setReady(true) - } - } - }() - - // Save stderr to a buffer. - go func() { - defer ioWork.Done() - - stderr := bufio.NewReader(stderrPipe) - data := make([]byte, stderr.Size()) - for { - n, err := stderr.Read(data) - auth.saveStderr(data[:n]) - if err == io.EOF { - return - } - if err := trace.Wrap(err); err != nil { - log.WithError(err).Error("failed to read process stderr") - return - } - } - }() - - // Wait for process completeness after processing both outputs. - go func() { - ioWork.Wait() - err := trace.Wrap(cmd.Wait()) - auth.setErr(err) - close(auth.doneCh) - }() - - <-auth.doneCh - - if !auth.IsReady() { - log.Error("Auth server is failed to initialize") - stdoutLines := strings.Split(auth.Stdout(), "\n") - for _, line := range stdoutLines { - log.Debug("AuthService log: ", line) - } - log.Debugf("AuthService stderr: %q", auth.Stderr()) - - // If it's still not ready lets signal that it's finally not ready. - auth.setReady(false) - // Set an err just in case if it's not set before. - auth.setErr(trace.Errorf("failed to initialize")) - } - - return trace.Wrap(auth.Err()) -} - -// AuthAddr returns auth service external address. -func (auth *AuthService) AuthAddr() Addr { - auth.mu.Lock() - defer auth.mu.Unlock() - return auth.authAddr -} - -// ConfigPath returns auth service config file path. -func (auth *AuthService) ConfigPath() string { - return auth.configPath -} - -// Err returns auth server error. It's nil If process is not done yet. -func (auth *AuthService) Err() error { - auth.mu.Lock() - defer auth.mu.Unlock() - return auth.error -} - -// Shutdown terminates the auth server process and waits for its completion. -func (auth *AuthService) Shutdown(ctx context.Context) error { - auth.doTerminate() - select { - case <-auth.doneCh: - return nil - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } -} - -// Stdout returns a collected auth server process stdout. -func (auth *AuthService) Stdout() string { - auth.mu.Lock() - defer auth.mu.Unlock() - return auth.stdout.String() -} - -// Stderr returns a collected auth server process stderr. -func (auth *AuthService) Stderr() string { - auth.mu.Lock() - defer auth.mu.Unlock() - return auth.stderr.String() -} - -// WaitReady waits for auth server initialization. -func (auth *AuthService) WaitReady(ctx context.Context) (bool, error) { - select { - case <-auth.readyCh: - return auth.IsReady(), nil - case <-ctx.Done(): - return false, trace.Wrap(ctx.Err(), "auth server is not ready") - } -} - -// IsReady indicates if auth server is initialized properly. -func (auth *AuthService) IsReady() bool { - auth.mu.Lock() - defer auth.mu.Unlock() - return auth.isReady -} - -func (auth *AuthService) doTerminate() { - auth.mu.Lock() - terminate := auth.terminate - auth.mu.Unlock() - terminate() -} - -func (auth *AuthService) parseLine(ctx context.Context, line string) { - submatch := regexpAuthStarting.FindStringSubmatch(line) - if submatch != nil { - auth.mu.Lock() - defer auth.mu.Unlock() - auth.authAddr = Addr{Host: "127.0.0.1", Port: submatch[1]} - return - } -} - -func (auth *AuthService) saveStdout(line string) { - auth.mu.Lock() - defer auth.mu.Unlock() - auth.stdout.WriteString(line) -} - -func (auth *AuthService) saveStderr(chunk []byte) { - auth.mu.Lock() - defer auth.mu.Unlock() - auth.stderr.Write(chunk) -} diff --git a/lib/testing/integration/bootstrap.go b/lib/testing/integration/bootstrap.go deleted file mode 100644 index 1433997ef..000000000 --- a/lib/testing/integration/bootstrap.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" -) - -type Bootstrap struct { - resources []types.Resource -} - -func (bootstrap *Bootstrap) Add(resource types.Resource) { - bootstrap.resources = append(bootstrap.resources, resource) -} - -func (bootstrap *Bootstrap) Resources() []types.Resource { - return bootstrap.resources -} - -func (bootstrap *Bootstrap) AddUserWithRoles(name string, roles ...string) (types.User, error) { - user, err := types.NewUser(name) - if err != nil { - return nil, trace.Wrap(err) - } - user.SetRoles(roles) - bootstrap.Add(user) - return user, nil -} - -func (bootstrap *Bootstrap) AddRole(name string, spec types.RoleSpecV6) (types.Role, error) { - role, err := types.NewRole(name, spec) - if err != nil { - return nil, trace.Wrap(err) - } - bootstrap.Add(role) - return role, nil -} diff --git a/lib/testing/integration/client.go b/lib/testing/integration/client.go deleted file mode 100644 index 073a2c4c6..000000000 --- a/lib/testing/integration/client.go +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "context" - "time" - - "github.com/gravitational/teleport/api/client" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/types/events" - "github.com/gravitational/trace" -) - -// Client is a wrapper around *client.Client with some additional methods helpful for testing. -type Client struct { - *client.Client -} - -// SubmitAccessRequestReview is a simpler version of SubmitAccessReview. -func (api *Client) SubmitAccessRequestReview(ctx context.Context, reqID string, review types.AccessReview) error { - _, err := api.SubmitAccessReview(ctx, types.AccessReviewSubmission{ - RequestID: reqID, - Review: review, - }) - return trace.Wrap(err) -} - -// ApproveAccessRequest sets an access request state to APPROVED. -func (api *Client) ApproveAccessRequest(ctx context.Context, reqID, reason string) error { - update := types.AccessRequestUpdate{ - RequestID: reqID, - State: types.RequestState_APPROVED, - Reason: reason, - } - return api.SetAccessRequestState(ctx, update) -} - -// ApproveAccessRequest sets an access request state to DENIED. -func (api *Client) DenyAccessRequest(ctx context.Context, reqID, reason string) error { - update := types.AccessRequestUpdate{ - RequestID: reqID, - State: types.RequestState_DENIED, - Reason: reason, - } - return api.SetAccessRequestState(ctx, update) -} - -// GetAccessRequest loads an access request. -func (api *Client) GetAccessRequest(ctx context.Context, reqID string) (types.AccessRequest, error) { - requests, err := api.GetAccessRequests(ctx, types.AccessRequestFilter{ID: reqID}) - if err != nil { - return nil, trace.Wrap(err) - } - if len(requests) == 0 { - return nil, trace.NotFound("request %q is not found", reqID) - } - return requests[0], nil -} - -// PollAccessRequestPluginData waits until plugin data for a give request became available. -func (api *Client) PollAccessRequestPluginData(ctx context.Context, plugin, reqID string) (map[string]string, error) { - filter := types.PluginDataFilter{ - Kind: types.KindAccessRequest, - Resource: reqID, - Plugin: plugin, - } - for { - pluginDatas, err := api.GetPluginData(ctx, filter) - if err != nil { - return nil, trace.Wrap(err) - } - if len(pluginDatas) > 0 { - pluginData := pluginDatas[0] - entry := pluginData.Entries()[plugin] - if entry != nil { - return entry.Data, nil - } - } - time.Sleep(25 * time.Millisecond) - } -} - -// SearchAccessRequestEvents searches for recent access request events in audit log. -func (api *Client) SearchAccessRequestEvents(ctx context.Context, reqID string) ([]*events.AccessRequestCreate, error) { - auditEvents, _, err := api.SearchEvents( - ctx, - time.Now().UTC().AddDate(0, -1, 0), - time.Now().UTC(), - "default", - []string{"access_request.update"}, - 100, - types.EventOrderAscending, - "", - ) - result := make([]*events.AccessRequestCreate, 0, len(auditEvents)) - for _, event := range auditEvents { - if event, ok := event.(*events.AccessRequestCreate); ok && event.RequestID == reqID { - result = append(result, event) - } - } - return result, trace.Wrap(err) -} diff --git a/lib/testing/integration/download.go b/lib/testing/integration/download.go deleted file mode 100644 index badc7ca02..000000000 --- a/lib/testing/integration/download.go +++ /dev/null @@ -1,246 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "context" - _ "embed" - "fmt" - "io" - "net/url" - "os" - "path" - "runtime" - "strings" - "syscall" - "time" - - "github.com/gravitational/trace" - "github.com/jonboulle/clockwork" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/backoff" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/tar" -) - -type downloadVersionKey struct { - ver string - os string - arch string - enterprise bool -} - -type downloadVersion struct { - sha256 lib.SHA256Sum -} - -//go:embed download_sha.dsv -var downloadVersionsDSV string - -func downloadVersionsHash(ctx context.Context, versionsHash string, key downloadVersionKey) (downloadVersion, bool) { - flavor := "" - if key.enterprise { - flavor = "ent-" - } - - fileNameFromKey := fmt.Sprintf("teleport-%s%s-%s-%s-bin.tar.gz", flavor, key.ver, key.os, key.arch) - for _, line := range strings.Split(versionsHash, "\n") { - if len(line) == 0 { - continue - } - - lineVals := strings.Split(line, " ") - if len(lineVals) != 2 { - logger.Get(ctx).Debugf("Invalid line in download_sha.dsv: %q", line) - continue - } - fileHash := lineVals[0] - fileName := lineVals[1] - if fileName == fileNameFromKey { - return downloadVersion{sha256: lib.MustHexSHA256(fileHash)}, true - } - } - - return downloadVersion{}, false -} - -// GetEnterprise downloads a Teleport Enterprise distribution. -func GetEnterprise(ctx context.Context, ver, outDir string) (BinPaths, error) { - logger.Get(ctx).Debugf("Looking up Teleport Enterprise distribution %s", ver) - key := downloadVersionKey{ - ver: ver, - os: runtime.GOOS, - arch: runtime.GOARCH, - enterprise: true, - } - version, ok := downloadVersionsHash(ctx, downloadVersionsDSV, key) - if !ok { - return BinPaths{}, trace.NotFound("teleport enterprise version %s-%s-%s is unknown", key.ver, key.os, key.arch) - } - distStr := fmt.Sprintf("teleport-ent-%s-%s-%s", key.ver, key.os, key.arch) - return getBinaries(ctx, distStr, outDir, version.sha256) -} - -// GetOSS downloads a Teleport OSS distribution. -func GetOSS(ctx context.Context, ver, outDir string) (BinPaths, error) { - logger.Get(ctx).Debugf("Looking up Teleport OSS distribution %s", ver) - key := downloadVersionKey{ - ver: ver, - os: runtime.GOOS, - arch: runtime.GOARCH, - } - version, ok := downloadVersionsHash(ctx, downloadVersionsDSV, key) - if !ok { - return BinPaths{}, trace.NotFound("teleport oss version %s-%s-%s is unknown", key.ver, key.os, key.arch) - } - distStr := fmt.Sprintf("teleport-%s-%s-%s", key.ver, key.os, key.arch) - return getBinaries(ctx, distStr, outDir, version.sha256) -} - -func getTarball(ctx context.Context, url *url.URL, outFile *os.File, checksum lib.SHA256Sum) (*os.File, error) { - log := logger.Get(ctx) - var err error - - outFileInfo, err := outFile.Stat() - if err != nil { - return nil, trace.NewAggregate(err, outFile.Close()) - } - if outFileInfo.Size() > 0 { - log.Debugf("Found Teleport tarball %s, calculating its checksum", outFile.Name()) - // Check if we have a tarball cached with a correct sha256 sum. - sha256 := lib.NewSHA256() - if _, err = io.Copy(sha256, outFile); err != nil { - return nil, trace.NewAggregate(err, outFile.Close()) - } - if sha256.Sum() == checksum { - log.Debugf("Checksum of the Teleport tarball %s is correct", outFile.Name()) - return outFile, nil - } - log.Warningf("Teleport tarball %s checksum is incorrect. Need to redownload it", outFile.Name()) - // Rewind the file to the beginning and rewrite it. - if _, err = outFile.Seek(0, 0); err != nil { - return nil, trace.NewAggregate(err, outFile.Close()) - } - } - log.Debugf("Downloading Teleport tarball from %s", url) - if err := outFile.Truncate(0); err != nil { - return nil, trace.NewAggregate(err, outFile.Close()) - } - if err := lib.DownloadAndCheck(ctx, url.String(), outFile, checksum); err != nil { - return nil, trace.NewAggregate(err, outFile.Close()) - } - return outFile, nil -} - -func getBinaries(ctx context.Context, distStr, outDir string, checksum lib.SHA256Sum) (BinPaths, error) { - log := logger.Get(ctx) - - if err := os.MkdirAll(outDir, 0755); err != nil { - return BinPaths{}, trace.Wrap(err) - } - - outExtractDir := path.Join(outDir, distStr+"-bin") - - outFileName := distStr + "-bin.tar.gz" - outFilePath := path.Join(outDir, outFileName) - outFile, err := os.OpenFile(outFilePath, os.O_RDWR|os.O_CREATE, 0666) - if err != nil { - return BinPaths{}, trace.Wrap(err) - } - - // Make sure no other downloader does access the tarball. - backoff := backoff.NewDecorrWithMul(500*time.Millisecond, 7*time.Second, 5, clockwork.NewRealClock()) - for { - err := syscall.Flock(int(outFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) - if err == nil { - // Successfully acquired the advisory lock. - // Once the file is closed it will be unlocked too. - break - } - if err != syscall.EWOULDBLOCK { - // Advisory lock is acquired by another process. - return BinPaths{}, trace.NewAggregate(trace.ConvertSystemError(err), outFile.Close()) - } - log.Debugf("File %s is occupied by another process, lets wait...", outFile.Name()) - if err := backoff.Do(ctx); err != nil { - return BinPaths{}, trace.NewAggregate(trace.ConvertSystemError(err), outFile.Close()) - } - } - - existingPaths := BinPaths{ - Teleport: path.Join(outExtractDir, "teleport"), - Tctl: path.Join(outExtractDir, "tctl"), - Tsh: path.Join(outExtractDir, "tsh"), - } - - if fileExists(existingPaths.Teleport) && fileExists(existingPaths.Tctl) && fileExists(existingPaths.Tsh) { - log.Debugf("Teleport binaries are found in %s. No need to download anything", outExtractDir) - return existingPaths, trace.Wrap(outFile.Close()) - } - - url, err := url.Parse("https://get.gravitational.com/" + outFileName) - if err != nil { - return BinPaths{}, trace.Wrap(err) - } - tarFile, err := getTarball(ctx, url, outFile, checksum) - if err != nil { - return BinPaths{}, trace.Wrap(err) - } - if _, err = tarFile.Seek(0, 0); err != nil { - return BinPaths{}, trace.NewAggregate(err, tarFile.Close()) - } - - // Downloading file could take a long time, lets check if can proceed further. - select { - case <-ctx.Done(): - return BinPaths{}, trace.NewAggregate(ctx.Err(), tarFile.Close()) - default: - } - - tarOptions := tar.ExtractOptions{ - Compression: tar.GzipCompression, - OutDir: outExtractDir, - StripComponents: 1, - OutFiles: make(map[string]string), - } - if strings.HasPrefix(distStr, "teleport-ent") { - tarOptions.Files = []string{"teleport-ent/teleport", "teleport-ent/tctl", "teleport-ent/tsh"} - } else { - tarOptions.Files = []string{"teleport/teleport", "teleport/tctl", "teleport/tsh"} - } - - log.Debugf("Extracting Teleport binaries into %s", outExtractDir) - - if err := os.MkdirAll(outExtractDir, 0755); err != nil { - return BinPaths{}, trace.NewAggregate(err, tarFile.Close()) - } - if err := trace.NewAggregate(tar.Extract(tarFile, tarOptions), tarFile.Close()); err != nil { - return BinPaths{}, trace.Wrap(err) - } - - return BinPaths{ - Teleport: tarOptions.OutFiles[tarOptions.Files[0]], - Tctl: tarOptions.OutFiles[tarOptions.Files[1]], - Tsh: tarOptions.OutFiles[tarOptions.Files[2]], - }, nil -} - -func fileExists(path string) bool { - info, err := os.Stat(path) - return err == nil && !info.IsDir() -} diff --git a/lib/testing/integration/download_sha.dsv b/lib/testing/integration/download_sha.dsv deleted file mode 100644 index fe322b350..000000000 --- a/lib/testing/integration/download_sha.dsv +++ /dev/null @@ -1,8 +0,0 @@ -e938b619c51c814c279c3d0ff8dad4444fd73e0e46c706e8ca0ee0599e1ac5e0 teleport-ent-v12.1.0-darwin-amd64-bin.tar.gz -7f1963f90990ba23d45a26eca8a897d48b20f51ccba25d55ea8ca9ec84417d89 teleport-ent-v12.1.0-linux-amd64-bin.tar.gz -540cce5aa113274843dcc0cb2567861fb91b02d96835df9cf04bd0c5c85498ba teleport-ent-v12.1.0-linux-arm64-bin.tar.gz -d6e355fdef4cfea338de74fe5f8203805b8209d0a174c0c471de2a1d444f370f teleport-ent-v12.1.0-linux-arm-bin.tar.gz -3d8dd1ef77691a7dccdcdc197f8c6470a8e73afd778b23a8726f2d1c964b3508 teleport-v12.1.0-darwin-amd64-bin.tar.gz -f13c96d2f49b43953e36826ee20fbd99623d10ea096170780a82f8d71cb78364 teleport-v12.1.0-linux-amd64-bin.tar.gz -e7a4b02548eefd532eb70f7468eb0bfd53313595236356d3e5b5c54f14cb21a5 teleport-v12.1.0-linux-arm64-bin.tar.gz -7733117c2c9c6ed869f89a2199b50965f1300268c76c4c9cd764c95fabf9bb96 teleport-v12.1.0-linux-arm-bin.tar.gz diff --git a/lib/testing/integration/download_sha.dsv_1204 b/lib/testing/integration/download_sha.dsv_1204 deleted file mode 100644 index 37060d0ce..000000000 --- a/lib/testing/integration/download_sha.dsv_1204 +++ /dev/null @@ -1,8 +0,0 @@ -54d13112aad1a16a73aab3725d313b2a36c8938ef76ab44c8ffbfee416bc91c2 teleport-ent-v12.0.4-darwin-amd64-bin.tar.gz -b27c3e16ce264e33feabda7e22eaa7917f585c28bf2eb31f60944f9e961aa7a8 teleport-ent-v12.0.4-linux-amd64-bin.tar.gz -b205b832c1d41f6624fd45a49f323eb037de68c540f76929dbeb2fc646ff2071 teleport-ent-v12.0.4-linux-arm64-bin.tar.gz -ab739dba7e6f920baab981fc3ff1e05fff1c38de3d7ffff0f8627b8f712822af teleport-ent-v12.0.4-linux-arm-bin.tar.gz -11382b897028f1a231de258ec74590dd25002ccf5ee47e7679999e5ea1d5717c teleport-v12.0.4-darwin-amd64-bin.tar.gz -84ce1cd7297499e6b52acf63b1334890abc39c926c7fc2d0fe676103d200752a teleport-v12.0.4-linux-amd64-bin.tar.gz -148f8a99d96c50fa34b5c72c32ff40387e55f8df3e8461fb0ca9a61f2542069a teleport-v12.0.4-linux-arm64-bin.tar.gz -b5a0694d4fa32a2a54fde94edd59932df3e717efd15322c846a2e4ed81dbaca4 teleport-v12.0.4-linux-arm-bin.tar.gz diff --git a/lib/testing/integration/download_test.go b/lib/testing/integration/download_test.go deleted file mode 100644 index 84b5cc237..000000000 --- a/lib/testing/integration/download_test.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2023 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 integration - -import ( - "context" - _ "embed" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/gravitational/teleport-plugins/lib" -) - -//go:embed download_sha.dsv_1204 -var downloadVersionsDSV1204 string - -func TestDownloadVersionsHash(t *testing.T) { - ctx := context.Background() - dv, ok := downloadVersionsHash(ctx, downloadVersionsDSV1204, downloadVersionKey{ - ver: "v12.0.4", - os: "linux", - arch: "amd64", - enterprise: false, - }) - require.True(t, ok, "expected to find hash for key, but didn't") - require.Equal(t, dv.sha256, lib.MustHexSHA256("84ce1cd7297499e6b52acf63b1334890abc39c926c7fc2d0fe676103d200752a")) - - dv, ok = downloadVersionsHash(ctx, downloadVersionsDSV1204, downloadVersionKey{ - ver: "v12.0.4", - os: "linux", - arch: "amd64", - enterprise: true, - }) - require.True(t, ok, "expected to find hash for key, but didn't") - require.Equal(t, dv.sha256, lib.MustHexSHA256("b27c3e16ce264e33feabda7e22eaa7917f585c28bf2eb31f60944f9e961aa7a8")) -} diff --git a/lib/testing/integration/fileconfig.go b/lib/testing/integration/fileconfig.go deleted file mode 100644 index edd41679d..000000000 --- a/lib/testing/integration/fileconfig.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -const teleportAuthYAML = ` -teleport: - data_dir: {{TELEPORT_DATA_DIR}} - auth_token: {{TELEPORT_AUTH_TOKEN}} - cache: - enabled: {{TELEPORT_CACHE_ENABLED}} - log: - output: stdout - -auth_service: - license_file: {{TELEPORT_LICENSE_FILE}} - cluster_name: local-site - enabled: true - listen_addr: 127.0.0.1:0 - public_addr: localhost - tokens: - - node,auth,proxy,app:{{TELEPORT_AUTH_TOKEN}} - authentication: - type: local - -proxy_service: - enabled: false - -ssh_service: - enabled: false -` - -const teleportProxyYAML = ` -teleport: - data_dir: {{TELEPORT_DATA_DIR}} - auth_servers: ['{{TELEPORT_AUTH_SERVER}}'] - auth_token: '{{TELEPORT_AUTH_TOKEN}}' - ca_pin: '{{TELEPORT_AUTH_CA_PIN}}' - cache: - enabled: false - log: - output: stdout - -auth_service: - enabled: false - -proxy_service: - enabled: true - tunnel_public_addr: localhost:{{PROXY_TUN_LISTEN_PORT}} - listen_addr: 127.0.0.1:0 - web_listen_addr: {{PROXY_WEB_LISTEN_ADDR}} - tunnel_listen_addr: {{PROXY_TUN_LISTEN_ADDR}} - -ssh_service: - enabled: false -` - -const teleportSSHYAML = ` -teleport: - data_dir: {{TELEPORT_DATA_DIR}} - auth_servers: ['{{TELEPORT_AUTH_SERVER}}'] - auth_token: '{{TELEPORT_AUTH_TOKEN}}' - ca_pin: '{{TELEPORT_AUTH_CA_PIN}}' - cache: - enabled: false - log: - output: stdout - -auth_service: - enabled: false - -proxy_service: - enabled: false - -ssh_service: - enabled: true - listen_addr: {{SSH_LISTEN_ADDR}} - public_addr: localhost:{{SSH_LISTEN_PORT}} -` diff --git a/lib/testing/integration/integration.go b/lib/testing/integration/integration.go deleted file mode 100644 index 5e6b37ff8..000000000 --- a/lib/testing/integration/integration.go +++ /dev/null @@ -1,586 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "context" - "crypto/rand" - "encoding/hex" - "fmt" - "net" - "os" - "os/exec" - "path" - "regexp" - "runtime" - "strings" - "sync" - "time" - - "github.com/gravitational/teleport/api/client" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/utils" - "github.com/gravitational/trace" - "github.com/hashicorp/go-version" - "google.golang.org/grpc" - - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/tctl" - "github.com/gravitational/teleport-plugins/lib/tsh" -) - -const IntegrationAdminRole = "integration-admin" -const DefaultLicensePath = "/var/lib/teleport/license.pem" - -var regexpVersion = regexp.MustCompile(`^Teleport( Enterprise)? ([^ ]+)`) - -type Integration struct { - mu sync.Mutex - paths struct { - BinPaths - license string - } - workDir string - cleanup []func() error - version Version - token string - caPin string -} - -type BinPaths struct { - Teleport string - Tctl string - Tsh string -} - -type Addr struct { - Host string - Port string -} - -type Auth interface { - AuthAddr() Addr -} - -type Service interface { - Run(context.Context) error - WaitReady(ctx context.Context) (bool, error) - Err() error - Shutdown(context.Context) error -} - -type Version struct { - *version.Version - IsEnterprise bool -} - -type SignTLSPaths struct { - CertPath string - KeyPath string - RootCAPath string -} - -const serviceShutdownTimeout = 10 * time.Second - -// New initializes a Teleport installation. -func New(ctx context.Context, paths BinPaths, licenseStr string) (*Integration, error) { - var err error - log := logger.Get(ctx) - - var integration Integration - integration.paths.BinPaths = paths - initialized := false - defer func() { - if !initialized { - integration.Close() - } - }() - - integration.workDir, err = os.MkdirTemp("", "teleport-plugins-integration-*") - if err != nil { - return nil, trace.Wrap(err, "failed to initialize work directory") - } - integration.registerCleanup(func() error { return os.RemoveAll(integration.workDir) }) - - teleportVersion, err := getBinaryVersion(ctx, integration.paths.Teleport) - if err != nil { - return nil, trace.Wrap(err, "failed to get teleport version") - } - - tctlVersion, err := getBinaryVersion(ctx, integration.paths.Tctl) - if err != nil { - return nil, trace.Wrap(err, "failed to get tctl version") - } - if !teleportVersion.Equal(tctlVersion.Version) { - return nil, trace.Wrap(err, "teleport version %s does not match tctl version %s", teleportVersion.Version, tctlVersion.Version) - } - - tshVersion, err := getBinaryVersion(ctx, integration.paths.Tsh) - if err != nil { - return nil, trace.Wrap(err, "failed to get tsh version") - } - if !teleportVersion.Equal(tshVersion.Version) { - return nil, trace.Wrap(err, "teleport version %s does not match tsh version %s", teleportVersion.Version, tshVersion.Version) - } - - if teleportVersion.IsEnterprise { - if licenseStr == "" { - return nil, trace.Errorf("%s appears to be an Enterprise binary but license path is not specified", integration.paths.Teleport) - } - if strings.HasPrefix(licenseStr, "-----BEGIN CERTIFICATE-----") || strings.Contains(licenseStr, "\n") { - // If it looks like a license file content lets write it to temporary file. - log.Debug("License is given as a string, writing it to a file") - licenseFile, err := integration.tempFile("license-*.pem") - if err != nil { - return nil, trace.Wrap(err, "failed to write license file") - } - if _, err := licenseFile.WriteString(licenseStr); err != nil { - return nil, trace.Wrap(err, "failed to write license file") - } - if err := licenseFile.Close(); err != nil { - return nil, trace.Wrap(err, "failed to write license file") - } - integration.paths.license = licenseFile.Name() - } else if licenseStr != "" { - integration.paths.license = licenseStr - if !fileExists(integration.paths.license) { - return nil, trace.NotFound("license file not found") - } - } - } - - integration.version = teleportVersion - - tokenBytes := make([]byte, 16) - _, err = rand.Read(tokenBytes) - if err != nil { - return nil, trace.Wrap(err) - } - integration.token = hex.EncodeToString(tokenBytes) - - initialized = true - return &integration, nil -} - -// NewFromEnv initializes Teleport installation reading binary paths from environment variables such as -// TELEPORT_BINARY, TELEPORT_BINARY_TCTL or just PATH. -func NewFromEnv(ctx context.Context) (*Integration, error) { - var err error - - licenseStr, ok := os.LookupEnv("TELEPORT_ENTERPRISE_LICENSE") - if !ok && fileExists(DefaultLicensePath) { - licenseStr = DefaultLicensePath - } - - var paths BinPaths - - if os.Getenv("CI") != "" { - if licenseStr == "" { - return nil, trace.AccessDenied("tests on CI should run with enterprise license") - } - } - - if version := os.Getenv("TELEPORT_GET_VERSION"); version == "" { - paths = BinPaths{ - Teleport: os.Getenv("TELEPORT_BINARY"), - Tctl: os.Getenv("TELEPORT_BINARY_TCTL"), - Tsh: os.Getenv("TELEPORT_BINARY_TSH"), - } - - // Look up binaries either in file system or in PATH. - - if paths.Teleport == "" { - paths.Teleport = "teleport" - } - if paths.Teleport, err = exec.LookPath(paths.Teleport); err != nil { - return nil, trace.Wrap(err) - } - - if paths.Tctl == "" { - paths.Tctl = "tctl" - } - if paths.Tctl, err = exec.LookPath(paths.Tctl); err != nil { - return nil, trace.Wrap(err) - } - - if paths.Tsh == "" { - paths.Tsh = "tsh" - } - if paths.Tsh, err = exec.LookPath(paths.Tsh); err != nil { - return nil, trace.Wrap(err) - } - } else { - _, goFile, _, ok := runtime.Caller(0) - if !ok { - return nil, trace.Errorf("failed to get caller information") - } - outDir := path.Join(path.Dir(goFile), "..", "..", "..", ".teleport") // subdir in repo root - if licenseStr != "" { - paths, err = GetEnterprise(ctx, version, outDir) - if err != nil { - return nil, trace.Wrap(err) - } - } else { - paths, err = GetOSS(ctx, version, outDir) - if err != nil { - return nil, trace.Wrap(err) - } - } - } - - return New(ctx, paths, licenseStr) -} - -// Close stops all the spawned processes and does a cleanup. -func (integration *Integration) Close() { - integration.mu.Lock() - cleanup := integration.cleanup - integration.cleanup = nil - integration.mu.Unlock() - - for idx := range cleanup { - if err := cleanup[len(cleanup)-idx-1](); err != nil { - logger.Standard().WithError(trace.Wrap(err)).Error("Cleanup operation failed") - } - } -} - -// Version returns an auth server version. -func (integration *Integration) Version() Version { - return integration.version -} - -type AuthServiceOption func(yaml string) string - -func WithCache() AuthServiceOption { - return func(yaml string) string { - return strings.ReplaceAll(yaml, "{{TELEPORT_CACHE_ENABLED}}", "true") - } -} - -// NewAuthService creates a new auth server instance. -func (integration *Integration) NewAuthService(opts ...AuthServiceOption) (*AuthService, error) { - dataDir, err := integration.tempDir("data-auth-*") - if err != nil { - return nil, trace.Wrap(err, "failed to initialize data directory") - } - - configFile, err := integration.tempFile("teleport-auth-*.yaml") - if err != nil { - return nil, trace.Wrap(err, "failed to write config file") - } - - yaml := teleportAuthYAML - for _, o := range opts { - yaml = o(yaml) - } - - yaml = strings.ReplaceAll(yaml, "{{TELEPORT_DATA_DIR}}", dataDir) - yaml = strings.ReplaceAll(yaml, "{{TELEPORT_CACHE_ENABLED}}", "false") - yaml = strings.ReplaceAll(yaml, "{{TELEPORT_LICENSE_FILE}}", integration.paths.license) - yaml = strings.ReplaceAll(yaml, "{{TELEPORT_AUTH_TOKEN}}", integration.token) - if _, err := configFile.WriteString(yaml); err != nil { - return nil, trace.Wrap(err, "failed to write config file") - } - if err := configFile.Close(); err != nil { - return nil, trace.Wrap(err, "failed to write config file") - } - - auth := newAuthService(integration.paths.Teleport, configFile.Name()) - integration.registerService(auth) - - return auth, nil -} - -// NewProxyService creates a new auth server instance. -func (integration *Integration) NewProxyService(auth Auth) (*ProxyService, error) { - dataDir, err := integration.tempDir("data-proxy-*") - if err != nil { - return nil, trace.Wrap(err, "failed to initialize data directory") - } - - configFile, err := integration.tempFile("teleport-proxy-*.yaml") - if err != nil { - return nil, trace.Wrap(err, "failed to write config file") - } - - yaml := strings.ReplaceAll(teleportProxyYAML, "{{TELEPORT_DATA_DIR}}", dataDir) - yaml = strings.ReplaceAll(yaml, "{{TELEPORT_AUTH_SERVER}}", auth.AuthAddr().String()) - yaml = strings.ReplaceAll(yaml, "{{TELEPORT_AUTH_TOKEN}}", integration.token) - yaml = strings.ReplaceAll(yaml, "{{TELEPORT_AUTH_CA_PIN}}", integration.caPin) - webListenAddr, err := getFreeTCPPort() - if err != nil { - return nil, trace.Wrap(err) - } - yaml = strings.ReplaceAll(yaml, "{{PROXY_WEB_LISTEN_ADDR}}", webListenAddr.String()) - yaml = strings.ReplaceAll(yaml, "{{PROXY_WEB_LISTEN_PORT}}", webListenAddr.Port) - tunListenAddr, err := getFreeTCPPort() - if err != nil { - return nil, trace.Wrap(err) - } - yaml = strings.ReplaceAll(yaml, "{{PROXY_TUN_LISTEN_ADDR}}", tunListenAddr.String()) - yaml = strings.ReplaceAll(yaml, "{{PROXY_TUN_LISTEN_PORT}}", tunListenAddr.Port) - - if _, err := configFile.WriteString(yaml); err != nil { - return nil, trace.Wrap(err, "failed to write config file") - } - if err := configFile.Close(); err != nil { - return nil, trace.Wrap(err, "failed to write config file") - } - - proxy := newProxyService(integration.paths.Teleport, configFile.Name()) - integration.registerService(proxy) - return proxy, nil -} - -// NewSSHService creates a new auth server instance. -func (integration *Integration) NewSSHService(auth Auth) (*SSHService, error) { - dataDir, err := integration.tempDir("data-ssh-*") - if err != nil { - return nil, trace.Wrap(err, "failed to initialize data directory") - } - - configFile, err := integration.tempFile("teleport-ssh-*.yaml") - if err != nil { - return nil, trace.Wrap(err, "failed to write config file") - } - yaml := strings.ReplaceAll(teleportSSHYAML, "{{TELEPORT_DATA_DIR}}", dataDir) - yaml = strings.ReplaceAll(yaml, "{{TELEPORT_AUTH_SERVER}}", auth.AuthAddr().String()) - yaml = strings.ReplaceAll(yaml, "{{TELEPORT_AUTH_TOKEN}}", integration.token) - yaml = strings.ReplaceAll(yaml, "{{TELEPORT_AUTH_CA_PIN}}", integration.caPin) - sshListenAddr, err := getFreeTCPPort() - if err != nil { - return nil, trace.Wrap(err) - } - yaml = strings.ReplaceAll(yaml, "{{SSH_LISTEN_ADDR}}", sshListenAddr.String()) - yaml = strings.ReplaceAll(yaml, "{{SSH_LISTEN_PORT}}", sshListenAddr.Port) - - if _, err := configFile.WriteString(yaml); err != nil { - return nil, trace.Wrap(err, "failed to write config file") - } - if err := configFile.Close(); err != nil { - return nil, trace.Wrap(err, "failed to write config file") - } - - ssh := newSSHService(integration.paths.Teleport, configFile.Name()) - integration.registerService(ssh) - return ssh, nil -} - -func (integration *Integration) Bootstrap(ctx context.Context, auth *AuthService, resources []types.Resource) error { - return integration.tctl(auth).Create(ctx, resources) -} - -// NewClient builds an API client for a given user. -func (integration *Integration) NewClient(ctx context.Context, auth *AuthService, userName string) (*Client, error) { - outPath, err := integration.Sign(ctx, auth, userName) - if err != nil { - return nil, trace.Wrap(err) - } - return integration.NewSignedClient(ctx, auth, outPath, userName) -} - -// NewSignedClient builds a client for a given user given the identity file. -func (integration *Integration) NewSignedClient(ctx context.Context, auth Auth, identityPath, userName string) (*Client, error) { - apiClient, err := client.New(ctx, client.Config{ - InsecureAddressDiscovery: true, - Addrs: []string{auth.AuthAddr().String()}, - Credentials: []client.Credentials{client.LoadIdentityFile(identityPath)}, - DialOpts: []grpc.DialOption{ - grpc.WithReturnConnectionError(), - }, - }) - if err != nil { - return nil, trace.Wrap(err) - } - client := &Client{Client: apiClient} - integration.registerCleanup(client.Close) - return client, nil -} - -func (integration *Integration) MakeAdmin(ctx context.Context, auth *AuthService, userName string) (*Client, error) { - var bootstrap Bootstrap - if _, err := bootstrap.AddRole(IntegrationAdminRole, types.RoleSpecV6{ - Allow: types.RoleConditions{ - NodeLabels: types.Labels{types.Wildcard: utils.Strings{types.Wildcard}}, - Rules: []types.Rule{ - { - Resources: []string{"*"}, - Verbs: []string{"*"}, - }, - }, - }, - }); err != nil { - return nil, trace.Wrap(err, fmt.Sprintf("failed to initialize %s role", IntegrationAdminRole)) - } - if _, err := bootstrap.AddUserWithRoles(userName, IntegrationAdminRole); err != nil { - return nil, trace.Wrap(err, fmt.Sprintf("failed to initialize %s user", userName)) - } - if err := integration.Bootstrap(ctx, auth, bootstrap.Resources()); err != nil { - return nil, trace.Wrap(err, fmt.Sprintf("failed to bootstrap admin user %s", userName)) - } - return integration.NewClient(ctx, auth, userName) -} - -// Sign generates a credentials file for the user and returns an identity file path. -func (integration *Integration) Sign(ctx context.Context, auth *AuthService, userName string) (string, error) { - outFile, err := integration.tempFile(fmt.Sprintf("credentials-%s-*", userName)) - if err != nil { - return "", trace.Wrap(err) - } - if err := outFile.Close(); err != nil { - return "", trace.Wrap(err) - } - outPath := outFile.Name() - if err := integration.tctl(auth).Sign(ctx, userName, "file", outPath); err != nil { - return "", trace.Wrap(err) - } - return outPath, nil -} - -// SignTLS generates a set of files to be used for generating the TLS Config: Cert, Key and RootCAs -func (integration *Integration) SignTLS(ctx context.Context, auth *AuthService, userName string) (*SignTLSPaths, error) { - outFile, err := integration.tempFile(fmt.Sprintf("credentials-%s-*", userName)) - if err != nil { - return nil, trace.Wrap(err) - } - if err := outFile.Close(); err != nil { - return nil, trace.Wrap(err) - } - outPath := outFile.Name() - if err := integration.tctl(auth).Sign(ctx, userName, "tls", outPath); err != nil { - return nil, trace.Wrap(err) - } - - return &SignTLSPaths{ - CertPath: outPath + ".crt", - KeyPath: outPath + ".key", - RootCAPath: outPath + ".cas", - }, nil -} - -// SetCAPin sets integration with the auth service's CA Pin. -func (integration *Integration) SetCAPin(ctx context.Context, auth *AuthService) error { - if integration.caPin != "" { - return nil - } - - if ready, err := auth.WaitReady(ctx); err != nil { - return trace.Wrap(err) - } else if !ready { - return trace.Wrap(auth.Err()) - } - - caPin, err := integration.tctl(auth).GetCAPin(ctx) - if err != nil { - return trace.Wrap(err) - } - - integration.caPin = caPin - return nil -} - -// NewTsh makes a new tsh runner. -func (integration *Integration) NewTsh(proxyAddr, identityPath string) tsh.Tsh { - return tsh.Tsh{ - Path: integration.paths.Tsh, - Proxy: proxyAddr, - Identity: identityPath, - Insecure: true, - } -} - -func getBinaryVersion(ctx context.Context, binaryPath string) (Version, error) { - cmd := exec.CommandContext(ctx, binaryPath, "version") - logger.Get(ctx).Debugf("Running %s", cmd) - out, err := cmd.Output() - if err != nil { - return Version{}, trace.Wrap(err) - } - submatch := regexpVersion.FindStringSubmatch(string(out)) - if submatch == nil { - return Version{}, trace.Wrap(err) - } - - version, err := version.NewVersion(submatch[2]) - if err != nil { - return Version{}, trace.Wrap(err) - } - - return Version{Version: version, IsEnterprise: submatch[1] != ""}, nil -} - -func (integration *Integration) tctl(auth *AuthService) tctl.Tctl { - return tctl.Tctl{ - Path: integration.paths.Tctl, - AuthServer: auth.AuthAddr().String(), - ConfigPath: auth.ConfigPath(), - } -} - -func (integration *Integration) registerCleanup(fn func() error) { - integration.mu.Lock() - defer integration.mu.Unlock() - integration.cleanup = append(integration.cleanup, fn) -} - -func (integration *Integration) registerService(service Service) { - integration.registerCleanup(func() error { - ctx, cancel := context.WithTimeout(context.Background(), serviceShutdownTimeout+10*time.Millisecond) - defer cancel() - return service.Shutdown(ctx) - }) -} - -func (integration *Integration) tempFile(pattern string) (*os.File, error) { - file, err := os.CreateTemp(integration.workDir, pattern) - if err != nil { - return nil, trace.Wrap(err) - } - integration.registerCleanup(func() error { return os.Remove(file.Name()) }) - return file, trace.Wrap(err) -} - -func (integration *Integration) tempDir(pattern string) (string, error) { - dir, err := os.MkdirTemp(integration.workDir, pattern) - if err != nil { - return "", trace.Wrap(err) - } - integration.registerCleanup(func() error { return os.RemoveAll(dir) }) - return dir, nil -} - -func getFreeTCPPort() (Addr, error) { - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return Addr{}, trace.Wrap(err) - } - if err := listener.Close(); err != nil { - return Addr{}, trace.Wrap(err) - } - addrStr := listener.Addr().String() - parts := strings.SplitN(addrStr, ":", 2) - return Addr{Host: parts[0], Port: parts[1]}, nil -} - -func (addr Addr) IsEmpty() bool { - return addr.Host == "" && addr.Port == "" -} - -func (addr Addr) String() string { - return fmt.Sprintf("%s:%s", addr.Host, addr.Port) -} diff --git a/lib/testing/integration/integration_test.go b/lib/testing/integration/integration_test.go deleted file mode 100644 index 0668f6c26..000000000 --- a/lib/testing/integration/integration_test.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "testing" - - "github.com/hashicorp/go-version" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -type IntegrationSuite struct { - BaseSetup -} - -func TestIntegration(t *testing.T) { suite.Run(t, &IntegrationSuite{}) } - -func (s *IntegrationSuite) SetupTest() { - s.BaseSetup.SetupService() -} - -func (s *IntegrationSuite) TestVersion() { - t := s.T() - - versionMin, err := version.NewVersion("v11.0.0") - require.NoError(t, err) - versionMax, err := version.NewVersion("v13") - require.NoError(t, err) - - assert.True(t, s.Integration.Version().GreaterThanOrEqual(versionMin)) - assert.True(t, s.Integration.Version().LessThan(versionMax)) -} diff --git a/lib/testing/integration/proxy_test.go b/lib/testing/integration/proxy_test.go deleted file mode 100644 index 84e2f0ed2..000000000 --- a/lib/testing/integration/proxy_test.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -type IntegrationProxySuite struct { - ProxySetup -} - -func TestIntegrationProxy(t *testing.T) { suite.Run(t, &IntegrationProxySuite{}) } - -func (s *IntegrationProxySuite) SetupTest() { - s.ProxySetup.SetupService() -} - -func (s *IntegrationProxySuite) TestPing() { - t := s.T() - - var bootstrap Bootstrap - user, err := bootstrap.AddUserWithRoles("vladimir", "editor") - require.NoError(t, err) - err = s.Integration.Bootstrap(s.Context(), s.Auth, bootstrap.Resources()) - require.NoError(t, err) - - identity, err := s.Integration.Sign(s.Context(), s.Auth, user.GetName()) - require.NoError(t, err) - - client, err := s.Integration.NewSignedClient(s.Context(), s.Proxy, identity, user.GetName()) - require.NoError(t, err) - t.Cleanup(func() { _ = client.Close() }) - _, err = client.Ping(s.Context()) - require.NoError(t, err) -} diff --git a/lib/testing/integration/proxyservice.go b/lib/testing/integration/proxyservice.go deleted file mode 100644 index bab88e8cd..000000000 --- a/lib/testing/integration/proxyservice.go +++ /dev/null @@ -1,349 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "bufio" - "bytes" - "context" - "fmt" - "io" - "os/exec" - "regexp" - "strings" - "sync" - "syscall" - "time" - - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib/logger" -) - -var regexpWebProxyStarting = regexp.MustCompile(`Web proxy service [^ ]+ is starting on [^ ]+:(\d+)`) -var regexpSSHProxyStarting = regexp.MustCompile(`SSH proxy service [^ ]+ is starting on [^ ]+:(\d+)`) -var regexpReverseTunnelStarting = regexp.MustCompile(`Reverse tunnel service [^ ]+ is starting on [^ ]+:(\d+)`) - -type ProxyService struct { - mu sync.Mutex - teleportPath string - configPath string - webProxyAddr Addr - sshProxyAddr Addr - reverseTunnelAddr Addr - isReady bool - readyCh chan struct{} - doneCh chan struct{} - terminate context.CancelFunc - setErr func(error) - setReady func(bool) - error error - stdout strings.Builder - stderr bytes.Buffer -} - -func newProxyService(teleportPath, configPath string) *ProxyService { - var proxy ProxyService - var setErrOnce, setReadyOnce sync.Once - readyCh := make(chan struct{}) - proxy = ProxyService{ - teleportPath: teleportPath, - configPath: configPath, - readyCh: readyCh, - doneCh: make(chan struct{}), - terminate: func() {}, // dummy noop that will be overridden by Run(), - setErr: func(err error) { - setErrOnce.Do(func() { - proxy.mu.Lock() - defer proxy.mu.Unlock() - proxy.error = err - }) - }, - setReady: func(isReady bool) { - setReadyOnce.Do(func() { - proxy.mu.Lock() - proxy.isReady = isReady - proxy.mu.Unlock() - close(readyCh) - }) - }, - } - return &proxy -} - -// Run spawns an proxy service instance. -func (proxy *ProxyService) Run(ctx context.Context) error { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - log := logger.Get(ctx) - - cmd := exec.CommandContext(ctx, proxy.teleportPath, "start", "--debug", "--config", proxy.configPath) - log.Debugf("Running Proxy service: %s", cmd) - - stdoutPipe, err := cmd.StdoutPipe() - if err != nil { - err = trace.Wrap(err, "failed to get stdout") - proxy.setErr(err) - return err - } - - stderrPipe, err := cmd.StderrPipe() - if err != nil { - err = trace.Wrap(err, "failed to get stderr") - proxy.setErr(err) - return err - } - - if err := cmd.Start(); err != nil { - err = trace.Wrap(err, "failed to start teleport") - proxy.setErr(err) - return err - } - - ctx, log = logger.WithField(ctx, "pid", cmd.Process.Pid) - log.Debug("Proxy service process has been started") - - proxy.mu.Lock() - var terminateOnce sync.Once - proxy.terminate = func() { - terminateOnce.Do(func() { - log.Debug("Terminating Proxy service process") - // Signal the process to gracefully terminate by sending SIGQUIT. - if err := cmd.Process.Signal(syscall.SIGQUIT); err != nil { - log.Warn(err) - } - // If we're not done in 5 minutes, just kill the process by canceling its context. - go func() { - select { - case <-proxy.doneCh: - case <-time.After(serviceShutdownTimeout): - log.Debug("Killing Proxy service process") - } - // cancel() results in sending SIGKILL to a process if it's still alive. - cancel() - }() - }) - } - proxy.mu.Unlock() - - var ioWork sync.WaitGroup - ioWork.Add(2) - - // Parse stdout of a Teleport process. - go func() { - defer ioWork.Done() - - stdout := bufio.NewReader(stdoutPipe) - for { - line, err := stdout.ReadString('\n') - if err == io.EOF { - return - } - if err := trace.Wrap(err); err != nil { - log.WithError(err).Error("failed to read process stdout") - return - } - - proxy.saveStdout(line) - - if proxy.IsReady() { - continue - } - - proxy.parseLine(ctx, line) - - if strings.Contains(line, "List of known proxies updated:") { - log.WithFields(logger.Fields{ - "addr_web": proxy.webProxyAddr, - "addr_ssh": proxy.sshProxyAddr, - "addr_tun": proxy.reverseTunnelAddr, - }).Debugf("Found all addrs of Proxy service process") - proxy.setReady(true) - } - } - }() - - // Save stderr to a buffer. - go func() { - defer ioWork.Done() - - stderr := bufio.NewReader(stderrPipe) - data := make([]byte, stderr.Size()) - for { - n, err := stderr.Read(data) - proxy.saveStderr(data[:n]) - if err == io.EOF { - return - } - if err := trace.Wrap(err); err != nil { - log.WithError(err).Error("failed to read process stderr") - return - } - } - }() - - // Wait for process completeness after processing both outputs. - go func() { - ioWork.Wait() - err := trace.Wrap(cmd.Wait()) - proxy.setErr(err) - close(proxy.doneCh) - }() - - <-proxy.doneCh - - if !proxy.IsReady() { - log.Error("Proxy service is failed to initialize") - stdoutLines := strings.Split(proxy.Stdout(), "\n") - for _, line := range stdoutLines[len(stdoutLines)-10:] { - log.Debug("Proxy service log: ", line) - } - log.Debugf("Proxy service stderr: %q", proxy.Stderr()) - - // If it's still not ready lets signal that it's finally not ready. - proxy.setReady(false) - // Set an err just in case if it's not set before. - proxy.setErr(trace.Errorf("failed to initialize")) - } - - return trace.Wrap(proxy.Err()) -} - -// AuthAddr returns auth service external address. -func (proxy *ProxyService) AuthAddr() Addr { - return proxy.WebProxyAddr() -} - -// WebProxyAddr returns Web Proxy external address. -func (proxy *ProxyService) WebProxyAddr() Addr { - proxy.mu.Lock() - defer proxy.mu.Unlock() - return proxy.webProxyAddr -} - -// SSHProxyAddr returns SSH Proxy external address. -func (proxy *ProxyService) SSHProxyAddr() Addr { - proxy.mu.Lock() - defer proxy.mu.Unlock() - return proxy.sshProxyAddr -} - -// ReverseTunnelAddr returns reverse tunnel external address. -func (proxy *ProxyService) ReverseTunnelAddr() Addr { - proxy.mu.Lock() - defer proxy.mu.Unlock() - return proxy.reverseTunnelAddr -} - -// WebAndSSHProxyAddr returns string in a format "host:webport,sshport" needed as tsh --proxy option. -func (proxy *ProxyService) WebAndSSHProxyAddr() string { - proxy.mu.Lock() - defer proxy.mu.Unlock() - return fmt.Sprintf("%s:%s,%s", proxy.webProxyAddr.Host, proxy.webProxyAddr.Port, proxy.sshProxyAddr.Port) -} - -// Err returns proxy service error. It's nil If process is not done yet. -func (proxy *ProxyService) Err() error { - proxy.mu.Lock() - defer proxy.mu.Unlock() - return proxy.error -} - -// Shutdown terminates the proxy service process and waits for its completion. -func (proxy *ProxyService) Shutdown(ctx context.Context) error { - proxy.doTerminate() - select { - case <-proxy.doneCh: - return nil - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } -} - -// Stdout returns a collected proxy service process stdout. -func (proxy *ProxyService) Stdout() string { - proxy.mu.Lock() - defer proxy.mu.Unlock() - return proxy.stdout.String() -} - -// Stderr returns a collected proxy service process stderr. -func (proxy *ProxyService) Stderr() string { - proxy.mu.Lock() - defer proxy.mu.Unlock() - return proxy.stderr.String() -} - -// WaitReady waits for proxy service initialization. -func (proxy *ProxyService) WaitReady(ctx context.Context) (bool, error) { - select { - case <-proxy.readyCh: - return proxy.IsReady(), nil - case <-ctx.Done(): - return false, trace.Wrap(ctx.Err(), "proxy service is not ready") - } -} - -// IsReady indicates if proxy service is initialized properly. -func (proxy *ProxyService) IsReady() bool { - proxy.mu.Lock() - defer proxy.mu.Unlock() - return proxy.isReady -} - -func (proxy *ProxyService) doTerminate() { - proxy.mu.Lock() - terminate := proxy.terminate - proxy.mu.Unlock() - terminate() -} - -func (proxy *ProxyService) parseLine(ctx context.Context, line string) { - if submatch := regexpWebProxyStarting.FindStringSubmatch(line); submatch != nil { - proxy.mu.Lock() - defer proxy.mu.Unlock() - proxy.webProxyAddr = Addr{Host: "localhost", Port: submatch[1]} - return - } - - if submatch := regexpSSHProxyStarting.FindStringSubmatch(line); submatch != nil { - proxy.mu.Lock() - defer proxy.mu.Unlock() - proxy.sshProxyAddr = Addr{Host: "127.0.0.1", Port: submatch[1]} - return - } - - if submatch := regexpReverseTunnelStarting.FindStringSubmatch(line); submatch != nil { - proxy.mu.Lock() - defer proxy.mu.Unlock() - proxy.reverseTunnelAddr = Addr{Host: "localhost", Port: submatch[1]} - return - } -} - -func (proxy *ProxyService) saveStdout(line string) { - proxy.mu.Lock() - defer proxy.mu.Unlock() - proxy.stdout.WriteString(line) -} - -func (proxy *ProxyService) saveStderr(chunk []byte) { - proxy.mu.Lock() - defer proxy.mu.Unlock() - proxy.stderr.Write(chunk) -} diff --git a/lib/testing/integration/setup.go b/lib/testing/integration/setup.go deleted file mode 100644 index 631deed42..000000000 --- a/lib/testing/integration/setup.go +++ /dev/null @@ -1,129 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/gravitational/teleport-plugins/lib/logger" -) - -type BaseSetup struct { - Suite - Integration *Integration -} - -type AuthSetup struct { - BaseSetup - Auth *AuthService - CacheEnabled bool -} - -type ProxySetup struct { - AuthSetup - Proxy *ProxyService -} - -type SSHSetup struct { - ProxySetup - SSH *SSHService -} - -func (s *BaseSetup) SetupSuite(t *testing.T) { - logger.Init() - err := logger.Setup(logger.Config{Severity: "debug"}) - require.NoError(t, err) -} - -func (s *BaseSetup) SetupService() { - t := s.T() - var err error - - // We set such a big timeout because integration.NewFromEnv could start - // downloading a Teleport *-bin.tar.gz file which can take a long time. - ctx := s.SetContextTimeout(5 * time.Minute) - integration, err := NewFromEnv(ctx) - require.NoError(t, err) - t.Cleanup(integration.Close) - s.Integration = integration -} - -func (s *AuthSetup) SetupSuite(t *testing.T) { - s.CacheEnabled = false - s.BaseSetup.SetupSuite(t) -} - -func (s *AuthSetup) SetupService(authServiceOptions ...AuthServiceOption) { - s.BaseSetup.SetupService() - t := s.T() - auth, err := s.Integration.NewAuthService(authServiceOptions...) - require.NoError(t, err) - s.StartApp(auth) - s.Auth = auth - - ready, err := s.Auth.WaitReady(s.Context()) - require.NoError(t, err) - require.True(t, ready, "auth is not ready") - - // Set CA Pin so that Proxy and SSH can register to auth securely. - err = s.Integration.SetCAPin(s.Context(), s.Auth) - require.NoError(t, err) -} - -func (s *ProxySetup) SetupSuite(t *testing.T) { - s.AuthSetup.SetupSuite(t) -} - -func (s *ProxySetup) SetupService() { - s.AuthSetup.SetupService() - t := s.T() - proxy, err := s.Integration.NewProxyService(s.Auth) - require.NoError(t, err) - s.StartApp(proxy) - s.Proxy = proxy - ready, err := s.Proxy.WaitReady(s.Context()) - require.NoError(t, err) - require.True(t, ready, "proxy is not ready") -} - -func (s *SSHSetup) SetupSuite(t *testing.T) { - s.ProxySetup.SetupSuite(t) -} - -func (s *SSHSetup) SetupService() { - s.ProxySetup.SetupService() - t := s.T() - ssh, err := s.Integration.NewSSHService(s.Auth) - require.NoError(t, err) - s.StartApp(ssh) - s.SSH = ssh - ready, err := s.SSH.WaitReady(context.Background()) - require.NoError(t, err) - require.True(t, ready, "ssh is not ready") - - // Wait for node to show up on the server. - require.Eventually(t, func() bool { - resources, err := s.Integration.tctl(s.Auth).GetAll(s.Context(), "nodes") - require.NoError(t, err) - - return len(resources) != 0 - }, 5*time.Second, time.Second) -} diff --git a/lib/testing/integration/ssh_test.go b/lib/testing/integration/ssh_test.go deleted file mode 100644 index e190769f2..000000000 --- a/lib/testing/integration/ssh_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "bytes" - "fmt" - "os/user" - "testing" - - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/utils" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -type IntegrationSSHSuite struct { - SSHSetup -} - -func TestIntegrationSSH(t *testing.T) { suite.Run(t, &IntegrationSSHSuite{}) } - -func (s *IntegrationSSHSuite) SetupTest() { - s.SSHSetup.SetupService() -} - -func (s *IntegrationSSHSuite) TestSSH() { - t := s.T() - me, err := user.Current() - require.NoError(t, err) - var bootstrap Bootstrap - role, err := bootstrap.AddRole(me.Username, types.RoleSpecV6{Allow: types.RoleConditions{ - Logins: []string{me.Username}, - NodeLabels: types.Labels{types.Wildcard: utils.Strings{types.Wildcard}}, - }}) - require.NoError(t, err) - user, err := bootstrap.AddUserWithRoles(me.Username, role.GetName()) - require.NoError(t, err) - err = s.Integration.Bootstrap(s.Context(), s.Auth, bootstrap.Resources()) - require.NoError(t, err) - identityPath, err := s.Integration.Sign(s.Context(), s.Auth, user.GetName()) - require.NoError(t, err) - tshCmd := s.Integration.NewTsh(s.Proxy.WebProxyAddr().String(), identityPath) - cmd := tshCmd.SSHCommand(s.Context(), user.GetName()+"@localhost") - - stdinPipe, err := cmd.StdinPipe() - require.NoError(t, err) - - cmdStdout := &bytes.Buffer{} - cmdStderr := &bytes.Buffer{} - - cmd.Stdout = cmdStdout - cmd.Stderr = cmdStderr - - err = cmd.Start() - require.NoError(t, err) - - _, err = stdinPipe.Write([]byte("echo MYUSER=$USER\r\n")) - require.NoError(t, err) - - _, err = stdinPipe.Write([]byte("exit\r\n")) - require.NoError(t, err) - - err = cmd.Wait() - t.Log("STDOUT", cmdStdout.String()) - t.Log("STDERR", cmdStderr.String()) - require.NoError(t, err) - - require.Contains(t, cmdStdout.String(), fmt.Sprintf("MYUSER=%s", user.GetName())) -} diff --git a/lib/testing/integration/sshservice.go b/lib/testing/integration/sshservice.go deleted file mode 100644 index 8aebd3483..000000000 --- a/lib/testing/integration/sshservice.go +++ /dev/null @@ -1,299 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "bufio" - "bytes" - "context" - "io" - "os/exec" - "regexp" - "strings" - "sync" - "syscall" - "time" - - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib/logger" -) - -var regexpSSHStarting = regexp.MustCompile(`Service [^ ]+ is starting on [^ ]+:(\d+)`) - -type SSHService struct { - mu sync.Mutex - teleportPath string - configPath string - sshAddr Addr - isReady bool - readyCh chan struct{} - doneCh chan struct{} - terminate context.CancelFunc - setErr func(error) - setReady func(bool) - error error - stdout strings.Builder - stderr bytes.Buffer -} - -func newSSHService(teleportPath, configPath string) *SSHService { - var ssh SSHService - var setErrOnce, setReadyOnce sync.Once - readyCh := make(chan struct{}) - ssh = SSHService{ - teleportPath: teleportPath, - configPath: configPath, - readyCh: readyCh, - doneCh: make(chan struct{}), - terminate: func() {}, // dummy noop that will be overridden by Run(), - setErr: func(err error) { - setErrOnce.Do(func() { - ssh.mu.Lock() - defer ssh.mu.Unlock() - ssh.error = err - }) - }, - setReady: func(isReady bool) { - setReadyOnce.Do(func() { - ssh.mu.Lock() - ssh.isReady = isReady - ssh.mu.Unlock() - close(readyCh) - }) - }, - } - return &ssh -} - -// Run spawns an ssh service instance. -func (ssh *SSHService) Run(ctx context.Context) error { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - log := logger.Get(ctx) - - cmd := exec.CommandContext(ctx, ssh.teleportPath, "start", "--debug", "--config", ssh.configPath) - log.Debugf("Running SSH service: %s", cmd) - - stdoutPipe, err := cmd.StdoutPipe() - if err != nil { - err = trace.Wrap(err, "failed to get stdout") - ssh.setErr(err) - return err - } - - stderrPipe, err := cmd.StderrPipe() - if err != nil { - err = trace.Wrap(err, "failed to get stderr") - ssh.setErr(err) - return err - } - - if err := cmd.Start(); err != nil { - err = trace.Wrap(err, "failed to start teleport") - ssh.setErr(err) - return err - } - - ctx, log = logger.WithField(ctx, "pid", cmd.Process.Pid) - log.Debug("SSH service process has been started") - - ssh.mu.Lock() - var terminateOnce sync.Once - ssh.terminate = func() { - terminateOnce.Do(func() { - log.Debug("Terminating SSH service process") - // Signal the process to gracefully terminate by sending SIGQUIT. - if err := cmd.Process.Signal(syscall.SIGQUIT); err != nil { - log.Warn(err) - } - // If we're not done in 5 minutes, just kill the process by canceling its context. - go func() { - select { - case <-ssh.doneCh: - case <-time.After(serviceShutdownTimeout): - log.Debug("Killing SSH service process") - } - // cancel() results in sending SIGKILL to a process if it's still alive. - cancel() - }() - }) - } - ssh.mu.Unlock() - - var ioWork sync.WaitGroup - ioWork.Add(2) - - // Parse stdout of a Teleport process. - go func() { - defer ioWork.Done() - - stdout := bufio.NewReader(stdoutPipe) - for { - line, err := stdout.ReadString('\n') - if err == io.EOF { - return - } - if err := trace.Wrap(err); err != nil { - log.WithError(err).Error("failed to read process stdout") - return - } - - ssh.saveStdout(line) - - if ssh.IsReady() { - continue - } - - ssh.parseLine(ctx, line) - if strings.Contains(line, "The new service has started successfully.") { - log.Debugf("Found addr of SSH service process: %v", ssh.sshAddr) - ssh.setReady(true) - } - } - }() - - // Save stderr to a buffer. - go func() { - defer ioWork.Done() - - stderr := bufio.NewReader(stderrPipe) - data := make([]byte, stderr.Size()) - for { - n, err := stderr.Read(data) - ssh.saveStderr(data[:n]) - if err == io.EOF { - return - } - if err := trace.Wrap(err); err != nil { - log.WithError(err).Error("failed to read process stderr") - return - } - } - }() - - // Wait for process completeness after processing both outputs. - go func() { - ioWork.Wait() - err := trace.Wrap(cmd.Wait()) - ssh.setErr(err) - close(ssh.doneCh) - }() - - <-ssh.doneCh - - if !ssh.IsReady() { - log.Error("SSH service is failed to initialize") - stdoutLines := strings.Split(ssh.Stdout(), "\n") - for _, line := range stdoutLines { - log.Debug("SSH service log: ", line) - } - log.Debugf("SSH service stderr: %q", ssh.Stderr()) - - // If it's still not ready lets signal that it's finally not ready. - ssh.setReady(false) - // Set an err just in case if it's not set before. - ssh.setErr(trace.Errorf("failed to initialize")) - } - - return trace.Wrap(ssh.Err()) -} - -// Addr returns SSH external address. -func (ssh *SSHService) Addr() Addr { - ssh.mu.Lock() - defer ssh.mu.Unlock() - return ssh.sshAddr -} - -// Err returns ssh service error. It's nil If process is not done yet. -func (ssh *SSHService) Err() error { - ssh.mu.Lock() - defer ssh.mu.Unlock() - return ssh.error -} - -// Shutdown terminates the ssh service process and waits for its completion. -func (ssh *SSHService) Shutdown(ctx context.Context) error { - ssh.doTerminate() - select { - case <-ssh.doneCh: - return nil - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } -} - -// Stdout returns a collected ssh service process stdout. -func (ssh *SSHService) Stdout() string { - ssh.mu.Lock() - defer ssh.mu.Unlock() - return ssh.stdout.String() -} - -// Stderr returns a collected ssh service process stderr. -func (ssh *SSHService) Stderr() string { - ssh.mu.Lock() - defer ssh.mu.Unlock() - return ssh.stderr.String() -} - -// WaitReady waits for ssh service initialization. -func (ssh *SSHService) WaitReady(ctx context.Context) (bool, error) { - select { - case <-ssh.readyCh: - return ssh.IsReady(), nil - case <-ctx.Done(): - return false, trace.Wrap(ctx.Err(), "ssh service is not ready") - } -} - -// IsReady indicates if ssh service is initialized properly. -func (ssh *SSHService) IsReady() bool { - ssh.mu.Lock() - defer ssh.mu.Unlock() - return ssh.isReady -} - -func (ssh *SSHService) doTerminate() { - ssh.mu.Lock() - terminate := ssh.terminate - ssh.mu.Unlock() - terminate() -} - -func (ssh *SSHService) parseLine(ctx context.Context, line string) { - if submatch := regexpSSHStarting.FindStringSubmatch(line); submatch != nil { - ssh.mu.Lock() - defer ssh.mu.Unlock() - ssh.sshAddr = Addr{Host: "127.0.0.1", Port: submatch[1]} - return - } -} - -func (ssh *SSHService) saveStdout(line string) { - ssh.mu.Lock() - defer ssh.mu.Unlock() - ssh.stdout.WriteString(line) -} - -func (ssh *SSHService) saveStderr(chunk []byte) { - ssh.mu.Lock() - defer ssh.mu.Unlock() - ssh.stderr.Write(chunk) -} diff --git a/lib/testing/integration/suite.go b/lib/testing/integration/suite.go deleted file mode 100644 index d54c05c63..000000000 --- a/lib/testing/integration/suite.go +++ /dev/null @@ -1,177 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package integration - -import ( - "context" - "os" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - - "github.com/gravitational/teleport-plugins/lib/logger" -) - -// Suite is a basic testing suite enhanced with context management. -type Suite struct { - suite.Suite - contexts map[*testing.T]contexts -} - -// AppI is an app that can be spawned along with running test. -type AppI interface { - // Run starts the application - Run(ctx context.Context) error - // WaitReady waits till the application finishes initialization - WaitReady(ctx context.Context) (bool, error) - // Err returns last error - Err() error - // Shutdown shuts the application down - Shutdown(ctx context.Context) error -} - -type contexts struct { - // baseCtx is the the base context for appCtx and testCtx. - // It could store some test-specific information stored using context.WithValue() - // such as test name for the logger etc. - baseCtx context.Context - - // appCtx inherits from baseCtx. Its purpose is to limit the lifetime of the apps running in parallel. - // By "app" we mean some plugin (e.g. access/slack) or the Teleport process (lib/testing/integration package). - // Its timeout is slightly higher than testCtx's for a reason. When the test example fails with timeout - // we want to see the exact line of the test file where the fail took place. But if the app dies at the same time - // as the some operation in the test example we probably we'll see the line where the app failed, not the test - // which is non-informative but we really want to see what line of the test caused the timeout and where it happened. - appCtx context.Context - - // testCtx inherits from baseCtx. Its purpose is to limit the lifetime of the test method. - // This context is guaranteed to be canceled earlier than appCtx for better error reporting (see explanation above). - testCtx context.Context -} - -// SetT sets the current *testing.T context. -func (s *Suite) SetT(t *testing.T) { - oldT := s.T() - s.Suite.SetT(t) - s.initContexts(oldT, t) -} - -func (s *Suite) initContexts(oldT *testing.T, newT *testing.T) { - if s.contexts == nil { - s.contexts = make(map[*testing.T]contexts) - } - contexts, ok := s.contexts[newT] - if ok { - // Context already initialized. - // This happens when testify sets the parent context back after running a subtest. - return - } - var baseCtx context.Context - if oldT != nil && strings.HasPrefix(newT.Name(), oldT.Name()+"/") { - // We are running a subtest so lets inherit the context too. - baseCtx = s.contexts[oldT].testCtx - } else { - baseCtx = context.Background() - } - baseCtx, _ = logger.WithField(baseCtx, "test", newT.Name()) - baseCtx, cancel := context.WithCancel(baseCtx) - newT.Cleanup(cancel) - - contexts.baseCtx = baseCtx - contexts.appCtx = baseCtx - contexts.testCtx = baseCtx - - // Just memoize the context in a map and that's all. - // Lets not bother with cleaning up this storage, it's not gonna be that big. - s.contexts[newT] = contexts -} - -// SetContextTimeout limits the lifetime of test and app contexts. -func (s *Suite) SetContextTimeout(timeout time.Duration) context.Context { - t := s.T() - t.Helper() - - contexts, ok := s.contexts[t] - require.True(t, ok) - - var cancel context.CancelFunc - // We set appCtx timeout slightly higher than testCtx for test assertions to fall earlier than - // app (plugin) fails. - contexts.appCtx, cancel = context.WithTimeout(contexts.baseCtx, timeout+500*time.Millisecond) - t.Cleanup(cancel) - contexts.testCtx, cancel = context.WithTimeout(contexts.baseCtx, timeout) - t.Cleanup(cancel) - - s.contexts[t] = contexts - - return contexts.testCtx -} - -// Context returns a current test context. -func (s *Suite) Context() context.Context { - t := s.T() - t.Helper() - contexts, ok := s.contexts[t] - require.True(t, ok) - return contexts.testCtx -} - -// NewTmpFile creates a new temporary file. -func (s *Suite) NewTmpFile(pattern string) *os.File { - t := s.T() - t.Helper() - - file, err := os.CreateTemp("", pattern) - require.NoError(t, err) - t.Cleanup(func() { - err := os.Remove(file.Name()) - require.NoError(t, err) - }) - return file -} - -// StartApp spawns an app in parallel with the running test/suite. -func (s *Suite) StartApp(app AppI) { - t := s.T() - t.Helper() - - contexts, ok := s.contexts[t] - require.True(t, ok) - - go func() { - ctx := contexts.appCtx - if err := app.Run(ctx); err != nil { - // We're in a goroutine so we can't just require.NoError(t, err). - // All we can do is to log an error. - logger.Get(ctx).WithError(err).Error("Application failed") - } - }() - - t.Cleanup(func() { - err := app.Shutdown(contexts.appCtx) - assert.NoError(t, err) - assert.NoError(t, app.Err()) - }) - - ok, err := app.WaitReady(contexts.testCtx) - require.NoError(t, err) - require.True(t, ok) -} diff --git a/lib/tsh/tsh.go b/lib/tsh/tsh.go deleted file mode 100644 index cba360add..000000000 --- a/lib/tsh/tsh.go +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package tsh - -import ( - "context" - "os/exec" - - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/lib/logger" -) - -// Tsh is a runner of tsh command. -type Tsh struct { - Path string - Proxy string - Identity string - Insecure bool -} - -// CheckExecutable checks if `tsh` executable exists in the system. -func (tsh Tsh) CheckExecutable() error { - _, err := exec.LookPath(tsh.cmd()) - return trace.Wrap(err, "tsh executable is not found") -} - -// SSHCommand creates exec.CommandContext for tsh ssh --tty on behalf of userHost -func (tsh Tsh) SSHCommand(ctx context.Context, userHost string) *exec.Cmd { - log := logger.Get(ctx) - args := append(tsh.baseArgs(), "ssh") - - // Otherwise the ssh client would need to confirm that it accepts the server's public key - /* - The authenticity of host 'localhost:0@default@local-site' can't be established. Its public key is: - ssh-rsa AAAA - - Are you sure you want to continue? [y/N]: - */ - args = append(args, "-o", "StrictHostKeyChecking no") - args = append(args, "--tty", userHost) - - cmd := exec.CommandContext(ctx, tsh.cmd(), args...) - log.Debugf("Running %s", cmd) - - return cmd -} - -func (tsh Tsh) cmd() string { - if tsh.Path != "" { - return tsh.Path - } - return "tsh" -} - -func (tsh Tsh) baseArgs() (args []string) { - if tsh.Insecure { - args = append(args, "--insecure") - } - if tsh.Identity != "" { - args = append(args, "--identity", tsh.Identity) - } - if tsh.Proxy != "" { - args = append(args, "--proxy", tsh.Proxy) - } - return -} diff --git a/lib/versions.go b/lib/versions.go deleted file mode 100644 index 97cfa9e9b..000000000 --- a/lib/versions.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2023 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 lib - -import ( - "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/trace" - "github.com/hashicorp/go-version" -) - -// AssertServerVersion returns an error if server version in ping response is -// less than minimum required version. -func AssertServerVersion(pong proto.PingResponse, minVersion string) error { - actual, err := version.NewVersion(pong.ServerVersion) - if err != nil { - return trace.Wrap(err) - } - required, err := version.NewVersion(minVersion) - if err != nil { - return trace.Wrap(err) - } - if actual.LessThan(required) { - return trace.Errorf("server version %s is less than %s", pong.ServerVersion, minVersion) - } - return nil -} diff --git a/lib/watcherjob/helpers_test.go b/lib/watcherjob/helpers_test.go deleted file mode 100644 index 6a7f03869..000000000 --- a/lib/watcherjob/helpers_test.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2023 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 watcherjob - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gravitational/teleport-plugins/lib" -) - -// MockWatcher is a mock events watcher. -type MockWatcher struct { - events <-chan types.Event - ctx context.Context - cancel context.CancelFunc -} - -// MockEvents is mock watcher builder. -type MockEvents struct { - sync.Mutex - channels []chan<- types.Event -} - -// NewWatcher creates a new watcher. -func (e *MockEvents) NewWatcher(ctx context.Context, watch types.Watch) (types.Watcher, error) { - events := make(chan types.Event, 1000) - e.Lock() - e.channels = append(e.channels, events) - e.Unlock() - ctx, cancel := context.WithCancel(ctx) - return MockWatcher{events: events, ctx: ctx, cancel: cancel}, ctx.Err() -} - -// Fire emits a watcher events for all the subscribers to consume. -func (e *MockEvents) Fire(event types.Event) { - e.Lock() - channels := e.channels - e.Unlock() - for _, events := range channels { - events <- event - } -} - -// WaitSomeWatchers blocks until either some watcher is subscribed or context is done. -func (e *MockEvents) WaitSomeWatchers(ctx context.Context) error { - ticker := time.NewTicker(5 * time.Millisecond) - defer ticker.Stop() - for { - select { - case <-ticker.C: - e.Lock() - n := len(e.channels) - e.Unlock() - if n > 0 { - return nil - } - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } - } -} - -// Events returns a stream of events. -func (w MockWatcher) Events() <-chan types.Event { - return w.events -} - -// Done returns a completion channel. -func (w MockWatcher) Done() <-chan struct{} { - return w.ctx.Done() -} - -// Close sends a termination signal to watcher. -func (w MockWatcher) Close() error { - w.cancel() - return nil -} - -// Error returns a watcher error. -func (w MockWatcher) Error() error { - return trace.Wrap(w.ctx.Err()) -} - -// MockEventsProcess is a new process with a mock events streamer. -type MockEventsProcess struct { - *lib.Process - eventsJob lib.ServiceJob - Events MockEvents -} - -// NewMockEventsProcess creates a new process. -func NewMockEventsProcess(ctx context.Context, t *testing.T, config Config, fn EventFunc) *MockEventsProcess { - t.Helper() - process := MockEventsProcess{ - Process: lib.NewProcess(ctx), - } - t.Cleanup(func() { - process.Terminate() - assert.NoError(t, process.Shutdown(ctx)) - process.Close() - }) - process.eventsJob = NewJobWithEvents(&process.Events, config, fn) - process.SpawnCriticalJob(process.eventsJob) - require.NoError(t, process.Events.WaitSomeWatchers(ctx)) - process.Events.Fire(types.Event{Type: types.OpInit}) - - return &process -} - -// Shutdown sends a termination signal and waits for process completion. -func (process *MockEventsProcess) Shutdown(ctx context.Context) error { - process.Terminate() - job := process.eventsJob - select { - case <-job.Done(): - select { - case <-process.Done(): - return trace.Wrap(job.Err()) - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } -} - -// Countdown is a convenient WaitGroup wrapper which you can wait with deadline. -type Countdown struct { - wg sync.WaitGroup - done chan struct{} -} - -// NewCountdown creates a countdown with a given number of count. -func NewCountdown(n int) *Countdown { - countdown := Countdown{done: make(chan struct{})} - countdown.wg.Add(n) - go func() { - countdown.wg.Wait() - close(countdown.done) - }() - return &countdown -} - -// Decrement atomically subtracts one from the counter. -func (countdown *Countdown) Decrement() { - countdown.wg.Done() -} - -// Wait blocks until either countdown or context is done. -func (countdown *Countdown) Wait(ctx context.Context) error { - select { - case <-countdown.done: - return nil - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } -} diff --git a/lib/watcherjob/watcherjob.go b/lib/watcherjob/watcherjob.go deleted file mode 100644 index f7b9d21da..000000000 --- a/lib/watcherjob/watcherjob.go +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright 2023 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 watcherjob - -import ( - "context" - "time" - - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - - "github.com/gravitational/teleport-plugins/access/common/teleport" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" -) - -const DefaultMaxConcurrency = 128 -const DefaultEventFuncTimeout = time.Second * 5 - -type EventFunc func(context.Context, types.Event) error - -type Config struct { - Watch types.Watch - MaxConcurrency int - EventFuncTimeout time.Duration -} - -type job struct { - lib.ServiceJob - config Config - eventFunc EventFunc - events types.Events - eventCh chan *types.Event -} - -type eventKey struct { - kind string - name string -} - -func NewJob(client teleport.Client, config Config, fn EventFunc) lib.ServiceJob { - return NewJobWithEvents(client, config, fn) -} - -func NewJobWithEvents(events types.Events, config Config, fn EventFunc) lib.ServiceJob { - if config.MaxConcurrency == 0 { - config.MaxConcurrency = DefaultMaxConcurrency - } - if config.EventFuncTimeout == 0 { - config.EventFuncTimeout = DefaultEventFuncTimeout - } - job := job{ - events: events, - config: config, - eventFunc: fn, - eventCh: make(chan *types.Event, config.MaxConcurrency), - } - job.ServiceJob = lib.NewServiceJob(func(ctx context.Context) error { - process := lib.MustGetProcess(ctx) - - // Run a separate event loop thread which does not depend on streamer context. - defer close(job.eventCh) - process.Spawn(job.eventLoop) - - // Create a cancellable ctx for event watcher. - ctx, cancel := context.WithCancel(ctx) - process.OnTerminate(func(_ context.Context) error { - cancel() - return nil - }) - - log := logger.Get(ctx) - for { - err := job.watchEvents(ctx) - switch { - case trace.IsConnectionProblem(err): - log.WithError(err).Error("Failed to connect to Teleport Auth server. Reconnecting...") - case trace.IsEOF(err): - log.WithError(err).Error("Watcher stream closed. Reconnecting...") - case lib.IsCanceled(err): - log.Debug("Watcher context is canceled") - // Context cancellation is not an error - return nil - default: - log.WithError(err).Error("Watcher event loop failed") - return trace.Wrap(err) - } - } - }) - return job -} - -// watchEvents spawns a watcher and reads events from it. -func (job job) watchEvents(ctx context.Context) error { - watcher, err := job.events.NewWatcher(ctx, job.config.Watch) - if err != nil { - return trace.Wrap(err) - } - defer func() { - if err := watcher.Close(); err != nil { - logger.Get(ctx).WithError(err).Error("Failed to close a watcher") - } - }() - - if err := job.waitInit(ctx, watcher, 15*time.Second); err != nil { - return trace.Wrap(err) - } - - logger.Get(ctx).Debug("Watcher connected") - job.SetReady(true) - - for { - select { - case event := <-watcher.Events(): - job.eventCh <- &event - case <-watcher.Done(): // When the watcher completes, read the rest of events and quit. - events := takeEvents(watcher.Events()) - for i := range events { - select { - case job.eventCh <- &events[i]: - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } - } - return trace.Wrap(watcher.Error()) - } - } -} - -// waitInit waits for OpInit event be received on a stream. -func (job job) waitInit(ctx context.Context, watcher types.Watcher, timeout time.Duration) error { - select { - case event := <-watcher.Events(): - if event.Type != types.OpInit { - return trace.ConnectionProblem(nil, "unexpected event type %q", event.Type) - } - return nil - case <-time.After(timeout): - return trace.ConnectionProblem(nil, "watcher initialization timed out") - case <-watcher.Done(): - return trace.Wrap(watcher.Error()) - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } -} - -// eventLoop goes through event stream and spawns the event jobs. -// -// Queue processing algorithm is a bit tricky. -// We want to process events concurrently each in its own job. -// On the other hand, we want to avoid potential race conditions so it seems -// that in some cases it's better to process events sequentially in -// the order they came to the queue. -// -// The algorithm combines two approaches, concurrent and sequential. -// It follows the rules: -// - Events for different resources being processed concurrently. -// - Events for the same resource being processed "sequentially" i.e. in the order they came to the queue. -// -// By "sameness" we mean that Kind and Name fields of one resource object are the same as in the other resource object. -func (job job) eventLoop(ctx context.Context) error { - var concurrency int - process := lib.MustGetProcess(ctx) - log := logger.Get(ctx) - queues := make(map[eventKey][]types.Event) - eventDone := make(chan eventKey, job.config.MaxConcurrency) - - for { - var eventCh <-chan *types.Event - if concurrency < job.config.MaxConcurrency { - // If haven't yet reached the limit then we allowed to read from the queue. - // Otherwise, eventCh would be nil which is a forever-blocking channel. - eventCh = job.eventCh - } - - select { - case eventPtr := <-eventCh: - if eventPtr == nil { // channel is closed when the parent job is done so just quit normally. - return nil - } - event := *eventPtr - resource := event.Resource - if resource == nil { - log.Error("received an event with empty resource field") - } - key := eventKey{kind: resource.GetKind(), name: resource.GetName()} - if queue, loaded := queues[key]; loaded { - queues[key] = append(queue, event) - } else { - queues[key] = nil - process.Spawn(job.eventFuncHandler(event, key, eventDone)) - } - concurrency++ - - case key := <-eventDone: - concurrency-- - queue, ok := queues[key] - if !ok { - continue - } - if len(queue) > 0 { - event := queue[0] - process.Spawn(job.eventFuncHandler(event, key, eventDone)) - queue = queue[1:] - queues[key] = queue - } - if len(queue) == 0 { - delete(queues, key) - } - - case <-ctx.Done(): // Stop processing immediately because the context was canceled. - return trace.Wrap(ctx.Err()) - } - } -} - -// eventFuncHandler returns an event handler ready to spawn. -func (job job) eventFuncHandler(event types.Event, key eventKey, doneCh chan<- eventKey) func(ctx context.Context) error { - return func(ctx context.Context) error { - defer func() { - select { - case doneCh <- key: - case <-ctx.Done(): - } - }() - eventCtx, cancel := context.WithTimeout(ctx, job.config.EventFuncTimeout) - defer cancel() - return job.eventFunc(eventCtx, event) - } -} - -// takeEvents reads all the buffered events from channel. -func takeEvents(events <-chan types.Event) []types.Event { - var result []types.Event - for { - select { - case event := <-events: - result = append(result, event) - default: - return result - } - } -} diff --git a/lib/watcherjob/watcherjob_test.go b/lib/watcherjob/watcherjob_test.go deleted file mode 100644 index c4b8c45e0..000000000 --- a/lib/watcherjob/watcherjob_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2023 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 watcherjob - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestSequential checks that events with the different resource names are being processed in parallel. -func TestConcurrent(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - t.Cleanup(cancel) - - config := Config{MaxConcurrency: 4} - countdown := NewCountdown(config.MaxConcurrency) - process := NewMockEventsProcess(ctx, t, config, func(ctx context.Context, event types.Event) error { - defer countdown.Decrement() - time.Sleep(time.Second) - return trace.Wrap(ctx.Err()) - }) - - timeBefore := time.Now() - for i := 0; i < config.MaxConcurrency; i++ { - resource, err := types.NewAccessRequest(fmt.Sprintf("REQ-%v", i+1), "foo", "admin") - require.NoError(t, err) - process.Events.Fire(types.Event{Type: types.OpPut, Resource: resource}) - } - require.NoError(t, countdown.Wait(ctx)) - - timeAfter := time.Now() - assert.InDelta(t, time.Second, timeAfter.Sub(timeBefore), float64(500*time.Millisecond)) -} - -// TestSequential checks that events with the same resource name are being processed one by one (no races). -func TestSequential(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - t.Cleanup(cancel) - - config := Config{MaxConcurrency: 4} - countdown := NewCountdown(config.MaxConcurrency) - process := NewMockEventsProcess(ctx, t, config, func(ctx context.Context, event types.Event) error { - defer countdown.Decrement() - time.Sleep(time.Second) - return trace.Wrap(ctx.Err()) - }) - - timeBefore := time.Now() - for i := 0; i < config.MaxConcurrency; i++ { - resource, err := types.NewAccessRequest("REQ-SAME", "foo", "admin") - require.NoError(t, err) - process.Events.Fire(types.Event{Type: types.OpPut, Resource: resource}) - } - require.NoError(t, countdown.Wait(ctx)) - - timeAfter := time.Now() - assert.InDelta(t, 4*time.Second, timeAfter.Sub(timeBefore), float64(750*time.Millisecond)) -} - -// TestConcurrencyLimit checks the case when the queue is full and there're incoming requests -func TestConcurrencyLimit(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - t.Cleanup(cancel) - - config := Config{MaxConcurrency: 4} - countdown := NewCountdown(config.MaxConcurrency * 2) - process := NewMockEventsProcess(ctx, t, config, func(ctx context.Context, event types.Event) error { - defer countdown.Decrement() - time.Sleep(time.Second) - return trace.Wrap(ctx.Err()) - }) - - timeBefore := time.Now() - for i := 0; i < config.MaxConcurrency; i++ { - resource, err := types.NewAccessRequest(fmt.Sprintf("REQ-%v", i+1), "foo", "admin") - require.NoError(t, err) - - for j := 0; j < 2; j++ { - process.Events.Fire(types.Event{Type: types.OpPut, Resource: resource}) - } - } - require.NoError(t, countdown.Wait(ctx)) - - timeAfter := time.Now() - assert.InDelta(t, 4*time.Second, timeAfter.Sub(timeBefore), float64(750*time.Millisecond)) -} diff --git a/terraform/_gen/plural_resource.go.tpl b/terraform/_gen/plural_resource.go.tpl index b5b713c63..a6bab599e 100644 --- a/terraform/_gen/plural_resource.go.tpl +++ b/terraform/_gen/plural_resource.go.tpl @@ -32,7 +32,7 @@ import ( {{.ProtoPackage}} "{{.ProtoPackagePath}}" {{.SchemaPackage}} "{{.SchemaPackagePath}}" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/_gen/singular_resource.go.tpl b/terraform/_gen/singular_resource.go.tpl index 132bd923e..3ab546ef5 100644 --- a/terraform/_gen/singular_resource.go.tpl +++ b/terraform/_gen/singular_resource.go.tpl @@ -27,7 +27,7 @@ import ( {{.ProtoPackage}} "{{.ProtoPackagePath}}" {{.SchemaPackage}} "{{.SchemaPackagePath}}" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/provider.go b/terraform/provider/provider.go index ae69471bd..afc2003e8 100644 --- a/terraform/provider/provider.go +++ b/terraform/provider/provider.go @@ -30,6 +30,7 @@ import ( "time" "github.com/gravitational/teleport/api/client" + "github.com/gravitational/teleport/integrations/lib" "github.com/gravitational/trace" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -37,8 +38,6 @@ import ( log "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/grpclog" - - "github.com/gravitational/teleport-plugins/lib" ) const ( diff --git a/terraform/provider/resource_teleport_app.go b/terraform/provider/resource_teleport_app.go index a87508492..cb76e6d22 100755 --- a/terraform/provider/resource_teleport_app.go +++ b/terraform/provider/resource_teleport_app.go @@ -28,7 +28,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_auth_preference.go b/terraform/provider/resource_teleport_auth_preference.go index 716e5860e..139f5edf6 100755 --- a/terraform/provider/resource_teleport_auth_preference.go +++ b/terraform/provider/resource_teleport_auth_preference.go @@ -27,7 +27,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_cluster_networking_config.go b/terraform/provider/resource_teleport_cluster_networking_config.go index ff1396c73..14f201176 100755 --- a/terraform/provider/resource_teleport_cluster_networking_config.go +++ b/terraform/provider/resource_teleport_cluster_networking_config.go @@ -27,7 +27,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_database.go b/terraform/provider/resource_teleport_database.go index ffcdf64cb..876eb1d15 100755 --- a/terraform/provider/resource_teleport_database.go +++ b/terraform/provider/resource_teleport_database.go @@ -28,7 +28,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_github_connector.go b/terraform/provider/resource_teleport_github_connector.go index b7621bef2..5fa595c22 100755 --- a/terraform/provider/resource_teleport_github_connector.go +++ b/terraform/provider/resource_teleport_github_connector.go @@ -28,7 +28,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_login_rule.go b/terraform/provider/resource_teleport_login_rule.go index 08eb372b4..5d8980629 100755 --- a/terraform/provider/resource_teleport_login_rule.go +++ b/terraform/provider/resource_teleport_login_rule.go @@ -28,7 +28,7 @@ import ( loginrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1" schemav1 "github.com/gravitational/teleport-plugins/terraform/tfschema/loginrule/v1" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_oidc_connector.go b/terraform/provider/resource_teleport_oidc_connector.go index fefa492ce..295f4b6c7 100755 --- a/terraform/provider/resource_teleport_oidc_connector.go +++ b/terraform/provider/resource_teleport_oidc_connector.go @@ -28,7 +28,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_provision_token.go b/terraform/provider/resource_teleport_provision_token.go index feef21a42..46d25f9ca 100755 --- a/terraform/provider/resource_teleport_provision_token.go +++ b/terraform/provider/resource_teleport_provision_token.go @@ -30,7 +30,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_role.go b/terraform/provider/resource_teleport_role.go index 3bd01cb9d..b2b0d0b39 100755 --- a/terraform/provider/resource_teleport_role.go +++ b/terraform/provider/resource_teleport_role.go @@ -28,7 +28,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_saml_connector.go b/terraform/provider/resource_teleport_saml_connector.go index 3ebf88077..a0c53669b 100755 --- a/terraform/provider/resource_teleport_saml_connector.go +++ b/terraform/provider/resource_teleport_saml_connector.go @@ -28,7 +28,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_session_recording_config.go b/terraform/provider/resource_teleport_session_recording_config.go index bfa0e6310..2ae3682ce 100755 --- a/terraform/provider/resource_teleport_session_recording_config.go +++ b/terraform/provider/resource_teleport_session_recording_config.go @@ -27,7 +27,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_trusted_cluster.go b/terraform/provider/resource_teleport_trusted_cluster.go index 9b9f662eb..fcdc95ef6 100755 --- a/terraform/provider/resource_teleport_trusted_cluster.go +++ b/terraform/provider/resource_teleport_trusted_cluster.go @@ -28,7 +28,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/provider/resource_teleport_user.go b/terraform/provider/resource_teleport_user.go index 6ca5b5904..b17fa39c7 100755 --- a/terraform/provider/resource_teleport_user.go +++ b/terraform/provider/resource_teleport_user.go @@ -28,7 +28,7 @@ import ( apitypes "github.com/gravitational/teleport/api/types" tfschema "github.com/gravitational/teleport-plugins/terraform/tfschema" - "github.com/gravitational/teleport-plugins/lib/backoff" + "github.com/gravitational/teleport/integrations/lib/backoff" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" ) diff --git a/terraform/test/main_test.go b/terraform/test/main_test.go index f54bf929f..4a0f544fb 100644 --- a/terraform/test/main_test.go +++ b/terraform/test/main_test.go @@ -26,14 +26,14 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/teleport/integrations/lib" + "github.com/gravitational/teleport/integrations/lib/testing/integration" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/testing/integration" "github.com/gravitational/teleport-plugins/terraform/provider" ) diff --git a/update_teleport_dep_version.sh b/update_teleport_dep_version.sh new file mode 100755 index 000000000..c83fe393a --- /dev/null +++ b/update_teleport_dep_version.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Replaces versions of 'teleport' and 'teleport/api' in go.mod +# with matching pseudo-versions of the given tag or branch. + +set -e + +which curl >/dev/null || { echo "curl is required" && exit 1; } +which date >/dev/null || { echo "date is required" && exit 1; } +which jq >/dev/null || { echo "jq is required" && exit 1; } +which sed >/dev/null || { echo "sed is required" && exit 1; } + +function sed_inline() { + sed -i'' "$@" +} + +version=$1 +[ -n "$version" ] || { echo "teleport version (tag like 'v1.2.3' or branch name like 'branch/v123') is required as the only argument to the script" && exit 1; } + +ref="heads/$version" +if [[ "$version" = v* ]]; then + ref="tags/$version" +fi + +object_url=$(curl -sS --fail \ + "https://api.github.com/repos/gravitational/teleport/git/ref/$ref" \ + | jq -r .object.url) + +object=$(curl -sS --fail "$object_url") +object_date=$(echo "$object" | jq -r .committer.date | sed 's/[-:TZ]//g') +object_sha="$(echo "$object" | jq -r .sha)" +pseudo_version="v0.0.0-${object_date}-${object_sha:0:12}" + +sed_inline -e $"s#^\tgithub.com/gravitational/teleport .*#\tgithub.com/gravitational/teleport $pseudo_version // ref: $ref#" go.mod +sed_inline -e $"s#^\tgithub.com/gravitational/teleport/api => .*#\tgithub.com/gravitational/teleport/api => github.com/gravitational/teleport/api $pseudo_version // ref: $ref#" go.mod + +go mod tidy