githubauth4datatug

package
v0.21.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: MIT Imports: 22 Imported by: 0

README

DataTug GitHub actor authorization

This package implements server-side GitHub App user authorization for the existing DataTug App (datatug, App ID 5223634). Browser code starts the OAuth redirect and receives only the callback result; the access and rotating refresh tokens stay encrypted in dedicated top-level DAL records keyed by a hash of the Firebase UID. These records are outside /users/{uid} and contain no client-membership fields. Repository operations must obtain a fresh AuthorizedGitHubRepository from Provider.AuthorizeRepository for each request.

Operator setup

Before enabling the provider:

  1. Configure the existing DataTug GitHub App (ID 5223634) with repository Contents: read and write. Its currently observed public permissions are empty, so repository operations remain denied until the App permission is configured and affected installations accept the updated permission.

  2. Enable user-to-server authorization with expiring user access tokens and refresh tokens. Keep the App's client secret server-side.

  3. Register https://datatug.app/github/callback as the App callback URL and provide that same URL as DATATUG_GITHUB_APP_CALLBACK_URL. The browser must remove the short-lived code and state query values before analytics, then send them in the authenticated completion request to the Cloud API.

  4. Configure server-side names only: DATATUG_GITHUB_APP_ID (must be 5223634), DATATUG_GITHUB_APP_CLIENT_ID, DATATUG_GITHUB_APP_CLIENT_SECRET, DATATUG_GITHUB_APP_PRIVATE_KEY (for the dedicated App installation authorizer), DATATUG_GITHUB_APP_CALLBACK_URL, and the existing SNEAT_PLATFORM_CRYPTO_KEY (32 decoded bytes).

  5. Before enabling production credentials, add both datatug_github_app_credentials and datatug_github_oauth_states to the server-only root exclusions in sneat-firebase/firebase/firestore.rules (isClientReadableRoot). The records live outside /users/{uid} so the recursive user read grant does not apply; the explicit exclusion prevents a future membership-shaped field or broad client grant from exposing them. This backend package does not change Firebase rules.

Do not reuse OpenVaultDB's App identity, grants, or credential records. The production installation authorizer reuses the existing Sneat/WB App-JWT signing helper, verifies the current installation and immutable selected repository directly. It validates App ID, account, suspension state, and effective Contents permission. The actor provider separately verifies the repository's immutable ID/owner/name with the user token. No installation token is minted; all project operations use the GitHub user actor token. Missing or invalid DataTug configuration must leave the feature unavailable. GitHub tokens stay server-side and encrypted. The one-time OAuth code/state and PKCE challenge are protocol values; never log them, persist them in project files, or retain the callback query after the browser captures it.

Documentation

Index

Constants

View Source
const (
	DataTugGitHubAppSlug = "datatug"
	DataTugGitHubAppID   = int64(5223634)
)

Variables

View Source
var (
	ErrCredentialMissing       = errors.New("GitHub connection is missing")
	ErrCredentialChanged       = errors.New("GitHub connection changed during refresh")
	ErrRefreshInProgress       = errors.New("GitHub token refresh is in progress")
	ErrReauthorizationRequired = errors.New("GitHub connection must be reauthorized")
	ErrOAuthStateInvalid       = errors.New("GitHub OAuth state is invalid or expired")
)
View Source
var (
	ErrGitHubAppNotConfigured = errors.New("DataTug GitHub App authorization is not configured")
	ErrGitHubActorMismatch    = errors.New("GitHub account does not match the connected Firebase user")
	ErrGitHubRepositoryDenied = errors.New("GitHub repository is not authorized")
	ErrGitHubPermissionDenied = errors.New("GitHub user does not have the required repository permission")
)

Functions

func ScrubOAuthCallbackURL

func ScrubOAuthCallbackURL(request *http.Request) (code, state string, err error)

ScrubOAuthCallbackURL must be the first callback-handler operation. It extracts the one-time code/state in memory, then removes every query value from both URL representations before application logging or routing.

Types

type AuthorizedGitHubRepository

type AuthorizedGitHubRepository struct {
	// contains filtered or unexported fields
}

AuthorizedGitHubRepository is an ephemeral, actor-bound API client. The underlying user token remains private in the transport and the client is restricted to the selected repository and operation permission.

func (*AuthorizedGitHubRepository) CreateCommitOnBranch

func (r *AuthorizedGitHubRepository) CreateCommitOnBranch(ctx context.Context, branch, expectedHeadOID, message string, changes []GitHubFileChange) (GitHubCommit, error)

func (*AuthorizedGitHubRepository) GetBlob

func (r *AuthorizedGitHubRepository) GetBlob(ctx context.Context, blobOID string) ([]byte, error)

func (*AuthorizedGitHubRepository) GetCommit

func (*AuthorizedGitHubRepository) GetRef

func (r *AuthorizedGitHubRepository) GetRef(ctx context.Context, branch string) (GitHubRef, error)

func (*AuthorizedGitHubRepository) GetTree

func (r *AuthorizedGitHubRepository) GetTree(ctx context.Context, treeOID string) ([]GitHubTreeEntry, error)

GetTree returns all descendants of treeOID, recursively. A truncated GitHub response is an error.

func (*AuthorizedGitHubRepository) ListBranches

func (r *AuthorizedGitHubRepository) ListBranches(ctx context.Context) ([]GitHubBranch, error)

func (*AuthorizedGitHubRepository) Scope

func (*AuthorizedGitHubRepository) UpdateRefs

func (r *AuthorizedGitHubRepository) UpdateRefs(ctx context.Context, updates []GitHubRefUpdate) error

type CredentialStore

type CredentialStore struct {
	// contains filtered or unexported fields
}

CredentialStore persists only purpose-encrypted GitHub tokens. Refresh state is updated in DAL transactions so concurrent Cloud API instances cannot both consume the same rotating refresh token.

func NewCredentialStore

func NewCredentialStore(db dal.DB, platformCryptoRoot []byte, now func() time.Time, refreshLease time.Duration) (*CredentialStore, error)

type GitHubActor

type GitHubActor struct {
	ID    int64
	Login string
}

type GitHubAppClient

type GitHubAppClient struct {
	// contains filtered or unexported fields
}

func NewGitHubAppClient

func NewGitHubAppClient(config GitHubAppConfig, client *http.Client) (*GitHubAppClient, error)

func (*GitHubAppClient) AuthorizationURL

func (c *GitHubAppClient) AuthorizationURL(state, codeChallenge string, repositoryID int64) (string, error)

func (*GitHubAppClient) Exchange

func (c *GitHubAppClient) Exchange(ctx context.Context, code, verifier string) (OAuthTokens, error)

func (*GitHubAppClient) Refresh

func (c *GitHubAppClient) Refresh(ctx context.Context, refreshToken string) (OAuthTokens, error)

func (*GitHubAppClient) ScopedHTTPClient

func (c *GitHubAppClient) ScopedHTTPClient(tokens OAuthTokens, repo RepositoryRef, nodeID string, operation RepositoryOperation) *http.Client

func (*GitHubAppClient) String

func (c *GitHubAppClient) String() string

func (*GitHubAppClient) UserClient

func (c *GitHubAppClient) UserClient(tokens OAuthTokens) repositoryUserClient

type GitHubAppConfig

type GitHubAppConfig struct {
	AppID         int64
	ClientID      string
	ClientSecret  string
	PrivateKeyPEM []byte
	CallbackURL   string
}

GitHubAppConfig contains server-side settings for the existing DataTug GitHub App. Secret values are accepted only by the server and are never persisted by this package.

type GitHubAppInstallationAuthorizer

type GitHubAppInstallationAuthorizer struct {
	// contains filtered or unexported fields
}

GitHubAppInstallationAuthorizer checks the dedicated DataTug App's current installation for the selected repository. Provider first verifies the immutable repository ID, owner, and name with the GitHub user actor token; this App-JWT request then proves that the same owner/repository path is in the App installation and returns its effective Contents permission.

func NewGitHubAppInstallationAuthorizer

func NewGitHubAppInstallationAuthorizer(appID int64, privateKeyPEM []byte) (*GitHubAppInstallationAuthorizer, error)

func (*GitHubAppInstallationAuthorizer) AuthorizeDataTugAppRepository

func (a *GitHubAppInstallationAuthorizer) AuthorizeDataTugAppRepository(ctx context.Context, repo RepositoryRef) (GitHubAppRepositoryAccess, error)

type GitHubAppRepositoryAccess

type GitHubAppRepositoryAccess struct {
	InstallationID int64
	RepositoryID   int64
	ContentsRead   bool
	ContentsWrite  bool
}

type GitHubBranch

type GitHubBranch struct {
	Name string
	OID  string
}

GitHubBranch is a branch name and its current immutable commit identifier.

type GitHubCommit

type GitHubCommit struct {
	OID        string
	TreeOID    string
	Message    string
	ParentOIDs []string
}

type GitHubFileChange

type GitHubFileChange struct {
	Path    string
	Content []byte
	Delete  bool
}

GitHubFileChange adds/replaces a file with raw bytes, or deletes it when Delete is true.

type GitHubRef

type GitHubRef struct {
	Name string
	OID  string
}

GitHubRef is an immutable reference snapshot. OID is the observed target.

type GitHubRefUpdate

type GitHubRefUpdate struct {
	Name      string
	BeforeOID string
	AfterOID  string
}

GitHubRefUpdate is an atomic compare-and-swap update for one branch ref.

type GitHubRepository

type GitHubRepository struct {
	ID                  int64
	NodeID              string
	Owner               string
	Name                string
	DefaultBranch       string
	Permissions         GitHubRepositoryPermissions
	EffectivePermission RepositoryOperation
}

type GitHubRepositoryPermissions

type GitHubRepositoryPermissions struct {
	Pull  bool
	Push  bool
	Admin bool
}

type GitHubTreeEntry

type GitHubTreeEntry struct {
	Path string
	Type string
	OID  string
	Mode string
	Size int64
}

GitHubTreeEntry identifies one path in a complete recursive repository tree.

type InstallationAuthorizer

type InstallationAuthorizer interface {
	AuthorizeDataTugAppRepository(context.Context, RepositoryRef) (GitHubAppRepositoryAccess, error)
}

InstallationAuthorizer proves that the existing DataTug GitHub App's selected-repository installation currently includes a project repository. Its adapter may use App authentication, but an installation token is never returned as the project actor.

type OAuthTokens

type OAuthTokens struct {
	// contains filtered or unexported fields
}

OAuthTokens exists only in memory. Its string representations and default JSON representation intentionally omit token material.

func (OAuthTokens) GoString

func (OAuthTokens) GoString() string

func (OAuthTokens) MarshalJSON

func (OAuthTokens) MarshalJSON() ([]byte, error)

func (OAuthTokens) String

func (OAuthTokens) String() string

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

func NewProvider

func NewProvider(db dal.DB, config GitHubAppConfig, platformCryptoRoot []byte, installation InstallationAuthorizer, options ProviderOptions) (*Provider, error)

func (*Provider) AuthorizeRepository

func (p *Provider) AuthorizeRepository(ctx context.Context, firebaseUID string, repoRef RepositoryRef, operation RepositoryOperation) (*AuthorizedGitHubRepository, error)

AuthorizeRepository rechecks Firebase UID to GitHub actor binding, selected App installation membership, repository identity, and the requested current permission on every read or write.

func (*Provider) BeginAuthorization

func (p *Provider) BeginAuthorization(ctx context.Context, firebaseUID string) (string, error)

BeginAuthorization connects a GitHub actor before repository selection. The returned URL contains only short-lived state and a PKCE challenge.

func (*Provider) BeginRepositoryAuthorization

func (p *Provider) BeginRepositoryAuthorization(ctx context.Context, firebaseUID string, repo RepositoryRef) (string, error)

BeginRepositoryAuthorization starts a repository-bound reconnection for an already selected project. Initial connection uses BeginAuthorization.

func (*Provider) CompleteAuthorization

func (p *Provider) CompleteAuthorization(ctx context.Context, firebaseUID, state, code string) error

CompleteAuthorization consumes the actor-bound one-time state before exchanging the callback code. Callers must scrub code and state from the callback URL before routing, logging, or analytics.

func (*Provider) ListRepositories

func (p *Provider) ListRepositories(ctx context.Context, firebaseUID string) ([]GitHubRepository, error)

ListRepositories returns only repositories the connected actor can read and the dedicated DataTug App currently has installed. It conveys no mutation authority; project operations still call AuthorizeRepository every time.

func (*Provider) ResolveRepositoryByName added in v0.17.1

func (p *Provider) ResolveRepositoryByName(ctx context.Context, firebaseUID, owner, name string) (GitHubRepository, error)

ResolveRepositoryByName resolves a selected owner/name through the current connected actor token. The caller must still run the fresh App and operation authorization path using the returned immutable ID.

func (*Provider) String

func (p *Provider) String() string

type ProviderOptions

type ProviderOptions struct {
	Now                 func() time.Time
	OAuthStateLifetime  time.Duration
	RefreshSkew         time.Duration
	RefreshLease        time.Duration
	RefreshPollInterval time.Duration
	RefreshWaitLimit    time.Duration
}

type RepositoryOperation

type RepositoryOperation string
const (
	RepositoryRead  RepositoryOperation = "read"
	RepositoryWrite RepositoryOperation = "write"
)

type RepositoryRef

type RepositoryRef struct {
	ID    int64  `json:"id" firestore:"id"`
	Owner string `json:"owner" firestore:"owner"`
	Name  string `json:"name" firestore:"name"`
}

func (RepositoryRef) Validate

func (r RepositoryRef) Validate() error

type RepositoryScope

type RepositoryScope struct {
	FirebaseUID string
	Actor       GitHubActor
	Repository  GitHubRepository
	Permission  RepositoryOperation
}

RepositoryScope returns a value snapshot of the verified actor and immutable repository identity. Mutating the returned value cannot widen the client.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL