Skip to content

Commit

Permalink
Refactor to support permission handling
Browse files Browse the repository at this point in the history
  • Loading branch information
gitkrakel committed Feb 28, 2024
1 parent 68c2290 commit 6c71fbf
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 11 deletions.
9 changes: 9 additions & 0 deletions scripts/makeManifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,21 @@ const getMakeManifest =
strict_min_version: '109.0',
},
},
// Delete this in favor of optional_host_permissions when https://bugzilla.mozilla.org/show_bug.cgi?id=1766026
// is resolved
optional_permissions: [
'*://*/*'
]
};

const chromiumKeys = {
background: {
service_worker: 'dist/service-worker.js',
},
optional_host_permissions: [
// TODO: Move this to `manifestBase` when Firefox supports optional_host_permissions
'*://*/*'
]
};

const manifest = {
Expand Down
15 changes: 14 additions & 1 deletion src/background.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { WebNavigation } from 'webextension-polyfill';
import { scripting, tabs, webNavigation } from 'webextension-polyfill';
import { runtime, scripting , tabs, webNavigation } from 'webextension-polyfill';
import { fetchUser } from './gkApi';
import { injectionScope as inject_azureDevops } from './hosts/azureDevops';
import { injectionScope as inject_bitbucket } from './hosts/bitbucket';
import { injectionScope as inject_github } from './hosts/github';
import { injectionScope as inject_gitlab } from './hosts/gitlab';
import { refreshPermissions } from './permissions-helper';
import { PopupInitMessage } from './shared';

webNavigation.onDOMContentLoaded.addListener(injectScript, {
url: [
Expand All @@ -26,6 +28,14 @@ webNavigation.onHistoryStateUpdated.addListener(details => {
}
});

runtime.onMessage.addListener(async (msg) => {
if (msg === PopupInitMessage) {
return refreshPermissions();
}
console.error('Recevied unknown runtime message', msg);
return undefined;
});

function injectScript(details: WebNavigation.OnDOMContentLoadedDetailsType) {
void scripting.executeScript({
target: { tabId: details.tabId },
Expand Down Expand Up @@ -60,6 +70,9 @@ function getInjectionFn(url: string): (url: string) => void {
const main = async () => {
// The fetchUser function also updates the extension icon if the user is logged in
await fetchUser();

// This removes unneded permissions
await refreshPermissions();
};

void main();
5 changes: 4 additions & 1 deletion src/domUtils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
export const createAnchor = (href: string, target?: string) => {
export const createAnchor = (href: string, target?: string, callback?: () => void) => {
const a = document.createElement('a');
a.href = href;
if (target) {
a.target = target;
}
if (callback) {
a.addEventListener('click', callback);
}

return a;
};
Expand Down
21 changes: 15 additions & 6 deletions src/gkApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ const gkDotDevUrl = 'https://gitkraken.dev';
const accessTokenCookieName = 'accessToken';

const getAccessToken = async () => {
// Attempt to get the access token cookie from GitKraken.dev
const cookie = await cookies.get({
url: gkDotDevUrl,
name: accessTokenCookieName,
});
try {
const cookie = await cookies.get({
url: gkDotDevUrl,
name: accessTokenCookieName,
});

return cookie?.value;
return cookie?.value;
} catch (e) {
if ((e as Error)?.message.includes('No host permissions for cookies at url')) {
// ignore as we are waiting for required permissions
} else {
// otherwise log error and continue as if logged out
console.error(e);
}
}
return undefined;
};

export const fetchUser = async () => {
Expand Down
49 changes: 49 additions & 0 deletions src/permissions-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Permissions } from 'webextension-polyfill';
import { permissions } from 'webextension-polyfill';
import { arrayDifference, CloudProviders } from './shared';

function domainToMatchPattern(domain: string): string {
return `*://*.${domain}/*`;
}

const RequiredOriginPatterns = [
// Without this permission, the extension cannot login
'gitkraken.dev'
].map(domainToMatchPattern);
const CloudProviderOriginPatterns = CloudProviders.map(domainToMatchPattern);

export type OriginTypes = 'required' | 'cloud';

export interface PermissionsRequest {
request: Permissions.Permissions;
hasRequired: boolean;
hasCloud: boolean;
}

export async function refreshPermissions(): Promise<PermissionsRequest | undefined> {
const exitingPermissions = await permissions.getAll();

const newRequiredOrigins = arrayDifference(RequiredOriginPatterns, exitingPermissions.origins);
const newCloudOrigins = arrayDifference(CloudProviderOriginPatterns, exitingPermissions.origins);
const newOrigins = [...newRequiredOrigins, ...newCloudOrigins];
const unusedOrigins = arrayDifference(exitingPermissions.origins, [...RequiredOriginPatterns, ...CloudProviderOriginPatterns]);

if (!unusedOrigins.length) {
const unusedPermissions: Permissions.Permissions = {
origins: unusedOrigins
};
const result = await permissions.remove(unusedPermissions);
if (!result) {
console.warn('Failed to remove unnecessary permissions');
}
}
return newOrigins.length
? {
request: {
origins: newOrigins,
},
hasRequired: Boolean(newRequiredOrigins.length),
hasCloud: Boolean(newCloudOrigins.length)
}
: undefined;
}
57 changes: 54 additions & 3 deletions src/popup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Note: This code runs every time the extension popup is opened.

import { permissions, runtime } from 'webextension-polyfill';
import { createAnchor, createFAIcon } from './domUtils';
import { fetchUser, logoutUser } from './gkApi';
import type { PermissionsRequest } from './permissions-helper';
import { PopupInitMessage } from './shared';
import type { User } from './types';

// Source: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#basic_example
Expand Down Expand Up @@ -137,16 +140,64 @@ const renderLoggedOutContent = () => {
mainEl.append(signUpPromo);
};

const main = async () => {
const syncWithBackground = async () => {
return await runtime.sendMessage(PopupInitMessage) as PermissionsRequest | undefined;
};

function reloadPopup() {
// This seems to work on Firefox and Chromium but I couldn't find any docs confirming this is the correct way
window.location.reload();
}

const renderPermissionRequest = (permissionsRequest: PermissionsRequest) => {
const mainEl = document.getElementById('main-content')!;

const permissionRequestLink = createAnchor('#', undefined, async () => {
await permissions.request(permissionsRequest.request);
reloadPopup();
});
permissionRequestLink.classList.add('menu-row');
if (permissionsRequest.hasRequired) {
permissionRequestLink.append(createFAIcon('fa-triangle-exclamation'), 'Allow required permissions to continue');
mainEl.append(permissionRequestLink);

const supportLink = createAnchor(
'https://help.gitkraken.com/browser-extension/gitkraken-browser-extension',
'_blank',
);
supportLink.append(createFAIcon('fa-question-circle'), 'Support');
supportLink.classList.add('menu-row');
mainEl.append(supportLink);
} else {
permissionRequestLink.append(createFAIcon('fa-exclamation'), `Allow permissions for cloud providers`);
mainEl.append(permissionRequestLink);
}
};

const finishLoading = () => {
const loadingIcon = document.getElementById('loading-icon');
loadingIcon?.remove();
};

async function main() {
const permissionsRequest = await syncWithBackground();
if (permissionsRequest) {
renderPermissionRequest(permissionsRequest);
if (permissionsRequest.hasRequired) {
// Only required permissions blocks the UI
finishLoading();
return;
}
}

const user = await fetchUser();
if (user) {
void renderLoggedInContent(user);
} else {
renderLoggedOutContent();
}

const loadingIcon = document.getElementById('loading-icon');
loadingIcon?.remove();
finishLoading();
};

void main();
20 changes: 20 additions & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { action } from 'webextension-polyfill';

export const PopupInitMessage = 'popupInit';

const IconPaths = {
Grey: {
16: '/icons/gk-grey-16.png',
Expand All @@ -15,5 +17,23 @@ const IconPaths = {
},
};

export const CloudProviders = [
'github.com',
'gitlab.com',
'bitbucket.org',
'dev.azure.com',
];

export const updateExtensionIcon = (isLoggedIn: boolean) =>
action.setIcon({ path: isLoggedIn ? IconPaths.Green : IconPaths.Grey });

// Basically ramda's difference() function but it accepts undefined as empty arrays
export function arrayDifference<T>(first: T[] | undefined, second: T[] | undefined): T[] {
if (!first) {
return [];
}
if (!second) {
return first;
}
return first.filter(x => !second.includes(x));
}

0 comments on commit 6c71fbf

Please sign in to comment.