Skip to content

Commit 970a8df

Browse files
authored
Add GitHub App auth provider (#15500)
Signed-off-by: Phillip Rak <[email protected]>
1 parent e8a80fc commit 970a8df

File tree

14 files changed

+488
-67
lines changed

14 files changed

+488
-67
lines changed

cypress/e2e/po/components/labeled-input.po.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default class LabeledInputPo extends ComponentPo {
2626
* @returns
2727
*/
2828
set(value: any, secret?: boolean, parseSpecialCharSequences?: boolean): Cypress.Chainable {
29+
this.input().scrollIntoView();
2930
this.input().should('be.visible');
3031
this.input().focus();
3132
this.input().clear();
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import PagePo from '@/cypress/e2e/po/pages/page.po';
2+
import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po';
3+
import AsyncButtonPo from '@/cypress/e2e/po/components/async-button.po';
4+
import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po';
5+
import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po';
6+
7+
export default class GithubAppPo extends PagePo {
8+
private static createPath(clusterId: string, id?: string ) {
9+
return `/c/${ clusterId }/auth/config/githubapp?mode=edit`;
10+
}
11+
12+
static goTo(clusterId: string): Cypress.Chainable<Cypress.AUTWindow> {
13+
return super.goTo(GithubAppPo.createPath(clusterId));
14+
}
15+
16+
constructor(clusterId: string) {
17+
super(GithubAppPo.createPath(clusterId));
18+
}
19+
20+
static navTo() {
21+
const sideNav = new ProductNavPo();
22+
23+
BurgerMenuPo.burgerMenuNavToMenubyLabel('Users & Authentication');
24+
sideNav.navToSideMenuEntryByLabel('Auth Provider');
25+
}
26+
27+
clientIdInputField() {
28+
return this.self().get('[data-testid="client-id"').invoke('val');
29+
}
30+
31+
enterClientId(id: string) {
32+
return new LabeledInputPo('[data-testid="client-id"]').set(id);
33+
}
34+
35+
clientSecretInputField() {
36+
return this.self().get('[data-testid="client-secret"').invoke('val');
37+
}
38+
39+
enterClientSecret(val: string) {
40+
return new LabeledInputPo('[data-testid="client-secret"]').set(val);
41+
}
42+
43+
gitHubAppIdInputField() {
44+
return this.self().get('[data-testid="app-id"').invoke('val');
45+
}
46+
47+
enterGitHubAppId(val: string) {
48+
return new LabeledInputPo('[data-testid="app-id"]').set(val);
49+
}
50+
51+
installationIdInputField() {
52+
return this.self().get('[data-testid="installation-id"').invoke('val');
53+
}
54+
55+
enterInstallationId(val: string) {
56+
return new LabeledInputPo('[data-testid="installation-id"]').set(val);
57+
}
58+
59+
privateKeyInputField() {
60+
return this.self().get('[data-testid="private-key"').invoke('val');
61+
}
62+
63+
enterPrivateKey(val: string) {
64+
return new LabeledInputPo('[data-testid="private-key"]').set(val);
65+
}
66+
67+
saveButton(): AsyncButtonPo {
68+
return new AsyncButtonPo('[data-testid="form-save"]', this.self());
69+
}
70+
71+
save() {
72+
return new AsyncButtonPo('[data-testid="form-save"]').click();
73+
}
74+
75+
gitHubAppBanner() {
76+
return this.self().get('[data-testid="github-app-banner"]');
77+
}
78+
79+
permissionsWarningBanner() {
80+
return this.self().get('[data-testid="auth-provider-admin-permissions-warning-banner"]');
81+
}
82+
}

cypress/e2e/po/pages/users-and-auth/authProvider.po.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import SelectIconGridPo from '@/cypress/e2e/po/components/select-icon-grid.po';
66
export enum AuthProvider {
77
AMAZON_COGNITO = 'Amazon Cognito', // eslint-disable-line no-unused-vars
88
AZURE = 'AzureAD', // eslint-disable-line no-unused-vars
9+
GITHUB_APP = 'GitHub App', // eslint-disable-line no-unused-vars
910
}
1011

1112
export class AuthProviderPo extends PagePo {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import HomePagePo from '@/cypress/e2e/po/pages/home.po';
2+
import GithubAppPo from '@/cypress/e2e/po/edit/auth/githubapp.po';
3+
import { AuthProvider, AuthProviderPo } from '@/cypress/e2e/po/pages/users-and-auth/authProvider.po';
4+
5+
const authProviderPo = new AuthProviderPo('local');
6+
const githubAppPo = new GithubAppPo('local');
7+
8+
const clientId = 'test-client-id';
9+
const clientSecret = 'test-client-secret';
10+
const appId = 'test-app-id';
11+
const privateKey = 'test-private-key';
12+
13+
const mockStatusCode = 200;
14+
const mockBody = {};
15+
16+
describe('GitHub App', { tags: ['@adminUser', '@usersAndAuths'] }, () => {
17+
beforeEach(() => {
18+
cy.login();
19+
HomePagePo.goToAndWaitForGet();
20+
AuthProviderPo.navTo();
21+
authProviderPo.waitForPage();
22+
authProviderPo.selectProvider(AuthProvider.GITHUB_APP);
23+
githubAppPo.waitForPage();
24+
});
25+
26+
it('can navigate to Auth Provider and Select GitHub App', () => {
27+
githubAppPo.mastheadTitle().should('include', `GitHub App`);
28+
29+
githubAppPo.gitHubAppBanner().should('be.visible');
30+
githubAppPo.permissionsWarningBanner().should('be.visible');
31+
});
32+
33+
it('sends correct request to create GitHub App auth provider', () => {
34+
cy.intercept('POST', 'v3/githubAppConfigs/githubapp?action=configureTest', (req) => {
35+
expect(req.body.enabled).to.equal(false);
36+
expect(req.body.id).to.equal('githubapp');
37+
expect(req.body.type).to.equal('githubAppConfig');
38+
expect(req.body.clientId).to.equal(clientId);
39+
expect(req.body.clientSecret).to.equal(clientSecret);
40+
expect(req.body.appId).to.equal(appId);
41+
expect(req.body.privateKey).to.equal(privateKey);
42+
43+
req.reply(mockStatusCode, mockBody);
44+
45+
return true;
46+
}).as('configureTest');
47+
48+
// save should be disabled before values are filled
49+
githubAppPo.saveButton().expectToBeDisabled();
50+
githubAppPo.enterClientId(clientId);
51+
githubAppPo.enterClientSecret(clientSecret);
52+
githubAppPo.enterGitHubAppId(appId);
53+
githubAppPo.enterPrivateKey(privateKey);
54+
55+
// save should be enabled after values are filled
56+
githubAppPo.saveButton().expectToBeEnabled();
57+
githubAppPo.save();
58+
cy.wait('@configureTest');
59+
});
60+
});
Lines changed: 13 additions & 0 deletions
Loading

shell/assets/translations/en-us.yaml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,51 @@ authConfig:
492492
allowedPrincipalIds:
493493
title: Authorized Users & Groups
494494
associatedWarning: 'The {provider} account that is used to enable the external provider will be granted admin permissions. If you use a test account or non-admin account, that account will still be granted admin-level permissions. See <a href="{docsBase}/how-to-guides/new-user-guides/authentication-permissions-and-global-configuration/authentication-config#external-authentication-configuration-and-principal-users" target="_blank" rel="noopener noreferrer nofollow">External Authentication Configuration and Principal Users</a> to understand why.'
495+
githubapp:
496+
clientId:
497+
label: Client ID
498+
clientSecret:
499+
label: Client Secret
500+
githubAppId:
501+
label: Github App ID
502+
installationId:
503+
label: Installation ID
504+
privateKey:
505+
label: Private Key
506+
warning: The GitHub App authentication provider only works with <a href="https://docs.github.com/en/get-started/learning-about-github/types-of-github-accounts" target="_blank" rel="noopener noreferrer nofollow">GitHub Organization accounts</a>. It does not support User accounts.
507+
form:
508+
prefix:
509+
1: <li>Open your <a href="{baseUrl}/settings/organizations" target="_blank" rel="noopener noreferrer nofollow">GitHub organization settings</a> in a new window</li>
510+
2: <li>To the right of the organization, click "Settings"</li>
511+
3: <li>In the left sidebar, click "Developer settings" -> "GitHub Apps"</li>
512+
4: <li>Click "New Github App"</li>
513+
instruction: 'Fill in the form with these values:'
514+
app:
515+
label: GitHub App name
516+
value: 'Anything you like, e.g. My {vendor}'
517+
callback:
518+
label: Callback URL
519+
value: '{serverUrl}/verify-auth'
520+
description:
521+
label: Application description
522+
value: 'Optional, can be left blank'
523+
homepage:
524+
label: Homepage URL
525+
create: Click "Create Github App"
526+
suffix:
527+
1: <li>Under Client Secrets, click "Generate a new client secret"</li>
528+
2: <li>Under Private Keys, click "Generate a private key"</li>
529+
3: <li>Copy and paste the "App ID", "Client ID", "Client Secret", and "Private Key" of your newly created OAuth app into the fields below</li>
530+
host:
531+
label: GitHub Enterprise Host
532+
placeholder: e.g. github.mycompany.example
533+
target:
534+
label: Which version of GitHub do you want to use?
535+
private: A private installation of GitHub Enterprise
536+
public: Public GitHub.com
537+
table:
538+
server: Server
539+
clientId: Client ID
495540
github:
496541
clientId:
497542
label: Client ID
@@ -7595,6 +7640,7 @@ model:
75957640
activedirectory: ActiveDirectory
75967641
azuread: AzureAD
75977642
github: GitHub
7643+
githubapp: GitHub App
75987644
keycloak: Keycloak
75997645
ldap: LDAP
76007646
openldap: OpenLDAP
@@ -9068,3 +9114,4 @@ component:
90689114
weekdaysAt0830: "At 30 minutes past the hour, every 1 hours, starting at 08:00 AM, Monday through Friday"
90699115
marchToMayHourly: "Every hour, only in March, April, and May"
90709116
every4Hours9to17: "At 0 minutes past the hour, every 4 hours, between 09:00 AM and 05:00 PM"
9117+

shell/config/product/auth.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export function init(store) {
177177
});
178178

179179
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/github`, 'auth/github');
180+
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/githubapp`, 'auth/github');
180181
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/openldap`, 'auth/ldap/index');
181182
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/freeipa`, 'auth/ldap/index');
182183
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/activedirectory`, 'auth/ldap/index');

shell/edit/auth/AuthProviderWarningBanners.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,20 @@ export default defineComponent({
2828
color="warning"
2929
data-testid="auth-provider-admin-permissions-warning-banner"
3030
>
31-
<span v-clean-html="t('authConfig.associatedWarning', tArgs, true)" />
31+
<span class="banner-content">
32+
<span v-clean-html="t('authConfig.associatedWarning', tArgs, true)" />
33+
<slot name="additional-warning">
34+
<!--Empty slot content-->
35+
</slot>
36+
</span>
3237
</Banner>
3338
</div>
3439
</template>
40+
41+
<style lang="scss" scoped>
42+
.banner-content {
43+
display: flex;
44+
flex-direction: column;
45+
gap: 1rem;
46+
}
47+
</style>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<script setup lang="ts">
2+
import InfoBox from '@shell/components/InfoBox';
3+
import CopyToClipboard from '@shell/components/CopyToClipboard';
4+
5+
type TArgs = {
6+
baseUrl: string;
7+
serverUrl: string;
8+
provider: string;
9+
username: string;
10+
}
11+
12+
defineProps<{
13+
name: string;
14+
tArgs: TArgs;
15+
}>();
16+
17+
</script>
18+
19+
<template>
20+
<InfoBox
21+
:step="1"
22+
class="step-box"
23+
>
24+
<ul class="step-list">
25+
<li v-clean-html="t(`authConfig.${name}.form.prefix.1`, tArgs, true)" />
26+
<li v-clean-html="t(`authConfig.${name}.form.prefix.2`, tArgs, true)" />
27+
<li v-clean-html="t(`authConfig.${name}.form.prefix.3`, tArgs, true)" />
28+
<li v-clean-html="t(`authConfig.${name}.form.prefix.4`, tArgs, true)" />
29+
</ul>
30+
</InfoBox>
31+
<InfoBox
32+
:step="2"
33+
class="step-box"
34+
>
35+
<ul class="step-list">
36+
<li>
37+
{{ t(`authConfig.${name}.form.instruction`, tArgs, true) }}
38+
<ul class="mt-10">
39+
<li>
40+
<b>
41+
{{ t(`authConfig.${name}.form.app.label`) }}
42+
</b>:
43+
<span v-clean-html="t(`authConfig.${name}.form.app.value`, tArgs, true)" />
44+
</li>
45+
<li>
46+
<b>
47+
{{ t(`authConfig.${name}.form.homepage.label`) }}
48+
</b>: {{ tArgs.serverUrl }}
49+
<CopyToClipboard
50+
label-as="tooltip"
51+
:text="tArgs.serverUrl"
52+
class="icon-btn"
53+
action-color="bg-transparent"
54+
/>
55+
</li>
56+
<li>
57+
<b>
58+
{{ t(`authConfig.${name}.form.description.label`) }}
59+
</b>:
60+
<span v-clean-html="t(`authConfig.${name}.form.description.value`, tArgs, true)" />
61+
</li>
62+
<li>
63+
<b>
64+
{{ t(`authConfig.${name}.form.callback.label`) }}
65+
</b>:
66+
{{ t(`authConfig.${name}.form.callback.value`, tArgs, true) }}
67+
<CopyToClipboard
68+
:text="tArgs.serverUrl"
69+
label-as="tooltip"
70+
class="icon-btn"
71+
action-color="bg-transparent"
72+
/>
73+
</li>
74+
</ul>
75+
</li>
76+
<li>
77+
{{ t(`authConfig.${name}.form.create`, tArgs, true) }}
78+
</li>
79+
</ul>
80+
</InfoBox>
81+
<InfoBox
82+
:step="3"
83+
class="mb-20"
84+
>
85+
<ul class="step-list">
86+
<li v-clean-html="t(`authConfig.${name}.form.suffix.1`, tArgs, true)" />
87+
<li v-clean-html="t(`authConfig.${name}.form.suffix.2`, tArgs, true)" />
88+
<li v-clean-html="t(`authConfig.${name}.form.suffix.3`, tArgs, true)" />
89+
</ul>
90+
</InfoBox>
91+
</template>
92+
93+
<style lang="scss" scoped>
94+
.step-list li:not(:last-child) {
95+
margin-bottom: 8px;
96+
}
97+
</style>

0 commit comments

Comments
 (0)