This is an automated email from the ASF dual-hosted git repository.

chanholee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 5484bebb6e [ZEPPELIN-6339] Fix - Anonymous users can access /login in 
the New UI, unlike the Classic UI
5484bebb6e is described below

commit 5484bebb6e988d474787147d725f7db17d619d91
Author: YONGJAE LEE(이용재) <[email protected]>
AuthorDate: Fri Oct 3 23:46:01 2025 +0900

    [ZEPPELIN-6339] Fix - Anonymous users can access /login in the New UI, 
unlike the Classic UI
    
    ### What is this PR for?
    #### [Classic UI]
    
    
https://github.com/user-attachments/assets/9ae01649-5525-4310-b467-4dc96f205987
    
    
    #### [As-Is]
    
    
    
https://github.com/user-attachments/assets/654ce13a-1770-4c61-bc4c-c9cc09ba397d
    
    
    #### [To-Be]
    
    
    
https://github.com/user-attachments/assets/9c8140e3-ff7f-4698-af93-8ae6859f6dee
    
    Anonymous users were able to access /login in the New UI, unlike the 
Classic UI.
    This has been restricted, and I have added tests to cover the change.
    
    ### What type of PR is it?
    Bug Fix
    
    ### Todos
    
    ### What is the Jira issue?
    ZEPPELIN-6339
    
    ### How should this be tested?
    
    ### Screenshots (if appropriate)
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    
    Closes #5087 from dididy/fix/login-issue.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 zeppelin-web-angular/e2e/models/home-page.ts       | 116 +++++++++++++++
 zeppelin-web-angular/e2e/models/home-page.util.ts  | 129 ++++++++++++++++
 .../anonymous-login-redirect.spec.ts               | 164 +++++++++++++++++++++
 zeppelin-web-angular/e2e/utils.ts                  |  23 ++-
 .../src/app/pages/login/login-routing.module.ts    |   4 +-
 .../src/app/pages/login/login.guard.ts             |  35 +++++
 .../src/app/services/ticket.service.ts             |   4 +
 7 files changed, 473 insertions(+), 2 deletions(-)

diff --git a/zeppelin-web-angular/e2e/models/home-page.ts 
b/zeppelin-web-angular/e2e/models/home-page.ts
new file mode 100644
index 0000000000..7d24fdf3ed
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/home-page.ts
@@ -0,0 +1,116 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Locator, Page, expect } from '@playwright/test';
+import { BasePage } from './base-page';
+import { getCurrentPath, waitForUrlNotContaining } from '../utils';
+
+export class HomePage extends BasePage {
+  readonly welcomeHeading: Locator;
+  readonly notebookSection: Locator;
+  readonly helpSection: Locator;
+  readonly communitySection: Locator;
+  readonly createNewNoteButton: Locator;
+  readonly importNoteButton: Locator;
+  readonly searchInput: Locator;
+  readonly filterInput: Locator;
+  readonly zeppelinLogo: Locator;
+  readonly anonymousUserIndicator: Locator;
+  readonly tutorialNotebooks: {
+    flinkTutorial: Locator;
+    pythonTutorial: Locator;
+    sparkTutorial: Locator;
+    rTutorial: Locator;
+    miscellaneousTutorial: Locator;
+  };
+  readonly externalLinks: {
+    documentation: Locator;
+    mailingList: Locator;
+    issuesTracking: Locator;
+    github: Locator;
+  };
+
+  constructor(page: Page) {
+    super(page);
+    this.welcomeHeading = page.locator('h1', { hasText: 'Welcome to Zeppelin!' 
});
+    this.notebookSection = page.locator('text=Notebook').first();
+    this.helpSection = page.locator('text=Help').first();
+    this.communitySection = page.locator('text=Community').first();
+    this.createNewNoteButton = page.locator('text=Create new Note');
+    this.importNoteButton = page.locator('text=Import Note');
+    this.searchInput = page.locator('textbox', { hasText: 'Search' });
+    this.filterInput = page.locator('input[placeholder*="Filter"]');
+    this.zeppelinLogo = page.locator('text=Zeppelin').first();
+    this.anonymousUserIndicator = page.locator('text=anonymous');
+
+    this.tutorialNotebooks = {
+      flinkTutorial: page.locator('text=Flink Tutorial'),
+      pythonTutorial: page.locator('text=Python Tutorial'),
+      sparkTutorial: page.locator('text=Spark Tutorial'),
+      rTutorial: page.locator('text=R Tutorial'),
+      miscellaneousTutorial: page.locator('text=Miscellaneous Tutorial')
+    };
+
+    this.externalLinks = {
+      documentation: page.locator('a[href*="zeppelin.apache.org/docs"]'),
+      mailingList: page.locator('a[href*="community.html"]'),
+      issuesTracking: page.locator('a[href*="issues.apache.org"]'),
+      github: page.locator('a[href*="github.com/apache/zeppelin"]')
+    };
+  }
+
+  async navigateToHome(): Promise<void> {
+    await this.page.goto('/', { waitUntil: 'load' });
+    await this.waitForPageLoad();
+  }
+
+  async navigateToLogin(): Promise<void> {
+    await this.page.goto('/#/login', { waitUntil: 'load' });
+    await this.waitForPageLoad();
+    // Wait for potential redirect to complete by checking URL change
+    await waitForUrlNotContaining(this.page, '#/login');
+  }
+
+  async isHomeContentDisplayed(): Promise<boolean> {
+    try {
+      await expect(this.welcomeHeading).toBeVisible();
+      return true;
+    } catch {
+      return false;
+    }
+  }
+
+  async isAnonymousUser(): Promise<boolean> {
+    try {
+      await expect(this.anonymousUserIndicator).toBeVisible();
+      return true;
+    } catch {
+      return false;
+    }
+  }
+
+  async clickZeppelinLogo(): Promise<void> {
+    await this.zeppelinLogo.click();
+  }
+
+  async getCurrentURL(): Promise<string> {
+    return this.page.url();
+  }
+
+  getCurrentPath(): string {
+    return getCurrentPath(this.page);
+  }
+
+  async getPageTitle(): Promise<string> {
+    return this.page.title();
+  }
+}
diff --git a/zeppelin-web-angular/e2e/models/home-page.util.ts 
b/zeppelin-web-angular/e2e/models/home-page.util.ts
new file mode 100644
index 0000000000..4211a722c0
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/home-page.util.ts
@@ -0,0 +1,129 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Page, expect } from '@playwright/test';
+import { HomePage } from './home-page';
+import { getBasicPageMetadata, waitForUrlNotContaining } from '../utils';
+
+export class HomePageUtil {
+  private homePage: HomePage;
+  private page: Page;
+
+  constructor(page: Page) {
+    this.page = page;
+    this.homePage = new HomePage(page);
+  }
+
+  async verifyAnonymousUserRedirectFromLogin(): Promise<{
+    isLoginUrlMaintained: boolean;
+    isHomeContentDisplayed: boolean;
+    isAnonymousUser: boolean;
+    currentPath: string;
+  }> {
+    await this.homePage.navigateToLogin();
+
+    const currentPath = this.homePage.getCurrentPath();
+    const isLoginUrlMaintained = currentPath.includes('#/login');
+    const isHomeContentDisplayed = await 
this.homePage.isHomeContentDisplayed();
+    const isAnonymousUser = await this.homePage.isAnonymousUser();
+
+    return {
+      isLoginUrlMaintained,
+      isHomeContentDisplayed,
+      isAnonymousUser,
+      currentPath
+    };
+  }
+
+  async verifyHomePageIntegrity(): Promise<void> {
+    await this.verifyHomePageElements();
+    await this.verifyNotebookFunctionalities();
+    await this.verifyTutorialNotebooks();
+    await this.verifyExternalLinks();
+  }
+
+  async verifyHomePageElements(): Promise<void> {
+    await expect(this.homePage.welcomeHeading).toBeVisible();
+    await expect(this.homePage.notebookSection).toBeVisible();
+    await expect(this.homePage.helpSection).toBeVisible();
+    await expect(this.homePage.communitySection).toBeVisible();
+  }
+
+  async verifyNotebookFunctionalities(): Promise<void> {
+    await expect(this.homePage.createNewNoteButton).toBeVisible();
+    await expect(this.homePage.importNoteButton).toBeVisible();
+
+    const filterInputCount = await this.homePage.filterInput.count();
+    if (filterInputCount > 0) {
+      await expect(this.homePage.filterInput).toBeVisible();
+    }
+  }
+
+  async verifyTutorialNotebooks(): Promise<void> {
+    await expect(this.homePage.tutorialNotebooks.flinkTutorial).toBeVisible();
+    await expect(this.homePage.tutorialNotebooks.pythonTutorial).toBeVisible();
+    await expect(this.homePage.tutorialNotebooks.sparkTutorial).toBeVisible();
+    await expect(this.homePage.tutorialNotebooks.rTutorial).toBeVisible();
+    await 
expect(this.homePage.tutorialNotebooks.miscellaneousTutorial).toBeVisible();
+  }
+
+  async verifyExternalLinks(): Promise<void> {
+    const docCount = await this.homePage.externalLinks.documentation.count();
+    const mailCount = await this.homePage.externalLinks.mailingList.count();
+    const issuesCount = await 
this.homePage.externalLinks.issuesTracking.count();
+    const githubCount = await this.homePage.externalLinks.github.count();
+
+    if (docCount > 0) await 
expect(this.homePage.externalLinks.documentation).toBeVisible();
+    if (mailCount > 0) await 
expect(this.homePage.externalLinks.mailingList).toBeVisible();
+    if (issuesCount > 0) await 
expect(this.homePage.externalLinks.issuesTracking).toBeVisible();
+    if (githubCount > 0) await 
expect(this.homePage.externalLinks.github).toBeVisible();
+  }
+
+  async testNavigationConsistency(): Promise<{
+    pathBeforeClick: string;
+    pathAfterClick: string;
+    homeContentMaintained: boolean;
+  }> {
+    const pathBeforeClick = this.homePage.getCurrentPath();
+
+    await this.homePage.clickZeppelinLogo();
+    await this.homePage.waitForPageLoad();
+
+    const pathAfterClick = this.homePage.getCurrentPath();
+    const homeContentMaintained = await this.homePage.isHomeContentDisplayed();
+
+    return {
+      pathBeforeClick,
+      pathAfterClick,
+      homeContentMaintained
+    };
+  }
+
+  async getPageMetadata(): Promise<{
+    title: string;
+    path: string;
+    isAnonymous: boolean;
+  }> {
+    const basicMetadata = await getBasicPageMetadata(this.page);
+    const isAnonymous = await this.homePage.isAnonymousUser();
+
+    return {
+      ...basicMetadata,
+      isAnonymous
+    };
+  }
+
+  async navigateToLoginAndWaitForRedirect(): Promise<void> {
+    await this.page.goto('/#/login', { waitUntil: 'load' });
+    await waitForUrlNotContaining(this.page, '#/login');
+  }
+}
diff --git 
a/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
 
b/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
new file mode 100644
index 0000000000..34b9f498f0
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
@@ -0,0 +1,164 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { ZeppelinHelper } from '../../helper';
+import { HomePageUtil } from '../../models/home-page.util';
+import { addPageAnnotationBeforeEach, PAGES, waitForUrlNotContaining, 
getCurrentPath } from '../../utils';
+
+test.describe('Anonymous User Login Redirect', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
+
+  let zeppelinHelper: ZeppelinHelper;
+  let homePageUtil: HomePageUtil;
+
+  test.beforeEach(async ({ page }) => {
+    zeppelinHelper = new ZeppelinHelper(page);
+    homePageUtil = new HomePageUtil(page);
+  });
+
+  test.describe('Given an anonymous user is already logged in', () => {
+    test.beforeEach(async ({ page }) => {
+      await page.goto('/', { waitUntil: 'load' });
+      await zeppelinHelper.waitForZeppelinReady();
+    });
+
+    test('When accessing login page directly, Then should redirect to home 
with proper URL change', async ({
+      page
+    }) => {
+      const redirectResult = await 
homePageUtil.verifyAnonymousUserRedirectFromLogin();
+
+      expect(redirectResult.isLoginUrlMaintained).toBe(false);
+      expect(redirectResult.isHomeContentDisplayed).toBe(true);
+      expect(redirectResult.isAnonymousUser).toBe(true);
+      expect(redirectResult.currentPath).toContain('#/');
+      expect(redirectResult.currentPath).not.toContain('#/login');
+    });
+
+    test('When accessing login page directly, Then should display all home 
page elements correctly', async ({
+      page
+    }) => {
+      await page.goto('/#/login', { waitUntil: 'load' });
+      await zeppelinHelper.waitForZeppelinReady();
+      await page.waitForURL(url => !url.toString().includes('#/login'));
+
+      await homePageUtil.verifyHomePageIntegrity();
+    });
+
+    test('When clicking Zeppelin logo after redirect, Then should maintain 
home URL and content', async ({ page }) => {
+      await page.goto('/#/login', { waitUntil: 'load' });
+      await zeppelinHelper.waitForZeppelinReady();
+      await page.waitForURL(url => !url.toString().includes('#/login'));
+
+      const navigationResult = await homePageUtil.testNavigationConsistency();
+
+      expect(navigationResult.pathBeforeClick).toContain('#/');
+      expect(navigationResult.pathBeforeClick).not.toContain('#/login');
+      expect(navigationResult.pathAfterClick).toContain('#/');
+      expect(navigationResult.homeContentMaintained).toBe(true);
+    });
+
+    test('When accessing login page, Then should redirect and maintain 
anonymous user state', async ({ page }) => {
+      await page.goto('/#/login', { waitUntil: 'load' });
+      await zeppelinHelper.waitForZeppelinReady();
+      await page.waitForURL(url => !url.toString().includes('#/login'));
+
+      const metadata = await homePageUtil.getPageMetadata();
+
+      expect(metadata.title).toContain('Zeppelin');
+      expect(metadata.path).toContain('#/');
+      expect(metadata.path).not.toContain('#/login');
+      expect(metadata.isAnonymous).toBe(true);
+    });
+
+    test('When accessing login page, Then should display welcome heading and 
main sections', async ({ page }) => {
+      await page.goto('/#/login', { waitUntil: 'load' });
+      await zeppelinHelper.waitForZeppelinReady();
+      await page.waitForURL(url => !url.toString().includes('#/login'));
+
+      await expect(page.locator('h1', { hasText: 'Welcome to Zeppelin!' 
})).toBeVisible();
+      await expect(page.locator('text=Notebook').first()).toBeVisible();
+      await expect(page.locator('text=Help').first()).toBeVisible();
+      await expect(page.locator('text=Community').first()).toBeVisible();
+    });
+
+    test('When accessing login page, Then should display notebook 
functionalities', async ({ page }) => {
+      await page.goto('/#/login', { waitUntil: 'load' });
+      await zeppelinHelper.waitForZeppelinReady();
+      await page.waitForURL(url => !url.toString().includes('#/login'));
+
+      await expect(page.locator('text=Create new Note')).toBeVisible();
+      await expect(page.locator('text=Import Note')).toBeVisible();
+
+      const filterInput = page.locator('input[placeholder*="Filter"]');
+      if ((await filterInput.count()) > 0) {
+        await expect(filterInput).toBeVisible();
+      }
+    });
+
+    test('When accessing login page, Then should display external links in 
help and community sections', async ({
+      page
+    }) => {
+      await page.goto('/#/login', { waitUntil: 'load' });
+      await zeppelinHelper.waitForZeppelinReady();
+      await page.waitForURL(url => !url.toString().includes('#/login'));
+
+      const docLinks = page.locator('a[href*="zeppelin.apache.org/docs"]');
+      const communityLinks = page.locator('a[href*="community.html"]');
+      const issuesLinks = page.locator('a[href*="issues.apache.org"]');
+      const githubLinks = 
page.locator('a[href*="github.com/apache/zeppelin"]');
+
+      if ((await docLinks.count()) > 0) await expect(docLinks).toBeVisible();
+      if ((await communityLinks.count()) > 0) await 
expect(communityLinks).toBeVisible();
+      if ((await issuesLinks.count()) > 0) await 
expect(issuesLinks).toBeVisible();
+      if ((await githubLinks.count()) > 0) await 
expect(githubLinks).toBeVisible();
+    });
+
+    test('When navigating between home and login URLs, Then should maintain 
consistent user experience', async ({
+      page
+    }) => {
+      await page.goto('/', { waitUntil: 'load' });
+      await zeppelinHelper.waitForZeppelinReady();
+
+      const homeMetadata = await homePageUtil.getPageMetadata();
+      expect(homeMetadata.path).toContain('#/');
+      expect(homeMetadata.isAnonymous).toBe(true);
+
+      await page.goto('/#/login', { waitUntil: 'load' });
+      await zeppelinHelper.waitForZeppelinReady();
+      await page.waitForURL(url => !url.toString().includes('#/login'));
+
+      const loginMetadata = await homePageUtil.getPageMetadata();
+      expect(loginMetadata.path).toContain('#/');
+      expect(loginMetadata.path).not.toContain('#/login');
+      expect(loginMetadata.isAnonymous).toBe(true);
+
+      const isHomeContentDisplayed = await 
homePageUtil.verifyAnonymousUserRedirectFromLogin();
+      expect(isHomeContentDisplayed.isHomeContentDisplayed).toBe(true);
+    });
+
+    test('When multiple page loads occur on login URL, Then should 
consistently redirect to home', async ({ page }) => {
+      for (let i = 0; i < 3; i++) {
+        await page.goto('/#/login', { waitUntil: 'load' });
+        await zeppelinHelper.waitForZeppelinReady();
+        await waitForUrlNotContaining(page, '#/login');
+
+        await expect(page.locator('h1', { hasText: 'Welcome to Zeppelin!' 
})).toBeVisible();
+        await expect(page.locator('text=anonymous')).toBeVisible();
+
+        const path = getCurrentPath(page);
+        expect(path).toContain('#/');
+        expect(path).not.toContain('#/login');
+      }
+    });
+  });
+});
diff --git a/zeppelin-web-angular/e2e/utils.ts 
b/zeppelin-web-angular/e2e/utils.ts
index 50f1be17c7..c5ceec8442 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -10,7 +10,7 @@
  * limitations under the License.
  */
 
-import { test, TestInfo } from '@playwright/test';
+import { test, TestInfo, Page } from '@playwright/test';
 
 export const PAGES = {
   // Main App
@@ -137,3 +137,24 @@ export function flattenPageComponents(pages: 
PageStructureType): string[] {
 export function getCoverageTransformPaths(): string[] {
   return flattenPageComponents(PAGES);
 }
+
+export async function waitForUrlNotContaining(page: Page, fragment: string) {
+  await page.waitForURL(url => !url.toString().includes(fragment));
+}
+
+export function getCurrentPath(page: Page): string {
+  const url = new URL(page.url());
+  return url.hash || url.pathname;
+}
+
+export async function getBasicPageMetadata(
+  page: Page
+): Promise<{
+  title: string;
+  path: string;
+}> {
+  return {
+    title: await page.title(),
+    path: getCurrentPath(page)
+  };
+}
diff --git a/zeppelin-web-angular/src/app/pages/login/login-routing.module.ts 
b/zeppelin-web-angular/src/app/pages/login/login-routing.module.ts
index 06dd0304db..d85802d19c 100644
--- a/zeppelin-web-angular/src/app/pages/login/login-routing.module.ts
+++ b/zeppelin-web-angular/src/app/pages/login/login-routing.module.ts
@@ -14,11 +14,13 @@ import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
 
 import { LoginComponent } from './login.component';
+import { LoginGuard } from './login.guard';
 
 const routes: Routes = [
   {
     path: '',
-    component: LoginComponent
+    component: LoginComponent,
+    canActivate: [LoginGuard]
   }
 ];
 
diff --git a/zeppelin-web-angular/src/app/pages/login/login.guard.ts 
b/zeppelin-web-angular/src/app/pages/login/login.guard.ts
new file mode 100644
index 0000000000..c52e702cfe
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/login/login.guard.ts
@@ -0,0 +1,35 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import { CanActivate, Router, UrlTree } from '@angular/router';
+import { of, Observable } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+
+import { TicketService } from '@zeppelin/services';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class LoginGuard implements CanActivate {
+  constructor(private ticketService: TicketService, private router: Router) {}
+
+  canActivate(): Observable<boolean> {
+    return this.ticketService.isAuthenticated().pipe(
+      map(() => {
+        this.router.navigate(['/'], { replaceUrl: true });
+        return false;
+      }),
+      catchError(() => of(true))
+    );
+  }
+}
diff --git a/zeppelin-web-angular/src/app/services/ticket.service.ts 
b/zeppelin-web-angular/src/app/services/ticket.service.ts
index d12da61120..868b36c225 100644
--- a/zeppelin-web-angular/src/app/services/ticket.service.ts
+++ b/zeppelin-web-angular/src/app/services/ticket.service.ts
@@ -65,6 +65,10 @@ export class TicketService {
     this.ticket$.next(this.ticket);
   }
 
+  isAuthenticated() {
+    return this.getTicket();
+  }
+
   clearTicket() {
     this.ticket = new ITicketWrapped();
     this.originTicket = new ITicket();

Reply via email to