706 lines
24 KiB
Go
706 lines
24 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const workspaceRateLimitSeedTimeout = 5 * time.Second
|
|
|
|
type githubAppRuntime struct {
|
|
registrationID string
|
|
source DeploymentAppSource
|
|
generation int64
|
|
appID int64
|
|
appClient *AppClient
|
|
installationTokens *InstallationTokenCache
|
|
installationProvider *CachedInstallationCredentialProvider
|
|
appInstallationAuth *AppInstallationService
|
|
personalAuth *PersonalAuthService
|
|
webhookAuth *GitHubWebhookService
|
|
}
|
|
|
|
type DeploymentAppRuntimeSnapshot struct {
|
|
RegistrationID string
|
|
Source DeploymentAppSource
|
|
Ready bool
|
|
AppID int64
|
|
Generation int64
|
|
}
|
|
|
|
func (s *Service) ValidateAppRegistrationRuntime(resolved ResolvedDeploymentAppConfig) error {
|
|
_, err := s.buildDeploymentAppRuntime(resolved)
|
|
return err
|
|
}
|
|
|
|
// ApplyAppRegistrationRuntime builds a complete App runtime before publishing
|
|
// it under its registration ID. A failure leaves every existing registration
|
|
// generation untouched.
|
|
func (s *Service) ApplyAppRegistrationRuntime(resolved ResolvedDeploymentAppConfig) error {
|
|
if s == nil {
|
|
return errors.New("GitHub service is not configured")
|
|
}
|
|
if resolved.Registration == nil || strings.TrimSpace(resolved.Registration.ID) == "" {
|
|
return errors.New("GitHub App registration ID is required")
|
|
}
|
|
runtime, err := s.buildDeploymentAppRuntime(resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.mu.Lock()
|
|
if s.appRegistrationRuntimes == nil {
|
|
s.appRegistrationRuntimes = make(map[string]*githubAppRuntime)
|
|
}
|
|
s.appRegistrationRuntimes[runtime.registrationID] = runtime
|
|
s.mu.Unlock()
|
|
if s.resolver != nil {
|
|
s.resolver.InvalidateAppRegistration(runtime.registrationID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// InvalidateAppRegistrationRuntime removes only the expected generation. A
|
|
// delayed invalidation cannot tear down a newer key rotation.
|
|
func (s *Service) InvalidateAppRegistrationRuntime(registrationID string, generation int64) {
|
|
if s == nil || strings.TrimSpace(registrationID) == "" {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
runtime := s.appRegistrationRuntimes[registrationID]
|
|
if runtime != nil && (generation <= 0 || runtime.generation == generation) {
|
|
delete(s.appRegistrationRuntimes, registrationID)
|
|
}
|
|
s.mu.Unlock()
|
|
if runtime != nil && (generation <= 0 || runtime.generation == generation) && s.resolver != nil {
|
|
s.resolver.InvalidateAppRegistration(registrationID)
|
|
}
|
|
}
|
|
|
|
func (s *Service) AppRegistrationRuntimeSnapshot(registrationID string) DeploymentAppRuntimeSnapshot {
|
|
runtime := s.currentAppRegistrationRuntime(registrationID)
|
|
if runtime == nil {
|
|
return DeploymentAppRuntimeSnapshot{RegistrationID: registrationID, Source: DeploymentAppSourceNone}
|
|
}
|
|
return DeploymentAppRuntimeSnapshot{
|
|
RegistrationID: registrationID, Source: runtime.source, Ready: true,
|
|
AppID: runtime.appID, Generation: runtime.generation,
|
|
}
|
|
}
|
|
|
|
func (s *Service) currentAppRegistrationRuntime(registrationID string) *githubAppRuntime {
|
|
if s == nil || strings.TrimSpace(registrationID) == "" {
|
|
return nil
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.appRegistrationRuntimes[registrationID]
|
|
}
|
|
|
|
// InitializeAppRegistrationRuntimes independently loads every catalog entry.
|
|
// Invalid entries are reported together after all valid entries are active.
|
|
func (s *Service) InitializeAppRegistrationRuntimes(ctx context.Context) error {
|
|
if s == nil || s.store == nil || s.connectionSecrets == nil {
|
|
return errors.New("GitHub App registration dependencies are not configured")
|
|
}
|
|
repository := NewAppRegistrationRepository(s.store, s.connectionSecrets)
|
|
registrations, err := s.store.ListAppRegistrations(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var loadErrors []error
|
|
for _, registration := range registrations {
|
|
if registration == nil {
|
|
continue
|
|
}
|
|
resolved, resolveErr := ResolveAppRegistrationConfig(ctx, registration.ID, repository)
|
|
if resolveErr == nil {
|
|
resolveErr = s.ApplyAppRegistrationRuntime(resolved)
|
|
}
|
|
if resolveErr != nil {
|
|
loadErrors = append(loadErrors, fmt.Errorf("load GitHub App registration %s: %w", registration.ID, resolveErr))
|
|
}
|
|
}
|
|
if cleanupErr := repository.CleanupOrphanedCredentialBundles(ctx); cleanupErr != nil {
|
|
loadErrors = append(loadErrors, cleanupErr)
|
|
}
|
|
return errors.Join(loadErrors...)
|
|
}
|
|
|
|
func (s *Service) buildDeploymentAppRuntime(
|
|
resolved ResolvedDeploymentAppConfig,
|
|
) (*githubAppRuntime, error) {
|
|
if s.store == nil || s.connectionSecrets == nil {
|
|
return nil, errors.New("GitHub App authentication dependencies are not configured")
|
|
}
|
|
if resolved.Source != DeploymentAppSourceManaged {
|
|
return nil, errors.New("GitHub App runtime source is invalid")
|
|
}
|
|
if resolved.Registration == nil || strings.TrimSpace(resolved.Registration.ID) == "" {
|
|
return nil, errors.New("GitHub App registration ID is required")
|
|
}
|
|
if resolved.Registration.Status != AppRegistrationStatusActive {
|
|
return nil, errors.New("GitHub App registration is not active")
|
|
}
|
|
if err := resolved.Config.Validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid GitHub App runtime configuration: %w", err)
|
|
}
|
|
privateKey, err := resolved.Config.PrivateKeyPEM()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load GitHub App private key: %w", err)
|
|
}
|
|
appClient, err := NewAppClient(resolved.Config.AppID, privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("initialize GitHub App client: %w", err)
|
|
}
|
|
return s.buildDeploymentAppRuntimeWithClient(resolved, appClient)
|
|
}
|
|
|
|
func (s *Service) buildDeploymentAppRuntimeWithClient(
|
|
resolved ResolvedDeploymentAppConfig,
|
|
appClient *AppClient,
|
|
) (*githubAppRuntime, error) {
|
|
config := resolved.Config
|
|
baseURL := strings.TrimRight(config.PublicBaseURL, "/")
|
|
if appClient == nil || baseURL == "" || config.ClientID == "" || config.ClientSecret == "" ||
|
|
config.WebhookSecret == "" || config.Slug == "" {
|
|
return nil, errors.New("GitHub App runtime configuration is incomplete")
|
|
}
|
|
flows := NewOAuthFlowManager(s.store)
|
|
oauth := NewGitHubOAuthClient(config.ClientID, config.ClientSecret)
|
|
personalRepo := s.personalConnections
|
|
if personalRepo == nil {
|
|
personalRepo = NewStorePersonalConnectionRepository(s.store, s.connectionSecrets)
|
|
}
|
|
connectionStore := &serviceAppConnectionStore{service: s}
|
|
personal := NewPersonalAuthService(PersonalAuthConfig{
|
|
RegistrationID: resolved.Registration.ID,
|
|
ClientID: config.ClientID,
|
|
CallbackURL: baseURL + "/api/v1/github/app/registrations/" + resolved.Registration.ID + "/personal/callback",
|
|
}, flows, personalRepo, oauth)
|
|
personal.SetWorkspaceMutationLock(s.workspaceConnectionMutationLock)
|
|
registrationID := resolved.Registration.ID
|
|
generation := resolved.Registration.CredentialGeneration
|
|
tokens := NewAppInstallationTokenCache(registrationID, generation, appClient)
|
|
return &githubAppRuntime{
|
|
registrationID: registrationID, source: resolved.Source, generation: generation, appID: config.AppID,
|
|
appClient: appClient, installationTokens: tokens,
|
|
installationProvider: NewCachedInstallationCredentialProvider(tokens),
|
|
appInstallationAuth: NewAppInstallationService(AppInstallationConfig{
|
|
RegistrationID: registrationID, Slug: config.Slug,
|
|
CallbackURL: baseURL + "/api/v1/github/app/registrations/" + registrationID + "/install/callback",
|
|
}, flows, connectionStore, appClient, oauth),
|
|
personalAuth: personal,
|
|
webhookAuth: NewAppRegistrationWebhookService(
|
|
registrationID, config.WebhookSecret, connectionStore,
|
|
&installationRepositorySettingsUpdater{service: s}, personalRepo, s.eventBus,
|
|
GitHubWebhookReconciliation{Installations: appClient, Personal: personal},
|
|
),
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) InitializeAppRegistrationLifecycle() error {
|
|
if s == nil || s.store == nil || s.connectionSecrets == nil {
|
|
return errors.New("GitHub App registration dependencies are not configured")
|
|
}
|
|
repository := NewAppRegistrationRepository(s.store, s.connectionSecrets)
|
|
s.mu.Lock()
|
|
s.appRegistrationLifecycle = NewAppRegistrationLifecycleService(AppRegistrationLifecycleConfig{
|
|
Repository: repository, Store: s.store, Runtime: s,
|
|
Converter: NewManifestConversionClient(),
|
|
})
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) currentAppRegistrationLifecycle() *AppRegistrationLifecycleService {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.appRegistrationLifecycle
|
|
}
|
|
|
|
func (s *Service) ListAppRegistrationCatalog(
|
|
ctx context.Context,
|
|
userID, workspaceID string,
|
|
) (AppRegistrationCatalog, error) {
|
|
if err := s.authorizeWorkspaceAccess(ctx, workspaceID); err != nil {
|
|
return AppRegistrationCatalog{}, err
|
|
}
|
|
lifecycle := s.currentAppRegistrationLifecycle()
|
|
if lifecycle == nil {
|
|
return AppRegistrationCatalog{}, ErrGitHubNotConfigured
|
|
}
|
|
return lifecycle.List(ctx, userID, workspaceID)
|
|
}
|
|
|
|
func (s *Service) StartAppRegistrationManifest(
|
|
ctx context.Context,
|
|
userID string,
|
|
request AppRegistrationManifestStartRequest,
|
|
) (AppRegistrationManifestStart, error) {
|
|
if err := s.authorizeWorkspaceAccess(ctx, request.WorkspaceID); err != nil {
|
|
return AppRegistrationManifestStart{}, err
|
|
}
|
|
lifecycle := s.currentAppRegistrationLifecycle()
|
|
if lifecycle == nil {
|
|
return AppRegistrationManifestStart{}, ErrGitHubNotConfigured
|
|
}
|
|
return lifecycle.StartManifest(ctx, userID, request)
|
|
}
|
|
|
|
func (s *Service) CompleteAppRegistrationManifest(
|
|
ctx context.Context,
|
|
registrationID string,
|
|
callback AppRegistrationManifestCallback,
|
|
) (AppRegistrationManifestResult, error) {
|
|
lifecycle := s.currentAppRegistrationLifecycle()
|
|
if lifecycle == nil {
|
|
return AppRegistrationManifestResult{}, ErrGitHubNotConfigured
|
|
}
|
|
return lifecycle.CompleteManifest(ctx, registrationID, callback)
|
|
}
|
|
|
|
func (s *Service) ImportAppRegistration(
|
|
ctx context.Context,
|
|
userID string,
|
|
request AppRegistrationImportRequest,
|
|
) (*AppRegistration, error) {
|
|
if err := s.authorizeWorkspaceAccess(ctx, request.WorkspaceID); err != nil {
|
|
return nil, err
|
|
}
|
|
lifecycle := s.currentAppRegistrationLifecycle()
|
|
if lifecycle == nil {
|
|
return nil, ErrGitHubNotConfigured
|
|
}
|
|
return lifecycle.Import(ctx, userID, request)
|
|
}
|
|
|
|
func (s *Service) PrepareAppRegistrationImport(
|
|
ctx context.Context,
|
|
userID string,
|
|
request AppRegistrationImportPrepareRequest,
|
|
) (AppRegistrationImportPreparationResult, error) {
|
|
if err := s.authorizeWorkspaceAccess(ctx, request.WorkspaceID); err != nil {
|
|
return AppRegistrationImportPreparationResult{}, err
|
|
}
|
|
lifecycle := s.currentAppRegistrationLifecycle()
|
|
if lifecycle == nil {
|
|
return AppRegistrationImportPreparationResult{}, ErrGitHubNotConfigured
|
|
}
|
|
return lifecycle.PrepareImport(ctx, userID, request)
|
|
}
|
|
|
|
func (s *Service) RenameAppRegistration(
|
|
ctx context.Context,
|
|
userID, registrationID, displayName string,
|
|
) (*AppRegistration, error) {
|
|
lifecycle := s.currentAppRegistrationLifecycle()
|
|
if lifecycle == nil {
|
|
return nil, ErrGitHubNotConfigured
|
|
}
|
|
return lifecycle.Rename(ctx, userID, registrationID, displayName)
|
|
}
|
|
|
|
func (s *Service) DeleteAppRegistration(
|
|
ctx context.Context,
|
|
userID, registrationID string,
|
|
) error {
|
|
lifecycle := s.currentAppRegistrationLifecycle()
|
|
if lifecycle == nil {
|
|
return ErrGitHubNotConfigured
|
|
}
|
|
return lifecycle.Delete(ctx, userID, registrationID)
|
|
}
|
|
|
|
func (s *Service) ResetAppRegistrationsForE2E(ctx context.Context, workspaceID string) error {
|
|
lifecycle := s.currentAppRegistrationLifecycle()
|
|
if lifecycle == nil {
|
|
return nil
|
|
}
|
|
return lifecycle.ResetForE2E(ctx, workspaceID)
|
|
}
|
|
|
|
type runtimeInstallationCredentialProvider struct{ service *Service }
|
|
|
|
func (p *runtimeInstallationCredentialProvider) ResolveInstallation(
|
|
ctx context.Context,
|
|
connection *WorkspaceConnection,
|
|
req ResolveCredentialRequest,
|
|
) (*ResolvedCredential, error) {
|
|
if connection == nil || strings.TrimSpace(connection.AppRegistrationID) == "" {
|
|
return nil, ErrGitHubNotConfigured
|
|
}
|
|
runtime := p.service.currentAppRegistrationRuntime(connection.AppRegistrationID)
|
|
if runtime == nil || runtime.installationProvider == nil {
|
|
return nil, ErrGitHubNotConfigured
|
|
}
|
|
return runtime.installationProvider.ResolveInstallation(ctx, connection, req)
|
|
}
|
|
|
|
func (p *runtimeInstallationCredentialProvider) AppCredentialGeneration(
|
|
registrationID string,
|
|
) (int64, bool) {
|
|
runtime := p.service.currentAppRegistrationRuntime(registrationID)
|
|
if runtime == nil {
|
|
return 0, false
|
|
}
|
|
return runtime.generation, true
|
|
}
|
|
|
|
type runtimeUserCredentialProvider struct{ service *Service }
|
|
|
|
func (p *runtimeUserCredentialProvider) ResolveUser(
|
|
ctx context.Context,
|
|
connection *UserConnection,
|
|
req ResolveCredentialRequest,
|
|
) (*ResolvedCredential, error) {
|
|
if connection == nil || strings.TrimSpace(connection.AppRegistrationID) == "" {
|
|
return nil, ErrGitHubPersonalRequired
|
|
}
|
|
runtime := p.service.currentAppRegistrationRuntime(connection.AppRegistrationID)
|
|
if runtime == nil || runtime.personalAuth == nil {
|
|
return nil, ErrGitHubPersonalRequired
|
|
}
|
|
return (&personalAuthCredentialProvider{service: runtime.personalAuth}).ResolveUser(ctx, connection, req)
|
|
}
|
|
|
|
func (s *Service) SetCredentialBroker(broker *CredentialBroker) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.credentialBroker = broker
|
|
}
|
|
|
|
func (s *Service) CredentialBrokerReady() bool {
|
|
if s == nil {
|
|
return false
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.credentialBroker != nil
|
|
}
|
|
|
|
func (s *Service) ConfigureCredentialBroker(authorizer BrokerScopeAuthorizer) error {
|
|
if s == nil || s.store == nil || s.resolver == nil || authorizer == nil {
|
|
return errors.New("GitHub credential broker dependencies are not configured")
|
|
}
|
|
s.SetCredentialBroker(NewCredentialBroker(s.store, s.resolver, authorizer))
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) IssueGitHubCredentialLease(
|
|
ctx context.Context,
|
|
request CredentialLeaseRequest,
|
|
) (*CredentialLease, error) {
|
|
s.mu.Lock()
|
|
broker := s.credentialBroker
|
|
s.mu.Unlock()
|
|
if broker == nil {
|
|
return nil, ErrGitHubNotConfigured
|
|
}
|
|
return broker.Issue(ctx, request)
|
|
}
|
|
|
|
func (s *Service) ResolveGitHubCredential(
|
|
ctx context.Context,
|
|
request BrokerCredentialRequest,
|
|
) (*BrokerCredential, error) {
|
|
s.mu.Lock()
|
|
broker := s.credentialBroker
|
|
s.mu.Unlock()
|
|
if broker == nil {
|
|
return nil, ErrGitHubNotConfigured
|
|
}
|
|
return broker.Resolve(ctx, request)
|
|
}
|
|
|
|
type WorkspaceAutomationStatus struct {
|
|
*WorkspaceConnection
|
|
Actor *AuthPrincipal `json:"actor,omitempty"`
|
|
Capabilities map[GitHubAppCapability]bool `json:"capabilities,omitempty"`
|
|
MissingCapabilities []GitHubAppCapability `json:"missing_capabilities,omitempty"`
|
|
RateLimit *GitHubRateLimitInfo `json:"rate_limit,omitempty"`
|
|
LegacyMigration bool `json:"legacy_migration"`
|
|
}
|
|
|
|
type WorkspaceAuthStatus struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Automation *WorkspaceAutomationStatus `json:"automation,omitempty"`
|
|
Personal *UserConnection `json:"personal,omitempty"`
|
|
EffectivePersonalActor *AuthPrincipal `json:"effective_personal_actor,omitempty"`
|
|
EffectiveManualMutationActor *AuthPrincipal `json:"effective_manual_mutation_actor,omitempty"`
|
|
AppRegistration *AppRegistration `json:"app_registration,omitempty"`
|
|
GitHubAppAvailable bool `json:"github_app_available"`
|
|
AppAvailable bool `json:"app_available"`
|
|
Authenticated bool `json:"authenticated"`
|
|
Username string `json:"username"`
|
|
AuthMethod string `json:"auth_method"`
|
|
TokenConfigured bool `json:"token_configured"`
|
|
RequiredScopes []string `json:"required_scopes"`
|
|
RateLimit *GitHubRateLimitInfo `json:"rate_limit,omitempty"`
|
|
}
|
|
|
|
func (s *Service) GetWorkspaceAuthStatus(
|
|
ctx context.Context,
|
|
workspaceID, userID string,
|
|
) (*WorkspaceAuthStatus, error) {
|
|
return s.getWorkspaceAuthStatus(ctx, workspaceID, userID, false)
|
|
}
|
|
|
|
// RefreshWorkspaceRateLimit returns the workspace status after fetching a
|
|
// fresh GitHub rate-limit snapshot for the resolved automation credential.
|
|
func (s *Service) RefreshWorkspaceRateLimit(
|
|
ctx context.Context,
|
|
workspaceID, userID string,
|
|
) (*WorkspaceAuthStatus, error) {
|
|
return s.getWorkspaceAuthStatus(ctx, workspaceID, userID, true)
|
|
}
|
|
|
|
func (s *Service) getWorkspaceAuthStatus(
|
|
ctx context.Context,
|
|
workspaceID, userID string,
|
|
refreshRateLimit bool,
|
|
) (*WorkspaceAuthStatus, error) {
|
|
if strings.TrimSpace(workspaceID) == "" {
|
|
return nil, ErrGitHubWorkspaceRequired
|
|
}
|
|
if err := s.authorizeWorkspaceAccess(ctx, workspaceID); err != nil {
|
|
return nil, err
|
|
}
|
|
if s == nil || s.store == nil {
|
|
return nil, ErrGitHubNotConfigured
|
|
}
|
|
connection, err := s.store.GetWorkspaceConnection(ctx, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
personal, err := s.workspacePersonalConnection(ctx, workspaceID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
status := &WorkspaceAuthStatus{
|
|
WorkspaceID: workspaceID, Personal: personal, RequiredScopes: RequiredGitHubScopes,
|
|
AuthMethod: AuthMethodNone,
|
|
}
|
|
if connection == nil {
|
|
return status, nil
|
|
}
|
|
status.AuthMethod = string(connection.Source)
|
|
status.TokenConfigured = connection.Source == ConnectionSourcePAT
|
|
status.Automation = &WorkspaceAutomationStatus{
|
|
WorkspaceConnection: connection,
|
|
LegacyMigration: connection.Source == ConnectionSourceLegacyShared,
|
|
}
|
|
if err := s.populateAppRegistrationStatus(ctx, connection, status); err != nil {
|
|
return nil, err
|
|
}
|
|
if connection.Status != ConnectionStatusActive || s.resolver == nil {
|
|
return status, nil
|
|
}
|
|
automation, resolveErr := s.resolver.Resolve(ctx, ResolveCredentialRequest{
|
|
WorkspaceID: workspaceID, Purpose: CredentialPurposeAutomation,
|
|
})
|
|
if resolveErr != nil || automation == nil {
|
|
return status, nil
|
|
}
|
|
status.Authenticated = true
|
|
status.Username = automation.Principal.Login
|
|
status.Automation.Actor = principalPointer(automation.Principal)
|
|
status.Automation.Capabilities = automation.Capabilities
|
|
status.Automation.MissingCapabilities = missingGitHubCapabilities(automation.Capabilities)
|
|
s.seedWorkspaceRateLimit(ctx, automation, refreshRateLimit)
|
|
status.Automation.RateLimit = rateLimitInfoForTracker(automation.RateTracker)
|
|
status.RateLimit = status.Automation.RateLimit
|
|
|
|
s.populateEffectivePersonalActors(ctx, workspaceID, userID, status)
|
|
return status, nil
|
|
}
|
|
|
|
func (s *Service) seedWorkspaceRateLimit(
|
|
ctx context.Context,
|
|
resolved *ResolvedCredential,
|
|
force bool,
|
|
) {
|
|
if resolved == nil || resolved.RateTracker == nil {
|
|
return
|
|
}
|
|
fetcher, ok := resolved.Client.(RateLimitFetcher)
|
|
if !ok {
|
|
return
|
|
}
|
|
tracker := resolved.RateTracker
|
|
for {
|
|
done, shouldFetch := tracker.beginRateLimitRefresh(force)
|
|
if shouldFetch {
|
|
defer tracker.finishRateLimitRefresh(done)
|
|
break
|
|
}
|
|
if done == nil {
|
|
return
|
|
}
|
|
select {
|
|
case <-done:
|
|
if hasCompleteRateLimitSnapshot(tracker) {
|
|
return
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
seedCtx, cancel := context.WithTimeout(ctx, workspaceRateLimitSeedTimeout)
|
|
defer cancel()
|
|
if err := fetcher.FetchRateLimit(seedCtx); err != nil && s.logger != nil {
|
|
s.logger.Debug("seed workspace GitHub rate limit failed", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
func hasCompleteRateLimitSnapshot(tracker *RateTracker) bool {
|
|
if tracker == nil {
|
|
return false
|
|
}
|
|
all := tracker.All()
|
|
_, hasCore := all[ResourceCore]
|
|
_, hasGraphQL := all[ResourceGraphQL]
|
|
_, hasSearch := all[ResourceSearch]
|
|
return hasCore && hasGraphQL && hasSearch
|
|
}
|
|
|
|
func (s *Service) workspacePersonalConnection(
|
|
ctx context.Context,
|
|
workspaceID, userID string,
|
|
) (*UserConnection, error) {
|
|
if userID == "" {
|
|
return nil, nil
|
|
}
|
|
return s.store.GetUserConnection(ctx, workspaceID, userID)
|
|
}
|
|
|
|
func (s *Service) populateAppRegistrationStatus(
|
|
ctx context.Context,
|
|
connection *WorkspaceConnection,
|
|
status *WorkspaceAuthStatus,
|
|
) error {
|
|
if connection.Source != ConnectionSourceGitHubAppInstallation || connection.AppRegistrationID == "" {
|
|
return nil
|
|
}
|
|
registration, err := s.store.GetAppRegistration(ctx, connection.AppRegistrationID)
|
|
if err != nil || registration == nil {
|
|
return err
|
|
}
|
|
status.AppRegistration = cloneDeploymentAppRegistration(registration)
|
|
runtime := s.AppRegistrationRuntimeSnapshot(registration.ID)
|
|
status.GitHubAppAvailable = registration.Status == AppRegistrationStatusActive && runtime.Ready
|
|
status.AppAvailable = status.GitHubAppAvailable
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) populateEffectivePersonalActors(
|
|
ctx context.Context,
|
|
workspaceID, userID string,
|
|
status *WorkspaceAuthStatus,
|
|
) {
|
|
personalActor, _ := s.resolver.Resolve(ctx, ResolveCredentialRequest{
|
|
WorkspaceID: workspaceID, UserID: userID, Purpose: CredentialPurposePersonalRead,
|
|
})
|
|
if personalActor != nil {
|
|
status.EffectivePersonalActor = principalPointer(personalActor.Principal)
|
|
}
|
|
mutationActor, _ := s.resolver.Resolve(ctx, ResolveCredentialRequest{
|
|
WorkspaceID: workspaceID, UserID: userID, Purpose: CredentialPurposePersonalWrite,
|
|
})
|
|
if mutationActor != nil {
|
|
status.EffectiveManualMutationActor = principalPointer(mutationActor.Principal)
|
|
}
|
|
}
|
|
|
|
func principalPointer(principal AuthPrincipal) *AuthPrincipal {
|
|
copy := principal
|
|
return ©
|
|
}
|
|
|
|
func missingGitHubCapabilities(capabilities map[GitHubAppCapability]bool) []GitHubAppCapability {
|
|
missing := make([]GitHubAppCapability, 0)
|
|
for _, capability := range allGitHubCapabilities {
|
|
if !capabilities[capability] {
|
|
missing = append(missing, capability)
|
|
}
|
|
}
|
|
return missing
|
|
}
|
|
|
|
func (s *Service) StartAppInstallation(
|
|
ctx context.Context,
|
|
workspaceID, userID, registrationID string,
|
|
) (AppInstallationStart, error) {
|
|
if err := s.authorizeWorkspaceAccess(ctx, workspaceID); err != nil {
|
|
return AppInstallationStart{}, err
|
|
}
|
|
runtime := s.currentAppRegistrationRuntime(registrationID)
|
|
if runtime == nil || runtime.appInstallationAuth == nil {
|
|
return AppInstallationStart{}, ErrGitHubNotConfigured
|
|
}
|
|
return runtime.appInstallationAuth.Start(ctx, workspaceID, userID)
|
|
}
|
|
|
|
func (s *Service) CompleteAppInstallation(
|
|
ctx context.Context,
|
|
registrationID string,
|
|
callback AppInstallationCallback,
|
|
) (AppInstallationResult, error) {
|
|
runtime := s.currentAppRegistrationRuntime(registrationID)
|
|
if runtime == nil || runtime.appInstallationAuth == nil {
|
|
return AppInstallationResult{}, ErrGitHubNotConfigured
|
|
}
|
|
return runtime.appInstallationAuth.Complete(ctx, callback)
|
|
}
|
|
|
|
func (s *Service) StartPersonalAuth(
|
|
ctx context.Context,
|
|
workspaceID, userID string,
|
|
) (PersonalAuthStart, error) {
|
|
if err := s.authorizeWorkspaceAccess(ctx, workspaceID); err != nil {
|
|
return PersonalAuthStart{}, err
|
|
}
|
|
connection, err := s.store.GetWorkspaceConnection(ctx, workspaceID)
|
|
if err != nil {
|
|
return PersonalAuthStart{}, err
|
|
}
|
|
if connection == nil || connection.Source != ConnectionSourceGitHubAppInstallation ||
|
|
strings.TrimSpace(connection.AppRegistrationID) == "" {
|
|
return PersonalAuthStart{}, ErrGitHubPersonalRequired
|
|
}
|
|
runtime := s.currentAppRegistrationRuntime(connection.AppRegistrationID)
|
|
if runtime == nil || runtime.personalAuth == nil {
|
|
return PersonalAuthStart{}, ErrGitHubNotConfigured
|
|
}
|
|
return runtime.personalAuth.Start(ctx, workspaceID, userID)
|
|
}
|
|
|
|
func (s *Service) CompletePersonalAuth(
|
|
ctx context.Context,
|
|
registrationID string,
|
|
callback PersonalAuthCallback,
|
|
) (PersonalAuthResult, error) {
|
|
runtime := s.currentAppRegistrationRuntime(registrationID)
|
|
if runtime == nil || runtime.personalAuth == nil {
|
|
return PersonalAuthResult{}, ErrGitHubNotConfigured
|
|
}
|
|
return runtime.personalAuth.Complete(ctx, callback)
|
|
}
|
|
|
|
func (s *Service) DisconnectPersonalAuth(ctx context.Context, workspaceID, userID string) error {
|
|
if err := s.authorizeWorkspaceAccess(ctx, workspaceID); err != nil {
|
|
return err
|
|
}
|
|
if s == nil || s.personalConnections == nil {
|
|
return nil
|
|
}
|
|
return s.personalConnections.RevokePersonalConnection(ctx, workspaceID, userID)
|
|
}
|