Files
kandev/apps/backend/internal/github/service_app_auth.go
T
2026-08-20 19:34:22 +01:00

719 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)
}
func (s *Service) ReissueGitHubCredentialLease(
ctx context.Context,
request CredentialLeaseReissueRequest,
) (*CredentialLease, error) {
s.mu.Lock()
broker := s.credentialBroker
s.mu.Unlock()
if broker == nil {
return nil, ErrGitHubNotConfigured
}
return broker.Reissue(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 &copy
}
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)
}