Add support for changing GitHub OAuth permissions (#4)

This commit is contained in:
Tanuj Mittal 2020-05-29 19:16:50 -07:00 committed by GitHub
parent f308aeb929
commit a6c44e0e69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 28 additions and 10 deletions

View File

@ -1,9 +1,9 @@
import { DefaultButton, IButtonProps, PrimaryButton } from "office-ui-fabric-react"; import { DefaultButton, IButtonProps, Link, PrimaryButton } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { IGitHubBranch, IGitHubRepo } from "../../../GitHub/GitHubClient"; import { IGitHubBranch, IGitHubRepo } from "../../../GitHub/GitHubClient";
import { AddRepoComponent, AddRepoComponentProps } from "./AddRepoComponent"; import { AddRepoComponent, AddRepoComponentProps } from "./AddRepoComponent";
import { AuthorizeAccessComponent, AuthorizeAccessComponentProps } from "./AuthorizeAccessComponent"; import { AuthorizeAccessComponent, AuthorizeAccessComponentProps } from "./AuthorizeAccessComponent";
import { ChildrenMargin, ButtonsFooterStyle, ContentFooterStyle } from "./GitHubStyleConstants"; import { ButtonsFooterStyle, ChildrenMargin, ContentFooterStyle } from "./GitHubStyleConstants";
import { ReposListComponent, ReposListComponentProps } from "./ReposListComponent"; import { ReposListComponent, ReposListComponentProps } from "./ReposListComponent";
export interface GitHubReposComponentProps { export interface GitHubReposComponentProps {
@ -45,6 +45,9 @@ export class GitHubReposComponent extends React.Component<GitHubReposComponentPr
) : ( ) : (
<> <>
<p>{GitHubReposComponent.ManageGitHubRepoDescription}</p> <p>{GitHubReposComponent.ManageGitHubRepoDescription}</p>
<Link style={{ marginTop: ChildrenMargin }} onClick={this.props.resetConnection}>
{GitHubReposComponent.ManageGitHubRepoResetConnection}
</Link>
<ReposListComponent {...this.props.reposListProps} /> <ReposListComponent {...this.props.reposListProps} />
</> </>
); );

View File

@ -60,20 +60,20 @@ describe("GitHubOAuthService", () => {
expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined(); expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined();
}); });
it("startOAuth resets OAuth state", () => { it("startOAuth resets OAuth state", async () => {
let url: string; let url: string;
const windowOpenCallback = jest.fn().mockImplementation((value: string) => { const windowOpenCallback = jest.fn().mockImplementation((value: string) => {
url = value; url = value;
}); });
window.open = windowOpenCallback; window.open = windowOpenCallback;
gitHubOAuthService.startOAuth("scope"); await gitHubOAuthService.startOAuth("scope");
expect(windowOpenCallback).toBeCalled(); expect(windowOpenCallback).toBeCalled();
const initialParams = new URLSearchParams(new URL(url).search); const initialParams = new URLSearchParams(new URL(url).search);
expect(initialParams.get("state")).toBeDefined(); expect(initialParams.get("state")).toBeDefined();
gitHubOAuthService.startOAuth("another scope"); await gitHubOAuthService.startOAuth("another scope");
expect(windowOpenCallback).toBeCalled(); expect(windowOpenCallback).toBeCalled();
const newParams = new URLSearchParams(new URL(url).search); const newParams = new URLSearchParams(new URL(url).search);
@ -106,7 +106,7 @@ describe("GitHubOAuthService", () => {
junoClient.getGitHubToken = getGitHubTokenCallback; junoClient.getGitHubToken = getGitHubTokenCallback;
const initialToken = gitHubOAuthService.getTokenObservable()(); const initialToken = gitHubOAuthService.getTokenObservable()();
const state = gitHubOAuthService.startOAuth("scope"); const state = await gitHubOAuthService.startOAuth("scope");
const params: IGitHubConnectorParams = { const params: IGitHubConnectorParams = {
state, state,
@ -120,7 +120,7 @@ describe("GitHubOAuthService", () => {
}); });
it("finishOAuth updates token to error if state doesn't match", async () => { it("finishOAuth updates token to error if state doesn't match", async () => {
gitHubOAuthService.startOAuth("scope"); await gitHubOAuthService.startOAuth("scope");
const params: IGitHubConnectorParams = { const params: IGitHubConnectorParams = {
state: "state", state: "state",
@ -135,7 +135,7 @@ describe("GitHubOAuthService", () => {
const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NotFound }); const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NotFound });
junoClient.getGitHubToken = getGitHubTokenCallback; junoClient.getGitHubToken = getGitHubTokenCallback;
const state = gitHubOAuthService.startOAuth("scope"); const state = await gitHubOAuthService.startOAuth("scope");
const params: IGitHubConnectorParams = { const params: IGitHubConnectorParams = {
state, state,

View File

@ -2,6 +2,7 @@ import ko from "knockout";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { Logger } from "../Common/Logger"; import { Logger } from "../Common/Logger";
import { config } from "../Config"; import { config } from "../Config";
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { JunoClient } from "../Juno/JunoClient"; import { JunoClient } from "../Juno/JunoClient";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
@ -39,7 +40,19 @@ export class GitHubOAuthService {
this.token = ko.observable<IGitHubOAuthToken>(); this.token = ko.observable<IGitHubOAuthToken>();
} }
public startOAuth(scope: string): string { public async startOAuth(scope: string): Promise<string> {
// If attempting to change scope from "Public & private repos" to "Public only" we need to delete app authorization.
// Otherwise OAuth app still retains the "public & private repos" permissions.
if (
this.token()?.scope === AuthorizeAccessComponent.Scopes.PublicAndPrivate.key &&
scope === AuthorizeAccessComponent.Scopes.Public.key
) {
const logoutSuccessful = await this.logout();
if (!logoutSuccessful) {
return undefined;
}
}
const params = { const params = {
scope, scope,
client_id: config.GITHUB_CLIENT_ID, client_id: config.GITHUB_CLIENT_ID,
@ -76,7 +89,7 @@ export class GitHubOAuthService {
return this.token; return this.token;
} }
public async logout() { public async logout(): Promise<boolean> {
try { try {
const response = await this.junoClient.deleteAppAuthorization(this.token()?.access_token); const response = await this.junoClient.deleteAppAuthorization(this.token()?.access_token);
if (response.status !== HttpStatusCodes.NoContent) { if (response.status !== HttpStatusCodes.NoContent) {
@ -84,10 +97,12 @@ export class GitHubOAuthService {
} }
this.resetToken(); this.resetToken();
return true;
} catch (error) { } catch (error) {
const message = `Failed to delete app authorization: ${error}`; const message = `Failed to delete app authorization: ${error}`;
Logger.logError(message, "GitHubOAuthService/logout"); Logger.logError(message, "GitHubOAuthService/logout");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
return false;
} }
} }