Currently the `GitClient` accepts a generic parameter for determining
whether the `githubToken` should be set or not. This worked fine so far
in terms of distinguishing between an authenticated and
non-authenticated git client instance, but if we intend to conditionally
show methods only for authenticated instances, the generic parameter
is not suitable.
This commit splits up the `GitClient` into two classes. One for
the base logic without any authorization, and a second class that
extends the base logic with authentication logic. i.e. the
`AuthenticatedGitClient`. This allows us to have specific methods only
for the authenticated instance. e.g.
  * `hasOauthScopes` has been moved to only exist for authenticated
    instances.
  * the GraphQL functionality within `gitClient.github` is not
    accessible for non-authenticated instances. GraphQL API requires
    authentication as per Github.
The initial motiviation for this was that we want to throw if
`hasOAuthScopes` is called without the Octokit instance having
a token configured. This should help avoiding issues as within
3b434ed94d
that prevented the caretaker process momentarily.
Additionally, the Git client has moved from `index.ts` to
`git-client.ts` for better discoverability in the codebase.
PR Close #42468
		
	
			
		
			
				
	
	
		
			128 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			128 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/**
 | 
						|
 * @license
 | 
						|
 * Copyright Google LLC All Rights Reserved.
 | 
						|
 *
 | 
						|
 * Use of this source code is governed by an MIT-style license that can be
 | 
						|
 * found in the LICENSE file at https://angular.io/license
 | 
						|
 */
 | 
						|
import {Octokit} from '@octokit/rest';
 | 
						|
 | 
						|
import {NgDevConfig} from '../config';
 | 
						|
import {yellow} from '../console';
 | 
						|
 | 
						|
import {GitClient} from './git-client';
 | 
						|
import {AuthenticatedGithubClient} from './github';
 | 
						|
import {getRepositoryGitUrl, GITHUB_TOKEN_GENERATE_URL, GITHUB_TOKEN_SETTINGS_URL} from './github-urls';
 | 
						|
 | 
						|
/** Github response type extended to include the `x-oauth-scopes` headers presence. */
 | 
						|
type RateLimitResponseWithOAuthScopeHeader = Octokit.Response<Octokit.RateLimitGetResponse>&{
 | 
						|
  headers: {'x-oauth-scopes': string|undefined};
 | 
						|
};
 | 
						|
 | 
						|
/** Describes a function that can be used to test for given Github OAuth scopes. */
 | 
						|
export type OAuthScopeTestFunction = (scopes: string[], missing: string[]) => void;
 | 
						|
 | 
						|
/**
 | 
						|
 * Extension of the `GitClient` with additional utilities which are useful for
 | 
						|
 * authenticated Git client instances.
 | 
						|
 */
 | 
						|
export class AuthenticatedGitClient extends GitClient {
 | 
						|
  /**
 | 
						|
   * Regular expression that matches the provided Github token. Used for
 | 
						|
   * sanitizing the token from Git child process output.
 | 
						|
   */
 | 
						|
  private readonly _githubTokenRegex: RegExp = new RegExp(this.githubToken, 'g');
 | 
						|
 | 
						|
  /** The OAuth scopes available for the provided Github token. */
 | 
						|
  private _cachedOauthScopes: Promise<string[]>|null = null;
 | 
						|
 | 
						|
  /** Instance of an authenticated github client. */
 | 
						|
  readonly github = new AuthenticatedGithubClient(this.githubToken);
 | 
						|
 | 
						|
  protected constructor(readonly githubToken: string, baseDir?: string, config?: NgDevConfig) {
 | 
						|
    super(baseDir, config);
 | 
						|
  }
 | 
						|
 | 
						|
  /** Sanitizes a given message by omitting the provided Github token if present. */
 | 
						|
  sanitizeConsoleOutput(value: string): string {
 | 
						|
    return value.replace(this._githubTokenRegex, '<TOKEN>');
 | 
						|
  }
 | 
						|
 | 
						|
  /** Git URL that resolves to the configured repository. */
 | 
						|
  getRepoGitUrl() {
 | 
						|
    return getRepositoryGitUrl(this.remoteConfig, this.githubToken);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Assert the GitClient instance is using a token with permissions for the all of the
 | 
						|
   * provided OAuth scopes.
 | 
						|
   */
 | 
						|
  async hasOauthScopes(testFn: OAuthScopeTestFunction): Promise<true|{error: string}> {
 | 
						|
    const scopes = await this._fetchAuthScopesForToken();
 | 
						|
    const missingScopes: string[] = [];
 | 
						|
    // Test Github OAuth scopes and collect missing ones.
 | 
						|
    testFn(scopes, missingScopes);
 | 
						|
    // If no missing scopes are found, return true to indicate all OAuth Scopes are available.
 | 
						|
    if (missingScopes.length === 0) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    // Pre-constructed error message to log to the user, providing missing scopes and
 | 
						|
    // remediation instructions.
 | 
						|
    const error =
 | 
						|
        `The provided <TOKEN> does not have required permissions due to missing scope(s): ` +
 | 
						|
        `${yellow(missingScopes.join(', '))}\n\n` +
 | 
						|
        `Update the token in use at:\n` +
 | 
						|
        `  ${GITHUB_TOKEN_SETTINGS_URL}\n\n` +
 | 
						|
        `Alternatively, a new token can be created at: ${GITHUB_TOKEN_GENERATE_URL}\n`;
 | 
						|
 | 
						|
    return {error};
 | 
						|
  }
 | 
						|
 | 
						|
  /** Fetch the OAuth scopes for the loaded Github token. */
 | 
						|
  private _fetchAuthScopesForToken() {
 | 
						|
    // If the OAuth scopes have already been loaded, return the Promise containing them.
 | 
						|
    if (this._cachedOauthScopes !== null) {
 | 
						|
      return this._cachedOauthScopes;
 | 
						|
    }
 | 
						|
    // OAuth scopes are loaded via the /rate_limit endpoint to prevent
 | 
						|
    // usage of a request against that rate_limit for this lookup.
 | 
						|
    return this._cachedOauthScopes = this.github.rateLimit.get().then(_response => {
 | 
						|
      const response = _response as RateLimitResponseWithOAuthScopeHeader;
 | 
						|
      const scopes = response.headers['x-oauth-scopes'];
 | 
						|
 | 
						|
      // If no token is provided, or if the Github client is authenticated incorrectly,
 | 
						|
      // the `x-oauth-scopes` response header is not set. We error in such cases as it
 | 
						|
      // signifies a faulty  of the
 | 
						|
      if (scopes === undefined) {
 | 
						|
        throw Error('Unable to retrieve OAuth scopes for token provided to Git client.');
 | 
						|
      }
 | 
						|
 | 
						|
      return scopes.split(',').map(scope => scope.trim()).filter(scope => scope !== '');
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  /** The singleton instance of the `AuthenticatedGitClient`. */
 | 
						|
  private static _authenticatedInstance: AuthenticatedGitClient;
 | 
						|
 | 
						|
  /**
 | 
						|
   * Static method to get the singleton instance of the `AuthenticatedGitClient`,
 | 
						|
   * creating it if it has not yet been created.
 | 
						|
   */
 | 
						|
  static get(): AuthenticatedGitClient {
 | 
						|
    if (!AuthenticatedGitClient._authenticatedInstance) {
 | 
						|
      throw new Error('No instance of `AuthenticatedGitClient` has been set up yet.');
 | 
						|
    }
 | 
						|
    return AuthenticatedGitClient._authenticatedInstance;
 | 
						|
  }
 | 
						|
 | 
						|
  /** Configures an authenticated git client. */
 | 
						|
  static configure(token: string): void {
 | 
						|
    if (AuthenticatedGitClient._authenticatedInstance) {
 | 
						|
      throw Error(
 | 
						|
          'Unable to configure `AuthenticatedGitClient` as it has been configured already.');
 | 
						|
    }
 | 
						|
    AuthenticatedGitClient._authenticatedInstance = new AuthenticatedGitClient(token);
 | 
						|
  }
 | 
						|
}
 |