Skip to content
Merged
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
1 change: 1 addition & 0 deletions cypress/e2e/po/components/labeled-input.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default class LabeledInputPo extends ComponentPo {
* @returns
*/
set(value: any, secret?: boolean, parseSpecialCharSequences?: boolean): Cypress.Chainable {
this.input().scrollIntoView();
this.input().should('be.visible');
this.input().focus();
this.input().clear();
Expand Down
82 changes: 82 additions & 0 deletions cypress/e2e/po/edit/auth/githubapp.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import PagePo from '@/cypress/e2e/po/pages/page.po';
import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po';
import AsyncButtonPo from '@/cypress/e2e/po/components/async-button.po';
import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po';
import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po';

export default class GithubAppPo extends PagePo {
private static createPath(clusterId: string, id?: string ) {
return `/c/${ clusterId }/auth/config/githubapp?mode=edit`;
}

static goTo(clusterId: string): Cypress.Chainable<Cypress.AUTWindow> {
return super.goTo(GithubAppPo.createPath(clusterId));
}

constructor(clusterId: string) {
super(GithubAppPo.createPath(clusterId));
}

static navTo() {
const sideNav = new ProductNavPo();

BurgerMenuPo.burgerMenuNavToMenubyLabel('Users & Authentication');
sideNav.navToSideMenuEntryByLabel('Auth Provider');
}

clientIdInputField() {
return this.self().get('[data-testid="client-id"').invoke('val');
}

enterClientId(id: string) {
return new LabeledInputPo('[data-testid="client-id"]').set(id);
}

clientSecretInputField() {
return this.self().get('[data-testid="client-secret"').invoke('val');
}

enterClientSecret(val: string) {
return new LabeledInputPo('[data-testid="client-secret"]').set(val);
}

gitHubAppIdInputField() {
return this.self().get('[data-testid="app-id"').invoke('val');
}

enterGitHubAppId(val: string) {
return new LabeledInputPo('[data-testid="app-id"]').set(val);
}

installationIdInputField() {
return this.self().get('[data-testid="installation-id"').invoke('val');
}

enterInstallationId(val: string) {
return new LabeledInputPo('[data-testid="installation-id"]').set(val);
}

privateKeyInputField() {
return this.self().get('[data-testid="private-key"').invoke('val');
}

enterPrivateKey(val: string) {
return new LabeledInputPo('[data-testid="private-key"]').set(val);
}

saveButton(): AsyncButtonPo {
return new AsyncButtonPo('[data-testid="form-save"]', this.self());
}

save() {
return new AsyncButtonPo('[data-testid="form-save"]').click();
}

gitHubAppBanner() {
return this.self().get('[data-testid="github-app-banner"]');
}

permissionsWarningBanner() {
return this.self().get('[data-testid="auth-provider-admin-permissions-warning-banner"]');
}
}
1 change: 1 addition & 0 deletions cypress/e2e/po/pages/users-and-auth/authProvider.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SelectIconGridPo from '@/cypress/e2e/po/components/select-icon-grid.po';
export enum AuthProvider {
AMAZON_COGNITO = 'Amazon Cognito', // eslint-disable-line no-unused-vars
AZURE = 'AzureAD', // eslint-disable-line no-unused-vars
GITHUB_APP = 'GitHub App', // eslint-disable-line no-unused-vars
}

export class AuthProviderPo extends PagePo {
Expand Down
60 changes: 60 additions & 0 deletions cypress/e2e/tests/pages/users-and-auth/githubapp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import HomePagePo from '@/cypress/e2e/po/pages/home.po';
import GithubAppPo from '@/cypress/e2e/po/edit/auth/githubapp.po';
import { AuthProvider, AuthProviderPo } from '@/cypress/e2e/po/pages/users-and-auth/authProvider.po';

const authProviderPo = new AuthProviderPo('local');
const githubAppPo = new GithubAppPo('local');

const clientId = 'test-client-id';
const clientSecret = 'test-client-secret';
const appId = 'test-app-id';
const privateKey = 'test-private-key';

const mockStatusCode = 200;
const mockBody = {};

describe('GitHub App', { tags: ['@adminUser', '@usersAndAuths'] }, () => {
beforeEach(() => {
cy.login();
HomePagePo.goToAndWaitForGet();
AuthProviderPo.navTo();
authProviderPo.waitForPage();
authProviderPo.selectProvider(AuthProvider.GITHUB_APP);
githubAppPo.waitForPage();
});

it('can navigate to Auth Provider and Select GitHub App', () => {
githubAppPo.mastheadTitle().should('include', `GitHub App`);

githubAppPo.gitHubAppBanner().should('be.visible');
githubAppPo.permissionsWarningBanner().should('be.visible');
});

it('sends correct request to create GitHub App auth provider', () => {
cy.intercept('POST', 'v3/githubAppConfigs/githubapp?action=configureTest', (req) => {
expect(req.body.enabled).to.equal(false);
expect(req.body.id).to.equal('githubapp');
expect(req.body.type).to.equal('githubAppConfig');
expect(req.body.clientId).to.equal(clientId);
expect(req.body.clientSecret).to.equal(clientSecret);
expect(req.body.appId).to.equal(appId);
expect(req.body.privateKey).to.equal(privateKey);

req.reply(mockStatusCode, mockBody);

return true;
}).as('configureTest');

// save should be disabled before values are filled
githubAppPo.saveButton().expectToBeDisabled();
githubAppPo.enterClientId(clientId);
githubAppPo.enterClientSecret(clientSecret);
githubAppPo.enterGitHubAppId(appId);
githubAppPo.enterPrivateKey(privateKey);

// save should be enabled after values are filled
githubAppPo.saveButton().expectToBeEnabled();
githubAppPo.save();
cy.wait('@configureTest');
});
});
13 changes: 13 additions & 0 deletions shell/assets/images/vendor/githubapp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions shell/assets/translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,51 @@ authConfig:
allowedPrincipalIds:
title: Authorized Users & Groups
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.'
githubapp:
clientId:
label: Client ID
clientSecret:
label: Client Secret
githubAppId:
label: Github App ID
installationId:
label: Installation ID
privateKey:
label: Private Key
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.
form:
prefix:
1: <li>Open your <a href="{baseUrl}/settings/organizations" target="_blank" rel="noopener noreferrer nofollow">GitHub organization settings</a> in a new window</li>
2: <li>To the right of the organization, click "Settings"</li>
3: <li>In the left sidebar, click "Developer settings" -> "GitHub Apps"</li>
4: <li>Click "New Github App"</li>
instruction: 'Fill in the form with these values:'
app:
label: GitHub App name
value: 'Anything you like, e.g. My {vendor}'
callback:
label: Callback URL
value: '{serverUrl}/verify-auth'
description:
label: Application description
value: 'Optional, can be left blank'
homepage:
label: Homepage URL
create: Click "Create Github App"
suffix:
1: <li>Under Client Secrets, click "Generate a new client secret"</li>
Copy link
Member

@eva-vashkevich eva-vashkevich Sep 26, 2025

Choose a reason for hiding this comment

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

Should these be in quotes too? And the next two lines

Copy link
Member Author

Choose a reason for hiding this comment

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

Just for clarity, what are these that you are referencing? I think that the convention I was going for was that targets of actions would be contained within quotation marks - for example, Click "Some label". With this in mind, I think that the targets of Copy & Paste can be contained in quotation marks.

With the current iteration, there's only one line below 521.. I think I've addressed the changes, but let me know if anything is missing.

2: <li>Under Private Keys, click "Generate a private key"</li>
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>
host:
label: GitHub Enterprise Host
placeholder: e.g. github.mycompany.example
target:
label: Which version of GitHub do you want to use?
private: A private installation of GitHub Enterprise
public: Public GitHub.com
table:
server: Server
clientId: Client ID
github:
clientId:
label: Client ID
Expand Down Expand Up @@ -7595,6 +7640,7 @@ model:
activedirectory: ActiveDirectory
azuread: AzureAD
github: GitHub
githubapp: GitHub App
keycloak: Keycloak
ldap: LDAP
openldap: OpenLDAP
Expand Down Expand Up @@ -9068,3 +9114,4 @@ component:
weekdaysAt0830: "At 30 minutes past the hour, every 1 hours, starting at 08:00 AM, Monday through Friday"
marchToMayHourly: "Every hour, only in March, April, and May"
every4Hours9to17: "At 0 minutes past the hour, every 4 hours, between 09:00 AM and 05:00 PM"

1 change: 1 addition & 0 deletions shell/config/product/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export function init(store) {
});

componentForType(`${ MANAGEMENT.AUTH_CONFIG }/github`, 'auth/github');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/githubapp`, 'auth/github');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/openldap`, 'auth/ldap/index');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/freeipa`, 'auth/ldap/index');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/activedirectory`, 'auth/ldap/index');
Expand Down
15 changes: 14 additions & 1 deletion shell/edit/auth/AuthProviderWarningBanners.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,20 @@ export default defineComponent({
color="warning"
data-testid="auth-provider-admin-permissions-warning-banner"
>
<span v-clean-html="t('authConfig.associatedWarning', tArgs, true)" />
<span class="banner-content">
<span v-clean-html="t('authConfig.associatedWarning', tArgs, true)" />
<slot name="additional-warning">
<!--Empty slot content-->
</slot>
</span>
</Banner>
</div>
</template>

<style lang="scss" scoped>
.banner-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>
97 changes: 97 additions & 0 deletions shell/edit/auth/github-app-steps.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<script setup lang="ts">
import InfoBox from '@shell/components/InfoBox';
import CopyToClipboard from '@shell/components/CopyToClipboard';

type TArgs = {
baseUrl: string;
serverUrl: string;
provider: string;
username: string;
}

defineProps<{
name: string;
tArgs: TArgs;
}>();

</script>

<template>
<InfoBox
:step="1"
class="step-box"
>
<ul class="step-list">
<li v-clean-html="t(`authConfig.${name}.form.prefix.1`, tArgs, true)" />
<li v-clean-html="t(`authConfig.${name}.form.prefix.2`, tArgs, true)" />
<li v-clean-html="t(`authConfig.${name}.form.prefix.3`, tArgs, true)" />
<li v-clean-html="t(`authConfig.${name}.form.prefix.4`, tArgs, true)" />
</ul>
</InfoBox>
<InfoBox
:step="2"
class="step-box"
>
<ul class="step-list">
<li>
{{ t(`authConfig.${name}.form.instruction`, tArgs, true) }}
<ul class="mt-10">
<li>
<b>
{{ t(`authConfig.${name}.form.app.label`) }}
</b>:
<span v-clean-html="t(`authConfig.${name}.form.app.value`, tArgs, true)" />
</li>
<li>
<b>
{{ t(`authConfig.${name}.form.homepage.label`) }}
</b>: {{ tArgs.serverUrl }}
<CopyToClipboard
label-as="tooltip"
:text="tArgs.serverUrl"
class="icon-btn"
action-color="bg-transparent"
/>
</li>
<li>
<b>
{{ t(`authConfig.${name}.form.description.label`) }}
</b>:
<span v-clean-html="t(`authConfig.${name}.form.description.value`, tArgs, true)" />
</li>
<li>
<b>
{{ t(`authConfig.${name}.form.callback.label`) }}
</b>:
{{ t(`authConfig.${name}.form.callback.value`, tArgs, true) }}
<CopyToClipboard
:text="tArgs.serverUrl"
label-as="tooltip"
class="icon-btn"
action-color="bg-transparent"
/>
</li>
</ul>
</li>
<li>
{{ t(`authConfig.${name}.form.create`, tArgs, true) }}
</li>
</ul>
</InfoBox>
<InfoBox
:step="3"
class="mb-20"
>
<ul class="step-list">
<li v-clean-html="t(`authConfig.${name}.form.suffix.1`, tArgs, true)" />
<li v-clean-html="t(`authConfig.${name}.form.suffix.2`, tArgs, true)" />
<li v-clean-html="t(`authConfig.${name}.form.suffix.3`, tArgs, true)" />
</ul>
</InfoBox>
</template>

<style lang="scss" scoped>
.step-list li:not(:last-child) {
margin-bottom: 8px;
}
</style>
Loading