Skip to content

feature: Added FGTs support in github integration #648

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 37 additions & 51 deletions web-server/src/content/Dashboards/ConfigureGithubModalBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useDispatch } from '@/store';
import {
checkGitHubValidity,
linkProvider,
getMissingPATScopes
getMissingTokenScopes
} from '@/utils/auth';
import { depFn } from '@/utils/fn';

Expand All @@ -27,7 +27,6 @@ export const ConfigureGithubModalBody: FC<{
const { enqueueSnackbar } = useSnackbar();
const dispatch = useDispatch();
const isLoading = useBoolState();

const showError = useEasyState<string>('');

const setError = useCallback(
Expand All @@ -49,49 +48,35 @@ export const ConfigureGithubModalBody: FC<{
return;
}
depFn(isLoading.true);
checkGitHubValidity(token.value)
.then(async (isValid) => {
if (!isValid) throw new Error('Invalid token');
})
.then(async () => {
try {
const res = await getMissingPATScopes(token.value);
if (res.length) {
throw new Error(`Token is missing scopes: ${res.join(', ')}`);
}
} catch (e) {
// @ts-ignore
throw new Error(e?.message, e);
}
})
.then(async () => {
try {
return await linkProvider(token.value, orgId, Integration.GITHUB);
} catch (e: any) {
throw new Error(
`Failed to link Github${e?.message ? `: ${e?.message}` : ''}`,
e
);
}
})
.then(() => {
dispatch(fetchCurrentOrg());
dispatch(
fetchTeams({
org_id: orgId
})
try {
const { isValid, tokenType } = await checkGitHubValidity(token.value);
if (!isValid) {
throw new Error('Invalid token');
}

const missingScopes = await getMissingTokenScopes(token.value, tokenType);
if (missingScopes.length) {
throw new Error(
`Token is missing ${
tokenType === 'PAT' ? 'scopes' : 'permissions'
}: ${missingScopes.join(', ')}`
);
enqueueSnackbar('Github linked successfully', {
variant: 'success',
autoHideDuration: 2000
});
onClose();
})
.catch((e) => {
setError(e.message);
console.error(`Error while linking token: ${e.message}`, e);
})
.finally(isLoading.false);
}

await linkProvider(token.value, orgId, Integration.GITHUB, { tokenType });
dispatch(fetchCurrentOrg());
dispatch(fetchTeams({ org_id: orgId }));
enqueueSnackbar(`GitHub linked successfully with ${tokenType}`, {
variant: 'success',
autoHideDuration: 2000
});
onClose();
} catch (e: any) {
setError(`Failed to link GitHub${e?.message ? `: ${e?.message}` : ''}`);
console.error(`Error while linking token: ${e.message}`, e);
} finally {
depFn(isLoading.false);
}
}, [
dispatch,
enqueueSnackbar,
Expand All @@ -106,7 +91,10 @@ export const ConfigureGithubModalBody: FC<{
return (
<FlexBox gap2>
<FlexBox gap={2} minWidth={'400px'} col>
<FlexBox>Enter you Github token below.</FlexBox>
<FlexBox>
Enter your GitHub Personal Access Token (PAT) or Fine-Grained Token
(FGT) below.
</FlexBox>
<FlexBox fullWidth minHeight={'80px'} col>
<TextField
onKeyDown={(e) => {
Expand All @@ -124,7 +112,7 @@ export const ConfigureGithubModalBody: FC<{
onChange={(e) => {
handleChange(e.currentTarget.value);
}}
label="Github Personal Access Token"
label="GitHub Token (PAT or FGT)"
type="password"
/>
<Line error tiny mt={1}>
Expand All @@ -143,7 +131,7 @@ export const ConfigureGithubModalBody: FC<{
textUnderlineOffset: '2px'
}}
>
Generate new classic token
Generate new token
</Line>
</Link>
<Line ml={'5px'}>{' ->'}</Line>
Expand All @@ -153,9 +141,9 @@ export const ConfigureGithubModalBody: FC<{

<FlexBox justifyBetween alignCenter mt={'auto'}>
<FlexBox col sx={{ opacity: 0.8 }}>
<Line>Learn more about Github</Line>
<Line>Learn more about GitHub</Line>
<Line>
Personal Access Token (PAT)
Personal Access Tokens and Fine-Grained Tokens
<Link
ml={1 / 2}
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens"
Expand Down Expand Up @@ -209,12 +197,10 @@ const TokenPermissions = () => {
},
{
height: '120px',

top: '378px'
},
{
height: '120px',

top: '806px'
}
].map((item) => ({ ...item, ...baseStyles }));
Expand Down
40 changes: 33 additions & 7 deletions web-server/src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const linkProvider = async (
reject(isNil, {
provider,
the_good_stuff: stuff,
meta_data: meta
meta_data: { ...meta, tokenType: meta?.tokenType || 'PAT' }
})
);
};
Expand All @@ -29,21 +29,48 @@ export const linkProvider = async (

export async function checkGitHubValidity(
good_stuff: string
): Promise<boolean> {
): Promise<{ isValid: boolean; tokenType: 'PAT' | 'FGT' }> {
try {
await axios.get('https://api.github.com/user', {
const response = await axios.get('https://api.github.com/user', {
headers: {
Authorization: `token ${good_stuff}`
}
});
return true;

const scopesString = response.headers['x-oauth-scopes'] as string;
// PATs have scopes in x-oauth-scopes, FGTs do not
const tokenType = scopesString ? 'PAT' : 'FGT';
return { isValid: true, tokenType };
} catch (error) {
return false;
return { isValid: false, tokenType: 'PAT' };
}
}

const PAT_SCOPES = ['read:org', 'read:user', 'repo', 'workflow'];
export const getMissingPATScopes = async (pat: string) => {

export const getMissingTokenScopes = async (
pat: string,
tokenType: 'PAT' | 'FGT'
) => {
if (tokenType === 'FGT') {
try {
const response = await axios.get('https://api.github.com/user/repos', {
headers: {
Authorization: `token ${pat}`
},
params: { per_page: 1 }
});

// FGTs don't return scopes in headers, so we infer permissions from API access
if (!response.data.length) {
return ['repository_access'];
}
Comment on lines +65 to +67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Clarify repository access requirement logic

The current logic assumes that if no repositories are returned, it means the token lacks repository access. However, this could also happen if the user genuinely has no repositories. Consider adding a more robust check.


🌐 Web query:

How to verify GitHub Fine-Grained Token permissions for repository access?

💡 Result:

Verifying GitHub Fine-Grained Token Permissions for Repository Access

Overview

GitHub Fine-Grained Personal Access Tokens (PATs) allow you to specify exactly which repositories and permissions a token has, providing more control and security compared to classic tokens[7]. However, verifying what a fine-grained token can access after it's been created is less straightforward than with classic tokens.


How to Check Permissions of a Fine-Grained Token

1. Review Token Settings in the GitHub UI

  • Navigate to Settings > Developer settings > Personal access tokens > Fine-grained tokens in your GitHub account.
  • Here, you can see each token, its associated repositories, and the granularity of its permissions (read, write, admin, etc.)[2][9].
  • This section presents the most complete view of the token's capabilities after creation.

2. Understand Permissions via the API

  • When you use a fine-grained token with the REST API, GitHub responds with a special HTTP header:
    X-Accepted-GitHub-Permissions
  • This header indicates the permissions your token provided during the request and what permissions are required for the endpoint. By making test API requests (e.g., to a repository endpoint), you can see if your token is authorized for the action you desire[1][4].

Note: There is currently no single API endpoint that returns a list of all permissions granted to a fine-grained token. Unlike classic tokens, simply requesting the API root does not reveal detailed permissions for fine-grained tokens[3].

3. Testing Permissions

  • Attempt actions (via CLI or API) that require specific permissions (e.g., listing issues, pushing commits). If your token lacks the required permission, the request will fail with an error indicating insufficient permissions.
  • The API documentation for each endpoint specifies which fine-grained permissions are required[1][4][10].

4. Reference Documentation

  • GitHub REST API docs list the fine-grained permissions required for each endpoint, helping you cross-check your token's settings with the needs of your automation or integration[1][4][10].

Key Points to Remember

  • Fine-grained PATs are limited to only those repositories you specify at creation, and only with the precise permissions granted.
  • To verify access, consult the token management page in GitHub or use the REST API and inspect the X-Accepted-GitHub-Permissions header in API responses[1][2][4].
  • If your token needs access to additional repositories or permissions, you must generate a new token or edit the existing one, as permissions cannot be expanded by the token itself after creation.
  • Classic methods for checking token scopes (e.g., requesting the API root) do not work with fine-grained PATs[3].

Summary Table: Fine-Grained Token Verification Methods

Method What You Learn Where to Use
GitHub UI (Token Settings) See repositories/permissions assigned Personal access tokens management page
REST API + X-Accepted-GitHub-Permissions See if a specific API call will succeed Any API request using the fine-grained PAT
Test Actions (CLI/API) Confirm permission by success/failure Command line or custom scripts
API Docs Understand required permissions GitHub REST API documentation

Conclusion

To verify a fine-grained PAT's permissions for repository access, review the token's configuration in GitHub account settings and test API or CLI actions for required functionality. For specific API permission details, inspect the X-Accepted-GitHub-Permissions header in API responses. There is no direct API endpoint to enumerate all permissions granted to a fine-grained token at this time[1][2][3][4].

Citations:


Clarify repository‐access check vs. “no repos” case

Right now, an empty response.data always signals missing repo permissions—even if the user simply has no repos. Instead, inspect the X-Accepted-GitHub-Permissions header to distinguish “no repos because you’re not allowed” from “no repos because there aren’t any.” For example:

- if (!response.data.length) {
-   return ['repository_access'];
- }
+ const perms = response.headers['x-accepted-github-permissions'] as string;
+ if (!response.data.length) {
+   // If the token wasn’t granted repository read access, flag it
+   if (!perms?.includes('contents:read')) {
+     return ['repository_access'];
+   }
+   // Otherwise it’s just an empty repo list
+   return [];
+ }

• File: web-server/src/utils/auth.ts (around lines 65–67)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!response.data.length) {
return ['repository_access'];
}
const perms = response.headers['x-accepted-github-permissions'] as string;
if (!response.data.length) {
// If the token wasn’t granted repository read access, flag it
if (!perms?.includes('contents:read')) {
return ['repository_access'];
}
// Otherwise it’s just an empty repo list
return [];
}

return [];
} catch (error) {
throw new Error('Failed to verify FGT permissions', error);
}
}

try {
const response = await axios.get('https://api.github.com', {
headers: {
Expand All @@ -62,7 +89,6 @@ export const getMissingPATScopes = async (pat: string) => {
};

// Gitlab functions

export const checkGitLabValidity = async (
accessToken: string,
customDomain?: string
Expand Down